Post

[JAVA] final

[JAVA] final

final 이란?

이름 그대로 ‘끝’ 이라는 뜻이다.

변수에 final 키워드가 붙으면 더 이상 값을 변경할 수 없다.

final 지역변수

1
2
3
4
5
6
7
final int number;
number = 10; // 최초 한 번은 할당 가능
// number = 20; // 컴파일 오류: 이미 값을 할당했기 때문

//선언과 동시 초기화
fianl int number2 = 20;
// number2 = 30; // 컴파일 오류: 이미 값을 할당했기 때문
  • final이 붙은 지역 변수는 최초 한 번만 값을 할당할 수 있습니다.
  • 한 번 값을 할당한 이후에는 다시 값을 변경할 수 없습니다.
  • 선언과 동시에 초기화 하면 이미 값이 정해진 상태이기 때문에 이후 재 할당 불가

final 매개변수

1
2
3
4
static void print(final int number) {
    // number = 20; // 컴파일 오류
    System.out.println(number);
}
  • final이 붙은 매개변수는 메서드 내부에서 값을 변경할 수 없다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용

final 필드(맴버 변수)

JAVA에서 final 키워드를 필드(맴버 변수)에 사용하면 값 변경이 불가능한 필드를 만들 수 있다.

그런데, 어디서 값을 초기화하느냐에 따라 사용방식이 달라지며 의미도 달라진다.

생성자를 통한 final 필드 초기화

1
2
3
4
5
6
7
public class ConstructInit {
    final int value;

    public ConstructInit(int value) {
        this.value = value; // 생성자를 통한 초기화
    }
}
1
2
3
4
ConstructInit c1 = new ConstructInit(10);
ConstructInit c2 = new ConstructInit(20);
System.out.println(c1.value); // 10
System.out.println(c2.value); // 20
  • final 필드는 생성자를 통해 단 한 번만 값을 초기화할 수 있음
  • 생성자에서 값을 정하기 때문에, 인스턴스마다 다른 값을 가질 수 있음
  • 이후에는 값을 변경할 수 없음

필드에서 바로 초기화

1
2
3
4
5
6
7
8
9
public class FieldInit {
    final int value = 10;
    
    /*public FieldInit(int value) {
        //초기값이 이미 들어가 있으면 생성자로 못넣음
        this.value = value;
        //final은 값이 한번 세팅되면 끝
    }*/
}
1
2
3
4
FieldInit f1 = new FieldInit();
FieldInit f2 = new FieldInit();
System.out.println(f1.value); // 10
System.out.println(f2.value); // 10
  • 여러 인스턴스를 생성하더라도 value는 항상 동일한 10
  • 이 경우, 불필요한 중복 메모리 사용이 발생할 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);

        //funal 필드 - 필드 초기화
        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);

    }
}

생성자 초기화
10
20
필드 초기화
10
10
10
  • ConstructInit은 생성자로 final 필드 초기화함 → 인스턴스마다 값 다름
  • FieldInit은 필드에서 직접 초기화함 → 인스턴스 모두 같은 값 가짐

필드 초기화 vs 생성자 초기화

  • 생성자 초기화: 인스턴스마다 다른 값 할당 가능함. 유연성 높음
  • 필드 초기화: 인스턴스마다 같은 값 반복 생성됨 → 중복 + 메모리 낭비
    • JVM이 내부 최적화할 수도 있지만, 명시적으로 static 쓰는 게 좋음
  • static final 상수: 클래스 로딩 시 한 번만 생성됨 → 효율적, 중복 없음
✅ 필드 초기화는 인스턴스마다 같은 값을 계속 생성하므로, 메모리 낭비를 줄이고자 할 경우 static을 붙이는 것이 좋음

static final 상수

1
2
3
public class FieldInit {
    static final int CONST_VALUE = 10;
}
  • static + final 조합은 클래스가 공유하는 상수를 의미
  • 모든 인스턴스가 아니라 클래스 로딩 시점에 딱 한 번 초기화되며, 공유
  • 메모리 낭비 없이, 코드의 명확성을 높임
  • 상수이므로 대문자 + 언더스코어( _ )로 변수명을 짓는 것이 자바 관례

왜 static final을 쓰는지?

매직 넘버(Magic Number)란? 코드 안에 의미 없는 숫자(또는 문자)를 직접 써놓는 걸 말함.

이 숫자가 무슨 의미인지 코드만 봐선 알기 어렵고 여러 군데에서 쓰이면 변경 시 실수 위험도 큼.

1
if (currentUserCount > 1000) { ... } // ← 1000이 뭘 의미하는지 모름

이럴 때 static final 상수를 사용하면 코드가 더 명확해짐:

1
if (currentUserCount > Constant.MAX_USERS) { ... }
  • MAX_USERS라는 이름 덕분에 의미 명확해짐
  • 같은 값이 반복되더라도 한 곳만 바꾸면 전체 반영됨 → 유지보수 편함
  • 실무에서는 설정값, 제한값, 기준값 등은 거의 다 public static final 상수로 관리함
  • 같은 값이 반복된다면 final만 쓰지 말고, static final로 상수화하는 것이 좋음

final 변수와 참조형 타입

기본형 변수에서의 final

1
2
final int number = 10;
// number = 20; // 컴파일 오류
  • final이 붙은 기본형 변수는 값 자체를 변경할 수 없음
  • → 10이란 값을 고정시킨 것

참조형 변수에서의 final

1
2
3
4
final Data data = new Data();
// data = new Data(); // 참조값 변경 불가
data.value = 10;      // 객체 내부 값은 변경 가능
data.value = 20;      // 객체 내부 값은 변경 가능
  • final이 붙은 참조형 변수는 객체의 참조값 자체는 변경 불가
  • 하지만 참조 대상 객체의 내부 값은 변경 가능
  • 즉, 다른 객체를 가리키게는 못하지만, 현재 가리키는 객체의 필드는 수정할 수 있음
✅ data는 Data 객체의 주소를 고정한 것. 하지만 data.value 는 final 이 아니기 때문에 자유롭게 바꿀 수 있음!

실무에서의 예시

불변 id 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Member {
    private final String id;
    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; // 가능
    }
}
1
2
3
4
5
6
7
8
9
10
11
public class MemberMain {
  public static void main(String[] args) {
    Member member = new Member("myId", "kim");
    member.print();
    member.changeData("myId2", "seo");
    member.print();
  }
}

id:myId,name:kim
id:myId,name:seo
  • idfinal이기 때문에 생성자에서만 한 번 할당 가능
  • name은 변경 가능
  • 이렇게 하면 중요한 식별자(id)실수로 변경되는 걸 방지할 수 있음
구분 final 의미
기본형 변수 값 자체를 변경할 수 없음
참조형 변수 참조 주소 변경 불가 (다른 객체로 바꿀 수 없음)
하지만 내부 필드는 변경 가능  
실무 활용 중요한 값(id 등)은 final로 선언해서 불변성 보장하고, 컴파일 타임에 실수 차단 가능
final은 단순한 키워드가 아니라 **의도를 명확히 표현하고, 실수를 줄이는 중요한 설계 도구**


출처: 김영한의 실전 자바 - 기본편

This post is licensed under CC BY 4.0 by the author.

Trending Tags