Java

final 키워드

final keyword

 

final 키워드를 떠올릴 때면 그냥 상수로만 생각할 때가 종종 있다.

final을 변수, 메소드, 클래스에 선언하면 조금씩 할 수 있는 부분들이 제한 된다.

 

자바에서 final 키워드는 여러 컨텍스트에서 오로지 한 번 할당될 수 있는 entity를 정의할 때 사용된다. [위키피디아]

  • 재 정의를 못하게 하는것
  • 한 번 인스턴스가 할당되면 절대 재할당 될 수 없는것

final 키워드는 총 3가지에 적용할 수 있다.

  1. final 변수
    원시타입,  객체타입,  클래스 필드,  메소드 인자
  2. final 메소드
  3. fianl 클래스

final 키워드 활용 및 사용법

final 변수(필드)

해당 변수가 생성자나 대입연산자를 통해 한 번만 초기화 가능함을 의미한다. 상수를 만들 때 응용한다.

1. 원시타입

  • 로컬 원시 변수에서 final로 선언하면 한번 초기화된 변수는 변경할 수 없는 상수값이 된다.

자바에서 상수를 정의하는 방법: 필드 맴버에 final 키워드를 덧붙여 상수를 정의한다.

public class FinalFieldClass {
  final int ROWS = 10; // 상수 정의, 이때 초깃값(10)을 반드시 설정
                                             // 한번 선언되면 변경할수 없다.
    void f() {
    int[] intArray = new int[ROWS]; // 상수 활용
    ROWS = 30; // 컴파일 오류. final 필드 값은 상수으므로 변경할 수 없다.
  }
}
  • final로 상수 필드를 정의할 때 선언 시에 초기값을 지정 해야 한다.
  • 상수 필드는 한 번 정의되면 값을 변경할 수 없다.
  • final 키워드만을 사용하여 상수를 만들면 FinalFieldClass의 객체들만 사용할 수 있는 상수가 된다.

 

2. 객체 타입

  • 객체 변수에 final로 선언하면 그 변수에 다른 참조 값을 지정할 수 없다.
  • 원시 타입과 동일하게 한번 쓰여진 변수는 재변경 불가하다.
    단, 객체 자체가 immutable(불변)하다는 의미는 아니다. 객체의 속성은 변경 가능하다.
public class FinalFieldClass {

  final Pet cat = new Pet();
    // cat = new Pet(); // 다른 객체로 변경할 수 없다
    cat.setWeight(15); //객체 필드는 변경할 수 있다.
}

 

3. 메소드 인자(argument)

  • 메소드 인자 final 키워드를 붙이면, 메소드 안에서 변수값을 변경할 수 없다.
public class Pet{
    int weight;
    public void setWeight(final int weight) {
        //weight = 1; //final 인자는 메서드안에서 변경할 수 없음
    }
}

 

4. 맴버 변수

  • 클래스의 맴버 변수에 final로 선언하면 상수값이 되거나 write-once 필드로 한 번만 쓰이게 된다.
  • final로 선언하면 초기화되는 시점은 생성자 메소드가 끝나기 전에 초기화 된다.
    하지만, static이냐 아니냐에 따라서도 초기화 시점이 달라진다.

static final 맴버 변수 (static final int x = 1)

  • 값과 함께 선언시
  • 정적 초기화 블록에서 (static initialization block)

instance final 맴버 변수 (final int x = 1)

  • 값과 함께 선언시
  • 인스턴스 초기화 블록에서 (instance initialization block)
  • 생성자 메서드에서

 

4_1. 인스턴스 초기화 블록 vs 정적 초기화 블록

정적 초기화 블록과 인스턴스 초기화 블록의 차이점을 간단하게 알아보자

표)

인스턴스 초기화 블록

  • 객체 생성할때마다 블록이 실행됨
  • 부모 생성자이후에 실행됨
  • 생성자보다 먼저 실행됨

정적 초기화 블록

  • 클래스 로드시 한번만 블록이 실행됨
