12 min read

<Java> 객체지향프로그래밍 2

<Java> 객체지향프로그래밍 2

우리는 지난 포스팅을 통해서 클래스의 기본적인 사용법을 배웠습니다.이번 포스팅은 오버로딩,생성자 등 메서드의 디테일한 부분에 대해 다뤄보겠습니다.

Overloading(오버로딩)

  • 오버로딩의 사전적 의미를 알고 있습니까? 이는 바로 “과적”입니다.즉 과하게 어떠한 것을 싣는다는것이죠.이러한 의미를 메서드에 적용시켜 비유해보겠습니다.메서드를 많이 싣는다는 곧 한 클래스 내에 동일한 이름의 메서드를 여러개 정의하는것을 비유 가능합니다.사실 이러한 설명만 들을 경우, “동일한 기능을 수행하는 메서드를 여러개 정의하는건 의미없는 일이 아니야?”라는 의문이 생길 수도 있습니다. 하지만 아래의 오버로딩 조건과 예제를 보시면 이해가 갈 것입니다.
System.out.println("hello");
System.out.println(123);
  • 위 코드는 다들 아시다시피 표준 출력을 수행하는 문장입니다.여기서 “hello”라는 매개변수와 123이라는 매개변수는 각각 String과 int로 명확하게 서로다른 자료형임을 알 수 있습니다.
  • 그러면 어떻게 서로다른 타입의 매개변수를 println()이라는 메서드가 다룰 수 있게 된것일까요?
  • 여기서 사용되는 기술이 바로 오버로딩입니다.동일한 기능을 수행하는 메서드들이 매개변수에 대한 조건만 다른 것입니다.

오버로딩의 조건

  1. 메서드의 이름이 동일해야한다.
  2. 매개변수의 개수 또는 타입이 달라야 한다.
  • 이제 예제를 통해 실제 오버로딩을 구현해봅시다.
public class OverloadingTest {
    public static void main(String[] args) {
        MyMath3 math3 = new MyMath3();
        System.out.println("math3.add(3,3) = " + math3.add(3,3));
        System.out.println("math3.add(3L,3) = " + math3.add(3L,3));
        System.out.println("math3.add(3,3L) = " + math3.add(3,3L));
        System.out.println("math3.add(3L,3L) = " + math3.add(3L,3L));

        int[] a = {100, 200, 300};
        System.out.println("math3.add(a) = " + math3.add(a));
    }
}

class MyMath3{
    int add(int a, int b) {
        System.out.print("int add(int a, int b) - ");
        return a + b;
    }
    long add(int a, long b) {
        System.out.print("long add(int a, long b) - ");
        return a + b;
    }
    long add(long a, int b) {
        System.out.print("long add(long a, int b) - ");
        return a + b;
    }
    long add(long a, long b) {
        System.out.print("long add(long a, long b) - ");
        return a + b;
    }

    int add(int[] a) {
        System.out.print("int add(int[] a) - ");
        int res = 0;
        for (int j : a) {
            res += j;
        }
        return res;
    }
}
  • 오버로딩이라는 개념의 경우 추후에 나오는 오버라이딩과 종종 헷갈려 하시는 분들이 있습니다.다시한번 강조하지만 오버로딩은 동일한 기능의 메서드를 여러개 만드는 것이라고 이해해두시면 좋습니다.

가변인자와 오버로딩

  • 지금껏 배운 메서드는 매개변수의 개수가 고정적인 경우에 해당됩니다.하지만 동적으로 매개변수의 개수를 지정해 줄 수 있는데 이러한 기능을 가변인자라합니다.
  • 아래와 같이 ‘타입… 변수명’와 같은 방식으로 선언하며, 항상 마지막에 매개변수 자리에 선언되어야 합니다.
public PrintStream printf(String format,Object... args)
  • 예제를 통해 직접 가변인자 메서드를 활용해 봅시다.
public class VarArgsEx {
    public static void main(String[] args) {
        String[] strArr = {"100", "200", "300"};

        System.out.println(concatenate(" "));
        System.out.println(concatenate("100","200","300"));
        System.out.println(concatenate("-",strArr));
        System.out.println(concatenate(",", new String[]{"1", "2", "3"}));

    }
    static String concatenate(String delim, String... args) {
        String res = "";
        for (String str : args) {
            res += str + delim;
        }
        return res;
    }
    /*
    static String concatenate(String... args) {
        return concatenate("", args);
    }*/
}
  • 하나의 String 타입의 인자만을 필수로 넣어주고,이후의 인자들은 있어도 되고 없어도 되는 것을 확인 할 수 있습니다.
  • 또한 가변인자의 경우, 내부 동작방식이 배열을 이용하는 것을 유추 하실수 있을것입니다.즉, 가변 인자를 호출 할 때마다 새로운 배열이 생성되는 비효율이 초래된다는 것도 참고해두면 좋을 것 같습니다.
  • 마지막의 주석처리된 코드는 가변인자가 존재하는 메서드를 오버로딩한 코드입니다.만약 주석을 해제할 경우 컴파일 에러가 빨갛게 뜨는것을 볼 수 있을 것입니다.그도 당연한 것이 입력된 매개변수들이 어떤 메서드의 매개변수인지 모르니 당연이 에러가 발생해야 합니다.

