Java 25 - Flexible Constructor Bodies (Final)
업데이트:
JAVA 251
Java 25에서는 생성자의 본문 앞에 super(...) 또는 this(...) 호출 이전에 명령문들을 작성할 수 있도록 허용합니다. 이는 생성자에서 인자 검증을 우선적으로 수행할 수 있게 하며, 객체의 무결성을 더욱 강화합니다.
기존의 문제점
Java 24 이전에는 다음과 같은 제한이 있었습니다:
- 생성자의 첫 문장은 반드시
super()또는this()호출이어야 함 - 인자 검증 후 상위 클래스 생성자 호출이 불가능
- 상위 클래스 생성자가 부분 초기화된 하위 클래스 필드에 접근할 수 있음
기본 예제: Fail-Fast 검증
1. 생성자 인자 검증
public class Employee extends Person {
private String officeID;
// Java 25 이전: 검증을 할 수 없었음
// Java 25 이후: super() 호출 전 검증 가능
public Employee(String name, int age, String officeID) {
// 프롤로그: super() 호출 전 명령문
if (age < 18 || age > 67) {
throw new IllegalArgumentException(
"직원은 18세 이상 67세 이하여야 합니다"
);
}
if (officeID == null || officeID.isEmpty()) {
throw new IllegalArgumentException(
"사무실 ID는 필수입니다"
);
}
// 상위 클래스 생성자 호출
super(name, age);
// 에필로그: super() 호출 후 명령문
this.officeID = officeID;
System.out.println("직원이 생성되었습니다: " + name);
}
public String getOfficeID() {
return officeID;
}
public static void main(String[] args) {
try {
// 유효한 직원
Employee emp1 = new Employee("Alice", 30, "OFF-001");
System.out.println("사무실: " + emp1.getOfficeID());
// 나이 범위 위반
Employee emp2 = new Employee("Bob", 15, "OFF-002");
} catch (IllegalArgumentException e) {
System.err.println("오류: " + e.getMessage());
}
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
실행 결과:
직원이 생성되었습니다: Alice
사무실: OFF-001
오류: 직원은 18세 이상 67세 이하여야 합니다
프롤로그와 에필로그
2. 생성자 실행 순서
public class ConstructorPhases {
static class Base {
protected String baseName;
Base() {
System.out.println("Base 생성자 본문");
baseName = "base";
}
}
static class Middle extends Base {
protected String middleName;
Middle() {
// 프롤로그: super() 호출 전
System.out.println("1. Middle 프롤로그");
middleName = "middle-setup";
// 상위 클래스 생성자 호출
super();
// 에필로그: super() 호출 후
System.out.println("4. Middle 에필로그");
middleName = "middle";
}
}
static class Derived extends Middle {
private String derivedName;
Derived() {
// 프롤로그
System.out.println("2. Derived 프롤로그");
derivedName = "derived-setup";
// 상위 클래스 생성자 호출
super();
// 에필로그
System.out.println("5. Derived 에필로그");
derivedName = "derived";
}
}
public static void main(String[] args) {
System.out.println("=== 객체 생성 시작 ===");
new Derived();
System.out.println("=== 객체 생성 완료 ===");
}
}
실행 결과:
=== 객체 생성 시작 ===
1. Middle 프롤로그
2. Derived 프롤로그
Base 생성자 본문
4. Middle 에필로그
5. Derived 에필로그
=== 객체 생성 완료 ===
복잡한 초기화
3. 고급 초기화 패턴
import java.util.regex.Pattern;
public class ConfigurationClass {
private Pattern validationPattern;
private String ipAddress;
private int port;
public ConfigurationClass(String pattern, String ipAddress, int port) {
// 프롤로그: 복잡한 계산 및 검증
if (port < 1 || port > 65535) {
throw new IllegalArgumentException(
"포트는 1-65535 사이여야 합니다"
);
}
// 정규표현식 컴파일 - 실패할 수 있음
Pattern compiled = compilePattern(pattern);
// IP 주소 검증
validateIpAddress(ipAddress);
// 복잡한 계산을 통한 초기값 설정
this.validationPattern = compiled;
this.ipAddress = ipAddress;
this.port = port;
System.out.println("설정이 성공적으로 초기화되었습니다");
}
private static Pattern compilePattern(String pattern) {
try {
return Pattern.compile(pattern);
} catch (Exception e) {
throw new IllegalArgumentException(
"유효하지 않은 정규표현식: " + pattern
);
}
}
private static void validateIpAddress(String ip) {
String[] parts = ip.split("\\.");
if (parts.length != 4) {
throw new IllegalArgumentException(
"유효하지 않은 IP 주소: " + ip
);
}
for (String part : parts) {
try {
int num = Integer.parseInt(part);
if (num < 0 || num > 255) {
throw new NumberFormatException();
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"각 옥텟은 0-255 사이여야 합니다"
);
}
}
}
public static void main(String[] args) {
try {
ConfigurationClass config = new ConfigurationClass(
"^[a-zA-Z0-9]+$",
"192.168.1.1",
8080
);
ConfigurationClass invalid = new ConfigurationClass(
"[invalid",
"192.168.1.1",
8080
);
} catch (IllegalArgumentException e) {
System.err.println("오류: " + e.getMessage());
}
}
}
실행 결과:
설정이 성공적으로 초기화되었습니다
오류: 유효하지 않은 정규표현식: [invalid
부분 초기화 방지
4. 객체 무결성 보장
public class IntegrityExample {
// 상위 클래스
static class Shape {
protected int sides;
Shape(int sides) {
System.out.println("Shape 생성자: sides=" + sides);
this.sides = sides;
describe(); // 오버라이드된 메서드 호출
}
protected void describe() {
System.out.println("도형: " + sides + "변");
}
}
// 하위 클래스
static class Polygon extends Shape {
private String color;
Polygon(int sides, String color) {
// 프롤로그: this.color를 먼저 초기화
this.color = color;
// 상위 생성자 호출 시 this.color가 이미 초기화됨
super(sides);
System.out.println("Polygon 완성: " + color);
}
@Override
protected void describe() {
// color가 null이 아님을 보장
System.out.println("도형: " + sides + "변, 색상: " + color);
}
}
public static void main(String[] args) {
Polygon polygon = new Polygon(5, "빨강");
}
}
실행 결과:
Shape 생성자: sides=5
도형: 5변, 색상: 빨강
Polygon 완성: 빨강
필드 초기화 제약
5. Early Construction Context의 규칙
public class ConstructionConstraints {
static class FieldExample {
int uninitializedField;
int initializedField = 10;
FieldExample() {
// 프롤로그에서 가능한 작업들
// ✓ 초기화되지 않은 필드에 할당 - 가능
uninitializedField = 42;
// ✗ 초기화된 필드에 할당 - 불가능
// initializedField = 20; // 컴파일 오류
// ✗ this 명시적 사용 - 불가능
// int x = this.uninitializedField; // 컴파일 오류
// ✗ 현재 인스턴스를 메서드로 전달 - 불가능
// processField(this); // 컴파일 오류
super();
// 에필로그에서는 모든 것이 가능
int x = this.uninitializedField; // ✓ 가능
initializedField = 20; // ✓ 가능
}
}
static class NestedClassExample {
int field;
class Inner {
Inner() {
// 프롤로그에서 외부 인스턴스 접근 가능
field = 100; // ✓ 가능 (외부 인스턴스의 필드)
super();
}
}
}
}
레코드와 열거형
6. Record에서의 사용
import java.time.LocalDate;
public record Person(String name, LocalDate birthDate) {
// Compact Constructor - Java 25 이전
// public Person {
// Objects.requireNonNull(name);
// if (birthDate.isAfter(LocalDate.now())) {
// throw new IllegalArgumentException("출생일이 미래입니다");
// }
// }
// Java 25: 더 유연한 생성자
public Person(String name, LocalDate birthDate) {
// this(...) 호출 전 검증
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("이름은 필수입니다");
}
if (birthDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("출생일이 미래입니다");
}
this(name, birthDate);
}
public static void main(String[] args) {
try {
Person person1 = new Person("Alice", LocalDate.of(1990, 5, 15));
System.out.println("사람: " + person1);
Person person2 = new Person("Bob", LocalDate.now().plusDays(1));
} catch (IllegalArgumentException e) {
System.err.println("오류: " + e.getMessage());
}
}
}
실행 결과:
사람: Person[name=Alice, birthDate=1990-05-15]
오류: 출생일이 미래입니다
열거형 생성자
7. Enum Constructor
public enum Status {
PENDING("대기중", 1),
ACTIVE("활성", 2),
DISABLED("비활성", 3);
private final String displayName;
private final int code;
Status(String displayName, int code) {
// Java 25: this() 호출 전 검증 가능
if (displayName == null || displayName.isEmpty()) {
throw new IllegalArgumentException("displayName은 필수입니다");
}
if (code < 1) {
throw new IllegalArgumentException("코드는 양수여야 합니다");
}
this(displayName, code);
}
public String getDisplayName() {
return displayName;
}
public int getCode() {
return code;
}
public static void main(String[] args) {
for (Status status : Status.values()) {
System.out.println(status.name() + ": " + status.getDisplayName()
+ " (코드: " + status.getCode() + ")");
}
}
}
실행 결과:
PENDING: 대기중 (코드: 1)
ACTIVE: 활성 (코드: 2)
DISABLED: 비활성 (코드: 3)
주요 이점
| 이점 | 설명 |
|---|---|
| Fail-Fast | 생성자 초반에 검증하여 불완전한 객체 생성 방지 |
| 객체 무결성 | 상위 클래스 생성자 호출 전 모든 필드 초기화 가능 |
| 가독성 | 검증 로직을 명확하게 표현 |
| 안전성 | 상위 클래스의 메서드 호출 시 초기화되지 않은 필드 접근 방지 |
주의사항
- Early Construction Context 규칙 준수 필수
this를 명시적/암묵적으로 사용 불가 (프롤로그에서)- 문서화 필요: 프롤로그에서 수행되는 작업 명시
호환성
- 소스 호환: 기존 코드는 변경 없이 작동
- 바이너리 호환: 컴파일된 클래스 파일은 호환됨
- 동작 호환: 기존과 동일한 의미 유지
댓글남기기