새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

14 분 소요

자바 12 버전부터 17버전까지 자바 feature를 정리했습니다.

자바 12~17 feature

자바 12부터 17까지 특징으로 새로운 언어적 기능(문법)이 꽤 추가되었습니다.

Java 문법이 상당 부분 추가된 밑바탕에는 Java 12에서 등장한 Preview Future 개념이 있습니다.

experimental

실험적인

  • JVM 레벨의 기능 초기 버전에 붙습니다.

  • 실험적이기 때문에 위험하거나 불완전합니다.

  • 호환성도 지켜지기 매우 어렵습니다. (25% 정도의 완성도)

  • experimental기능 사용을 원한다면, 전용 플래그 사용으로 (ON/OFF) 합니다.

    // 자바 14버전에 ZGC를 experimental로 적용할 수 있습니다.
    use the JVM flags -XX:+UnlockExperimentalVMOptions -XX:+UseZGC
    

incubating

  • 모듈 형태로 배포되는 실험용 API

  • 모듈과 패키지 앞에 jdk.incubator 가 붙습니다.

  • 호환성이 지켜지지 않을 수 있고, 정식 모듈 채택시 jdk.incubator 가 사라집니다.

  • incubating 기능 사용을 원한다면, JPMS 의존성 설정을 추가합니다.

    module com.api {
        requires jdk.incubator.vector;
    }
    
    import jdk.incubator.vector.IntVector;
    import jdk.incubator.vector.VectorSpecies;
      
    public class VectorIncu {
        public static void main(String[] args) {
            VectorSpecies<Integer> SPECIES = IntVector.SPECIES_256;
            int[] a = {1, 2, 3, 4, 5, 6, 7, 8};
            int[] b = {1, 2, 3, 4, 5, 6, 7, 8};
            int[] c = new int[8];
      
            for (int i = 0; i < a.length; i += SPECIES.length()) {
                var va = IntVector.fromArray(SPECIES, a, i);
                var vb = IntVector.fromArray(SPECIES, b, i);
                var vc = va.add(vb);
                vc.intoArray(c, i);
            }
      
            for (int i : c) {
                System.out.print(i + " ");  // Output: 2 4 6 8 10 12 14 16
            }
        }
    }
    
    // console
    WARNING: Using incubator modules: jdk.incubator.vector
    2 4 6 8 10 12 14 16 
    

preview feature

  • 자바 언어 혹은 JVM과 관련된 새로운 기능
  • 완전히 구현되었지만, 피드백을 받기 위한 목적(95% 완성도)
  • 다음 버전과 호환되지 않을 수 있어, 프로덕션 사용은 비권장
  • --enable-preview 옵션적용시 사용가능

자바 12

indent API

String 메서드로 입력한 숫자만큼 들여쓰기를 합니다.

public static void main(String[] args) {
    String x = "Hello world!";
    String indent = x.indent(5);
    System.out.println(indent);
    
    // java 15부터는 text block에 indent를 사용할 수 있습니다.
    String textBlock = """
        Hello,
        world!
        """.indent(5);
    System.out.println(textBlock);
    
    // 음수 사용시 인수만큼 들여쓰기를 제거합니다.
    // indent -2
    String indentMinus = textBlock.indent(-2);
    System.out.println(indentMinus);
}
//     Hello world!
//     Hello,
//     world!	
//   Hello,
//   world!

Files.mismatch 함수

서로 다른 파일의 내용물을 비교할 때, 파일의 내용물이 같은지 다른지 쉽게 확인할 수 있습니다.

내용이 다를 경우 첫번째로 다른 부분의 Index를 반환하며, 내용이같은 경우 -1을 반환합니다.

Colletors teeing 함수

Colletors는 스트림의 최종 연산의 결과를 모으는 Collector의 구현체입니다.

