Skip to content

7. 메서드와 생성자(Method and Constructor)


1. 키워드

  • 메서드(Method)
  • 생성자(Constructor)
  • thisthis()
  • 메서드 시그니처(Method Signature)와 메서드 오버로딩(Method Overloading)
  • 재귀 호출(Recursive Call)


2. 메서드(Method)

  • 자바에서 클래스는 멤버로 속성을 표현하는 필드와 기능을 표현하는 메서드를 가진다.
  • 그중에서 메서드란 어떠한 특정 작업을 수행하기 위한 명령문의 집합이라고 할 수 있다.


1) 메서드의 사용 목적

  • 클래스에서 메서드를 작성하여 사용하는 이유는 중복되는 코드의 반복적인 프로그래밍을 피할 수 있기 때문이다.
  • 또한, 모듈화로 인해 코드의 가독성도 좋아진다.
  • 그리고 프로그램에 문제가 발생하거나 기능의 변경이 필요할 때도 손쉽게 유지보수를 할 수 있게 된다.


Note

  • 메서드를 작성할 때는 되도록 하나의 메서드가 하나의 기능만을 수행하도록 작성하는 것이 좋다.


2) 메서드 정의

  • 클래스에서 메서드를 정의하는 방법은 일반 함수를 정의하는 방법과 크게 다르지 않다.


  • 자바에서 메서드를 정의하는 방법은 다음과 같다.


접근제어자 반환타입 메서드이름(매개변수목록) { // 선언부
 구현부
}


1] 접근 제어자: 해당 메서드에 접근할 수 있는 범위를 명시한다.

2] 반환 타입: 메서드가 모든 작업을 마치고 반환하는 데이터의 타입을 명시한다.

3] 메서드 이름: 메서드를 호출하기 위한 이름을 명시한다.

4] 매개변수 목록: 메서드 호출 시에 전달되는 인수의 값을 저장할 변수들을 명시한다.

5] 구현부: 메서드의 고유 기능을 수행하는 명령문의 집합이다.


Note

  • 메서드 시그니처(Method Signature)란 메서드의 선언부에 명시되는 매개변수 목록을 가리킨다.
  • 만약 두 메서드가 매개변수의 개수와 타입, 그 순서까지 모두 같다면, 이 두 메서드의 시그니처는 같다고 할 수 있다.


  • 다음 예제는 Car 클래스의 accelerate() 메서드를 정의하는 예제이다.


class Car {

  private int currentSpeed;
  private int accelerationTime;

  public void accelerate(int speed, int second) {
    System.out.println(
      second + "초간 속도를 시속 " + speed + "(으)로 가속함!!"
    );
  }
}


  • 위 예제에서 ①번 라인에서는 accelerate() 메서드를 정의하고 있다.
  • 이 메서드는 public 접근 제어자를 사용하여 선언되어 해당 객체를 사용하는 프로그램 어디에서나 직접 접근할 수 있다.
  • 반환 타입에는 어떠한 값도 반환하지 않는다는 의미를 가진 void를 명시한다.
  • 그 다음으로 메서드의 이름을 명시하고, 매개변수로 int형 변수인 speedsecond를 전달받는다.
  • 이렇게 전달받은 매개변수를 가지고 메서드 구현부에서 고유한 작업을 수행할 수 있는 것이다.


3) 메서드 호출

  • 자바에서 위와 같은 방법으로 정의한 메서드는 멤버 참조 연산자(.)를 사용하여 호출할 수 있다.


  • 자바에서 메서드를 호출하는 방법은 다음과 같다.


1. 객체참조변수이름.메서드이름();
2. 객체참조변수이름.메서드이름(인수1, 인수2, ...);


  • 다음 예제는 앞서 정의한 accelerate() 메서드를 호출하는 예제이다.


Car myCar = new Car();
myCar.accelerate(60, 3);


  • 다음 예제는 실제로 accelerate() 메서드를 정의하고 호출하는 예제이다.


class Car {

