본문 바로가기
Java

Java의 버전별로 간단하게 톺아보기 (~ Java 17)

by Ramos 2025. 1. 7.

본 글은 이전 팀에서 팀 내부 Java 표준 가이드를 위해 필자가 작성했던 문서에 해당한다.

해당 팀의 표준은 Java 17을 사용하고 있어 해당 버전까지의 내용을 간단하게 정리한다.

 

개발실장님 comment

 


  • 본 글에선 Java 5 ~ Java 17 까지의 문법적인 수준의 주요 변화를 간단하게 톺아봅니다.
  • Java 9의 모듈 시스템, RxJava와 리액티브 프로그래밍, GC의 변화, reflection과 같은 내용은 포함하지 않습니다.
  • Version 별로 문법적인 업데이트가 아닌 경우 본 글에서 제외합니다.
  • 참고로 제가 미처 발견하지 못한 누락된 부분이 있을 수 있으니 언제든지 댓글로 추가해주세요.

Java 5

  • Release: 2004년 9월
  • 이 때부터 버전의 앞에 1을 빼고 표시하기 시작하여 Java 5로 칭해진다. (JDK 1.5)

Generics

  • 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
  • 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
// Before Java 5
public class Test {
    private Object object;
    public Object get() { return this.object; }
    public void set(Object object) { this.object = object; }
}

// Since Java 5
public class Test<T> {
    private T t;
    public T get() { return this.t; }
    public void set(T t) { this.t = t; }
}
// 타입 제한 활용
public static <T extends Fruit> Juice makeJuice(FruitBox<T> Box) {
    String temp = "";
    for (Fruit f : box.getList()) temp += f + " ";
    return new Juice(temp);
}

Enumeration

  • 한정된 값만 갖는 데이터 타입
  • Java의 열거형은 타입에 안전한 열거형
public enum Week{
    MONDAY("월요일"),
    TUESDAY("화요일"),
    // ...
    SUNDAY("일요일");
    private String name;
    Week(String name) { this.name = name; }
}

Annotation

  • 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것.
  • 컴파일 과정과 실행 과정에서 코드를 어떻게 컴파일하고 실행 시킬 지 알려준다.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // ...
    }

    // ...
}

아시다 싶이, 표준 애너테이션 외에도 사용자 정의 애너테이션을 만들 수 있는데 적용 대상유지 정책(언제까지 유지할 것인지) 을 지정할 수 있습니다.

@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD }) 
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}

Java 7

  • Release: 2011년 7월

Diamond Operator

// Before Java 7
List<Integer> list = new ArrayList<Integer>();

// Since Java 7
List<Integer> list = new ArrayList<>();

Java 8 (LTS)

  • Release: 2014년 3월
  • 이 시기부터 모던 자바라고 칭해진다. 람다, 스트림을 포함하여 정말 많은 변화가 일어났다. 또한 최초의 LTS(장기 지원) 버전으로 출시 되었다.

Java 8 ~ 11은 모던 자바 인 액션을 꼭 참고하세요.

람다 표현식

익명 함수를 생성하기 위한 식으로, OOP 보단 FP 방식에 더 가깝다.

// Before Java 8
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple apple) {
        return RED.equals(apple.getColor());
    }
});

// Since Java 8
List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

그 외에도 Java의 함수형 인터페이스를 반드시 찾아보세요.

메서드 참조

// Before Java 8
// 익명 클래스 활용
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden();
    }
});

// Since Java 8
File[] hiddenFiles = new File(".").listFiles(File::isHidden);

Optional

Java를 사용하면 NPE 때문에 코드가 굉장히 지저분해진다. Java 8에선 Optional 클래스가 도입되어 예상치 못한 NPE를 방지할 수 있게 되었다.

// Before Java 8
Object value = map.get("key");

// Since Java 8
Optional<Object> value = Optional.ofNullable(map.get("key"));

Stream API

  • 스트림을 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.
  • 여기서 선언형은 데이터를 처리하는 임시 구현 코드 대신 질의로 표현하는 방식을 말한다.
  • 또한 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬 처리할 수 있다.
  • 이 외에도 파이프라이닝, 내부반복 등의 특징이 존재한다.

아래 레포에서 연습을 추천합니다.