public class TeeingExample {
    public static void main(String[] args) {
        // 멤버 리스트 생성
        List<Member> members = List.of(
            new Member("홍길동", 25),
            new Member("김철수", 30),
            new Member("이영희", 35),
            new Member("박영수", 40),
            new Member("최지영", 28)
        );

        // teeing을 사용하여 평균 나이와 최고령 나이 계산
        Result result = members.stream()
            .collect(Collectors.teeing(
                Collectors.averagingInt(Member::age),  // 평균 나이 계산
                Collectors.maxBy((m1, m2) -> Integer.compare(m1.age(), m2.age())),  // 최고령 멤버 찾기
                (averageAge, oldestMember) -> new Result(averageAge, oldestMember.orElse(null))  // 결과 병합
            ));

        // 결과 출력
        System.out.println("평균 나이: " + result.averageAge);
        System.out.println("최고령 멤버: " + result.oldestMember);
    }

    // Member record 정의
    public record Member(String name, int age) {}

    // 결과를 담기 위한 Result record 정의
    public record Result(double averageAge, Member oldestMember) {}
}

teeing API는 인수가 3개가 필요하며 첫번째 컬렉트할 방법, 두번째 컬렉트할 방법,최종 결과를 처리하는 방법을 추가할 수 있습니다.

마지막 인수에는 (t1, t2) -> { }에서 t1은 첫번째의 결과, t2는 두번째의 결과가 들어오게 됩니다.

자바 14

Switch Expression

statement는 문장을 말하며 System.out.println("출력")으로 하나의 문장으로 사용할 수 있습니다.

expression은 프로그래밍 문장이지만, 어떠한 결과 값이 정해지는 문장입니다. 30+40,(parameters) -> expression

statement는 사각형이고, expression은 정사각형으로 생각하면 됩니다. statement안에 expression이 있습니다.

정리하면, expression은 하나의 값을 반환하는 프로그래밍 문장이라고 생각하면 됩니다.

기존 switch는 조건문(statement)에서는 값으로 사용할 수 없었습니다.

그래서 아래와 같은 문장은 사용 할 수 없습니다.

String gender = if(personNo.startWith("1")) {
	"M";	
} else {
	"F";
}

대신 삼항 연산자는 expression으로 사용할 수 있습니다.

String gender = personNo.startWith("1") == true ? "M" : "F"

기존 월별 마지막 날짜를 반환하는 함수를 만든다고 하면 다음과 같습니다.

public static int getLastDayOfMonth(int year, int month) {
    switch (month) {
        case 1: // 1월
        case 3: // 3월
        case 5: // 5월
        case 7: // 7월
        case 8: // 8월
        case 10: // 10월
        case 12: // 12월
            return 31;
        case 4: // 4월
        case 6: // 6월
        case 9: // 9월
        case 11: // 11월
            return 30;
        case 2: // 2월
            return 28;
        default:
            throw new IllegalArgumentException("잘못된 월: " + month);
    }
}

기존 switch를 사용하려면 if 문과 같이 분기마다 return 혹은 break를 사용해야합니다.

그리고 동일한 처리를 하고 싶은 경우 case x :(colon case label)를 연이어 작성해야합니다.

switch expression 적용

public static int getLastDayOfMonth(int year, int month) {
    return switch (month) {
        case 1, 3, 5, 7, 8, 10, 12 -> 31;
        case 4, 6, 9, 11 -> 30;
        case 2 -> {
            System.out.println("윤년은 이후에 추가합니다.");
            yield 28;
        };
        default -> throw new IllegalArgumentException("잘못된 월: " + month);
    };
}
  • expression으로 ;이 반드시 붙어야합니다
  • 반환하고 싶은 값 앞에 break 대신 yield으로 지정합니다.
  • 반복적인 case label을 사용하지 않고 ` , `으로 추가할 수 있습니다.
  • ` -> ` 이 방식을 사용하면 yield를 생략 할 수 있습니다. 중괄호를 사용할 경우 yield는 생략할 수 없습니다.

반드시 최종 결과는 하나의 값(예외 포함)이 반환되어야 합니다 (expression 특징)

Enum과 함께 사용할 경우 컴파일때 타입이 정해져있으므로 default가 생략이 가능합니다.

Helpful NullPointerException

NullPointerException이 발생할 때, 보다 유용하고 상세한 정보를 제공하는 기능입니다.

public class NullPointerExample {
    public static void main(String[] args) {
        Person person = null;
        System.out.println(person.getName().toUpperCase());
    }
}

class Person {
    private String name;
    public String getName() {
        return name;
    }
}

