요약

새 공책을 사면 첫 장에 이름부터 적는다. 객체를 새로 만들 때 이렇게 처음 값을 채워 넣는 코드가 따로 있다. 이 코드 덕분에 이름이 비어 있는 학생 같은 덜 만들어진 객체가 돌아다니지 않는다. 이 코드를 하나도 쓰지 않으면 아무 일도 하지 않는 빈 것이 저절로 하나 붙는다. 단, 하나라도 직접 쓰면 그 빈 것은 더는 붙지 않아서, 값을 넘기지 않고 만드는 코드가 갑자기 막힌다.

예시로 보기

필드만 있는 클래스로 학생을 만들면 이렇게 된다.

Student s = new Student();
s.name = "Bob";
s.grades = new int[] {90, 85, 88};

객체를 만드는 줄과 값을 채우는 줄이 떨어져 있다. 둘째 줄을 잊으면 이름이 null인 학생이 프로그램 안을 돌아다닌다. 만들 때 값을 함께 받도록 하면 잊을 수가 없다.

public class Student {
    public String name;
    public int[] grades;

    public Student(String name, int[] grades) {   // 생성자
        this.name = name;
        this.grades = grades;
    }
}
int[] studentGrades = {90, 85, 88};
Student student = new Student("Bob", studentGrades);
System.out.println(student.name);                      // Bob
System.out.println(Arrays.toString(student.grades));   // [90, 85, 88]

new Student("Bob", studentGrades)에서 new 뒤의 Student(...)가 생성자를 부르는 자리다. 괄호 안의 값이 생성자의 매개변수로 들어간다.

Arrays.toString을 쓰려면 파일 맨 위에 import java.util.Arrays;를 둔다. 빠뜨리면 cannot find symbol ... variable Arrays 오류가 난다1.

검증: 생성자 세 개의 출력, this를 빼먹은 예(null과 Alice), this(...) 예(SCV 45, SCV-7 45), 카드 C3의 답(10, 13)을 JDK 21에서 실행해 비교. 기본 생성자는 빈 클래스를 컴파일한 뒤 javap로 확인. 오류 문구는 따로 컴파일해 확인 — 15_java-constructor_impl.java

정의

객체 만들기. 클래스에서 객체를 만들 때는 new 연산자를 쓴다. new는 객체를 만들고 그 객체의 주소를 돌려준다. 그 주소를 변수에 담아 두고 객체를 쓴다(참조 타입과 메모리 영역).

생성자는 new로 객체를 만들 때 객체를 처음 상태로 채우는 멤버다. 메서드처럼 생겼지만 두 가지가 다르다.

  • 돌려주는 타입을 적지 않는다(void도 적지 않는다).
  • 이름이 클래스 이름과 같다.

매개변수의 타입은 new에서 넘길 값의 타입에 맞춰 적는다.

기본 생성자

클래스에 생성자를 하나도 쓰지 않으면, 번역기가 아무 일도 하지 않는 생성자를 하나 넣어 준다. 이것을 기본 생성자라고 한다.

public class Student {
}

이 파일을 번역해 만든 Student.class를 javap Student로 열어 보면 쓴 적 없는 생성자가 들어 있다1.

public class Student {
  public Student();
}

그래서 생성자가 없는 클래스도 new Student()로 만들 수 있다.

생성자를 하나라도 직접 쓰면 기본 생성자는 들어가지 않는다. Student(String name)만 있는 클래스에서 new Student()를 쓰면 이런 오류가 난다1.

error: constructor Student in class Student cannot be applied to given types;
  required: String
  found:    no arguments

생성자 오버로딩

매개변수가 다른 생성자를 여러 개 둘 수 있다. 객체를 여러 방법으로 만들 수 있게 하려는 것이다.

public class Student {
    public String name;
    public int[] grades;

    public Student() {}                             // 생성자 1

    public Student(String name) {                   // 생성자 2
        this.name = name;
    }

    public Student(String name, int[] grades) {     // 생성자 3
        this.name = name;
        this.grades = grades;
    }
}
new 식 불리는 생성자 name grades
new Student() 1 null null
new Student("Alice") 2 Alice null
new Student("Bob", grades) 3 Bob [90, 85, 88]

