1. 개요

Java SE 15 릴리스 에는 미리 보기 기능으로 봉인된 클래스( JEP 360 )가 도입되었습니다 .

이 기능은 Java에서 보다 세분화된 상속 제어를 활성화하는 것입니다. 봉인을 통해 클래스와 인터페이스는 허용되는 하위 유형을 정의할 수 있습니다.

즉, 클래스 또는 인터페이스는 이제 이를 구현하거나 확장할 수 있는 클래스를 정의할 수 있습니다. 도메인 모델링 및 라이브러리 Security 강화에 유용한 기능입니다.

2. 동기

클래스 계층 구조를 사용하면 상속을 통해 코드를 재사용할 수 있습니다. 그러나 클래스 계층 구조는 다른 목적을 가질 수도 있습니다. 코드 재사용은 훌륭하지만 항상 우리의 주요 목표는 아닙니다.

2.1. 모델링 가능성

클래스 계층 구조의 다른 목적은 도메인에 존재하는 다양한 가능성을 모델링하는 것일 수 있습니다.

예를 들어 오토바이가 아닌 자동차와 트럭에서만 작동하는 비즈니스 도메인을 상상해 보십시오. Java에서 Vehicle 추상 클래스를 생성할 때 CarTruck 클래스만 이를 확장 할 수 있어야 합니다. 그런 식으로 도메인 내 에서 Vehicle 추상 클래스가 오용되지 않도록 하고 싶습니다 .

이 예에서 우리는 알려지지 않은 모든 서브클래스에 대해 방어하는 것보다 알려진 서브클래스를 처리하는 코드의 명확성에 더 관심이 있습니다 .

버전 15 이전에 Java는 코드 재사용이 항상 목표라고 가정했습니다. 모든 클래스는 여러 하위 클래스로 확장할 수 있습니다.

2.2. 패키지-프라이빗 접근 방식

이전 버전에서 Java는 상속 제어 영역에서 제한된 옵션을 제공했습니다.

최종 클래스 에는 서브 클래스를 가질 수 없습니다. 패키지 개인 클래스 는 같은 패키지의 서브 클래스를 가질 수 있습니다.

package-private 접근 방식을 사용하면 사용자는 확장을 허용하지 않고는 추상 클래스에 액세스할 수 없습니다.

public class Vehicles {

    abstract static class Vehicle {

        private final String registrationNumber;

        public Vehicle(String registrationNumber) {
            this.registrationNumber = registrationNumber;
        }

        public String getRegistrationNumber() {
            return registrationNumber;
        }

    }

    public static final class Car extends Vehicle {

        private final int numberOfSeats;

        public Car(int numberOfSeats, String registrationNumber) {
            super(registrationNumber);
            this.numberOfSeats = numberOfSeats;
        }

        public int getNumberOfSeats() {
            return numberOfSeats;
        }

    }

    public static final class Truck extends Vehicle {

        private final int loadCapacity;

        public Truck(int loadCapacity, String registrationNumber) {
            super(registrationNumber);
            this.loadCapacity = loadCapacity;
        }

        public int getLoadCapacity() {
            return loadCapacity;
        }

    }

}

2.3. 접근 가능한 슈퍼클래스, 확장 불가

하위 클래스 세트로 개발된 상위 클래스는 하위 클래스를 제한하지 않고 의도한 사용법을 문서화할 수 있어야 합니다. 또한 제한된 하위 클래스를 갖는 것이 상위 클래스의 액세스 가능성을 제한해서는 안 됩니다.

따라서 봉인된 클래스 뒤에 있는 주요 동기는 슈퍼클래스가 광범위하게 액세스할 수 있지만 광범위하게 확장할 수 없는 가능성을 갖는 것입니다.

3. 창조

봉인된 기능은 Java에서 몇 가지 새로운 수정자와 절인 봉인되지 않은 봉인허가를 도입 합니다.

3.1. 밀폐형 인터페이스

인터페이스를 봉인하려면 해당 선언에 봉인된 수정자를 적용할 수 있습니다 . 을 허용 절은 다음 밀봉 된 인터페이스를 구현하도록 허용하는 클래스를 지정합니다 :

public sealed interface Service permits Car, Truck {

    int getMaxServiceIntervalInMonths();

    default int getMaxDistanceBetweenServicesInKilometers() {
        return 100000;
    }

}

3.2. 봉인된 수업

인터페이스와 유사하게 동일한 봉인된 수정자를 적용하여 클래스를 봉인할 수 있습니다 . 을 허용 어떤이 후 절은 정의되어야한다 확장 또는 구현의 조항 :

public abstract sealed class Vehicle permits Car, Truck {

    protected final String registrationNumber;

    public Vehicle(String registrationNumber) {
        this.registrationNumber = registrationNumber;
    }

    public String getRegistrationNumber() {
        return registrationNumber;
    }

}

허용된 하위 클래스는 수정자를 정의해야 합니다. 추가 확장을 방지하기 위해 final선언 될 수 있습니다 .

