생성자와 this
초기화가 누락된 객체의 실패를 재현하고 생성자와 this로 유효한 Member의 시작 상태를 보장합니다.
객체가 생성된 뒤 필드를 하나씩 대입하면 “만들어졌지만 아직 쓸 수 없는” 시간이 생깁니다.
다른 메서드가 그 중간 상태를 먼저 사용하거나 한 필드 대입을 빼먹을 수 있습니다.
생성자는 객체 생성과 필수 초기화를 하나의 연산으로 묶어 처음부터 유효한 상태를 만들게 합니다.
초기화 누락
public final class MissingInitialization {
public static void main(String[] args) {
Member member = new Member();
member.age = 40;
System.out.println(member.email.toUpperCase());
}
private static final class Member {
String email;
int age;
}
}Exception in thread "main" java.lang.NullPointerExceptionnew Member()는 필드를 기본값으로 초기화합니다.
참조형 email은 null, int age는 0입니다.
호출자가 age만 대입했으므로 email의 메서드를 호출할 때 실패했습니다.
클래스를 보는 것만으로는 어떤 필드를 반드시 채워야 하는지 강제되지 않습니다.
생성자 없이 다음 절차를 반복하면 순서와 누락을 모든 호출자가 책임집니다.
Member member = new Member();
member.email = email;
member.age = age;필수 값이 늘어날수록 기존 생성 위치를 모두 찾아 수정해야 합니다.
생성자 매개변수로 필수 값을 요구하면 컴파일러가 누락된 호출을 찾습니다.
생성자 선언
public final class ConstructorMember {
public static void main(String[] args) {
Member member = new Member("kim@example.com", 40);
System.out.println(member.describe());
}
private static final class Member {
private String email;
private int age;
Member(String email, int age) {
this.email = email;
this.age = age;
}
String describe() {
return email + "=" + age;
}
}
}kim@example.com=40Member(String email, int age)는 메서드처럼 보이지만 반환 타입이 없고 클래스 이름과 같습니다.
new Member("kim@example.com", 40)는 메모리 할당, 필드 기본값 설정, 생성자 본문 실행을 거쳐 참조를 반환합니다.
개발자가 생성자를 직접 일반 메서드처럼 호출할 수는 없습니다.
생성자는 객체 생성 시 한 번 실행됩니다.
이후 같은 검증을 거쳐 값을 바꾸려면 별도의 행동 메서드가 필요합니다.
생성자를 다시 호출해 기존 객체를 재초기화하는 방식은 없습니다.
this 필드 참조
public final class MissingThisAssignment {
public static void main(String[] args) {
Member member = new Member("lee@example.com", 50);
System.out.println(member.email + "=" + member.age);
}
private static final class Member {
String email;
int age;
Member(String email, int age) {
email = email;
age = age;
}
}
}null=0이름을 찾을 때 가장 가까운 지역 범위의 매개변수가 우선합니다.
email = email은 매개변수 값을 같은 매개변수에 다시 대입하고, 필드는 건드리지 않습니다.
this.email은 현재 생성 중인 객체의 필드를 명시합니다.
Member(String email, int age) {
this.email = email;
this.age = age;
}왼쪽은 객체 필드, 오른쪽은 생성자 매개변수입니다.
매개변수 이름을 inputEmail으로 바꾸면 this 없이도 구분되지만, 필드와 매개변수가 같은 개념이라면 같은 이름과 this가 대응을 분명하게 보여 줍니다.
this는 “현재 인스턴스의 참조”입니다.
인스턴스 메서드에서 this.describe()처럼 메서드를 호출하거나 다른 객체에 현재 인스턴스를 전달할 수도 있습니다.
다만 명확하지 않은 곳마다 this를 붙이는 것이 목표가 아니라 지역 변수와 필드의 경계를 드러내는 데 사용합니다.
생성자 불변식
public final class ValidatedConstructor {
public static void main(String[] args) {
printCreation("kim@example.com", 40);
printCreation("", 30);
printCreation("lee@example.com", 130);
}
private static void printCreation(String email, int age) {
try {
Member member = new Member(email, age);
System.out.println("created=" + member.describe());
} catch (IllegalArgumentException exception) {
System.out.println("rejected=" + exception.getMessage());
}
}
private static final class Member {
private final String email;
private final int age;
Member(String email, int age) {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("email");
}
if (age < 14 || age > 120) {
throw new IllegalArgumentException("age");
}
this.email = email;
this.age = age;
}
String describe() {
return email + "=" + age;
}
}
}created=kim@example.com=40
rejected=email
rejected=age검증을 필드 대입보다 먼저 수행해 실패한 객체가 외부로 전달되지 않게 합니다.
생성자가 예외를 던지면 new 표현식이 정상 참조를 반환하지 않습니다.
생성된 모든 Member가 이메일과 나이 범위를 만족한다는 가정을 다른 메서드가 사용할 수 있습니다.
CLI에서 사용자 입력 오류를 예외로 끝내면 안 되므로 입력 경계에서 예외를 잡아 rejected로 바꿉니다.
생성자는 클래스 규칙을 지키고, CLI는 사용자와 대화를 계속하는 책임을 맡습니다.
생성 책임 분리
MemberRegistry의 add가 원시 값으로 Member를 생성할 수 있습니다.
public final class ConstructingMemberRegistry {
public static void main(String[] args) {
MemberRegistry registry = new MemberRegistry(2);
System.out.println(registry.add("kim@example.com", 40));
System.out.println(registry.add("lee@example.com", 50));
System.out.println(registry.add("park@example.com", 30));
System.out.println(registry.summary());
}
private static final class MemberRegistry {
private final Member[] members;
private int size;
MemberRegistry(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity");
}
this.members = new Member[capacity];
}
boolean add(String email, int age) {
if (size == members.length) {
return false;
}
try {
members[size] = new Member(email, age);
size++;
return true;
} catch (IllegalArgumentException exception) {
return false;
}
}
String summary() {
int total = 0;
for (int index = 0; index < size; index++) {
total += members[index].age();
}
return "count=" + size + ", total=" + total;
}
}
private static final class Member {
private final String email;
private final int age;
Member(String email, int age) {
if (email == null || email.isBlank() || age < 14 || age > 120) {
throw new IllegalArgumentException("invalid member");
}
this.email = email;
this.age = age;
}
int age() {
return age;
}
}
}true
true
false
count=2, total=90세 번째 요청은 용량 검사에서 거절돼 생성자도 호출하지 않습니다.
성공한 생성 뒤에만 size를 증가시킵니다.
Member 생성 규칙과 MemberRegistry 용량 규칙이 각각 자기 상태 가까이에 있습니다.
연습 문제
SignupQuota에 캠페인 이름과 목표 가입자 수를 필수로 받는 생성자를 추가하세요.
이름은 공백일 수 없고 목표 가입자 수는 1~1,000명이어야 합니다.
this로 필드와 매개변수를 구분하고 describe()가 daily=120을 반환하게 하세요.
해설 보기
public final class SignupQuotaConstructor {
public static void main(String[] args) {
SignupQuota goal = new SignupQuota("daily", 120);
System.out.println(goal.describe());
}
private static final class SignupQuota {
private final String name;
private final int targetCount;
SignupQuota(String name, int targetCount) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name");
}
if (targetCount < 1 || targetCount > 1_000) {
throw new IllegalArgumentException("targetCount");
}
this.name = name;
this.targetCount = targetCount;
}
String describe() {
return name + "=" + targetCount;
}
}
}daily=120필수 값 두 개가 생성자 서명에 나타나므로 인수를 빼먹은 호출은 컴파일되지 않습니다.
생성 완료 뒤에는 두 필드가 항상 유효합니다.