new에 넘긴 값의 타입과 개수를 보고 어느 생성자를 돌릴지가 정해진다. 매개변수의 타입·개수·순서는 그대로 두고 이름만 바꾼 것은 오버로딩이 아니다. Student(String name) 옆에 Student(String nickname)을 두면 constructor Student(String) is already defined 오류가 난다1.

this

생성자의 매개변수 이름을 필드 이름과 같게 쓰면, 생성자 안에서 name은 매개변수를 가리킨다. 가까이서 선언한 쪽이 이기기 때문이다. 필드를 가리키려면 this.name이라고 쓴다. this는 “지금 만들어지고 있는 바로 이 객체”다1.

class Wrong {
    String name;
    Wrong(String name) {
        name = name;        // 매개변수에 매개변수를 넣는다. 필드는 그대로 null
    }
}

class Right {
    String name;
    Right(String name) {
        this.name = name;   // 왼쪽은 이 객체의 필드, 오른쪽은 매개변수
    }
}

new Wrong("Alice").name은 null, new Right("Alice").name은 Alice다. Wrong은 오류 없이 번역되므로 실행해 보기 전에는 눈에 띄지 않는다.

this(...)라고 쓰면 같은 클래스의 다른 생성자를 부른다. 값을 채우는 코드를 생성자마다 다시 쓰지 않아도 된다. 생성자의 첫 줄에만 쓸 수 있다1.

class Scv {
    String name;
    int health;

    Scv() { this("SCV"); }
    Scv(String name) { this(name, 45); }
    Scv(String name, int health) {
        this.name = name;
        this.health = health;
    }
}

new Scv()는 이름 SCV, 체력 45로, new Scv("SCV-7")은 이름 SCV-7, 체력 45로 만들어진다.

활용

  • 객체가 꼭 가져야 하는 값은 생성자의 매개변수로 받는다. 값 없이 만드는 길을 막고 싶으면 매개변수 없는 생성자를 쓰지 않으면 된다.
  • 생성자가 도는 순서: 필드에 적어 둔 초기값이 먼저 들어가고, 그다음 생성자 본문이 돈다(카드 C3)1.
  • 흔한 실수: 생성자에 void를 붙이는 것. void Student() { }는 생성자가 아니라 이름이 Student인 보통 메서드가 되고, new할 때 불리지 않는다1.
  • 부모 클래스가 있을 때 생성자가 도는 순서는 부모 생성자 호출에 있다.

연결

확인 문제

C1 생성자가 메서드와 다른 점 두 가지와, 기본 생성자가 언제 생기는지 쓰라.

답: 돌려주는 타입이 없고, 이름이 클래스 이름과 같다. 기본 생성자는 클래스에 생성자를 하나도 쓰지 않았을 때 번역기가 바이트코드 파일에 넣어 준다. 하나라도 쓰면 넣어 주지 않는다.
이유: “기본 생성자는 늘 있다”가 흔한 오답이다.

C2 class Student { String name; Student(String name) { this.name = name; } }가 있다. 번역 단계에서 오류가 나는 줄은? (가) Student a = new Student("Alice"); (나) Student b = new Student(); (다) Student c = a;

답: (나). constructor Student in class Student cannot be applied to given types.
이유: 생성자를 하나 직접 썼으므로 기본 생성자가 들어가지 않는다. 매개변수 없는 생성자가 없다. (다)는 생성자를 부르지 않고 주소만 옮겨 담는 줄이라 된다.

C3 class Tank { int fuel = 10; Tank(int extra) { System.out.println(fuel); fuel = fuel + extra; } }에서 Tank t = new Tank(3); System.out.println(t.fuel);의 출력은?

답: 10과 13이 차례로 나온다.
이유: 필드에 적은 초기값 10이 먼저 들어간 뒤 생성자 본문이 돈다. 본문 첫 줄이 10을 찍고, 3을 더해 13이 된다. 첫 줄이 0이라고 답하는 것은 초기값이 생성자 뒤에 들어간다고 본 것이다1.

C4 생성자 Student(String name) { name = name; }로 만든 객체의 name이 null인 이유는?

답: 생성자 안에서 name은 매개변수를 가리킨다. 그래서 name = name;은 매개변수의 값을 매개변수에 다시 넣는다. 필드에는 아무것도 넣지 않았으니 기본값 null 그대로다. this.name = name;이라고 써야 필드에 들어간다.

  1. 모두 JDK 21에서 직접 실행·컴파일해 확인했다. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9