public final class Truck extends Vehicle implements Service {

    private final int loadCapacity;

    public Truck(int loadCapacity, String registrationNumber) {
        super(registrationNumber);
        this.loadCapacity = loadCapacity;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 18;
    }

}

허용된 하위 클래스는 또한 sealing 으로 선언될 수 있습니다 . 그러나 밀봉되지 않은 것으로 선언하면 확장이 가능합니다.

public non-sealed class Car extends Vehicle implements Service {

    private final int numberOfSeats;

    public Car(int numberOfSeats, String registrationNumber) {
        super(registrationNumber);
        this.numberOfSeats = numberOfSeats;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 12;
    }

}

3.4. 제약

봉인된 클래스는 허용되는 하위 클래스에 세 가지 중요한 제약 조건을 부과합니다.

  1. 허용된 모든 하위 클래스는 봉인된 클래스와 동일한 모듈에 속해야 합니다.
  2. 모든 허용된 하위 클래스는 봉인된 클래스를 명시적으로 확장해야 합니다.
  3. 모든 허용된 하위 클래스는 final , sealing 또는 non-sealed같은 수정자를 정의해야 합니다 .

4. 사용

4.1. 전통적인 방법

클래스를 봉인할 때 클라이언트 코드가 허용된 모든 하위 클래스에 대해 명확하게 추론할 수 있습니다.

하위 클래스에 대해 추론하는 전통적인 방법은 if-else 문과 instanceof 검사를 사용하는 것입니다.

if (vehicle instanceof Car) {
    return ((Car) vehicle).getNumberOfSeats();
} else if (vehicle instanceof Truck) {
    return ((Truck) vehicle).getLoadCapacity();
} else {
    throw new RuntimeException("Unknown instance of Vehicle");
}

4.2. 패턴 매칭

패턴 일치 를 적용 하면 추가 클래스 캐스트를 피할 수 있지만 여전히 i f-else집합이 필요합니다 .

if (vehicle instanceof Car car) {
    return car.getNumberOfSeats();
} else if (vehicle instanceof Truck truck) {
    return truck.getLoadCapacity();
} else {
    throw new RuntimeException("Unknown instance of Vehicle");
}

i f-else를 사용하면 컴파일러에서 허용된 모든 하위 클래스를 포함한다고 판단하기 어렵습니다. 그런 이유로 우리는 RuntimeException을 던지고 있습니다.

Java의 향후 버전에서 클라이언트 코드는 i f-else 대신 switch을 사용할 수 있습니다 ( JEP 375 ).

유형 테스트 패턴 을 사용 하여 컴파일러는 허용된 모든 하위 클래스가 포함되는지 확인할 수 있습니다. 따라서 기본 절/케이스 가 더 이상 필요하지 않습니다 .

4. 호환성

이제 레코드 및 리플렉션 API와 같은 다른 Java 언어 기능과 봉인된 클래스의 호환성을 살펴보겠습니다.

4.1. 기록

봉인된 클래스는 레코드 와 매우 잘 작동 합니다 . 레코드는 암시적으로 최종이므로 봉인된 계층 구조는 훨씬 더 간결합니다. 레코드를 사용하여 클래스 예제를 다시 작성해 보겠습니다.

public sealed interface Vehicle permits Car, Truck {

    String getRegistrationNumber();

}

public record Car(int numberOfSeats, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

}

public record Truck(int loadCapacity, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

}

4.2. 반사

봉인된 클래스는 또한 두 개의 공개 메소드가 java.lang.Class 에 추가된 리플렉션 API에 의해 지원됩니다 .

  • isSealed의 방법은 반환 사실 지정된 클래스 또는 인터페이스가 봉인되어있는 경우.
  • allowedSubclasses 메서드는 허용된 모든 하위 클래스를 나타내는 객체 배열을 반환합니다.

이러한 방법을 사용하여 예제를 기반으로 하는 주장을 만들 수 있습니다.

Assertions.assertThat(truck.getClass().isSealed()).isEqualTo(false);
Assertions.assertThat(truck.getClass().getSuperclass().isSealed()).isEqualTo(true);
Assertions.assertThat(truck.getClass().getSuperclass().permittedSubclasses())
  .contains(ClassDesc.of(truck.getClass().getCanonicalName()));

5. 결론

이 기사에서는 Java SE 15의 미리보기 기능인 봉인된 클래스 및 인터페이스를 살펴보았습니다. 봉인된 클래스 및 인터페이스의 생성 및 사용, 제약 조건 및 다른 언어 기능과의 호환성에 대해 설명했습니다.

예제에서 우리는 봉인된 인터페이스와 봉인된 클래스의 생성, 봉인된 클래스의 사용(패턴 일치 유무에 관계없이), 봉인된 클래스와 레코드 및 리플렉션 API와의 호환성을 다루었습니다.

항상 그렇듯이 전체 소스 코드는 GitHub에서 사용할 수 있습니다 .

Junit footer banner