// Before Java 8
List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish dish : menu) {
    if(dish.getCalories() < 400) {
        lowCaloricDishes.add(dish);
    }
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
    public int compare(Dish dish1, Dish dish2) {
        return Integer.compare(dish1.getCalories(), dish2.getCalories());
    }
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish dish : lowCaloricDishes) {
    lowCaloricDishesName.add(dish.getName());
}

// Since Java 8
List<String> lowCaloricDishesName = menu.stream()
                                        .filter(d -> d.getCalories() < 400)
                                        .sorted(comparing(Dish::getCalories))
                                        .map(Dish::getName)
                                        .collect(toList());

Default methods in interface

기존의 인터페이스에선 메서드 정의만 가능하고, 구현은 할 수 없지만, Java 8에 추가된 디폴트 메서드를 이용하면 구현 내용을 작성할 수 있다.

public interface Example {
    default void hi() {
        System.out.println("Hi");
    }
}

Java 9

  • Release: 2017년 9월

Stream API 추가

  • takeWhile()
    • 조건에 대해 참이 아닐 경우 바로 멈춘다.
    • 이미 정렬되어 있다면 false가 등장한 위치부터 반복을 중단할 수 있기 때문에 크기가 큰 Stream의 경우 많은 시간을 절약할 수 있다.
  • dropWhile()
    • takeWhile()의 정반대 작업으로, 처음으로 false가 등장하는 시점까지의 요소를 모두 버리고 남은 요소를 반환한다.
List<Integer> takeWhileList = numbers.stream()
                                     .takeWhile(i -> i < 50)
                                     .collect(toList()); // 12, 17, 29, 35, 41, 

List<Integer> dropWhileList = numbers.stream()
                                     .dropWhile(i -> i < 50)
                                     .collect(toList()); // 50, 66, 72, 80

try-with-resources 개선

우선, try-with-resource를 사용하는 이유 자체는 Java 7부터 이를 사용하여 자원의 close()를 직접 호출하지 않아도 자동 호출할 수 있도록하여 실수를 미연에 방지하도록 한다.

public static void main(String args[]) throws IOException {
    FileInputStream is = null;
    BufferedInputStream bis = null;
    try {
        is = new FileInputStream("file.txt");
        bis = new BufferedInputStream(is);
        int data = -1;
        while((data = bis.read()) != -1){
            System.out.print((char) data);
        }
    } finally {
        // close resources
        if (is != null) is.close();
        if (bis != null) bis.close();
    }
}

// try-with-resource로 개선
public static void main(String args[]) throws IOException {
    try (FileInputStream is = new FileInputStream("file.txt"); BufferedInputStream bis = new BufferedInputStream(is)) {
        int data;
        while ((data = bis.read()) != -1) {
            System.out.print((char) data);
        }
    }
}
  • try block에서 선언된 객체들에 대해서 try 문이 종료될 때 자동으로 자원을 해제해주는 기능이다.
  • Java 9 부턴 try block 밖에서 선언한 변수를 가져와 사용할 수 있다.
// Before Java 9
void tryWithResources1() throws IOException {
    BufferedReader reader1 = new BufferedReader(new FileReader("test.txt"));
    try (BufferedReader reader2 = reader1) {
        // do something
    }
}

// Since Java 9
void tryWithResources2() throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
    try (reader) {
        // do something
    }
}

Interface private method

인터페이스 내에 private method 사용이 가능해졌다.

public interface MyInterface {
    private static void test(){
        System.out.println("private method");
    }
}

Java 10

  • Release: 2018년 3월

var 키워드

  • 지역 변수의 타입을 추론한다.
  • 타입 추론 예약어이므로 값이 없거나 null 이면 안된다.
  • 람다식, 혹은 배열 타입은 추론할 수 없다.
  • 다이아몬드 연산자와 함께 사용하면 <Object>로 추론한다.
  • 익명 클래스와 함께 사용할 순 있지만 별도의 타입으로 간주한다.
var num = 3;
num = 10; // 가능
num = "String"; // 타입이 달라서 불가
var list = List.of(1, 2, 3);

final var t = "불변 String";

Collection 추가 API

  • copyOf() 추가
    • List, Set, Map 의 원본 컬렉션을 깊게 복사한다.
    • Collections.unmodifieableXxx()와는 다르다.
