객체지향언어
특징
- 코드의 재사용성이 높다.
- 코드의 관리가 용이하다.
- 신뢰성이 높은 프로그래밍을 가능하게 한다.
클래스와 객체
클래스 : 객체를 정의해 놓은것으로 객체를 생성하는데 사용한다.
객체 : 실제로 존재하는 것으로 사물 또는 개념을 말한다 EX) 책상, 의자, 수학공식, 프로그램 에러
클래스가 제품의 설계도라면 객체는 설계도로 만들어진 제품이라고 생각하면 된다!!

인스턴스화(instantiate) : 클래스로부터 객체를 만드는 과정
인스턴스(instance) : 클래스로부터 만들어진 객체
TV에대한 설계도는 클래스 설계도를 통해서 만든 걸 객체(인스턴스) 만드는 과정을 인스턴스화 라고 생각하면 될거 같다.
그리고 각 객체는 속성, 기능을 가지고 있는데
속성 : 멤버변수(member variable), 특성(attribute), 필드(field), 상태(state)
기능 : 메서드(method), 함수(function), 행위(behavior)
클래스는 사용자정의 타입(user-defined type)로도 사용된다.
class Refrigerator
{
//속성
boolean power;
float temperature;
//기능
void power() {this.power = !this.power;}
void setTemperature(float temperature){this.temperature = temperature;}
}
냉장고 클래스가 있다고 한다면 속성과 기능을 이렇게 정의할 수 있을 거 같다.
속성 : 냉장고 전원, 냉장고 온도
기능 : 냉장고 전원 켜기/끄기, 냉장고 온도 설정하기
class Refrigerator
{
boolean power;
float temperature;
void power() {this.power = !this.power;}
void setTemperature(float temperature){this.temperature = temperature;}
}
public class Main {
public static void main(String[] args) {
//냉장고 생성
Refrigerator refrigerator;
refrigerator = new Refrigerator();
//냉장고 설정
refrigerator.power = true;
refrigerator.temperature = 10.0f;
}
}
그럼 냉장고를 생성하고 온도, 전원을 설정하는 과정을 생각해보자