기존은 발생한 위치를 알려주지만, 어떤 값이 null이었는지 구체적인 정보를 제공하지 않습니다.

Exception in thread “main” java.lang.NullPointerException:
Cannot invoke “api3.Main$Person.getName()” because “person” is null at com.api/api3.Main.main(Main.java:6)

이제는 자세하게 알려주며 14버전에서는 기본적으로 비활성화 되어있어 활성화를 해야합니다.

-XX:+ShowCodeDetailsInExceptionMessages

자바 15

Text Block

여러 줄에 걸친 문자열을 만들기 위한 새로운 자바 문법

String base = "A\nB\nCD";

String str = """
        A
        B
        CD""";
System.out.println("isEquals : " + (str.equals(base)));

text block은 마지막 개행을 제거하려면 """의 위치를 확인해야합니다.

  1. """ 뒤에 문자가 들어 올 수 없습니다.

    String str = """A
            B
            CD""";
    //llegal text block start: missing new line after opening quotes
    
  2. """ABC"""과 같이 한 줄로 문자열을 적을 수 없습니다.

    String str = """ABC""" ( X ) 
    
  3. 내부에 사용하는 목적으로 ", '\를 작성하지 않아도 됩니다.
    String str = """
            A"
            B"'
            CD""";
    System.out.println(str);
    // A"
    // B"'
    // CD
    
  4. 문자열(오른쪽) 끝에 공백은 사라집니다.
    // 공백이 필요한 경우
    String str = """
            A$$
            B$
            CD""";
    System.out.println(str.replace('$',' ')); // $를 공백 변환하기
       
    // 공백이 필요한 경우
    String str = """
            A    |
            B    |
            CD""";
    System.out.println(str.replace('\|\n','\n')); // 제일 끝의 문자를 개행으로 변경하여 사용(fence)
       
    // 공백이 필요한 경우
    String str = """
            A\040\040\040
            B\040\040
            CD""";
    System.out.println(str); // octal escape sequence를 사용하는 방식입니다.
       
    // 공백이 필요한 경우
    String str = """
            A \S
            B\S
            CD""";
    System.out.println(str); // A \S 는 A뒤에 공백이 2개가 됩니다. 스페이스 공백과 기호 공백이 사용됩니다.
    

    octal escape sequence는 8진법으로 ASCII 코드로 호횐되는 문자를 말합니다.

    \040은 아스키 코드로 ` ‘ ‘`이므로 적용됩니다.

  5. 한 줄로 긴 문자열을 표현하고 싶은경우 \을 추가합니다.

    // 개행이 필요없는 경우
    String str = """
            A \
            B\
            CD""";
    System.out.println(str); // A BCD  가됩니다.
    
  6. 들여쓰기 기준선의 왼쪽은 무시됩니다.
    String str = """
                A
                B
                CD""";
       
    

    들여쓰기 문자와 텍스트 블럭 안에 있는 문자, 닫히는 큰따옴표 세개를 기준으로 가장 왼쪽에 있는 문자를 기준으로 들여쓰기가 됩니다. 현재는 C가 가장 왼쪽에 있으므로 기준선이 됩니다.
    들여쓰기는 탭과 공백은 동일하게 한칸으로 표현됩니다.

String.stripIndent 함수

Text Block의 로직을 왼쪽으로 붙여쓰기를 도와주는 메서드입니다.

String textBlock2 = " A\n B\n C\n";
String stripIndent = textBlock2.stripIndent();
 A
 B
 C

실행해보면 안됩니다.. 왜..?

stripIndent 메서드를 찾아가보면 답이 있습니다.

public String stripIndent() {
	// 생략
    char lastChar = charAt(length - 1);
    boolean optOut = lastChar == '\n' || lastChar == '\r';
    List<String> lines = lines().toList();
    final int outdent = optOut ? 0 : outdent(lines); // 0 이됩니다.
	// .. 생략
}

마지막 문자에 \n이나 \r이 있는 경우에는 인덴트가 0으로 되므로 동작하지 않습니다.

String textBlock = """
         A
         B
         C
""";
String lines = " A\n";

저렇게 \r,\n으로 개행되는 경우에는 정상적으로 동작되지 않습니다.