  private int currentSpeed;
  private int accelerationTime;

  public void accelerate(int speed, int second) {
    System.out.println(
      second + "초간 속도를 시속 " + speed + "(으)로 가속함!!"
    );
  }
}

class Test {

  public static void main(String[] args) {
    Car myCar = new Car();

    myCar.accelerate(60, 3);
  }
}

// 3초간 속도를 시속 60(으)로 가속함!!


3. 생성자(Constructor)

1) 인스턴스 변수의 초기화

  • 클래스를 가지고 객체를 생성하면, 해당 객체는 메모리에 즉시 생성된다.
  • 하지만 이렇게 생성된 객체는 모든 인스턴스 변수가 아직 초기화되지 않은 상태이다.


  • 자바에서 클래스 변수와 인스턴스 변수는 별도로 초기화하지 않으면, 다음 값으로 자동 초기화된다.


변수의 타입 초깃값
char '\u0000'
byte, short, int 0
long 0L
float 0.0F
double 0.0 또는 0.0D
boolean false
배열, 인스턴스 null


  • 하지만 사용자가 원하는 값으로 인스턴스 변수를 초기화하려면, 일반적인 초기화 방식으로는 초기화할 수 없다.
  • 인스턴스 변수 중에는 private 변수도 있으며, 이러한 private 변수에는 사용자나 프로그램이 직접 접근할 수 없기 때문이다.
  • 따라서 private 인스턴스 변수에도 접근할 수 있는 초기화만을 위한 public 메서드가 필요해진다.
  • 이러한 초기화만을 위한 메서드는 객체가 생성된 후부터 사용되기 전까지 반드시 인스턴스 변수의 초기화를 위해 호출되어야 한다.


2) 생성자

  • 자바에서는 객체의 생성과 동시에 인스턴스 변수를 원하는 값으로 초기화할 수 있는 생성자라는 메서드를 제공한다.
  • 자바에서 생성자의 이름은 해당 클래스의 이름과 같아야 한다.
  • 즉, Car 클래스의 생성자의 이름은 Car가 된다.
  • 이러한 생성자는 다음과 같은 특징을 가진다.


1] 생성자는 반환값이 없지만, 반환 타입을 void형으로 선언하지 않는다.

2] 생성자는 초기화를 위한 데이터를 인수로 전달받을 수 있다.

3] 객체를 초기화하는 방법이 여러 개 존재할 경우에는 하나의 클래스가 여러 개의 생성자를 가질 수 있다. 즉, 생성자도 하나의 메서드이므로 메서드 오버로딩이 가능하다는 의미이다.


  • 다음 예제는 Car 클래스를 선언하면서 여러 개의 생성자를 선언하는 예제이다.


Car(String modelName) {}
Car(String modelName, int modelYear) {}
Car(String modelName, int modelYear, String color) {}
Car(String modelName, int modelYear, String color, int maxSpeed) {}


3) 생성자의 선언

  • 자바에서 클래스 생성자를 선언하는 문법은 다음과 같다.


1. 클래스이름() { ... }
2. 클래스이름(인수1, 인수2, ...) { ... }


  • 위와 같이 생성자 중에는 매개변수를 전달받아 인스턴스 변수를 초기화하는 생성자도 선언할 수 있다.


  • 다음 예제는 앞서 살펴본 Car 클래스의 생성자를 선언하는 예제이다.


Car(String modelName, int modelYear, String color, int maxSpeed) {
 this.modelName = modelName;
 this.modelYear = modelYear;
 this.color = color;
 this.maxSpeed = maxSpeed;
 this.currentSpeed = 0;
}


Note

  • 위의 예제처럼 클래스의 생성자는 어떠한 반환값도 명시하지 않아야 한다.


4) 생성자의 호출

  • 자바에서는 new 키워드를 사용하여 객체를 생성할 때 자동으로 생성자가 호출된다.


class Car {

  private String modelName;
  private int modelYear;
  private String color;
  private int maxSpeed;
  private int currentSpeed;

