final 키워드는 이름 그대로 끝! 이라는 뜻입니다.
변수에 final 키워드가 붙으면 더는 값을 변경할 수 없습니다.
참고로 final은 class, method를 포함한 여러 곳에 붙을 수 있는데요.
지금은 변수에 붙는 final 키워드를 알아봅시다.
public class FinalLocalMain {
public static void main(String[] args) {
//final 지역 변수1
final int data1;
data1 = 10; //최초 한번만 할당 가능
//data1 = 20; //컴파일 오류
//final 지역 변수2
final int data2 = 10;
//data2 = 20; //컴파일 오류
method(10);
}
static void method(final int parameter) {
//parameter = 20; //컴파일 오류
}
}
- final을 지역 변수에 설정할 경우 최초 한번만 할당할 수 있다. 이후에 변수의 값을 변경하려면 컴파일 오류가 발생한다.
- final을 지역 변수 선언시 바로 초기화 한 경우 이미 값이 할당되었기 때문에 값을 할당할 수 없다.
- 매개변수에 final이 붙으면 메서드 내부에서 매개변수의 값을 변경할 수 없다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용된다.
final 필드(멤버 변수)
public class ConstructInit {
//final 필드 (생성자 초기화)
final int value;
public ConstructInit(int value) {
this.value = value;
}
}
- final을 필드에 사용할 경우 해당 필드는 생성자를 통해서 한번만 초기화 될 수 있다.
public class FieldInit {
//final 필드 (필드 초기화)
static final int CONST_VALUE = 10; //static final이 붙으면 대문자로 (관례)
final int value = 10;
}
- final 필드를 필드에서 초기화 하면 이미 값이 설정되었기 때문에 생성자를 통해서도 초기화 할 수 없다.
- 코드에서 보는 것 처럼 static 변수에도 final을 선언할 수 있다.
- 매개 변수에 final이 붙으면 메서드 내부에서 매개 변수의 값을 변경할 수 없다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용된다.
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();
FieldInit fieldInit3 = new FieldInit();
System.out.println(fieldInit1.value);
System.out.println(fieldInit2.value);
System.out.println(fieldInit3.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 이다.
static final
- FieldInit.CONST_VALUE 는 static 영역에 존재한다. 그리고 final 키워드를 사용해서 초기화 값이 변하지 않는다.
- static 영역은 단 하나만 존재하는 영역이다. CONST_VALUE 변수는 JVM 상에서 하나만 존재하므로 앞서 설명한 중복과 메모리 비효율 문제를 모두 해결할 수 있다.
- 이런 이유로 필드에 final + 필드 초기화를 사용하는 경우 static을 붙여서 사용하는 것이 효과적이다.
상수(Constant)
상수는 변하지 않고, 항상 일정한 값을 갖는 수를 말합니다.
자바에서 상수란 보통 단 하나만 존재하는 변하지 않는 고정된 값을 뜻합니다.
이런 이유로 static final 키워드를 사용합니다.
자바 상수 특징
static final 키워드를 사용한다.
- 일반적인 변수와 상수를 구분하기 위해 대문자를 사용하고 구분은 _(언더 스코어)로 한다. (관례)
필드를 직접 접근해서 사용한다.
- 상수는 기능이 아니라 고정된 값 자체를 사용하는 것이 목적이다.
- 상수는 값을 변경할 수 없다. 따라서 필드에 직접 접근해도 데이터가 변하는 문제가 발생하지 않는다.
public class Constant {
//수학 상수
public static final double PI = 3.14;
//시간 상수
public static final int HOURS_IN_DAY = 24;
public static final int MINUTES_IN_HOUR = 60;
public static final int SECONDS_IN_MINUTES = 60;
//애플리케이션 설정 상수
public static final int MAX_USERS = 1000;
}
- 애플리케이션 안에는 다양한 상수가 존재할 수 있다. 수학, 시간 등등 실생활에서 사용하는 상수부터, 애플리케이션의 다양한 설정을 위한 상수들도 있다.
- 보통 이런 상수들은 애플리케이션 전반에서 사용되기 때문에 public를 자주 사용한다. 물론 특정 위치에서만 사용된다면 다른 접근제어자를 사용하면 된다.
- 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있다.
- 상수는 런타임에 변경할 수 없다. 상수를 변경하려면 프로그램을 종료하고, 코드를 변경한 다음에 프로그램을 다시 실행해야 한다.
다음 두 코드를 비교해봅시다.
public class ConstantMain1 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수: " + 1000);
int currentUserCount = 999;
proess(currentUserCount++);
proess(currentUserCount++);
proess(currentUserCount++);
}
private static void proess(int currentUserCount) {
System.out.println("현재 참여자 수: " + currentUserCount);
if(currentUserCount > 1000) {
System.out.println("대기자로 등록합니다.");
} else {
System.out.println("게임에 참가합니다.");
}
}
}
- 만약 프로그램 최대 참여자 수를 현재 1000명에서 2000명으로 변경해야 한다면 2곳의 변경 포인트가 생긴다.
- 만약 애플리케이션의 100곳에서 이 숫자를 사용했다면 100곳을 모두 수정해줘야 한다.
- 1000이 무엇인지 인지하기 어렵다.
public class ConstantMain2 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수: " + Constant.MAX_USERS);
int currentUserCount = 999;
proess(currentUserCount++);
proess(currentUserCount++);
proess(currentUserCount++);
}
private static void proess(int currentUserCount) {
System.out.println("현재 참여자 수: " + currentUserCount);
if(currentUserCount > Constant.MAX_USERS) {
System.out.println("대기자로 등록합니다.");
} else {
System.out.println("게임에 참가합니다.");
}
}
}
- Constant.Max_USERS 상수를 사용했다. 만약 프로그램 최대 참여자 수를 변경해야 하면 Constant.MAX_USER 의 상수값만 변경하면 된다.
- 매직 넘버 문제를 해결했다. 숫자 1000이 아니라 사람이 인지할 수 있게 MAX_USER 라는 변수명으로 코드를 이해할 수 있다.
final 변수와 참조
final은 변수의 값을 변경하지 못하게 막습니다. 그런데 변수의 값이라는 것은 뭘까요?
- 변수는 크게 기본형 변수와 참조형 변수가 있다.
- 기본형 변수는 10, 20 같은 값을 보관하고, 참조형 변수는 객체의 참조값을 보관한다.
- final을 기본형 변수에 사용하면 값을 변경할 수 없다.
- final을 참조형 변수에 사용하면 참조값을 변경할 수 없다.
public class Data {
public int value;
}
public class FinalRefMain {
public static void main(String[] args) {
final Data data = new Data();
// data = new Data(); final 변경 불가 컴파일 오류
//참조 대상의 값은 변경 가능
data.value = 10;
System.out.println(data.value);
data.value = 30;
System.out.println(data.value);
}
}
final Data data = new Data();
// data = new Data();
참조형 변수 data에 final이 붙었습니다. 변수 선언 시점에서 참조값을 할당했으므로 더는 참조값을 변경할 수 없습니다.
data.value = 10;
data.value = 30;
그런데 참조 대상의 객체 값은 변경할 수 있습니다.
- 참조형 변수 data에 final이 붙었다. 이 경우 참조형 변수에 들어있는 참조값을 다른 값으로 변경하지 못한다. 이제 다른 객체를 참조할 수 없다는 뜻이다. 참조형 변수에 들어있는 참조값만 변경하지 못한다는 뜻이다. 이 변수 이외에 다른 곳에 영향을 주는 것이 아니다.
- data.value는 final이 아니다. 따라서 값을 변경할 수 있다.
- 참조형 변수에 final이 붙으면 참조 대상 자체를 다른 대상으로 변경하지 못하는 것이지, 참조하는 대상의 값은 변경할 수 있다.
final은 매우 유용한 제약입니다. 만약 특정 변수의 값을 할당한 이후에 변경하지 않아야 한다면 final을 사용해봅시다!
예를 들어 고객의 id을 변경해서 큰 문제가 발생한다면 final로 선언하고 생성자로 값을 할당합시다.
만약 어디선가 실수로 id 값을 변경한다면 컴파일러가 문제를 찾아줄 것입니다.
public class Member {
private final String id; //final 키워드 사용
private String name;
public Member(String id, String name) {
this.id = id;
this.name = name;
}
public void changeData(String id, String name) {
//this.id = id; 컴파일 오류발생
this.name = name;
}
public void print() {
System.out.println("id: " + id + "name: " + name);
}
}
- changeData( ) 메서드에서 final인 id 값 변경을 시도하면 컴파일 오류가 발생한다.
public class MemberMain {
public static void main(String[] args) {
Member member = new Member("ssambbong", "coding");
member.print();
member.changeData("coding","ssambbong");
member.print();
}
}
실행 결과
id: ssambbongname: coding
id: ssambbongname: ssambbong
만약 쇼핑몰 프로그램의 장바구니 기능이나, 이력조회 기능이 id로 묶어있는데 changData( ) 메서드로 id를 바꾸게 되면
어떻게 될까요? 아마 고객들은 자신의 장바구니를 잃고 슬퍼할 것입니다. 사전에 데이터를 잃거나 변경되지 않도록 final을 잘 활용해줍시다!
'Java' 카테고리의 다른 글
[Java] 다형적 참조(업캐스팅 vs 다운캐스팅) (0) | 2024.03.18 |
---|---|
[Java] 상속이 왜 필요할까? (feat.오버라이딩) (0) | 2024.03.14 |
[Java] static 에 대해서 궁금하다면 (0) | 2024.03.12 |
[Java] 자료 구조 스택 (0) | 2024.03.11 |
[Java] 접근 제어자 (0) | 2024.03.07 |