final 변수·필드·상수
final 지역 변수와 필드의 재대입 금지 계약을 확인하고 인스턴스 설정과 클래스 상수를 초기화 시점에 맞게 구분합니다.
final은 변수에 값을 한 번만 대입할 수 있게 합니다.
값이 절대 변하지 않는다는 막연한 표시보다, 어떤 이름이 초기화 뒤 다른 값을 가리키지 않는다는 컴파일 계약입니다.
지역 변수, 인스턴스 필드, static 필드에서 초기화 시점과 공유 범위가 다릅니다.
final 지역 변수
public final class FinalLocalReassignment {
public static void main(String[] args) {
final int targetCount = 120;
targetCount = 180;
System.out.println(targetCount);
}
}error: cannot assign a value to final variable targetCountfinal 변수는 선언과 동시에 초기화하거나, 모든 경로에서 정확히 한 번 대입할 수 있습니다.
final int target;
if (args.length == 0) {
target = 60;
} else {
target = Integer.parseInt(args[0]);
}
System.out.println(target);어느 경로에서도 두 번 대입되지 않고 사용 전에 값이 정해지므로 컴파일됩니다.
final은 실행 중 값을 비교하는 검증이 아니라 컴파일러가 대입 횟수를 추적하는 규칙입니다.
final 필드 초기화
public final class FinalInstanceFields {
public static void main(String[] args) {
MemberRegistry personal = new MemberRegistry("personal", 3);
MemberRegistry work = new MemberRegistry("work", 10);
System.out.println(personal.describe());
System.out.println(work.describe());
}
private static final class MemberRegistry {
private final String name;
private final int capacity;
MemberRegistry(String name, int capacity) {
if (name.isBlank() || capacity <= 0) throw new IllegalArgumentException();
this.name = name;
this.capacity = capacity;
}
String describe() {
return name + ":" + capacity;
}
}
}personal:3
work:10name과 capacity는 객체마다 다른 값이지만 각 객체에서는 생성 뒤 바뀌지 않습니다.
final 인스턴스 필드는 필드 선언식 또는 모든 생성자 경로에서 초기화해야 합니다.
생성자 오버로딩이 있다면 this() 위임으로 초기화 경로를 모으면 누락을 줄일 수 있습니다.
final 필드를 생성자에서 받으면 객체의 필수 설정이 생성 계약에 드러납니다.
변경이 필요하다면 같은 객체의 필드를 바꾸는 대신 새 설정으로 새 객체를 만드는 설계를 고려합니다.
클래스 상수
public final class SignupPolicyConstants {
public static void main(String[] args) {
System.out.println(SignupPolicy.MIN_MEMBER_AGE);
System.out.println(SignupPolicy.MAX_MEMBER_AGE);
System.out.println(SignupPolicy.isAllowed(40));
System.out.println(SignupPolicy.isAllowed(130));
}
private static final class SignupPolicy {
static final int MIN_MEMBER_AGE = 14;
static final int MAX_MEMBER_AGE = 120;
private SignupPolicy() {}
static boolean isAllowed(int age) {
return age >= MIN_MEMBER_AGE && age <= MAX_MEMBER_AGE;
}
}
}14
120
true
false모든 객체에 동일하고 바뀌지 않는 정책 값은 static final로 클래스에 하나만 둡니다.
상수 이름은 대문자와 밑줄을 사용하는 관례가 있습니다.
단지 final이라는 이유로 모든 지역 변수 이름을 대문자로 만들지 않습니다.
대문자 관례는 클래스 수준 상수에 적용합니다.
상수로 만들 값은 변하지 않는다는 기술적 조건뿐 아니라 모든 인스턴스가 공유하는 개념이어야 합니다.
가입 캠페인별 목표 인원은 final이어도 static이 아니라 인스턴스 필드입니다.
private final int targetCount;사용자마다 값이 다르지만 한 SignupQuota 안에서는 바뀌지 않는 설정입니다.
매직 숫자와 의미
if (age > 120) { ... }숫자만 보면 120이 최대 회원 나이인지 다른 업무 한도인지 알 수 없습니다.
if (age > MAX_MEMBER_AGE) { ... }이름이 규칙의 의미와 단위를 보여 줍니다.
같은 값 60이라도 회원 나이 기준과 게시판 페이지 크기는 다른 상수입니다.
static final int SENIOR_MEMBER_AGE = 60;
static final int DEFAULT_PAGE_SIZE = 60;숫자가 같다는 이유로 하나의 상수를 재사용하면 회원 분류와 게시판 조회 규칙이 우연히 결합합니다.
이후 고령 회원 기준만 65로 바꿀 때 게시판 페이지 크기까지 바뀌는 결함이 생깁니다.
상수는 값 중복이 아니라 개념 중복을 제거합니다.
모든 숫자를 상수로 뽑을 필요는 없습니다.
배열의 첫 인덱스 0, 한 단계 증가 1처럼 문맥에서 구조적 의미가 명확한 값은 그대로 읽기 쉬울 수 있습니다.
정책 상수와 인스턴스 설정
public final class FinalMemberRegistryPolicy {
public static void main(String[] args) {
MemberRegistry registry = new MemberRegistry("daily", 2);
System.out.println(registry.add("kim@example.com", 40));
System.out.println(registry.add("invalid@example.com", 130));
System.out.println(registry.add("lee@example.com", 50));
System.out.println(registry.summary());
}
private static final class MemberRegistry {
private static final int MAX_MEMBER_AGE = 120;
private final String name;
private final Member[] members;
private int size;
MemberRegistry(String name, int capacity) {
if (name == null || name.isBlank() || capacity <= 0) {
throw new IllegalArgumentException("configuration");
}
this.name = name;
this.members = new Member[capacity];
}
boolean add(String email, int age) {
if (size == members.length || age < 14 || age > MAX_MEMBER_AGE) {
return false;
}
members[size++] = new Member(email, age);
return true;
}
String summary() {
int total = 0;
for (int index = 0; index < size; index++) {
total += members[index].age();
}
return name + ":count=" + size + ", total=" + total;
}
}
private record Member(String email, int age) {}
}true
false
true
daily:count=2, total=90MAX_MEMBER_AGE는 모든 MemberRegistry가 공유하는 클래스 정책입니다.
name과 members 참조는 객체별로 다르지만 생성 뒤 재대입하지 않는 final 필드입니다.
size는 회원 추가와 삭제에 따라 변해야 하므로 final이 아닙니다.
연습 문제
SignupQuota에 객체별 final targetCount를 두고, 클래스 전체 상수 PERCENT_SCALE=100을 사용해 달성률을 계산하세요.
누적 등록 수는 변경 가능해야 하며 달성률은 100을 넘지 않게 합니다.
해설 보기
public final class FinalSignupQuota {
public static void main(String[] args) {
SignupQuota quota = new SignupQuota(120);
quota.register(50);
quota.register(90);
System.out.println("progress=" + quota.progress());
}
private static final class SignupQuota {
private static final int PERCENT_SCALE = 100;
private final int targetCount;
private int registeredCount;
SignupQuota(int targetCount) {
if (targetCount <= 0) throw new IllegalArgumentException("target");
this.targetCount = targetCount;
}
void register(int count) {
if (count <= 0) throw new IllegalArgumentException("count");
registeredCount += count;
}
int progress() {
int raw = registeredCount * PERCENT_SCALE / targetCount;
return Math.min(raw, PERCENT_SCALE);
}
}
}progress=100목표는 객체별로 한 번 정하고, 백분율 배율은 모든 객체가 공유합니다.
누적 값만 행동에 따라 변합니다.