public class Main {
    public static void main(String[] args) {
        List<Integer> olds = new ArrayList<>();
        olds.add(1);
        olds.add(2);

        List<Integer> news = List.copyOf(olds);
        olds.add(3);

        olds.forEach(x -> System.out.println(x)); // 1, 2, 3
        news.forEach(x -> System.out.println(x)); // 1, 2
    }
}
  • Collectors.toUnmodifiableXxx() 추가
List<Integer> list = Stream.of(1, 2, 3, 4, 5)
    .filter((x) -> x % 2 == 0)
    .collect(Collectors.toUnmodifiableList());

Optional 추가 API

  • orElseThrow()
    • 매개변수 없이 사용하면 NoSuchElementException이 발생
// 매개변수를 안넣었기에 Optional 내부에 원소가 없다면 NoSuchElementException이 발생함.
Optional.ofNullable(3).orElseThrow();

Java 11 (LTS)

  • Release: 2018년 9월
  • Java의 두 번째 LTS 버전이다.

String, File API 개선

"Ramos".isBlank(); // 공백인지 판단
"Ram\os".lines(); // 문자열을 줄 단위로 쪼갬
"Ramos  ".strip(); // 문자열 앞, 뒤의 공백 제거

Lambda 파라미터로 var 사용

(var x, var y) -> x.process(y) => (x, y) -> x.process(y)

Java 12

  • Release: 2019년 3월

Switch Expression 확장 (Preview)

// Before Java 12
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

// Since Java 12
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

Java 13

  • Release: 2019년 9월

Switch Expression 개선 (Preview)

switch 문이 값을 반환할 수 있도록 yield 추가

var a = switch (day) {
    case MONDAY, FRIDAY, SUNDAY:
        yield 6;
    case TUESDAY:
        yield 7;
    case THURSDAY, SATURDAY:
        yield 8;
    case WEDNESDAY:
        yield 9;
};

Multiline Strings (Preview)

String str = """
   This
   is
   text block
""";

Java 14

  • Release: 2020년 3월
  • Java 12, 13에서 프리뷰로 제공되었던 스위치 표현식이 표준화(Standard)되었다.

Record Class (Preview)

  • 변수의 타입과 이름을 이용해 private final 필드를 자동 생성
  • 생성자, getter, hashCode(), equals(), toString() 또한 자동 생성
  • Java 16에서 정식 기능으로 채택됨.
  • 다른 클래스를 상속 받을 수 없다.
  • 인터페이스는 구현 가능하다.
  • static 필드, 함수, 인스턴스 함수 등을 만들 수 있다.
  • 하지만 인스턴스 필드는 만들 수 없다.
// Before Java 14
final class Point {
    public final int x;
    public final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

// Since Java 14
record Point(int x, int y) { }

참고로 요즘 실무에선 DTO 클래스를 record 클래스로 작성하는 방식도 많이 쓰입니다.
Spring Boot 2.5.x 이상엔 Jackson 2.12.0 이상 버전이 들어가서 record class에 대한 호환이 됩니다.

NullPointerException 개선

  • 정확히 어떤 변수가 null인지 자세하게 나타난다.
author.age = 35;
---

Exception in thread *"main"* java.lang.NullPointerException:
     Cannot assign field *"age"* because *"author"* is null

Pattern Matching for instanceof (Preview)

// Before Java 14 : 형변환 후 작업 수행
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.contains("Ramos"));
}

// Since Java 14
if (obj instanceof String s) {
    System.out.println(s.contains("Ramos"));
}

Java 15

  • Release: 2020년 9월

Sealed Class (Preview)

  • Java 15에서 preview, Java 17에서 정식 기능으로 채택된 기능입니다.
  • 하위 클래스를 지정된 클래스로만 제한한다. (봉인한다)
  • 상위 클래스를 설계할 때 호환성 문제에 대한 걱정을 덜 할 수 있다.
  • sealed 키워드를 이용해 선언하고, permits 키워드로 상속받을 서브 클래스를 명시한다.

public sealed interface CarBrand permits Hyundai, Kia {}
public final class Hyundai implements CarBrand {}
public non-sealed class Kia implements CarBrand {}
// Benz는 하위 클래스로 지정 불가

Java 17 (LTS)

  • Release: 2021년 9월

Pattern Matching for switch (Preview)

// Before Java 17
static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

// Since Java 17
static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}