볼펜으로 한 번 적으면 못 고치는 칸이라고 생각하면 된다. 학번처럼 처음 한 번만 정하고 끝까지 그대로여야 하는 값을 이런 칸에 둔다. 실수로 다른 값을 넣으려 하면 프로그램을 번역할 때 걸러진다. 다만 막는 것은 “그 칸에 다른 것을 적기”뿐이다. 칸에 적힌 것이 어떤 물건이 있는 곳이라면, 그 물건의 속을 바꾸는 것까지는 막지 못한다.
예시로 보기
학생의 이름은 바뀔 수 있지만 학번은 입학 때 정해지면 그대로다. 학번을 보통 필드로 두면 어느 코드나 s.id = "0";이라고 쓸 수 있고, 그런 실수는 실행해서 결과가 이상해질 때까지 드러나지 않는다. 필드 앞에 final을 붙이면 번역기가 막아 준다.
class Student {
final String school = "CAU"; // 선언할 때 값을 준다
final String id; // 생성자에서 값을 준다
String name;
Student(String id, String name) {
this.id = id;
this.name = name;
}
}
Student a = new Student("2024001", "Alice");
Student b = new Student("2024002", "Bob");
a.name = "Alicia";
System.out.println(a.school + " " + a.id + " " + a.name); // CAU 2024001 Alicia
System.out.println(b.school + " " + b.id + " " + b.name); // CAU 2024002 Bob
name은 바꿀 수 있다. id는 학생마다 다른 값을 가지되, 한 번 들어가면 못 바꾼다. a.id = "0";이나 a.school = "SNU";를 쓰면 이런 오류가 난다1.
error: cannot assign a value to final variable id
정의
final 필드는 값을 한 번 넣으면 프로그램이 도는 동안 바꿀 수 없는 필드다.
final 타입 필드 [= 초기값];
처음 값을 주는 방법은 둘 중 하나다.
- 필드를 선언하면서 준다(
school). 모든 객체가 같은 값을 가진다. - 생성자에서 준다(
id). 객체마다 다른 값을 가질 수 있다.
둘 다 하지 않으면 번역되지 않는다. final String id;만 두고 생성자에서 넣지 않으면 variable id might not have been initialized 오류가 난다. 값을 줄 기회가 다시는 없기 때문이다1.
상수는 바뀌지 않는 값을 담는 필드다. 상수는 객체마다 따로 둘 까닭이 없고(static), 값이 여럿이어서도 안 되므로(final), static과 final을 함께 붙인다.
static final 타입 상수 [= 초기값];
class Game {
static final int MAX_HEALTH = 45;
static final double PI = 3.14159;
}
Game.MAX_HEALTH는 45, 10 * 10 * Game.PI는 314.159다. 객체 없이 클래스 이름으로 쓴다(static 멤버). Game.MAX_HEALTH = 999;는 cannot assign a value to static final variable MAX_HEALTH 오류다. 상수 이름은 MAX_HEALTH처럼 모두 대문자로 쓰고 낱말 사이를 밑줄로 잇는 것이 자바의 관례다1.
| final 필드 | 상수(static final) | |
|---|---|---|
| 있는 곳 | 객체마다 하나 | 클래스에 하나 |
| 값을 주는 곳 | 선언할 때 또는 생성자 | 선언할 때(또는 정적 블록) |
| 객체마다 값이 | 다를 수 있다(학번) | 모두 같다(최대 체력) |
final은 칸을 잠그지, 물건을 잠그지 않는다
배열이나 객체를 가리키는 필드에 final을 붙이면, 못 바꾸는 것은 “무엇을 가리키는가”다. 가리키는 대상의 속은 그대로 바꿀 수 있다1.
class Squad {
final int[] healths = {45, 45, 45};
}
Squad s = new Squad();
s.healths[0] = 100; // 된다
System.out.println(Arrays.toString(s.healths)); // [100, 45, 45]
s.healths = new int[3]; // error: cannot assign a value to final variable healths
s.healths ──(final: 이 화살표를 못 바꾼다)──▶ [ 100 | 45 | 45 ] ← 칸의 값은 바꿀 수 있다
변수에 들어 있는 것이 배열 자체가 아니라 배열이 있는 곳의 주소이기 때문이다(참조 타입과 메모리 영역).
활용
- 한 번 정하면 그대로여야 하는 값(학번, 만든 날짜)은
final필드로, 프로그램 전체에서 같은 값(원주율, 최대 체력)은 상수로 둔다. - 코드 곳곳에
45를 직접 쓰는 대신Game.MAX_HEALTH를 쓰면, 값을 바꿀 때 한 곳만 고치면 되고 숫자가 무슨 뜻인지도 이름에 남는다1. - 흔한 실수:
static final로 배열을 만들어 놓고 내용도 못 바꾼다고 믿는 것. 칸의 값은 바뀐다. final은 필드 말고 메서드와 클래스에도 붙는다. 메서드에 붙이면 자식이 다시 쓸 수 없다(메서드 오버라이딩)1.
연결
확인 문제
C1 final 필드에 처음 값을 주는 두 가지 방법과, 상수를 선언하는 방법을 쓰라.
답: 필드를 선언하면서 주거나, 생성자에서 준다. 상수는 static final 타입 이름 = 값;으로 선언한다.
이유: “아무 메서드에서나 한 번은 줄 수 있다”가 흔한 오답이다. 생성자가 끝날 때까지 값이 들어가 있어야 한다.
C2 상수에 final만 붙이지 않고 static도 함께 붙이는 이유는?
답: final만 붙이면 바뀌지는 않지만 객체마다 한 칸씩 생긴다. 상수는 모든 객체에서 같은 값이라 객체마다 둘 까닭이 없다. static을 붙여 클래스에 하나만 두고, final로 그 하나가 바뀌지 않게 한다.
C3 class Squad { final int[] healths = {45, 45, 45}; static final int MAX = 45; }와 Squad s = new Squad();가 있다. 번역 단계에서 오류가 나는 줄을 모두 고르라. (가) s.healths[0] = 100; (나) s.healths = new int[3]; (다) Squad.MAX = 50; (라) int x = Squad.MAX + 1;
답: (나)와 (다).
이유: (나)는 final 변수가 가리키는 대상을 바꾸려 해서, (다)는 상수에 값을 다시 넣으려 해서 막힌다. (가)는 가리키는 배열은 그대로 두고 칸의 값만 바꾸므로 된다. (가)를 오류로 고르는 것이 가장 흔한 오답이다1.