Refrigerator refrigerator;
이 과정에서 참조변수 refrigerator를 만들게 된다
refrigerator = new Refrigerator();
그리고 new Refrigerator로 새로운 객체를 생성하고 대입연산자를 통해서 참조변수에 주소값이 저장된다.
이후 메서드를 통해서 값을 조정하는게 가능하다.
여기서 클래스를 통해 객체를 만들면 참조변수를 이용해서 새로운 공간에 생성된 객체의 주소값을 가지고 있다는 걸 알고 있자!
여기서 궁금한 부분이 생겼다.
int, float같은 자료형은 참조를 하는게 아니라 자신의 공간에 바로 값을 저장하는데 왜 객체는 참조변수를 통해서만 접근을 하는걸까??
class Refrigerator
{
boolean power;
float temperature;
void power() {this.power = !this.power;}
void setTemperature(float temperature){this.temperature = temperature;}
}
public class Main {
public static void main(String[] args) {
Refrigerator refrigerator1;
Refrigerator refrigerator2;
refrigerator1 = new Refrigerator();
refrigerator2 = new Refrigerator();
refrigerator1.power = true;
refrigerator1.temperature = 10.0f;
refrigerator2.power = true;
refrigerator2.temperature = 20.0f;
System.out.println(refrigerator1.temperature);
System.out.println(refrigerator2.temperature);
refrigerator1 = refrigerator2;
System.out.println(refrigerator1.temperature);
System.out.println(refrigerator2.temperature);
}
}
이렇게 접근한다면 결과값은?
10.0
20.0
20.0
20.0
이렇게 나오게 된다. 중간에 refrigerator1 = refrigerator2 로 변환하는 과정에서 참조하는 값이 바뀐다.
그래서 출력값이 달라지게 되는데 이러면 기존에 만들어진 객체는 우째!! -> 가비지 컬렉터(Grabage Collector)가 자동으로 제거를 해준다!
만약 냉장고를 100개 만들어야 한다면??
public class Main {
public static void main(String[] args) {
Refrigerator[] refrigerators = new Refrigerator[100];
for(int i = 0;i<100;i++){
refrigerators[i] = new Refrigerator();
}
}
}
배열을 사용해서 100개를 저장하면 된다. -> 이때 배열은 참조변수를 저장하고 있는 값이다.
이처럼 클래스는 여러개의 속성을 묶어서 한번에 사용할 수 있는 환경을 제공해주고 배열은 서로 같은 자료형만을 저장, 구조체는 서로 다른 자료형도 저장 가능이라면 클래스는 함수까지 함께 사용할 수 있는 편리함을 제공한다.
변수와 메서드
3가지 종류의 변수 유형이 존재한다
- 클래스변수 : 클래스가 메모리에 올라갈 때 생성된다.
- 인스턴스변수 : 인스턴스가 생성되었을 때 생성된다.
- 지역변수 : 변수 선언문이 수행되었을 때 생성된다.
만약 고기를 저장하는 회서에서 100개의 냉장고를 사용하고 있는데 어느날 전체 냉장고의 온도를 10도로 맞춰야 고기가 상하지 않는다는 소식을 들었다 모든 냉장고의 온도는 항상 동일해야 한다고 가정했을 때 온도를 바꿀려면 어떻게 해야할까!!
class Refrigerator
{
boolean power;
float temperature;
void power() {this.power = !this.power;}
void setTemperature(float temperature){this.temperature = temperature;}
}
public class Main {
public static void main(String[] args) {
Refrigerator[] refrigerators = new Refrigerator[100];
for(int i = 0;i<100;i++){
refrigerators[i] = new Refrigerator();
}
for(int i = 0;i<100;i++){
refrigerators[i].temperature = 10.0f;
}
}
}
이렇게 for문을 통해서 모든 객체의 온도를 10으로 정해주는 방법이 있을 것이다. 하지만
class Refrigerator
{
boolean power;
static float temperature = 10;
void power() {this.power = !this.power;}
void setTemperature(float temperature){this.temperature = temperature;}
}
public class Main {
public static void main(String[] args) {
Refrigerator[] refrigerators = new Refrigerator[100];
for(int i = 0;i<100;i++){
refrigerators[i] = new Refrigerator();
}
}
}
static을 사용하게 된다면 하나의 저장공간을 모든 객체가 공유하기 때문에 하나의 공간값만 바꿔주게 된다면 모두가 10도로 설정이 가능해진다. 이런 static이 붙은 변수를 클래스 변수라고 이야기 한다.
그리고 하나의 공각을 공유하기 때문에 하나의 객체에서 값을 변경하면 모든 객체에게 적용된다. -> 항상 공통된 값을 유지하는게 가능하다.
우리가 클래스를 정의하면서 속성도 정의하고 있지만 그 외에도 정의하는 부분이 바로 메서드(기능)이다.
메서드를 사용하는 이유
- 높은 재사용성
- 중복된 코드의 제거
- 프로그램의 구조화
public class Main {
public static void main(String[] args) {
int result1 = calculator(1, 1,'+');
int result2 = calculator(1, 1,'*');
int result3 = calculator(1, 1,'/');
int result4 = calculator(1, 1,'-');
}
static int calculator(int a, int b, char ch)
{
if(ch == '*'){return a * b;}
else if(ch == '+'){return a + b;}
else if(ch =='/'){return a/b;}
else{return a-b;}
}
}
다음과 같이 메서드를 정의하게 된다면 간단한 계산기를 구현하는게 가능하다.
메서드를 구현하면서 주의해야 하는 부분은
- return해주는 값과 반환타입이 같아야 한다. 아니면 형변환이 가능해야 한다(float -> double)
- 그리고 반드시 return을 해줘야 하는데 void반환타입에서 선언하지 안아도 괜찮은 이유는 컴파일러가 자동으로 넣어주기 때문이다.
- 적절하지 않은 값이 매개변수를 통해 넘어온다면 매개변수의 값을 보정하거나 메서드를 중단해야 한다.(유효성 검사)
- 나누기 연산을 할때 0으로 나눠주는 상황은 없어야 한다.
메소드를 사용할 때 Return문은 필수적이다!!
근데 왜 void 형으로 선언을 하면 return문을 적지 않아도 괜찮을까??
컴파일러가 메서드의 마지막에 return;을 자동적으로 추가해주기 때문이다!
매개변수의 종류
1. 기본형 매개변수 : 변수의 값을 읽기만 할 수 있다. (read only)
2. 참조형 매개변수 : 변수의 값을 읽고 변경할 수 있다. (read, write)
public class Main {
static class Data{
int x;
Data(int x){this.x = x;}
public int getX(){return x;}
}
public static void main(String[] args) {
Data data = new Data(10);
int num = 10;
System.out.println("기본형");
System.out.println(num);
change(num);
System.out.println(num);
System.out.println("참조형");
System.out.println(data.getX());
change(data);
System.out.println(data.getX());
}
public static void change(Data data){
data.x = 10000;
}
public static void change(int data){
data = 10000;
}
}
두개의 change함수가 있다.
- change(Data data)
- change(int data)
함수의 차이점은 매개변수의 차이점이다!!
int의 경우 기본형 매개변수인데 Data의 경우 참조형 매개변수이다.
int는 공간을 할당하고 할당된 공간에 값을 저장하는 반면에 Data는 공간을 할당하고 new Data(10)으로 생성된 인스턴스의 주소값을 저장하고 있다. 따라서 매개변수로 전달해줬을 때 change는 값을 저장하는게 아닌 주소값을 저장하기 때문에 x의 값을 변경하였을 때 기존의 data변수에 영향을 주게 된다.
이 외에도 배열로 매개변수를 전달해줘도 배열은 Int[] arr = new int[10]형식으로 생성하였을때 arr은 첫번째 공간의 값의 주소값을 저장하고 있기 때문에 매개변수로 전달해줄 때 참조형 매개변수로 전달되는것이다.
public class Main {
static class Data{
int x;
Data(int x){this.x = x;}
Data(){this.x = 0;}
public int getX(){return x;}
}
public static void main(String[] args) {
Data data = new Data(10);
Data data2 = create(data);
}
public static Data create(Data data){
Data tmp = new Data();
tmp.x = data.getX();
return tmp;
}
}
만약 이렇게 선언한다면 create를 했을 때 new Data()로 새로운 인스턴스를 생성하고 tmp가 주소값을 저장하게 된다. 그리고 반환을 하는데 이때!! 조심해야 하는 부분이 있다!!
참조형을 반환한다면 메서드가 객체의 주소를 반환하는 것을 의미하는걸 알고 있자.
public class Main {
static class Data{
int x;
Data(int x){this.x = x;}
Data(){this.x = 0;}
public int getX(){return x;}
}
public static void main(String[] args) {
Data data = new Data(10);
Data data2 = create(data);
System.out.println(data.getX());
System.out.println(data2.getX());
data2.x = 100;
System.out.println(data.getX());
System.out.println(data2.getX());
}
public static Data create(Data data){
return data;
}
}
그래서 매개변수로 참조형을 받아서 그대로 반환해준다면

