본문 바로가기
java/자바의 정석

자바의 정석 - 객체지향 프로그래밍 3편

by choi-dev 2024. 2. 17.

1, 2편 뒤에 이어지는 내용이다.

 

오버로딩

변수를 선언할 때도 중복된 이름의 변수를 사용할 순 없다. 메소드 또한 마찬가지이다. 하지만 동일한 이름의 메소드를 가지더라도 매개변수가 다르면 중복하게 사용할 수가 있다. 한 클래스 내에서 이처럼 중복된 이름의 메소드를 정의하는 것을 오버로딩이라고 한다.

 

public class Main {
    void println() {

    }
    
    void println(int x) {
    	
    }
}

이런 식으로 중복된 이름의 메소드를 정의할 수 있다.

 

public class Main {
    int add(int a, int b) {
        return a + b;
    }
    
    int add(int x, int y) {
        return x + y;
    }
}

중복된 이름이지만 매개변수를 다르게 하면 오버로딩이 적용될까? 결과는 적용되지 않는다.

 

public class Main {
    int add(int a, int b) {
        return a + b;
    }

    long add(int x, int y) {
        return (long) x + y;
    }
}

리턴되는 값의 자료형이 달라도 적용될까? 역시나 적용되지 않는다.

 

 

public class Main {
    int add(int a, int b) {
        return a + b;
    }

    long add(long x, int y) {
        return (long) x + y;
    }
}

매개변수의 자료형을 다르게 했다. 적용될까? 이번에는 적용된다. int 자료형과 long 자료형은 각기 다른 자료형이기 때문에 오버로딩으로 간주한다.

 

생성자

생성자는 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메소드이다. 인스턴스의 초기화 작업에 사용된다고 생각하면 된다. 메소드처럼 클래스 내에 선언되고 메소드와 비슷하지만 리턴값이 존재하지 않는다. 그렇다고 리턴하지 않는다는 void를 사용하지 않는다.

 

class Point {
    Point() {
        
    }
}

이러한 형태를 띄게 되는 것이 생성자이다. 사람들이 착각하는 부분이 간혹 존재하는데 인스턴스를 생성하는 건 생성자가 생성하는 것이 아닌 new 연산자가 생성해주는 것이다. 생성자는 생성된 인스턴스의 변수를 초기화해줄뿐, 실질적인 인스턴스 생성은 new 연산자가 관여한다.

 

기본 생성자

클래스 파트를 공부하면서 우리는 생성자를 여태 선언하지 않고 있었다. 하지만 모든 클래스는 생성자를 반드시 하나 이상 정의되어 있어야 한다. 그렇다면 생성자를 정의하지 않았는데 왜 에러가 발생하지 않고 있었던 것일까? 그것은 컴파일러가 제공하는 기본 생성자가 있었기 때문이다.

 

class Point {
    Point() {
        
    }
}

위의 코드를 다시 가져왔다. 여기서 생성자 Point() { } 선언부를 지워주겠다.

 

class Point {
    
}

에러가 발생하였을까? 발생하지 않았다. 그 이유는 컴파일러가 기본 생성자를 제공해줘서이다. 위에서 봤던 Point() { }의 형태가 바로 기본 생성자의 형태이고 이걸 컴파일러가 그동안 제공해주고 있었던 것이다. 다른 예시로 기본 생성자를 확인해보겠다.

 

public class Main {
    public static void main(String[] args) {
        D1 d1 = new D1();
        D2 d2 = new D2();
    }
}

class D1 {
    int value;
}

class D2 {
    int value;
    D2(int x) {
        value = x;
    }
}

위의 코드를 보면 정상적으로 보이겠지만 D2 클래스의 인스턴스 생성에 빨간줄이 그어져있을 것이다. 왜 그런지 이유를 확인해보자. D1 클래스에 컴파일러가 임의로 추가해준 기본 생성자를 추가해주겠다.

 

public class Main {
    public static void main(String[] args) {
        D1 d1 = new D1();
        D2 d2 = new D2();
    }
}

class D1 {
    int value;
    D1() {
        
    }
}

class D2 {
    int value;
    D2(int x) {
        value = x;
    }
}

D1 클래스의 형태가 위와 같은 형태로 생성자가 존재해야하고 없을 시에는 컴파일러가 추가해주었다. D2 클래스를 보자. 생성자 부분에 매개변수를 입력받는 형태가 존재한다. 이럴 경우에는 컴파일러가 기본 생성자를 추가해주지 않는다. 그렇기 때문에 인스턴스를 생성하는데 있어서 D2 클래스는 아무런 값을 입력받지 않으면 인스턴스를 생성할 수 없는 것이다.

 

public class Main {
    public static void main(String[] args) {
        D1 d1 = new D1();
        D2 d2 = new D2(3);
    }
}

class D1 {
    int value;

    D1() {

    }
}

class D2 {
    int value;
    D2(int x) {
        value = x;
    }
}

이런 식으로 수정하거나 D2 클래스에 기본 생성자를 따로 선언해주어야 한다.

 

public class Main {
    public static void main(String[] args) {
        
    }
}

class D1 {
    int x;
    int y;
    int z;
    
    D1() {
        
    }
    
    D1 (int a, int b, int c) {
        x = a;
        y = b;
        z = c;
    }
}

다시 생성자의 이야기를 하자면 메소드에 매개변수를 선언해 호출 시 값을 넘겨받을 수 있는 것처럼 생성자 또한 값을 넘겨 작업에 사용할 수 있다.

 

