final 키워드는 이름 그대로 끝! 이라는 뜻이다.
변수에 final 키워드가 붙으면 더는 값을 변경할 수 없다.
참고로 final 은 class, method 를 포함한 여러 곳에서 붙을 수 있다. 지금은 변수에 붙는 final 키워드를 알아보자. 나머지는 final 의 사용법은 상속을 설명한 이후에 설명한다.
package final1;
public class FinalLocalMain {
public static void main(String[] args) {
// final 지역 변수
final int data1;
data1 = 10; // 최초 한번만 할당 가능
// data1 = 20; // 컴파일 에러
final int data2 = 10;
// data2 = 20; // 컴파일 에러
}
static void method(final int parameter) {
// parameter = 20; // 컴파일 에러
// parameter 를 바꿀 수 없다.
}
}
package final1;
public class ConstructInit {
final int value;
ConstructInit(int value) {
this.value = value;
}
}
package final1;
public class FieldInit {
static final int CONST_VALUE = 10; // 상수 필드
final int value = 10;
public FieldInit() {
// CONST_VALUE = 20; // 컴파일 에러
// value = 20; // 컴파일 에러
}
}
package final1;
public class FinalFieldMain {
public static void main(String[] args) {
// final 필드 - 생성자 초기화
System.out.println("생성자 초기화");
ConstructInit constructInit1 = new ConstructInit(10);
ConstructInit constructInit2 = new ConstructInit(20);
System.out.println(constructInit1.value);
System.out.println(constructInit2.value);
// final 필드 - 필드 초기화
System.out.println("필드 초기화");
FieldInit fieldInit1 = new FieldInit();
FieldInit fieldInit2 = new FieldInit();
System.out.println(fieldInit1.value);
System.out.println(fieldInit2.value);
// 상수
System.out.println("상수");
System.out.println(FieldInit.CONST_VALUE);
}
}
생성자 초기화
10
20
필드 초기화
10
10
상수
10
ConstructInit 과 같이 생성자를 사용해서 final 필드를 초기화 하는 경우, 각 인스턴스마다 final 필드에 다른 값을 할당할 수 있다. 물론 final 을 사용했기 때문에 생성 이후에 이 값을 변경하는 것은 불가능 하다.
그런데 FieldInit 과 같이 final 필드를 필드에서 초기화 하는 경우, 모든 인스턴스가 같은 값을 가진다.
FieldInit 인스턴스의 모든 value 값을 10이 된다.
왜냐하면 생성자 초기화와 다르게 필드 초기화는 필드의 코드에 해당 값이 미리 정해져있기 때문이다.
모든 인스턴스가 같은 값을 사용하기 때문에 결과적으로 메모리를 낭비하게 된다.
(물론 JVM에 따라서 내부 최적화를 시도할 수 있다.) 또 메모리 낭비를 떠나서 같은 값이 계속 생성되는 것은 개발자가 보기에 명확한 중복이다.
이럴 때 사용하면 좋은 것이 바로 static 영역이다.
FieldInit.MY_VALUE 는 static 영역에 존재한다. 그리고 final 키워드를 사용해서 초기화 값이 변하지 않는다.
static 영역은 단 하나만 존재하는 영역이다. MY_VALUE 변수는 JVM 상에서 하나만 존재하므로 앞서 설명한 중복과 메모리 비효율 문제를 모두 해결 할 수 있다.
이런 이유로 필드에 final + 필드 초기화를 사용하는 경우 static 을 붙여서 사용하는 것이 효과적이다.
상수는 변하지 않고, 항상 일정한 값을 갖는 수를 말한다. 자바에서는 보통 단 하나만 존재하는 변하지 않는 고정된 값을 상수라 한다.
이런 이유로 상수는 static final 키워드를 사용한다.
자바 상수 특징
package final1;
public class Constant {
// 수학 상수
public static final double PI = 3.14159;
// 시간 상수
public static final int HOURS_IN_DAY = 24;
public static final int MINUTES_IN_HOUR = 60;
public static final int SECONDS_IN_MINUTE = 60;
// 애플리케이션 설정 상수
public static final String APP_NAME = "MyApp";
public static final int MAX_USER = 100;
}
보통 이런 상수들은 애플리케이션 전반에서 사용되기 때문에 public 키워드를 자주 사용한다. 물론 특정 위치에서만 사용된다면 다른 접근 제어자를 사용하면 된다.
추가로 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있다. 다음 두 예제를 비교해보자.
ConstantMain1 - 상수 없음
package final1;
public class ConstantMain1 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수 " + 1000);
int currentUserCount = 999;
process(currentUserCount++);
process(currentUserCount++);
process(currentUserCount++);
}
private static void process(int currentUserCount) {
System.out.println("참여자 수: " + currentUserCount);
if (currentUserCount > 1000) {
System.out.println("대기자로 등록합니다.");
} else {
System.out.println("게임에 참여합니다.");
}
}
}
프로그램 최대 참여자 수 1000
참여자 수: 999
게임에 참여합니다.
참여자 수: 1000
게임에 참여합니다.
참여자 수: 1001
대기자로 등록합니다.
1000 이라는 값은 매직 넘버이다.
ConstantMain1 - 상수 있음
package final1;
public class ConstantMain1 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수 " + Constant.MAX_USER);
int currentUserCount = 999;
process(currentUserCount++);
process(currentUserCount++);
process(currentUserCount++);
}
private static void process(int currentUserCount) {
System.out.println("참여자 수: " + currentUserCount);
if (currentUserCount > Constant.MAX_USER) {
System.out.println("대기자로 등록합니다.");
} else {
System.out.println("게임에 참여합니다.");
}
}
}
Constant.MAX_USER 상수를 사용헀다. 만약 프로그램 최대 참여자 수를 변경해야 하면 Constant.MAX_USER 의 상수 값만 변경하면 된다.
또한 매직 넘버 문제를 해결했다. 숫자 1000 이 아니라 사람이 인지할 수 있게 MAX_USER 라는 변수명으로 코드를 이해할 수 있다.
final 은 변수의 값을 변경하지 못하게 막는다. 그런데 여기서 변수의 값이라는 것이 뭘까?
여기까지는 이해하는데 어려움이 없을 것이다. 이번에는 약간 복잡한 예제를 만들어 보자.
package final1;
public class Data {
public int value;
}
int value 는 final 이 아니다 변경할 수 있는 변수다.
package final1;
public class FinalRefMain {
public static void main(String[] args) {
final Data data = new Data();
// data = new Data(); // 컴파일 에러
// 참조 대상의 값은 변경 가능
data.value = 10;
System.out.println(data.value);
data.value = 20;
System.out.println(data.value);
}
}
10
20
참조형 변수 data 에 final 이 붙었다. 변수 선언 시점에 참조값을 할당했으므로 더는 참조값을 변경할 수 없다.
그런데 참조 대상의 객체 값은 변경할 수 있다.