public void initializeBlockTest() {
    Cat.s_value = 5; //static 초기화 블록 호출됨
    System.out.println("s_value: " + Cat.s_value);

    System.out.println("");
    System.out.println("Cat 객체 생성1");
    Cat cat1 = new Cat();=

    System.out.println("");=
    System.out.println("Cat 객체 생성2");
    Cat cat2 = new Cat();
}

public class Pet {
    public Pet() {
        System.out.println("super construtor : Pet");
    }
}

정적 초기화 블록은 클래스가 로딩되는 시점에 한 번만 호출되고 static 블록 안에서 static 맴버 변수를 초기화 할 수 있다.

결과)

인스턴스 초기화 블록은 객체를 생성할 때마다 호출되고 자식 객체의 생성자가 호출되기 전에 그리고 부모 생성자 이후에 실행된다.

public class Cat extends Pet {
    final int i_value;
    static int s_value;

    {
        System.out.println("instance initializer block");
        i_value = 3;
        System.out.println("i_value: " + i_value);
        System.out.println("s_value: " + s_value);
    }

    static {
        System.out.println("static initializer block");
//        System.out.println("i_value: " + i_value); //static 블록에서 필드 접근 안됨
        System.out.println("s_value: " + s_value);
    }

    public Cat() {
        System.out.println("contructor: Cat");
    }
}

 


 

프로그램 전체에서 공유하여 사용할 수 있는 상수

  • final 키워드 + static
  • Ex) public static final
class ShareClass {
  public static final double PI = 3.141592653589793;
}
/* ShareClass 내에서의 사용 */
double area = PI * radius * radius;
/* 다른 클래스에서의 사용 */
double area = ShareClass.PI * radius * radius;

 


 

final 메소드

메소드 앞에 final 속성이 붙으면, 해당 메소드를 오버라이드하거나 숨길 수 없음을 의미한다.

public class SuperClass {
  protected final int finalMethod() { ... }
}

class ChildClass extends SuperClass { // SuperClass를 상속 받음
  protected int finalMethod() { ... } // 컴파일 오류
}
  • 자식 클래스가 부모 클래스의 특정 메서드를 오버라이딩하지 못하게 하고 무조건 상속받아 사용하도록 하고자 한다면 final로 지정하면 된다

참고)

 

private 메소드와 final 클래스의 모든 메소드는 명시하지 않아도 final 처럼 동작한다.

이유)

왜냐면 오버라이드가 불가능하기 때문이다.

하지만 private 메소드에 여전히 final 명시는 가능하다.

불필요한가? 사실 그렇다.

그래도 일단 의미는 구분된다.

  • private: 자식 클래스에서 안 보입니다. (오버라이드 물론 금지)
  • final: 자식 클래스에서 보이지만, 오버라이드가 금지됩니다.

이렇게 불필요한 명시를 그렇다고 특별 취급해서 막을 필요는 없기 때문에, 컴파일러가 에러를 내거나 경고하지는 않는다.

비슷한 예로 인터페이스의 메소드에 public을 붙이거나, final 클래스 메소드에 final을 붙이는 등의 경우도 문제가 생기지는 않는다.

 


 

final 클래스

final이 클래스 이름 앞에 사용되면 클래스를 상속받을 수 없음을 지정한다.

  • 해당 클래스는 상속할 수 없음을 의미한다. 문자 그대로 상속 계층 구조에서 ‘마지막’ 클래스이다.
  • 보안과 효율성을 얻기 위해 자바 표준 라이브러리 클래스에서 사용할 수 있는데, 대표적으로 java.lang.System, java.lang.String 등이 있다.
final class FinalClass {
    ...
}

class OtherClass extends FianlClass{ //컴파일 오류
    ...
}
  • FinalClass를 상속받아 OtherClass를 만들 수 없다.

    그냥 클래스 그대로 사용해야 한다.

  • Util 형식의 클래스나 여러 상수 값을 모아둔 Contants 클래스를 final로 선언한다.

 

상수 클래스

  • 상수 값을 모아준 클래스는 굳이 상속해서 쓸 필요 없다.
public final class Constants {
    public static final int SIZE = 10;
}
//public class SubConstants extends Constants {
//}

 