  Car(String modelName, int modelYear, String color, int maxSpeed) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.color = color;
    this.maxSpeed = maxSpeed;
    this.currentSpeed = 0;
  }

  public String getModel() {
    return this.modelYear + "년식 " + this.modelName + " " + this.color;
  }
}

class Test {

  public static void main(String[] args) {
    Car myCar = new Car("아반떼", 2016, "흰색", 200);

    System.out.println(myCar.getModel());
  }
}

// 2016년식 아반떼 흰색


5) 기본 생성자(Default Constructor)

  • 자바의 모든 클래스에는 하나 이상의 생성자가 정의되어 있어야 한다.
  • 하지만 특별히 생성자를 정의하지 않고도 인스턴스를 생성할 수 있다.
  • 이것은 자바 컴파일러가 기본 생성자라는 것을 기본적으로 제공해 주기 때문이다.
  • 기본 생성자는 매개변수를 하나도 가지지 않으며, 아무런 명령어도 포함하고 있지 않다.


  • 자바 컴파일러는 컴파일 시 클래스에 생성자가 하나도 정의되어 있지 않으며, 자동으로 다음과 같은 기본 생성자를 추가한다.


클래스이름() {}


  • 다음 예제는 자바 컴파일러가 Car 클래스에 자동으로 추가해 주는 기본 생성자의 예제이다.


Car() {}


  • 위와 같이 기본 생성자는 어떠한 매개변수도 전달받지 않으며, 기본적으로 아무런 동작도 하지 않는다.


  • 다음 예제는 Car 클래스에 생성자를 정의하지 않고, 기본 생성자를 호출하는 예제이다.


class Car {

  private String modelName = "소나타";
  private int modelYear = 2016;
  private String color = "파란색";

  public String getModel() {
    return this.modelYear + "년식 " + this.color + " " + this.modelName;
  }
}

class Test {

  public static void main(String[] args) {
    Car myCar = new Car();

    System.out.println(myCar.getModel());
  }
}

// 2016년식 파란색 소나타


  • 위의 예제에서 Car 클래스의 인스턴스 myCar는 기본 생성자를 사용하여 생성된다.
  • 하지만 기본 생성자는 아무런 동작도 하지 않으므로, 인스턴스 변수를 클래스 필드에서 바로 초기화하고 있다.
  • 이처럼 인스턴스 변수의 초기화는 생성자를 사용하여 수행할 수도 있지만, 클래스 필드에서 바로 수행할 수도 있다.
  • 하지만 만약 매개변수를 가지는 생성자를 하나라도 정의했다면, 기본 생성자는 자동으로 추가되지 않는다.
  • 따라서 매개변수를 가지는 생성자를 하나 이상 정의한 후 기본 생성자를 호출하면, 오류가 발생할 것이다.


class Car {

  private String modelName;
  private int modelYear;
  private String color;
  private int maxSpeed;
  private int currentSpeed;

  Car(String modelName, int modelYear, String color, int maxSpeed) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.color = color;
    this.maxSpeed = maxSpeed;
    this.currentSpeed = 0;
  }

  public String getModel() {
    return this.modelYear + "년식 " + this.modelName + " " + this.color;
  }
}

class Test {

  public static void main(String[] args) {
    Car myCar = new Car(); // 오류 발생
    Car myCar = new Car("아반떼", 2016, "흰색", 200);

    System.out.println(myCar.getModel());
  }
}

// 2016년식 아반떼 흰색


  • 위의 예제는 ①번 라인에서 4개의 매개변수를 갖는 생성자를 정의하고 있다.
  • 따라서 자바 컴파일러는 Car 클래스에 별도의 기본 생성자를 추가하지 않을 것이다.
  • 이때 ②번 라인에서는 기본 생성자 Car()를 호출하여 인스턴스를 생성하려고 하면, 자바 컴파일러는 오류를 발생시킬 것이다.
  • ③번 라인과 같이 4개의 매개변수를 전달해야만 인스턴스가 생성될 것이다.