해당 그림처럼 된다고 생각하면 될 거 같다.
재귀호출
💡재귀함수는??
메서드 내부에서 메서드 자신을 다시 호출하는 것.
static void func1(int n){
if(n == 0){
return;
}
System.out.println(n);
func1(n-1);
}
static void func2(int n){
while(n != 0)
{
System.out.println(n);
n--;
}
}
두개의 함수가 있다고 생각해보자. 역할은 비슷한데 형태가 다른걸 볼 수 있다. func1을 보면 함수가 자신을 다시 호출하고 있는데 이런 구조를 재귀함수라고 부른다.
재귀함수는 메서드를 호출하기 때문에 매개변수 복사와 종료 후 복귀할 주소저장 등이 추가로 필요해지는데 반복문을 사용하면 그렇지 않다. 근데 왜 쓸까??
이유는 재귀호출이 주는 논리적 간결함이 있기 때문이다!
public class Main {
static int factorial(int n){
if(n <= 0 || n > 20)return -1;
if(n == 1) return 1;
return n * factorial(n-1);
}
static int factorial2(int n){
if(n <= 0 || n > 20)return -1;
int result = 1;
for(int i = 1;i <= n;i++){
result = result * i;
}
return result;
}
public static void main(String[] args) {
System.out.println(factorial(10));
System.out.println(factorial2(10));
}
}
이런식으로 factorial 연산을 하는게 가능하다.
이정도만 보면 큰 차이가 없어보일 수 있지만 백트래킹, 재귀 관련된 코딩 문제를 풀어보면 3중, 4중 for문으로 해야하는 문제들을 재귀 함수로 간단하게 할 수 있는걸 볼 수 있을것이다.
메서드의 종류(클래스, 인스턴스)
클래스 메서드 : 인스턴스와 관계없는 메서드.
인스턴스 메서드 : 인스턴스 변수와 관련된 작업을 하는, 메서드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메서드.
각각은 어떤 상황에 사용해야 적합할까??
1. 클래스를 사용할 때 멤버변수 중 모든 인스턴스에 공통으로 사용하는 것에 static
2. 클래스 변수는 인스턴스를 생성하지 않아도 사용이 가능
3. 클래스 메서드는 인스턴스 변수를 사용할 수 없다.
4. 메서드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 걸 고려한다.
class MyMath{
long a, b;
long add(){return a + b;}
long subtract(){return a - b;}
long multiply(){return a * b;}
long divide(){return a / b;}
static long add(long a, long b){return a + b;}
static long subtract(long a, long b){return a - b;}
static long multiply(long a, long b){return a * b;}
static long divide(long a, long b){return a / b;}
}
책에서는 연산을 클래스의 메소드를 정의하고 있는데 이런식으로 나눠서 생각하면 좋을거 같다!!
static은 클래스 안에 있는 요소를 사용하지 않고 매개변수로 받아온 값을 이용해서 연산을 수핸한다.
static이 없는 메소드는 클래스에 정의되어 있는 인스턴스 변수를 사용한다.
인스턴스 메소드 안에서는
class MyMath{
long a, b;
long add(){return a + b;}
long subtract(){return a - b;}
long multiply(){return a * b;}
long divide(){return a / b;}
static long add(){return a + b;}//오류
static long subtract(){return a - b;}//오류
static long multiply(){return a * b;}//오류
static long divide(){return a / b;}//오류
}
이런식으로 생성하면 오류가 발생한다. 왜일까??
*중요*
static으로 생성된 클래스 메소드는 MyMath인스턴스가 생성되지 않아도 사용이 가능하다. 메모리에 클래스가 생성될 때 바로 만들어지기 때문이다. 하지만 인스턴스 메소드, 변수는 그렇지 않다!!
따라서 MyMath의 메소드는 인스턴스 생성전에 사용이 가능하지만 MyMath인스턴스안에 만들어지는 a, b는 아직 생성이 안될 수 있기 때문에 클래스 메소드에서는 오류가 발생하게 된다.
오버로딩
💡오버로딩이란??
한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것을 이야기 한다. -> 정확하게는 메서드 오버로딩
조건
- 메서드 이름이 같아야 한다.
- 매개변수의 개수 또는 타입이 달라야 한다.
- 반환 타입은 아무 영향이 없다
장점
- 메서드의 이름을 절약하는게 가능하다 -> 이름 짓는다고 고민하는 일을 덜어준다!!(이거 힘듭니다)
- 같은 기능을 하지만 자료형만 다른 구조가 있다면 간략하게 하나의 이름만 기억해도 된다.
class MyMath{
int add(int a, int b){return a + b;}
long add(long a, long b){return a + b;}
int add(String a, String b){return Integer.parseInt(a) + Integer.parseInt(b);}
int add(char a, char b){return Character.getNumericValue(a) + Character.getNumericValue(b);}
}
public class Main {
public static void main(String[] args) {
MyMath myMath = new MyMath();
System.out.println(myMath.add(1,1));
System.out.println(myMath.add(1L,1L));
System.out.println(myMath.add("1","1"));
System.out.println(myMath.add('1','1'));
}
}
이런식으로 하나의 이름으로 여러개의 메소드를 정의하는게 가능하다.
가변인자(varargs)와 오버로딩
이 부분을 책을 읽으면서 처음 알게된 부분이다!!
class MyMath{
int add(int... args){
int result = 0;
for(int num : args){
result += num;
}
return result;
}
int subtract(int...args){
int result = 0;
for(int num : args){
result -= num;
}
return result;
}
}
public class Main {
public static void main(String[] args) {
MyMath myMath = new MyMath();
System.out.println(myMath.add(1,1,3,3,3,3,3,33,3,3,3,3,3,3));
System.out.println(myMath.subtract(1,1,3,3,3,3,3,33,3,3,3,3,3,3));
}
}
이런식으로 사용이 가능하다. 물론 int형 말고도 String, char 다 가능하다!!
단!! 가변인자가 선언된 메서드를 호출할 때마다 배열이 새로 생성되기 때문에 꼭 필요한 경우에만 사용하는게 좋다.
C언어와 다르게 자바는 길이가 0인 배열을 생성하는게 가능하다.
배열로 매개변수를 선언하는 거 보다 좋은점은 인자를 지정하지 않아도 괜찮다는 것이다!!
하지만 오버로딩에 사용하면 두 오버로딩된 메서드를 구분하지 않아서 오류가 발생할 수 있으니까 조심하자!
생성자
💡생성자란?
인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드.
조건
- 생성자의 이름은 클래스의 이름과 같아야 한다.
- 생성자는 리턴 값이 없다.
예시)
MyMath myMath = new MyMath()
- new연산자로 힙공간에 인스턴스 생성
- MyMath()생성자 호출
- myMath에 인스턴스 주소값 저장
기본 생성자
정의 : 컴파일러가 자동적으로 생성해준다.
class MyMath{
int a;
int b;
}
class MyMath2{
int a;
int b;
MyMath2(int a, int b){
this.a = a;
this.b = b;
}
}
public class Main {
public static void main(String[] args) {
MyMath myMath = new MyMath();
MyMath2 myMath2 = new MyMath2(1,2);
MyMath2 myMath3 = new MyMath2();//오류 발생!!
}
}
하지만 개발자가 클래스에 생성자를 정희해주는 경우 기본 생성자는 없어진다. 클래스에 정의된 생성자가 하나도 없을 때만 생성된다!!
매개변수가 있는 생성자
class MyMath2{
int a;
int b;
MyMath2(int a, int b){
this.a = a;
this.b = b;
}
}
이런식으로 생성되어 있는 걸 이야기 한다. 사용자가 정의해서 사용이 가능하고, 인스턴스를 생성하면서 원하는 값으로 초기화가 가능하다!
생성자에서 다른 생성자 호출하기 - this(), this
조건
- 생성자의 이름으로 클래스이름 대신 this를 사용한다.
- 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출이 가능하다.
class MyMath2{
int a;
int b;
MyMath2(int a, int b){
this.a = a;
this.b = b;
}
MyMath2(int a){
this(a,3);
}
}
이런식으로 매개변수가 하나인 경우에 매개변수가 두개인 생성자를 호출해서 사용이 가능하다!!
그리고 매개변수로 같은 자료형의 인스턴스를 받아서 똑같이 복사하는 방법도 가능하다!
class MyMath2{
int a;
int b;
MyMath2(int a, int b){
this.a = a;
this.b = b;
}
MyMath2(int a){
this(a,3);
}
MyMath2(MyMath2 myMath2){
this(myMath2.a,myMath2.b);
}
}
이런식으로 가능하다!!
변수의 초기화
멤버변수(클래스 변수와 인스턴스 변수)와 배열의 초기화는 선택적이지만, 지역변수의 초기화는 필수적이다!!!
class MyMath2{
int a;
int b = a;
int getA(){return a;}
int getB(){return b;}
int plus(){
int i;//지역변수
return a + i;
}
}
public class Main {
public static void main(String[] args) {
MyMath2 myMath2 = new MyMath2();
System.out.println(myMath2.getA());
System.out.println(myMath2.getB());
}
}
이런식으로 하면 안된다.
초기화 블럭(initialization block)
클래스 초기화 블럭 : 클래스변수의 복잡한 초기화에 사용된다. -> 클래스가 메모리에 로딩될 때 한번만
인스턴스 초기화 블럭 : 인스턴스변수의 복잡한 초기화에 사용된다. -> 인스턴스가 생성될 때마다, 생성자보다 먼저 실행된다.
class Car{
static int count;
int serialNo;
String color;
String gearType;
static {//클래스 초기화 블럭
count = 0;
}
{//인스턴스 초기화 블럭
count++;
serialNo = count;
}
Car(){
color = "White";
gearType = "Auto";
}
Car(String color, String gearType){
this.color = color;
this.gearType = gearType;
}
}
이렇게 하면 중복된 코드도 제거하고 사용이 가능하다.
멤버변수의 초기화 시기와 순서
클래스 변수 : 클래스가 처음 로딩될 때 단 한번
클래스 변수 초기화순서 : 기본값 -> 명시적초기화 -> 클래스 초기화 블럭
인스턴스 변수 : 인스턴스가 생성될 때마다 각 인스턴스별로 초기화
인스턴시 변수 초기화순서 : 기본값 -> 명시적초기화 -> 인스턴스 초기화 블럭 -> 생성자
처음 알게 된 부분
- 클래스를 초기화 할 때 생성자만 사용했었는데. 초기화 블록이 있는걸 알 수 있었다.
- 가변인자를 이용한 매개변수 선언을 알 수 있었다.
- 매개변수를 선언할 때 참조형과 기본형을 차이를 제대로 알 수 있었다. c를 사용할때 포인터를 사용했었는데 JAVA와 혼동되는 부분이 있었다. 이걸 해결할 수 있었다.
'자바(Java)' 카테고리의 다른 글
선택 안됨 [자바][Java Programming Language]객체지향 프로그래밍 -package와 import (0) | 2025.02.08 |
---|---|
[자바][Java Programming Language]객체지향 프로그래밍 - 오버라이딩 (0) | 2025.01.24 |
[자바][Java Programming Language]객체지향 프로그래밍 - 상속 (0) | 2025.01.23 |
[자바][Java Programming Language] 배열(Array) (0) | 2024.12.31 |