Java/자바개념

final과 final class

_Jin_ 2024. 3. 8.
 

GitHub - KOSA-Group-04-Study/JAVA-Study

Contribute to KOSA-Group-04-Study/JAVA-Study development by creating an account on GitHub.

github.com

 

final과 final class

자바스터디 2회 주제인 ‘클래스’를 맞이하여 클래스의 모든 주제를 정리하기엔 양이 너무 많아 final이 클래스와 필드를 구성함에 적절한 사용법를 모른다고 판단하여, final 및 final 클래스를 필자의 스터디 주제로 선정하였다.

따라서,간단하게 final이 무엇인지 살펴보고, 필드/메서드/인스턴스/클래스에 붙는 경우 어떤 기능을 담당하는 지 정리하였다.

또한, final의 사용 사례를 보며, 사용되는 패턴을 통해 해당 기능이 필요한 경우를 고려해 final에 대한 이해를 높여보자.

◎ final

final은 ‘최종적인’이라는 뜻을 가지고 있다.

따라서 대략적인 의미에서 접근하자면, 불변으로 파악되고 절대 변하지 않는다는 것으로 해석할 수 있는데, 엄밀히 말하자면, 재할당 / 재정의 할 수 없도록 만드는 의미에 가깝다.

즉 선언 및 초기화 이후에는 변경이 불가능하도록 제한을 두는 기능을 하는 것이 final 키워드인 것이다.

좀 더 구체적으로 상황들을 고려해보자.

final이 붙는 다양한 경우를 고려하여 내용을 파악해보자면, 다음과 같다.

1) final 필드

  • 초기화 이후 값 변경 불가
  • 필드 선언 시점이나 생성자에서 초기화를 해야한다.

2) final 메서드

  • 자식 클래스에서 오버라이딩 불가하다.

3) final 인스턴스

  • 다른 값을 넣는 것 불가하다.
  • 필드는 변경 가능하다.

4) final 클래스

  • 상속 불가(자식 클래스를 만들 수 없다.)

 

사용 예시 1

// public 클래스를 생성하였고 안에는 final 멤버 필드인 MEMBER와 no가 있다.
// 또한 task에도 final이 붙어있다.
public class School {
    protected static final String MEMBER = "우리는 KOSA 4반이다.!";

    private final int no;
    public String name;

    // no가 final이고 초기화되지 않았으므로 생성자에서는 받아줘야함
    public School (int no, String name) {
        this.no = no;
        this.name = name;
    }

    public void changeNo () {
        // 불가능하다.
        this.no++;
    }

    public final void task () {
        System.out.println("코딩, 자바 코딩ing");
    }
}

 

 

사용 예시 2

// School_1은 School의 상속을 받는 final 인스턴스이다. 
// 해당 자식 객체를 이용하여 부모 객체의 task매서드를 오버라이딩을 시도하면, 컴파일 에러가 난다.
// final 메서드이여서 재정의 불가능
public final class School_1 extends School {
		
        public School_1(int no, String name) {
        super(no, name);
    }

    // 불가
    public void task () {
        System.out.println("코딩, 파이썬 코딩ing");
    }
}

 

 

사용 예시 3

// School 객체의 static이면서 final인 멤버변수 CREED를 받는다.(static이기에 바로 클래스에서 
// 접근가능 그러나 바로 접근하여 값을 변경하려고 하면, 컴파일 에러가 발생한다.(final)
// 한편, shool_2인 final 인스턴스 변수는 생성자에서 초기화로
// 할당 받은 이후에는 변경이 불가능하다. 다만, 내부의 필드 값은 변경 가능하다.

		
  String Creed = School.MEMBER;
  School.MEMBER = "우리는 KOSA 3반이다.!"; // 불가

  final School school_2 = new School(5, "삼성");

	//  불가
  school_2 = new School(2, "LG");
  //  요소 변경은 가능
  store1.name = "선릉";

 

 

◎ final의 필요 이유(사용 사례)

설계적 관점(클래스를 만드는 시각)에서 final이 필요한 경우가 있을 수 있다.

예를 들어, final 클래스를 사용하여 클래스를 상속받지 못하게 해야하는 경우가 고려할 수 있다.

대표적인 예시로 자바의 System클래스는 아래처럼 이뤄져있다.

public final class System {
    private System() {} // private 생성자
    static PrintStream out;
    static PrintStream err;
    static InputStream in;
    ...
}