Util 형식의 클래스

  • JDK에서 String도 final 클래스로 선언되어 있다.
    자바의 코어 라이브러리이기 때문에 side-effect가 있으면 안된다.
    다른 개발자가 상속을 해서 새로운 SubString을 만들어 라이브러리로 다른 곳에서 사용하게 되면 유지보수, 정상 실행 보장이 어려워질 수 있다.
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
}

 


 

final, finally, finalize의 차이

final 키워드

변수나 메서드 또는 클래스가 ‘변경 불가능’하도록 만든다.

  • 원시(Primitive) 변수에 적용 시
    : 해당 변수의 값은 변경이 불가능하다.
  • 참조(Reference) 변수에 적용 시
    : 참조 변수가 힙(heap) 내의 다른 객체를 가리키도록 변경할 수 없다.
  • 메서드에 적용 시
    : 해당 메서드를 오버라이드할 수 없다.
  • 클래스에 적용 시
    : 해당 클래스의 하위 클래스를 정의할 수 없다.

finally 키워드

try/catch 블록이 종료될 때 항상 실행될 코드 블록을 정의하기 위해 사용한다.

  • finally는 선택적으로 try 혹은 catch 블록 뒤에 정의할 때 사용한다.
  • finally 블록은 예외가 발생하더라도 항상 실행된다.
    단, JVM이 try 블록 실행 중에 종료되는 경우는 제외한다.
  • finally 블록은 종종 뒷마무리 코드를 작성하는 데 사용된다.
  • finally 블록은 try와 catch 블록 다음과, 통제권이 이전으로 다시 돌아가기 전 사이에 실행된다.
public static String lem() {
  System.out.println("lem");
  return "return from lem";
}
public static String foo() {
  int x = 0;
  int y = 5;
  try {
    System.out.println("start try");
    int b = y / x;
    System.out.println("end try");
    return "returned from try"
  } catch (Exception ex) {
    System.out.println("catch");
    return lem() + " | returned from catch";
  } finally {
    System.out.println("finally");
  }
}
public static void bar() {
  System.out.println("start bar");
  String v = foo();
  System.out.println(v);
  System.out.println("end bar");
}
// 출력
public static void main(String[] args) {
  bar();
}
//결과
start bar
start try
catch
lem
finally
return from lem | returned from catch
end bar
https://gmlwjd9405.github.io/2018/08/06/java-final.html

finalize() 메소드

쓰레기 수집기(GC, Garbage Collector)가 더 이상의 참조가 존재하지 않는 객체를 메모리에서 삭제하겠다고 결정하는 순간 호출된다.

  • Object 클래스의 finalize() 메서드를 오버라이드해서 맞춤별 GC를 정의할 수 있다
protected void finalize() throws Throwable {
/* 파일 닫기, 자원 반환 등등 */
}

 

정리)

final 키워드는 다음과 같이 상태 변경이 안되도록 한다.

  • final 변수, 인자: 값이 변경되지 않도록 만듬
  • final 클래스: 클래스를 상속하지 못하도록 만듬
  • final 메소드: 메소드가 오버라이드되지 못하도록 만듬

주의 할점)

final 변수는 초기화 이후 값 변경이 발생하지 않도록 만든다.

다음 코드를 보면 List에 final을 선언하여 list 변수의 변경은 불가능하다.

하지만 list 내부에 있는 변수들은 변경이 가능하여 문자열을 계속 추가할 수 있다.

final List<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");

따라서, final 변수의 변경을 막아주지만 final 변수 내부에 갖고 있는 변수들은 변경이 가능하다는 점을 기억해야된다.

 

 

 

 

 

참고 및 출처)

https://gmlwjd9405.github.io/2018/08/06/java-final.html

https://advenoh.tistory.com/13

https://djkeh.github.io/articles/Why-should-final-member-variables-be-conventionally-static-in-Java-kor/

'Java' 카테고리의 다른 글

Enum  (0) 2020.12.18
Try-with-resource  (2) 2020.11.01
static(정적) 키워드  (0) 2020.10.30
지역변수, 전역변수, 인스턴스 변수, 클래스 변수  (0) 2020.10.01
기본자료형 & 참조자료형  (0) 2020.10.01