Constructor(생성자)

  • 생성자는 인스턴스가 생성될 때 호출되는 ‘인스턴스 초기화 메서드’입니다.따라서 인스턴스 변수의 초기화 작업에 주로 사용되며, 인스턴스 생성 시에 실행되어야 할 작업을 위해서도 사용됩니다.
  • 생성자 역시 메서드처럼 클래스 내에 선언되며, 구조도 유사합니다. 하지만 리턴값이 존재하지 않는다는 큰 차이점이 있습니다.
  • 메서드와 동일하게 오버로딩도 가능합니다. 즉, 매개변수의 갯수 또는 타입만 달리하여 여러개의 생성자를 만들 수 있습니다.
  • Order order = new Order()와 같은 방식으로 인스턴스를 생성합니다. 여기서 인스턴스를 직접 생성하는 연산자는 new 연산자입니다.생성자가 인스턴스를 직접 생성하지는 않습니다.다만 Order라는 객체가 가지는 field값들을 초기화하는데 사용되는 일종의 메서드라고 생각하시면 좋습니다.
  • 기억이 나지 않는 분들을 위해 인스턴스가 생성되는 과정을 다시 알려드리겠습니다.
  1. 연산자 new에 의해서 메모리(heap)에 Order클래스의 인스턴스가 생성됩니다.
  2. 생성자 Order()가 호출되어 수행됩니다.
  3. 연산자 new의 결과로, 생성된 Order 인스턴스의 주소값이 order라는 참조변수에 저장됩니다.
  • 이때까지 인스턴스을 생성하기 위해 클래스이름()라는 코드를 쳐왔는데, 이가 바로 생성자였던 것입니다.인스턴스를 생성할 때는 반드시 클래스 내에 정의된 생성자 중 하나를 호출해야합니다.
  • 그러면 이때까지 우리는 생성자를 따로 만들지 않았는데 어떻게 인스턴스를 생성할 수 있었을까요??이에 대한 답을 바로 아래에서 말씀드리겠습니다.

Default Constructor(기본 생성자)

  • 앞서 말한듯이 저희가 따로 생성자를 만들지 않아도 자동으로 제공하는 생성자가 기본 생성자입니다.컴파일러가 자동적으로 추가해주는 기본 생성자는 매개변수도 없고 블럭 내에 내용도 없는 아주 간단한 형태입니다.
  • 예제를 통해 생성자의 형태를 알아봅시다.
class Data1{
	Data1(){}
    int value;
}
class Data2{
    int value;

    Data2(int x) {
        value = x;
    }
}
public class ConstructorTest {
    public static void main(String[] args) {
        Data1 data1 = new Data1();
        Data2 data2 = new Data2(100);
    }
}
  • Data1 인스턴스의 경우 기본 생성자를 통해 만들어졌습니다.생략되어있는 부분을 보여드리기위해 타이핑했습니다.
  • 반면에 Data2는 value라는 변수에 값을 초기화하는 생성자를 하나 생성해주었기 때문에 기본 생성자가 자동으로 생성되지 않는 상태입니다.즉, Argument로 int타입의 데이터를 넣어줘야 인스턴스를 생성할 수 있습니다.만약, 여기서 기본 생성자를 사용하고 싶다면 Data1의 기본생성자처럼 직접 입력해줘야합니다.
  • 요약하자면, 컴파일러가 자동적으로 기본생성자를 추가해주는 경우는 ‘클래스 내에 생성자가 하나도 없을 경우’만 해당됩니다.

매개변수가 있는 생성자

  • 생성자도 메서드처럼 매개변수를 선언하여 호출 시 값을 넘겨받아서 인스턴스의 초기화 작업에 사용할 수 있습니다.인스턴스도 각기 서로 다른 값을 가질수 있기에, 이러한 방식의 생성자로 인스턴스의 값들을 초기화하는 방법이 필요합니다.
  • 아래의 예제는 기본생성자와 매개변수가 있는 생성자 두가지를 모두 사용합니다.
class Car{
    String color;
    String gearType;
    int door;

    Car() {
    }

    public Car(String color, String gearType, int door) {
        color = color;
        gearType = gearType;
        door = door;
    }