String.formatted 함수

String.format()과 동일한 함수이지만 인스턴스 메서드로 리터럴이나 텍스트 블럭이나 그자리에 바로 사용이 가능합니다.

// formatted는 문자열을 포맷팅합니다.
String formatted = "Hello %s".formatted("world!");
System.out.println(formatted);
//Hello world!

ZGC 활성화 가능

기본 GC는 G1GC 이지만 Z-GC 를 사용하고 싶다면 -XX:+UseZGC 옵션으로 변경가능합니다.

자바 16

instanceof pattern matching

instanceof는 변수의 타입을 확인 할 수 있는 기능입니다. 타입을 확인하는 이유는 타입 확인후 캐스팅이 목적입니다.

  1. 타입을 확인한다.
  2. 변수에 캐스팅 결과를 저장한다.
  3. 변수를 사용한다.
if(obj instanceof String){
    System.out.println(((String) obj).isEmpty());
}

자바 16부터는 변수에 캐스팅 결과를 저장하는 과정이 생략됩니다.

public static void verifyValue(Object obj) {
    if (obj instanceof String str) {
        System.out.println(str.isEmpty());
    } else if (obj instanceof Integer num) {
        System.out.println(num > 0);
    } else if (obj instanceof Double dou) {
        System.out.println(dou > 0.0);
    }
    throw new IllegalArgumentException("체크하는 타입이 아닙니다.");
}

str 변수의 범위는 if() { }까지라고 생각할 수 있는데 아래 있는 문장도 허용됩니다.

public static void verifyValue(Object obj) {
    if (!(obj instanceof String str)) {
        return;
    }
    // section
    int idx = str.indexOf("010");
}

if 중괄호를 벗어나서도 사용이 가능합니다. 위 경우처럼 obj가 String 타입이 확실한 경우 메서드 내에서 사용이 가능하게 됩니다.

section 구간에서 str 변수가 확실하게 타입이 String 타입일 경우 가능합니다.

public static <T extends String> void verifyString(T t) {
    int idx = t.indexOf("010");
}

제네릭 메서드의T extends String이 통과된 문장이라고 생각하면 됩니다.

다만 만약 형제 클래스(부모가 동일)가 있다면 instanseOf로 검증한 클래스의 속성과 메서드만 사용이 가능합니다.

class Person {
    void eat(){

    }
}
class Son extends Person {
    void run(){

    }
}
class baby extends Person {
    void sleep(){

    }
}

public class InstanceOfTest {
    public static void checkPerson(Object object) {
        if (!(object instanceof Person per)){
            return;
        }
        // per은 현재 Person만 확실하므로 Person만 사용이 가능합니다.
        per.run();
        if (!(per instanceof Son son)) {
            return;
        }
        // Son의 메타 정보만 활용가능
        son.run();
    }
}

이 방식을 활용하여 조건문에서도 적용이 가능합니다. 단 조건 방식에 따라 per 사용 유무가 달라집니다.

// ||
if (object instanceof Person per || per...)
    
// &&
if (object instanceof Person per && per...)

||는 앞 조건이 false일 경우에만 per...구간을 확인합니다. per은 현재 앞조건에 만족하지 않으므로 per 변수는 사용할 수 없습니다.

&& 는 앞 조건이 true일 경우에만 per 구간을 확인합니다. 따라서 object 인스턴스는 Person을 가지고 있으므로 per변수를 사용할 수 있습니다.

instanceOf 정리

어떤 조건을 확인하고 조건이 true라면 특정 변수에 값을 바로 할당해주는 기능입니다.

이때 조건이 true이면 특정 변수의 값을 바로 할당하는 것Parttern Matching이라고 합니다.

Record Class

자바 16버전 이전까지는 데이터 전달 전용 클래스가 존재하지 않았습니다.

자바 16버전부터 데이터 전달을 목적으로 사용하는 클래스인 Record class가 추가되었습니다.

수동 DTO 클래스

public final class Member {
    private final String name;
    private final int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String name() {
        return name;
    }

    public int age(){
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Member member = (Member) o;
        return age == member.age && Objects.equals(name, member.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return String.format("Member{name='%s', age=%d}", name, age);
    }
}

데이터 전달을 위한 클래스 생성 방법

