자바의 final 예약어는 클래스, 메서드, 변수에 사용가능하며 사용 시 해당 대상에 대해 수정 및 확장이 불가하다. 예시를 통해 각 상황에서 어떻게 사용되는지 자세히 확인해보자.
final class
public final class Car {
private String color = "blue";
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
public class Bus extends Car {
}
final 클래스인 Car 클래스와 이를 상속받으려는 Bus 클래스가 있다. Car 클래스를 상속 받으려고 했더니 구문 오류와 함께 다음과 같은 메세지를 확인할 수 있었다.
The type Bus cannot subclass the final class Car
Bus 클래스는 Car 클래스의 서브 클래스가 될 수 없다는 뜻이다. 앞서 final 예약어를 사용하면 수정이나 확장이 불가능하다고 설명했다. 이러한 특성에 따라 final 예약어를 선언한 클래스는 상속이 불가능하다.
하지만 확장이 불가능하다고 해서 final 클래스 객체 내부가 변경 불가능하다는 뜻은 아니다. 해당 클래스의 상속을 막을 뿐 객체 내부의 필드는 자유롭게 변경 가능하다는 것을 기억하자.
Car car = new Car();
System.out.println(car.getColor());
car.setColor("yellow");
System.out.println(car.getColor());
blue
yellow
이렇게 직접 예제를 만들어 확인하지 않더라도 우리는 이미 많은 곳에서 final 클래스를 사용하고 있다. 바로 자바 API가 그 예시이다. 자바 API 내 많은 클래스들이 final 로 선언되어 있다.(전부는 아니다. ex) Thread )
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
...
}
String 클래스는 자바 라이브러리 중에서도 많이 사용되는 클래스 중 하나다. 그런데 만약 이 클래스에 final 예약어가 없다면 어떻게 될까? 미리 정의되어 있던 String 클래스는 무분별하게 상속되어 기존에 의도한 바와는 다른 방향으로 변경되고 작업 결과를 예측할 수 없게 될 것이다. final 예약어를 사용하여 불변 클래스로 만듦으로써 어디에서 사용하던 일관된 동작을 보장할 수 있다.
final method
final 키워드를 사용하여 메서드를 선언한 것을 final 메소드라고 하며, final 메소드는 재정의될 수 없다. 클래스 확장을 금지할 필요는 없지만 일부 메소드만 재정의를 방지하고 싶을 때 사용한다.
public class Animal {
final void sound() {
System.out.println("...");
}
}
class Cat extends Animal {
@Override
final void sound() {
System.out.println("meow");
}
}
>javac Animal.java
Animal.java:10: error: sound() in Cat cannot override sound() in Animal
final void sound() {
^
overridden method is final
1 error
Animal 클래스를 상속받은 Cat 클래스에서 울음소리를 출력하는 sound() 메서드를 재정의하려고 하면 컴파일 에러가 발생하면서 위와 같은 메세지를 확인할 수 있다. final 메서드를 포함하고 있더라도 클래스 자체는 일반적인 클래스이므로 상속 자체는 가능하다. 하지만 final 메서드는 재정의할 수 없다.
final Variable
final 키워드를 사용한 변수는 필수로 초기화를 해줘야 한다. 그렇지 않으면 구문 오류가 발생하여 다음과 같은 메세지를 확인할 수 있다.
The blank final field MINIMUM may not have been initialized
final 변수는 초기화나 할당문을 통해 한 번만 초기화될 수 있는데 그 방법으로 3가지가 있다.
- final 변수 선언과 동시에 초기화 (가장 일반적인 접근 방식)
- 선언 시 초기화하지 않은 경우, 인스턴스 초기화 블록 또는 생성자를 통해 초기화
** 생성자가 2개 이상인 경우 모든 생성자에서 초기화 필수 - static final 변수인 경우 static 블록에서 초기화
final 변수는 본질적으로는 상수가 되어 초기화되면 값을 재할당할 수 없다. 이는 기본 타입, 참조 타입 모두 마찬가지이다.
public static void main(String[] args) {
final double PI = 3.141592653589793;
PI = 3.14;
System.out.println("PI :: " + PI);
}
>javac GFG.java
GFG.java:36: error: cannot assign a value to final variable PI
PI = 3.14;
^
1 error
final 변수 PI 를 초기화하고 값을 재할당하면 컴파일 에러가 발생하면서 위와 같이 값 할당이 불가하다는 메세지를 확인할 수 있다.
일반 변수와 final 변수의 유일한 차이점은 일반 변수에는 값을 다시 할당할 수 있지만 final 변수의 값은 변경할 수 없다는 것이다. 따라서 final 변수는 실행 내내 일정하게 유지하려는 값에만 사용해야 한다.
그러나 클래스의 경우에서도 그랬듯 final 변수가 참조 타입인 경우 재할당은 불가하지만 해당 객체 내부의 상태는 변경될 수 있다.
final StringBuilder sb = new StringBuilder("apple");
System.out.println(sb);
sb.append(" is delicious");
System.out.println(sb);
apple
apple is delicious
final 변수는 메서드의 매개변수로도 사용가능하다. 다만 메서드 내에서 final 변수의 값을 변경하는 것은 불가능하므로 마찬가지로 컴파일 에러가 발생한다.
public countNum(final int num) {
num = 2;
}
>javac GFG.java
GFG.java:26: error: final parameter num may not be assigned
num = 2;
^
1 error
그 외에도 선언 시 명명규칙에 따라 변수명을 모두 대문자로 나타내고 밑줄(_)로 구분하여 사용한다는 특징이 있다.
final 키워드의 장점
- 불변성을 보장한다. 한 번 할당되면 변경할 수 없으므로 실수 혹은 악의적으로 무분별하게 수정될 수 없도록 보장한다.
- 성능면에서 이점이 있다. JVM이 특정 값이나 참조를 변경할 수 없음을 알고 있을 때 코드를 보다 효과적으로 최적화할 수 있어 final 예약어를 적절히 사용하면 성능 향상에 도움이 된다.
- 보안 강화. 악성 코드가 민감한 데이터나 동작을 수정하는 것을 방지하여 보안 강화에 도움이 될 수 있다.
주의
이미 final 로 정의한 클래스 내 속성에서 문제가 발생한 경우, 메서드를 재정의하고 문제를 해결하기 위해 클래스를 확장하는 방법을 사용할 수 없다. 즉, 객체지향 프로그래밍의 장점 중 하나인 확장성을 잃게 되므로 final 클래스 생성 시 주의를 기울여야 한다.
참고
'Java' 카테고리의 다른 글
Gradle 과 JVM Crash 에 대하여 (0) | 2024.07.09 |
---|---|
String 객체 간 '+' 연산에 대하여 (0) | 2024.06.22 |
[Java] 환경변수 변경하기 (0) | 2023.06.21 |
[Java] String sample = “” VS String sample = new String(””) (0) | 2022.06.15 |
[Java] List 와 hashmap 의 Null (0) | 2022.06.07 |