static메서드를 사용하여, 해당 클래스에서 메소드를 직접 호출할 수 있도록 만들었다.

 

그리고 클래스는 final 키워드를 사용하여 상속이 불가하다. 이처럼 설계한다면, System 클래스에 접근은 가능하지만 상속은 불가하다. 게다가 해당 클래스를 싱글톤으로 구성하여, 상속과 더불어 인스턴스화를 막았다.

 

이런 클래스를 보통 유틸리티 클래스라고 부르며, 매서드를 직접 호출할 수 있는 기능을 제공하기 위해 static을 붙인 것이다. 메소드를 편하게 활용하기 위한 용도의 클래스로 인스턴스화는 필요없기에 싱글톤으로 만들고 final로 상속을 막아둔 것이다. ( 상속과 인스턴스화는 다르다 )

 

만약 상속 가능한 클래스 내의 특정 메서드 내용의 사용을 강제하고 싶다면,

public class System {
    private System() {} // private 생성자
    static final PrintStream out;
    static PrintStream err;
    static InputStream in;
    ...
}

위와 같은 코드로 초기화 된 out 메서드의 오버라이딩을 막을 수 있다. (여전히 싱글톤으로 인스턴스화는 불가능)

 

또한

public class Math {
	static final int PI = 3.14;
	...
}

위의 코드처럼 Math라는 클래스의 구성에서 PI 변수가 필요할 경우 3.14라는 값이 바뀔 이유도 없으며, 자주 필요한 경우 클래스를 통해 직접 호출하기 편하도록 static final 키워드의 변수로 만들 수 있다.

 

관례적으로 상수를 선언하는 경우, static final을 붙이며 이는 일관된 사용을 위한 final과 더불어 static이 새로운 인스턴스가 만들어질 때마다 새로운 메모리에 초기화하지 않고, 하나의 메모리 공간만 사용하여 효율적이기 때문이다.

 

※ static 사용 이유( 좀 더 자세히 )

만약 상수를 final int PI의 형태로 만든다면, (1) 선언 시점이나 (2) 생성자를 통한 초기화가 강제된다. 만약 인스턴스마다 PI라는 변수가 다른 상수값으로 필요하다면 생성자를 통한 초기화가 필요하겠지만, PI라는 변수의 특성을 고려하여 배제하겠다.

 

그럼 (1) 선언 시점에 Math 클래스 내에서

final int PI =3.14;

의 형태로 클래스가 상수값을 강제한다면, 같은 기능으로 사용되기는 한다.

하지만, static을 버림으로 직접 호출이 가능하다는 편리함과 인스턴스 마다 상수값을 따로 참조한다는 메모리 효율성을 잃는다. 따라서 대게 상수로 사용한다면, static final을 붙이는 것이 관례적이다.

 

불변하는 구조체가 필요한 경우도 고려할 수 있는데, 아래의 코드를 먼저 살펴보자.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Apartment {
    // final 리스트
    private final List<String> numberOfHouseholds;

    public Apartment (List<String> list) {
        // 생성자에서 새로운 ArrayList로 복사하여 초기화
        this.numberOfHouseholds= new ArrayList<>(list);
    }

    // final 리스트를 읽기 위한 메서드 제공
    public List<String> getNumberOfHouseholds() {
        return numberOfHouseholds;
    }
}

아파트라는 클래스에 private final을 사용한 리스트로 아파트의 전체 가구 정보를 받는 구조이다.

 

단, 아파트의 구성원이 변경되지 않는다는 가정으로 각 아파트 인스턴스 구성원 정보를 생성자에서 주입받아 초기화한다.

따라서 각 아파트마다 변하지 않는 가구원 정보를 할당한 클래스의 구조이다.

 

이처럼 final 키워드는 한번 값이 할당된 이후 변경 제한 기능을 부여해준다. 따라서 해당 기능을 필요에 따라 적절하게 사용하는 패턴 및 활용법에 대해 앞으로 추가적인 학습이 필요하겠다.

 

 

🙏 잘못된 내용이 있다면 알려주심 감사하겠습니다.

'Java > 자바개념' 카테고리의 다른 글

Optional 기초 사용법  (1) 2025.01.18
상속  (0) 2024.03.14
배열과 제어문  (1) 2024.03.08
자바의 메모리 구조  (0) 2024.02.27

댓글