  • class에 final이 선언되어있어 상속받지 못한다.
  • private final 필드만 선언되어 있다.
  • 모든 private final 필드에 대해 생성자가 존재한다.
  • 클래스가 갖고 있는모든 필드에 접근할 수 있는 메소드가 있고, 메소드의 이름은 필드의 이름과 동일하다.
  • equals(),hashCode(),toString()이 존재한다.

자바에서는 @Lombok어노테이션이나 IDE를 활용하여 구현을 했었지만 16부터는 record 타입을 활용하여 간단하게 사용할 수 있습니다.

public record MemberRecord(
        String name,
        int age) {
}

수동DTOrecord DTO는 동일한 역할을 합니다.

차이점

record class가 사용할 필드는 ( )안에 선언합니다. 소괄호 안에 있는 필드를 컴포넌트라고 합니다.

MemberRecordString name,int age 컴포넌트를 가지고 있다고 할 수 있습니다.

MemberRecord memberRecord = new MemberRecord("Kamser", 23);
System.out.println("memberRecord = " + memberRecord);
//print component
System.out.println("memberRecord.name() = " + memberRecord.name());
System.out.println("memberRecord.age() = " + memberRecord.age());
Record class 특징

enum 클래스를 생성하면 자동으로 Enum을 상속하듯이 Record 클래스를 상속합니다.

  • 다른 클래스를 상속 받을 수 없습니다.

  • 인터페이스 구현 가능합니다.(enum과 동일)

  • 구현부에 static필드, 함수, 인스턴스 함수 등을 만들 수 있습니다.
    public record MemberRecord(
            String name,
            int age
    ) {
        private static final String DEFAULT_NAME = "Unknown";
        private static final int DEFAULT_AGE = 1;
          
        public boolean isAdult() {
            return age > 20;
        }
    }
    
  • 구현부에 인스턴스 필드는 만들수 없습니다.

  • 자동 생성되는 메서드들은 직접 재정의 할 수 있습니다. (생성자, 접근자(get) , toString 등 )
    public record MemberRecord(
            String name,
            int age
    ) {
        // 모든 필드에 대한 생성자를 재정의
        public MemberRecord(String name, int age) {
            if (name == null || name.isBlank()) {
                this.name = DEFAULT_NAME;
            }
            if (age < 0) {
                this.age = DEFAULT_AGE;
            }
        }
          
        // name을 호출할시 메서드 재정의
        public String name() {
            return "[재정의]" + name;
        }
    }
    
compact constructor

생성자를 재정의하는 경우 단순히 값에 대한 검증이 필요하다면 compact constructor를 사용하여 값 검증이 가능합니다. 컴포넌트(필드)를 매개변수로 받을 필요없이 record 클래스가 자동으로 만든 생성자 함수 안에 compact constructor가 들어간다고 생각하면됩니다.

매개변수를 직접 받지 않기 때문에 코드가 간결해진다는 장점이 있습니다.

public record MemberRecord(
        String name,
        int age
) {
    public MemberRecord {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름이 없어요");
        }
        if (age < 0) {
            throw new IllegalArgumentException("나이가 음수에요");
        }
    }
}

직접 this.name = 1 이렇게 초기화 할 수 없습니다.

public MemberRecord {
    if (name == null || name.isBlank()) {
        name = DEFAULT_NAME;
    }
    if (age < 0) {
        age = DEFAULT_AGE;
   	//  this.age = DEFAULT_AGE 사용 불가
    }
}

대신 들어온 변수의 값을 변경할 수 있습니다.

public static void main(String[] args) {
    MemberRecord memberRecord2 = new MemberRecord(null, -1);
    System.out.println("memberRecord2 = " + memberRecord2);
    // MemberRecord[name=Unknown, age=1]
}
Record class와 어노테이션
public record MemberRecord(
        String name,
        @CustomAnnotation int age
) {}

컴포넌트에 @Target이 없는 어노테이션을 사용할 경우

  • 필드: @CustomAnnotation private final int age

  • 접근자: @CustomAnnotation public int age(){…}
  • 생성자 매개변수: public MemberRecord(… ,@CustomAnnotation int age)

