본문 바로가기

Programming Language/Java

[JAVA] final 키워드에 대하여

[JAVA] final 키워드에 대하여


개요

  JAVA의 final 키워드에 대해 알아보고, 미처 생각하지 못했던 final 배열 변수의 값의 수정에 대해서 고민해본다.

 

목차

 

소개

내가 알고있던 final 키워드

 

기존에는 final 키워드를 사용하면, 단순히 값을 재할당하지 못해 변경이 불가능하다고만 생각하였다. 그래서, 코드를 구현할 때 재사용을 하면 안되는 변수에 final 키워드를 사용해 왔었다.

 

하지만 코드를 구현하면서 List형의 변수에 요소들이 추가되는것을 보며 아래와 같은 의문을 갖게 되었다.

 

"final 키워드는 정말 변수의 값이 재할당되는것을 막아줄까?"

 

대답을 해보자면, 변수의 값이 재할당 되는 것은 막아준다. 

final int value = 1;
value = 3;	// Cannot assign a value to final variable 'number'

 

하지만, 실제 변수가 갖는 값의 상태가 바뀌는것은 막지 못한다. 아래와 같은 상황에서 List의 변수에 final이 붙었음에도 불구하고 변수의 상태가 바뀌는것에 의문을 품을수 있다. 만약 이러한 의문이 든다면값의 변경과 값의 상태의 변경에 대해 이해를 할 필요가 있다. 이와 관련되어 이해하기 쉽게 설명되어 있는 글이 있어 읽어본다면 이해해 도움이 될것이다.

final List<Integer> values = new ArrayList<>(Arrays.asList(1, 2, 3));
System.out.println(values); // [1, 2, 3]

values.add(4);
System.out.println(values); // [1, 2, 3, 4]

 

그렇다면, final 키워드는 무엇이고 어떻게 사용되는 것일까?

 

final 키워드는 크게 아래와 같은 3가지의 특징을 가진다.

 

1. 변수에 사용된다면 한번 메모리에 할당된 값은 재할당되지 못한다.

 

이때, 위에서와 같은

"List 형태의 변수의 상태가 변하게 되는 것은 왜 막지 못할까?"

하는 의문을 가질 수 있다.

하지만, 아래의 개념을 생각하면 어렵지 않게 의문을 해결할 수 있게 된다.

 

1. 자바의 클래스는 Stack 영역에 생성되고, 객체(인스턴스)는 Heap 영역에 생성된다.

2. Class A = new Class()는 Stack 영역의 변수(A)에 Heap 영역 객체(new Class())의 주소값을 넣는것이라고 할 수 있다.

A 변수Stack 영역에, new Class() 객체는 Heap 영역에 생성된다.
A는 Stack 영역에 있으므로, Heap 영역에 있는 new Class()를 찾을 수 없어 Heap 영역의 new Class()의 주소값을 갖게 된다.
ex) Class A = new Class() 객체의 Heap 영역주소(3a5b352d)

 

아래의 List 형태의 변수의 주소값들을 추적해보면 더욱 이해가 될 것이다.

final int[] A = new int[5]; // = 3a5b352d = 리스트의 시작 주소

 

이 때 A는 final 임으로 당연히 A의 변수값에 새로운 주소값을 재할당 할 수는 없다.

A = null;	// Cannot assign a value to final variable 'A'

 

하지만 A[0]은 A에 저장되어 있는 값인 객체의 시작 주소값과는 연관이 없게 됨으로 값을 상태를 수정할 수 있게되는 것이다.

A[0] = 3;	// 에러가 발생하지 않는다.
System.out.println(System.identityHashCode(A));		// 3a5b352d
System.out.println(System.identityHashCode(A[0]));	// 307b4058

 

정리하자면, final 변수는 값의 재할당을 막아준다. 따라서 Stack 영역의 변수에서 저장하고 있는 Heap 영역 객체의 주소값을 바꿀 수 없다. 하지만, List 형태의 경우 배열의 각 원소값들은 Stack 영역에서의 변수가 저장하고 있는 배열의 시작 주소값과는 다른 값이기 때문에 값의 상태의 변경이 가능하게 되는 것이다.

 

2. 메소드에 사용된다면 해당 메소드는 Override(재정의) 할 수 없다.

class Parent {
    public final void testMethod() {
        System.out.println("I want to override in a child.");
    }
}

class Child extends Parent {
    // 'testMethod()' cannot override 'testMethod()' in 'Parent'; overridden method is final
    public void testMethod() {
        System.out.println("But, I cannot override you.");
    }
}

 

이를 통해 다른 메소드가 재정의하여 사용할 수 없도록 하고자 하는 메소드에 final을 붙혀 사용할 수 있게 된다.

 

3. 클래스에 사용된다면 해당 클래스는 Inherit(상속) 할 수 없다.

class Parent {
    public final void testMethod() {
        System.out.println("I want to override in a child.");
    }
}

// Cannot inherit from final 'Parent'
class Child extends Parent {
    public void printMethod() {
        System.out.println("But, I cannot override you.");
    }
}

 

이를 통해 해당 클래스가 상속되어 사용되면 안되는 경우 final을 붙혀 사용할 수 있게 된다.