    @Override
    public String toString() {
        return "Car{" +
                "color='" + color + '\'' +
                ", gearType='" + gearType + '\'' +
                ", door=" + door +
                '}';
    }
}
public class CarTest {
    public static void main(String[] args) {
        Car car1 = new Car();
        car1.color = "blue";
        car1.gearType = "auto";
        car1.door = 4;

        Car car2 = new Car("white", "auto", 4);

        System.out.println("car1.toString() = " + car1.toString());
        System.out.println("car2.toString() = " + car2.toString());
    }
}
  • car1 인스턴스의 경우 직접 field에 접근하여 값들을 초기화하는 반면,car2의 경우 매개변수가 있는 생성자를 사용해서 인스턴스를 생성하기 때문에 별다른 초기화 과정을 가지지 않아도 됩니다.
  • 이러한 방식은 코드를 보다 간결하고 직관적으로 만들어 줍니다.

생성자에서 다른 생성자 호출하기 - this(), this

  • 동일한 클래스내의 멤버 함수간 서로 호출이 가능한 것처럼 생성자간에도 서로 호출이 가능합니다.단, 다음의 두 조건을 만족해야합니다.
  1. 생성자의 이름으로 클래스이름() 대신 this()를 사용합니다.
  2. 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에만 호출이 가능합니다.
Car(String color){
	door = 5;
	Car(color,"auto",4);
}
  • 위의 코드는 두 조건을 만족시키지 않았기에 오류가 발생합니다.
  • 우선 Car()이라는 이름대신 this()를 사용해야 하며, 생성자 내에서 생성자의 호출이 첫번째 줄에서 이루어지지 않았다는 점도 있습니다.
  • 생성자에서 다른 생성자를 첫 줄에서만 호출이 가능하도록 한 이유는 생성자 내에서 초기화 작업도중에 다른 생성자를 호출하게 될 경우,기존에 초기화 작업을 무효화 시켜버리고 새롭게 초기화 시켜버릴수 있기 때문입니다.
class Car1{
    String color;
    String gearType;
    int door;

    Car1() {
        this("white", "auto", 4);
    }
    Car1(String color) {
        this.color = color;
    }
    Car1(String color, String gearType, int door) {
        this.color = color;
        this.gearType = gearType;
        this.door = door;
		//color = color;
        //gearType = gearType;
        //door = door;
    }
    @Override
    public String toString() {
        return "Car1{" +
                "color='" + color + '\'' +
                ", gearType='" + gearType + '\'' +
                ", door=" + door +
                '}';
    }
}
public class CarTest2 {
    public static void main(String[] args) {
        Car1 c1 = new Car1();
        Car1 c2 = new Car1("blue");

        System.out.println("c1.toString() = " + c1.toString());
        System.out.println("c2.toString() = " + c2.toString());
    }
}
  • c1의 경우, 아무런 매개변수를 주지 않고 Default Constructor를 호출합니다.하지만 기본 생성자의 내부에서 this를 통해 기본값을 초기화해줍니다.그렇기 때문에 아무런 매개변수를 받지 않는 기본생성자임에도 인스턴스의 값이 초기화됩니다.
  • this.color = color 코드에서 this라는 키워드는 Parameter로 들어온 color와 인스턴스의 멤버 변수를 구별하기 위한 키워드입니다.color = color라는 코드는 어떤 변수가 인스턴스의 멤버이고, Parameter인지 구별하기 어렵기에 이러한 점을 보완하고자 this.color는 인스턴스의 멤버변수라고 구별해주는 것 입니다.
  • 참고로 this는 참조변수로 인스턴스 자신의 주소를 가리킵니다.참조변수를 통해 인스턴스의 멤버에 접근하는것처럼, this라는 참조변수를 통해 자신의 인스턴스 변수에 접근가능합니다.
  • 하지만, this를 사용할 수 있는 것은 인스턴스 멤버뿐입니다.즉, 객체를 생성하지 않고 사용 가능한 클래스 변수는 this라는 키워드로 접근 할 수 없다는 뜻입니다.(복습 : 클래스 변수는 static 키워드가 붙습니다)

생성자를 이용한 인스턴스 복사

  • 생성한 인스턴스와 동일한 상태를 갖는 인스턴스를 만들고자 할때 사용하는 생성자는 아래와 같습니다.
Car(Car car) {
        this.color = car.color;
        this.gearType = car.gearType;
        this.door = car.door;
    }
  • 매개변수로 Car 타입의 참조변수를 받고, 생성하려는 인스턴스의 변수를 this로 접근한 뒤 car라는 참조변수로 복사하려는 인스턴스에 접근하여 값을 대입시켜줍니다.
  • 이렇게 생성된 인스턴스는 기존에 복사된 인스턴스와 같은 상태를 가지지만 서로 다른 메모리 공간에 존재하기에 별도의 존재입니다.

Ref: 자바의 정석 - 남궁성