필드, 생성자, 접근 메서드에 모두 추가된 것을 확인 할 수 있습니다.

따라서 어노테이션이 필요한 부분에만 붙기를 원한다면 @Target을 사용하여 사용하는 위치를 지정하면 해결할 수 있습니다.

스프링 부트에서 사용한다면 Jackson 2.12.0(스프링 부트 2.55)이상에서 record class가 지원됩니다.

Stream.mapMulti 함수

flatMap()을 조금 더 효율적으로 사용하면서 filter,map 연산도 동시에 할 수 있는 API입니다.

여기서 효율적으로 한다는 말은 불필요한 스트림 객체를 만들지 않는다는 의미입니다.

스트림 연산을 하기 위해서 스트림 객체를 생성하는 경우가 발생하게 됩니다.

ublic static void main(String[] args) {
    // 멤버 리스트 생성
    List<Member> members = List.of(
        new Member("홍길동", 25, "Y", List.of("010-1234-5678", "010-2345-6789")),
        new Member("김철수", 30, "N", List.of("010-3456-7890")),
        new Member("이영희", 35, "Y", List.of("010-4567-8901", "010-5678-9012", "010-6789-0123")),
        new Member("박영수", 40, "N", List.of()),
        new Member("최자영", 28, "Y", List.of("010-7890-1234"))
    );

    // flatMap을 사용하여 모든 연락처를 스트림 요소로 분리
    List<String> mobiles = members.stream()
        .flatMap(member -> member.mobiles().stream()) // 전화번호를 연산하기 위해 스트림 객체 생성
        .collect(Collectors.toList());

    System.out.println(mobiles);
}

// Member record 정의
public record Member(String name, int age, String state, List<String> mobiles) {}

mapMulti 사용

public static void main(String[] args) {
    // 멤버 리스트 생성
    List<Member> members = List.of(
        new Member("홍길동", 25, "Y", List.of("010-1234-5678", "010-2345-6789")),
        new Member("김철수", 30, "N", List.of("010-3456-7890")),
        new Member("이영희", 35, "Y", List.of("010-4567-8901", "010-5678-9012", "010-6789-0123")),
        new Member("박영수", 40, "N", List.of()),
        new Member("최자영", 28, "Y", List.of("010-7890-1234"))
    );

    // mapMulti를 사용하여 모든 연락처를 스트림 요소로 분리
    List<String> mobiles = members.stream()
        .mapMulti((member, downstream) -> {
            for (String contact : member.mobiles()) {
                downstream.accept(contact);
                // contact 값만 다음 스트림으로 필터링됩니다.
            }
        })
        .collect(Collectors.toList());

    System.out.println(mobiles);
}

정리하면 명령형 스타일로 코드를 작성하는 것이 가독성이 좋거나, 불필요한 Stream 생성 오버헤드를 줄일때 사용할 수 있습니다.

Stream.toList 함수

최종연산으로 불변 리스트를 간단하게 toList로 작성할 수 있습니다.

// mapMulti를 사용하여 모든 연락처를 스트림 요소로 분리
List<String> contactNumbers = members.stream()
    .mapMulti((member, downstream) -> {
        for (String contact : member.contactNumbers()) {
            downstream.accept(contact);  // 각 연락처를 스트림 요소로 전달
        }
    })
    .collect(Collectors.toUnmodifiableList()); // Java 10에서 추가된 불변 리스트를 반환

자바 16 이후에는 아래와 같이 작성이 가능합니다

// mapMulti를 사용하여 모든 연락처를 스트림 요소로 분리
List<String> contactNumbers = members.stream()
    .mapMulti((member, downstream) -> {
        for (String contact : member.contactNumbers()) {
            downstream.accept(contact);  // 각 연락처를 스트림 요소로 전달
        }
    })
    .toList(); // Java 16에서 추가된 메서드, 불변 리스트를 반환

자바 17

Sealed Class

추상 클래스, 일반 클래스를 상속하는 경우 몇 가지 단점이 있었습니다.

  1. 상속받는 클래스에 대한 제한이 없습니다: 설계자가 자세한 의도를 작성하지 않는 이상, 의도와 다른 클래스로 확장될 수 있습니다.
  2. 보안 및 안정성 문제: 상속 구조가 예측 불가능하게 되면, 코드의 보안과 안정성이 떨어지게 됩니다. 중요한 메서드를 오버라이드하거나 중요한 필드를 변경하여 사용할 수 있기 때문입니다.