4. thisthis()

1) this 참조 변수

  • this 참조 변수는 인스턴스가 바로 자기 자신을 참조하는 데 사용하는 변수이다.
  • 이러한 this 참조 변수는 해당 인스턴스의 주소를 가리키고 있다.


  • 다음 예제는 Car 클래스의 생성자를 나타낸 예제이다.


class Car {
  private String modelName;
  private int modelYear;
  private String color;
  private int maxSpeed;
  private int currentSpeed;

  Car(String modelName, int modelYear, String color, int maxSpeed) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.color = color;
    this.maxSpeed = maxSpeed;
    this.currentSpeed = 0;
  }

 ...

}


  • 위의 예제처럼 생성자의 매개변수 이름과 인스턴스 변수의 이름이 같을 경우에는 인스턴스 변수 앞에 this 키워드를 붙여 구분해야만 한다.
  • 이렇게 자바에서는 this 참조 변수를 사용하여 인스턴스 변수에 접근할 수 있다.
  • 이러한 this 참조 변수를 사용할 수 있는 영역은 인스턴스 메서드뿐이며, 클래스 메서드에서는 사용할 수 없다.
  • 모든 인스턴스 메서드에는 this 참조 변수가 숨겨진 지역 변수로 존재하고 있다.


2) this() 메서드

  • this() 메서드는 생성자 내부에서만 사용할 수 있으며, 같은 클래스의 다른 생성자를 호출할 때 사용한다.
  • this() 메서드에 인수를 전달하면, 생성자 중에서 메서드 시그니처가 일치하는 다른 생성자를 찾아 호출해 준다.


  • 다음 예제는 this 참조 변수와 this() 메서드를 사용한 예제이다.


class Car {

  private String modelName;
  private int modelYear;
  private String color;
  private int maxSpeed;
  private int currentSpeed;

  Car(String modelName, int modelYear, String color, int maxSpeed) {
    this.modelName = modelName;
    this.modelYear = modelYear;
    this.color = color;
    this.maxSpeed = maxSpeed;
    this.currentSpeed = 0;
  }  // 주석 처리하면 this() 메서드에 해당하는 생성자가 존재하지 않게 되어 오류 발생

  Car() {
  // 첫 번째 라인에서만 다른 생성자를 호출할 수 있다.
    this("소나타", 2012, "검정색", 160);
  }

  public String getModel() {
    return this.modelYear + "년식 " + this.modelName + " " + this.color;
  }
}

class Test {

  public static void main(String[] args) {
    Car myCar = new Car();

    System.out.println(myCar.getModel());
  }
}

// 2012년식 소나타 검정색


  • 위의 예제에서 매개변수를 가지는 첫 번째 생성자는 this 참조 변수를 사용하여 인스턴스 변수에 접근하고 있다.
  • 또한, 매개변수를 가지지 않는 두 번째 생성자는 내부에서 this() 메서드를 이용하여 첫 번째 생성자를 호출한다.
  • 이렇게 내부적으로 다른 생성자를 호출하여 인스턴스 변수를 초기화할 수 있다.


Note

  • 단, 한 생성자 B에서 다른 생성자 A를 호출할 때에는 반드시 B 생성자의 첫 번째 라인에서만 호출할 수 있다.
  • 만약 B 생성자의 첫 번째 라인에 다른 코드가 존재한다면 오류가 발생할 것이다.


5. 메서드 오버로딩(Method Overloading)

1) 메서드 시그니처(Method Signature)

  • 메서드 오버로딩의 핵심은 바로 메서드 시그니처에 있다.
  • 메서드 시그니처란 메서드의 선언부에 명시되는 매개변수의 목록을 가리킨다.
  • 만약 두 메서드가 매개변수의 개수와 타입, 그 순서까지 모두 같다면, 이 두 메서드의 시그니처는 같다고 할 수 있다.