public class Main {
    public static void main(String[] args) {
        D1 d1 = new D1();
        d1.x = 3;
        d1.y = 4;
        d1.z = 5;

        System.out.println("x의 값은 " + d1.x);
        System.out.println("y의 값은 " + d1.y);
        System.out.println("z의 값은 " + d1.z);
    }
}

class D1 {
    int x;
    int y;
    int z;

    D1() {

    }

    D1 (int a, int b, int c) {
        x = a;
        y = b;
        z = c;
    }
}

일반적으로 우리가 했던 방식은 D1 인스턴스를 생성하고 해당 인스턴스 변수의 값을 초기화했었다. 이번엔 다른 형태로 해보겠다.

 

public class Main {
    public static void main(String[] args) {
        D1 d1 = new D1(3, 4, 5);

        System.out.println("x의 값은 " + d1.x);
        System.out.println("y의 값은 " + d1.y);
        System.out.println("z의 값은 " + d1.z);

    }
}

class D1 {
    int x;
    int y;
    int z;

    D1() {

    }

    D1 (int a, int b, int c) {
        x = a;
        y = b;
        z = c;
    }
}

인스턴스를 생성하는 동시에 원하는 값으로 초기화를 할 수도 있다. 첫번째 방식과 두번째 방식 모두 같은 값을 보여주지만 아래의 방식이 좀 더 코드를 간결하고 직관적으로 만들어준다.

 

this()

같은 클래스 내에 메소드를 호출할 수 있듯이 생성자 또한 서로 호출이 가능하다. 코드로 바로 확인해보자.

 

public class Main {
    public static void main(String[] args) {

    }
}

class D1 {
    int x;
    int y;
    int z;

    D1 (int x) {
        x = 3;
        D1(x, 4, 5);
    }

    D1 (int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

매개변수 x를 받는 D1 클래스의 생성자 부분에서 D1 (int x, int y, int z); 생성자를 호출했다. 여기서 에러는 두 가지가 존재한다. 첫번째 에러는 생성자 호출이 두번째 줄에서 일어나는 부분이고 두번째 에러는 클래스명으로 호출해서 생겼다. 이걸 수정해보겠다.

 

public class Main {
    public static void main(String[] args) {

    }
}

class D1 {
    int x;
    int y;
    int z;
    
    D1(int x) {
        this(x, 4, 5);
    }

    D1 (int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

this(x, 4, 5)의 호출은 D1 (int x, int y, int z) 생성자를 호출한다는 의미이다.

 

D1() {
    x = 3;
    y = 4;
    z = 5;
}

정리를 좀 하자면 기존의 멤버변수를 초기화할 때, 이렇게 값을 초기화한다.

 

D1() {
    this(3, 4, 5);
}

하지만 생성자 호출을 통해서 초기화하면 위의 코드보다 더 간결하게 변하지만 요구하는 것은 그대로 얻을 수 있다.

 

this

위에서 했던 this() 와는 조금 다른 이야기다. this()는 생성자에서 다른 생성자를 호출할 때 사용하는 것이고 지금 할 this는 객체 자신을 가리키는 참조변수를 말한다. this에는 인스턴스의 주소가 저장되어 있어 해당 인스턴스 변수를 찾아준다. 예시를 통해 알아보자.

 

D1 (int a, int b, int c) {
    x = a;
    y = b;
    z = c;
}

이 코드에는 현재 문제가 없다. 여기서 코드를 조금만 수정해보겠다.

 

D1 (int x, int y, int z) {
    x = x;
    y = y;
    z = z;
}

좌측의 x는 지역변수 x이고 우측의 x는 매개변수 x이다. 우리는 이렇게 의미했지만 실질적으로 둘 다 지역변수 x로 간주해버린다. 그렇기 때문에 인스턴스 변수임을 가리키는 this를 사용하여 구별해주어야 한다.

 

D1 (int x, int y, int z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

바로 이렇게 수정해주면 된다. 스태틱 메소드에서 인스턴스 멤버를 사용할 수 없던 것처럼 마찬가지로 this도 사용할 수 없다. 스태틱메소드는 인스턴스를 생성하지 않아도 호출할 수 있어서 호출되는 시점에 인스턴스가 존재하지 않을 수도 있기 때문이다.

 

변수의 초기화

게시물을 작성하면서 내가 계속 초기화라는 단어를 사용했었는데, 의미를 알고있어 따로 고려하진 않았다. 초기화라는 것은 변수를 선언하고 처음 값을 저장하는 걸 의미한다. 가능하면 선언과 동시에 초기화하는 것이 바람직하다. 여기서 주의해야 할 것은 멤버변수는 초기화하지 않고 사용해도 괜찮으나 지역변수는 반드시 초기화해주어야 한다.

 

멤버변수의 초기화

지역변수와 달리 멤버변수는 선언되었을 때, 각 타입의 기본값으로 자동 초기화가 된다. 그 다음 명시적 초기화, 초기화 블럭, 생성자의 순서로 초기화된다.

 

명시적 초기화는 변수의 선언과 동시에 값을 초기화하는 것을 명시적 초기화라고 한다. 여기서 조금 더 복잡한 초기화 작업이 필요하면 초기화 블럭 또는 생성자를 사용해야 한다. 인스턴스 초기화 블럭은 단순히 클래스 내에 블럭 { }을 만들고 안에 코드를 작성하면 되고 클래스 초기화 블럭은 인스턴스 초기화 블럭 앞에 static을 붙이면 된다.

 

내일 할 부분

객체지향 프로그래밍의 연장선으로 상속, 오브젝트, 오버라이딩 등을 학습할 예정이다.