상속받는 클래스를 제한없이 사용하려면 원본 클래스에는 자세한 설계의도와 오버라이딩 메서드에 대한 의도를 작성해야합니다. 추가로 어디까지 상속하여 사용하는지 알수 없기 때문에 변경이나 확장하기 어렵습니다.

이런 문제를 해결하기 위해 원본 클래스에서 상속을 제한할 수 있는 클래스가 등장합니다.

목적

Sealed 클래스를 사용하면 상속 가능한 클래스를 명시적으로 지정할 수 있어, 코드의 안정성, 유지 보수성, 보안성을 높일 수 있습니다.

추상 클래스나 인터페이스를 만드려고 할때, 하위 클래스의 경우의 수를 제한하고 싶을 때 사용합니다.

장점

상위 클래스를 설계할 때 호환성 걱정을 줄일 수 있으며, enum 클래스 처럼 sealed 클래스를 사용할 수 있다는 장점이 있습니다.

상속 가능한 클래스의 명시적 제한
public sealed class Shape permits Circle, Rectangle, Square {
}

sealed abstract, sealed class, sealed interface로 선언할 수 있으며 반드시 하위 클래스가 존재해야합니다.

  • 자바 9 이후로 module-info.java파일이 있는 named module의 경우는 같은 모듈안에 있는 클래스만 상속이 가능합니다.
  • unnamed module인 경우 같은 패키지 안에 있어야 상속받는 클래스 대상이 됩니다.
permits 생략 가능

같은 파일 내에 있는 경우라면 생략이 가능합니다.

package api;

public sealed class Shape {
}
final class Circle extends Shape { }
final class Rectangle extends Shape { }
final class Square extends Shape { }
하위 클래스 키워드
  • sealed: 상속 계층 구조를 제한하여 특정 클래스만 상속할 수 있도록 합니다.

  • non-sealed: 제한을 풀어, 자유롭게 상속할 수 있도록 합니다.

  • final: 상속 계층 구조의 끝을 나타내며, 더 이상 상속할 수 없도록 합니다.

public sealed class Shape permits Circle, Rectangle, Square {}

public final class Circle extends Shape {
	// Circle 클래스를 상속할 수 없습니다.
}

public sealed class Rectangle extends Shape permits ColoredRectangle {
	// Rectangle 클래스는 하위 클래스를 지정할 수 있습니다.
}

public non-sealed class Square extends Shape {
	// 기존 상속 클래스처럼 Square 클래스를 자유롭게 상속 가능합니다.
}
sealed 와 추상 메서드

인터페이스와 추상 클래스는 추상 메서드를 가지고 있으므로 하위 클래스가 상속할 수 있어야합니다.

  • sealed class를 상속하는 abstruct classfinal 키워드를 사용할 수 없습니다.
  • sealed interface를 상속하는 interfacefinal키워드를 사용할 수 없습니다.

랜덤 API

자바 17 이전에 랜덤 숫자를 반환하는 클래스를 Ramdom을 일반적으로 사용했습니다.

  • ThreadLocalRandom: 멀티쓰레드 환경에서 사용하기 위한 난수
  • SecureRandom: 암호학적인 난수가 필요할 경우 사용하는 난수

기존에는 Ramdom 구현체를 사용하다보니 종속적인 구현이나 방식을 사용할 수 밖에 없었습니다.

자바 17에서는 상위 인터페이스로 RandomGenerator가 추가되었습니다.

image-20240630155412624

새로 생긴 RamdomGenerator구현체를 사용하면 기존 Ramdom 클래스보다 더 좋은 성능을 보인다고 합니다. 단 thread-safe하지는 않습니다.

// 기본 RandomGeneratorFactory 사용
RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.getDefault();
RandomGenerator randomGenerator = factory.create();

// 1부터 10 사이의 난수 생성
int i = randomGenerator.nextInt(1, 10);
System.out.println("i = " + i);

댓글남기기