2) 메서드 오버로딩

  • 메서드 오버로딩이란 같은 이름의 메서드를 중복하여 정의하는 것을 의미한다.
  • 자바에서는 원래 한 클래스 내에 같은 이름의 메서드를 둘 이상 가질 수 없다.
  • 하지만 매개변수의 개수나 타입을 다르게 하면, 하나의 이름으로 메서드를 작성할 수 있다.
  • 즉, 메서드 오버로딩은 서로 다른 시그니처를 갖는 여러 메서드를 같은 이름을 정의하는 것이라고 할 수 있다.
  • 이러한 메서드 오버로딩을 사용함으로써 메서드에 사용되는 이름을 절약할 수 있다.
  • 또한, 메서드를 호출할 때 전달해야 할 매개변수의 타입이나 개수에 대해 크게 신경을 쓰지 않고 호출할 수 있게 된다.
  • 메서드 오버로딩은 OOP의 특징 중 하나인 다형성(Polymorphism)을 구현하는 방법 중 하나이다.
  • 메서드 오버로딩의 대표적인 예로는 println() 메서드를 들 수 있다.


  • println() 메서드는 전달받는 매개변수의 타입에 따라 다음과 같이 다양한 원형 중에서 적절한 원형을 호출하게 된다.


1. println()
2. println(boolean x)
3. println(char x)
4. println(char[] x)
5. println(double x)
6. println(float x)
7. println(int x)
8. println(long x)
9. println(Object x)
10. println(String x)


3) 메서드 오버로딩의 조건

  • 자바에서 메서드 오버로딩이 성립하기 위해서는 다음과 같은 조건을 만족해야 한다.


1] 메서드의 이름이 같아야 한다.

2] 메서드의 시그니처, 즉 매개변수의 개수 또는 타입이 달라야 한다.


  • 메서드 오버로딩은 반환 타입과는 관계가 없다.
  • 만약 메서드의 시그니처는 같은데 반환 타입만이 다른 경우에는 오버로딩이 성립하지 않는다.


4) 메서드 오버로딩의 예제

  • 자바 컴파일러는 사용자가 오버로딩된 함수를 호출하면, 전달된 매개변수의 개수와 타입과 같은 시그니처를 가지는 메서드를 찾아 호출한다.


  • 다음 예제는 함수의 오버로딩을 이용하여 정의한 display() 메서드의 원형 예제이다.


1. void display(int num1)
2. void display(int num1, int num2)
3. void display(int num1, double num2)


  • 이제 사용자가 display() 메서드를 호출하면, 컴파일러는 자동으로 같은 시그니처를 가지는 메서드를 찾아 호출한다.


1. display(10); // 1번 display() 메서드 호출 -> 10
2. display(10, 20 // 2번 display() 메서드 호출 -> 200
3. display(10, 3.14 // 3번 display() 메서드 호출 -> 13.14
4. display(10, 'a'); // 2번과 3번 모두 호출 가능


  • 문제는 4번과 같이 첫 번째 인수로는 정수를, 두 번째 인수로는 실수를 전달받는 호출이다.
  • 자바에서는 char형 데이터는 int형뿐만 아니라 double형으로도 타입 변환될 수 있기 때문이다.
  • 따라서 이와 같은 호출은 자바 컴파일러가 어느 시그니처의 display() 메서드를 호출해야 할지 불명확하다.
  • 자바에서는 오버로딩한 메서드의 이러한 모호한 호출을 허용하지 않으며, 위와 같은 경우에는 더 작은 표현 범위를 가지는 int형으로 자동 타입 변환하게 된다.


  • 다음 예제는 위에서 살펴본 display() 메서드를 다양한 시그니처로 오버로딩하는 예제이다.


class OverloadingTest {

  public void display(int num1) {
    System.out.println(num1);
  }

  public void display(int num1, int num2) {
    System.out.println(num1 * num2);
  }

  public void display(int num1, double num2) {
    System.out.println(num1 + num2);
  }
}

class Test {

  public static void main(String[] args) {
    OverloadingTest myfunc = new OverloadingTest();

    myfunc.display(10);
    myfunc.display(10, 20);
    myfunc.display(10, 3.14);
    myfunc.display(10, 'a');
  }
}

// 10
// 200
// 13.14
// 970


  • 위의 예제처럼 메서드의 오버로딩은 매개변수의 타입뿐만 아니라 매개변수의 개수를 달리해도 작성할 수 있다.
  • 위 예제의 ②번 라인의 display() 메서드 호출은 영문 소문자 'a'의 아스키 코드값이 97이므로, int형으로 자동 타입 변환되어 ①번 라인의 display() 메서드가 호출된 것이다.


6. 재귀 호출(Recursive Call)

  • 재귀 호출이란 메서드 내부에서 해당 메서드가 또다시 호출되는 것을 의미한다.
  • 이러한 재귀 호출은 자기가 자신을 계속해서 호출하므로, 끝없이 반복될 것이다.
  • 따라서 메서드 내에 재귀 호출을 중단하도록 조건이 변경될 명령문을 반드시 포함해야 한다.


1) 재귀 호출의 개념

  • 재귀 호출의 개념을 파악하기 위해서 우선 재귀 호출을 사용하지 않고 1부터 n까지의 합을 구하는 메서드를 만들어보자.


class Test {

  static int sum(int n) {
    int result = 0;

    for (int i = 1; i <= n; i++) {
      result += i;
    }

    return result;
  }

  public static void main(String[] args) {
    System.out.println(sum(4));
  }
}

// 10


  • 위의 예제에서 sum() 메서드는 재귀 호출을 사용하지 않고 만든 메서드이다.
  • 이러한 메서드는 그냥 봐서는 그 목적을 바로 알 수 없으며, 코드를 해석해야 무슨 목적으로 만든 메서드인지 알 수 있다.
  • 즉, 변수 iresult는 왜 정의되었으며, for 문은 왜 사용되었는지 바로 알 수가 없다.
  • 이제 재귀 호출을 사용하여 1부터 n까지의 합을 구하는 recursiveSum() 메서드를 만들어보자.
  • 우선 1부터 4까지의 합을 구하는 알고리즘을 생각해 보자.


1] 1부터 4까지의 합은 1부터 3까지의 합에 4를 더하면 된다.

2] 1부터 3까지의 합은 1부터 2까지의 합에 3을 더하면 된다.

3] 1부터 2까지의 합은 1부터 1까지의 합에 2를 더하면 된다.

4] 1부터 1까지의 합은 그냥 1이다.


  • 위의 알고리즘을 의사 코드(Pseudo Code)로 작성하면 다음과 같다.


시작
 1. n이 1 아니면, n과 1부터 (n~1)까지의 합을 더한 값을 반환함
 2. n이 1이면, 그냥 1 반환함


  • 위와 같이 논리적인 재귀 알고리즘을 구상하고 의사 코드를 작성하면, 재귀 호출을 이용해 바로 코드로 옮길 수 있다.


class Test {

  static int recursiveSum(int n) {
    if (n == 1) {
      return 1;
    }
    return n + recursiveSum(n - 1);
  }

  public static void main(String[] args) {
    System.out.println(recursiveSum(4));
  }
}

// 10


  • 위의 예제에서 if 문이 존재하지 않으면, 이 프로그램은 실행 직후 스택 오버플로우(Stack Overflow)에 의해 종료될 것이다.
  • 따라서 if 문처럼 재귀 호출을 중단하기 위한 조건문을 반드시 포함해야 한다.
  • 이처럼 재귀 호출은 다양한 알고리즘을 표현한 의사 코드를 그대로 코드로 옮길 수 있게 해주므로, 직관적인 프로그래밍을 하는 데 도움이 된다.


Note

  • 스택 오버플로우는 메모리 구조 중 스택 영역에서 해당 프로그램이 사용할 수 있는 메모리 공간 이상을 사용하려고 할 때 발생한다.

References