본문 바로가기
Java

Java 8, 11, 14 이후 에서 나온 신규 문법들 소개

by fygoo-826 2025. 1. 22.
728x90

각 자바 버전들 릴리스 이후 언어와 플랫폼에 추가된 새로운 기능들을 정리해보고자 한다.

각 버전에 모든 문법들은 정리하는 것을 목적으로 하는 문서는 아니고, 알아두면 좋을 것들을 기록해보고자 한다.

 

Java 8

함수형 프로그래밍

Java 8에서는 함수형 프로그래밍을 지원하기 위해 여러 기능이 추가되었다. 이로 인해 더 간결하고 효율적인 코드를 작성할 수 있다.

특히 아래에서 소개할 람다식과 함께 사용해서 더 이상 익명 클래스를 사용하지 않아도 되게 되었다.

import java.util.Arrays;
import java.util.List;

public class FunctionalProgrammingExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        names
           .forEach(name -> System.out.println(name));
    }
}

 

람다식

람다식은 익명 함수로, 코드를 간결하게 작성할 수 있도록 돕는다. 주로 컬렉션 API와 함께 사용된다. 과거에는 익명 클래스를 통해서 메서드 혹은 함수를 일급으로 전달했었다. 람다식이 등장한 이후 코드가 훨씬 간결하게 처리되었다.

import java.util.Arrays;
import java.util.List;

//과거
public class TraditionalLoopExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // 전통적인 for 루프 사용
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            System.out.println(name);
        }
    }
}

// 현재
public class LambdaExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        names.forEach(name -> System.out.println(name));
    }
}

 

Stream API

Stream API는 컬렉션에 대한 함수형 스타일의 연산을 지원한다. 데이터의 연속적인 처리에 유용하다. 과거에 비해서 Imperative 했던 코드들이 Declaritive 하게 변경되어 가시성이 매우 좋게 확보되었다. Java 8 이상의 스펙이 선호되는 이유는 여러가지 이유가 있겠지만 Stream 의 등장이 주가 되었다고 해도 무방하다고 개인적으로 생각한다.

 

장점은 다음과 같다.

  • 가독성 향상: 코드가 간결하고 명확해서 데이터 처리의 의도를 쉽게 이해할 수 있다
  • 함수형 프로그래밍: 람다식을 사용하여 데이터 처리 로직을 함수형 프로그래밍 스타일로 작성할 수 있어서 코드 재사용성과 유지보수성이 높아진다
  • 지연 실행: Stream은 필요할 때만 데이터를 처리하기 때문에 성능을 최적화할 수 있다
  • 병렬 처리: Stream API를 활용하면 데이터의 병렬 처리를 쉽게 적용할 수 있어 성능 향상이 가능하다
  • 다양한 연산 지원: 필터, 변환, 정렬 등 다양한 연산을 체계적으로 연결하여 복잡한 데이터 처리를 쉽게 구현할 수 있다
import java.util.Arrays;
import java.util.List;

// 과거 버전
public class TraditionalLoopExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // 전통적인 for 루프와 if 조건문 사용
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (name.startsWith("A")) {
                System.out.println(name);
            }
        }
    }
}

// stream 의 등장
public class StreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        names.stream()
             .filter(name -> name.startsWith("A"))
             .forEach(System.out::println);
    }
}

 

Optional

Optional 클래스는 null값 처리를 위한 컨테이너이다. 이로 인해 Null Pointer Exception을 방지할 수 있다. 많은 개발자들이 과거에는 null 값 체크하는 것을 validation 의 기본으로 생각하곤 했는데, 덕분에 많이 개선되었다.

Optional 에서 제공하는 유명한 API 들은 아래 예제 코드에 추가해두었다.

import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        // 1. of(T value)
        Optional<String> name1 = Optional.of("Alice"); // null이 아닌 값으로 Optional 생성
        
        // 2. ofNullable(T value)
        Optional<String> name2 = Optional.ofNullable(null); // null인 값으로 빈 Optional 생성

        // 3. isPresent()
        boolean hasValue = name1.isPresent(); // name1이 존재하는지 확인

        // 4. ifPresent(Consumer<? super T> action)
        name1.ifPresent(n -> System.out.println(n)); // 값이 존재하면 출력, "Alice" 출력

        // 5. orElse(T other)
        String result1 = name2.orElse("Default Name"); // name2가 빈 Optional일때 "Default Name" 반환
        
        // 6. orElseGet(Supplier<? extends T> other)
        String result2 = name2.orElseGet(() -> "Default Name"); // 공급자를 사용하여 기본값 반환

        // 7. orElseThrow(Supplier<? extends X> exceptionSupplier)
        try {
            String result3 = name2.orElseThrow(() -> new IllegalArgumentException("Name is absent")); // 빈 Optional에서 예외 발생
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage()); // "Name is absent" 출력
        }

        // 8. map(Function<? super T, ? extends U> mapper)
        Optional<Integer> nameLength = name1.map(String::length); // "Alice"의 길이인 5 반환

        // 9. flatMap(Function<? super T, Optional<U>> mapper)
        Optional<String> upperName = name1.flatMap(n -> Optional.of(n.toUpperCase())); // "ALICE" 반환

        // 10. filter(Predicate<? super T> predicate)
        Optional<String> filteredName = name1.filter(n -> n.startsWith("A")); // "Alice" 반환
    }
}

 

Date & Time API

Date와 Time API가 새롭게 도입되었다. 이 API는 날짜와 시간의 처리를 보다 간편하게 관리할 수 있도록 돕는다.

import java.time.LocalDate;

public class DateTimeExample {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        System.out.println(today);
    }
}

 

Java 11

단일 소스파일 실행

Java 11에서는 단일 소스파일을 실행할 수 있는 기능이 추가되었다. 이로 인해 별도의 컴파일 과정 없이 바로 실행 가능하다.

정리하면 아래와 같다.

  • 컴파일 과정: Java 11 이전에는 소스파일을 컴파일하여 클래스 파일을 생성해야 했지만, Java 11 이후에는 단일 소스파일을 직접 실행할 수 있다
  • 사용자 편의성: Java 11부터는 하나의 명령어로 실행이 가능하므로 개발자 경험이 향상되었으며 이를 통해 특히 간단한 프로그램이나 스크립트 작성 시 시간과 노력을 절약할 수 있다
// 이전
$ javac HelloWorld.class
$ java HelloWorld
// 이후
$ java HelloWorld.java

Java 14 이후

Text block

Java 14에서 도입된 Text Block은 여러 줄의 문자열을 간편하게 작성할 수 있다.

public class TextBlockExample {
    public static void main(String[] args) {
        String textBlock = """
                          Hello,
                          World!
                          """;
        System.out.println(textBlock);
    }
}

 

Switch Expression

Switch Expression은 switch 문을 표현식으로 사용할 수 있게 해준다. 이를 통해 변수를 선언할 수 있다. 과거에는 선언한 변수에서 switch 문을 통해 값을 할당해주는 방식으로 코드를 작성했는데, 이제는 switch 를 바로 변수에 할당해줄 수 있다.

만일 이 차이에 대해 이해하지 못한다고 하면 "값, 식, 문" 에 대해서 이해해보자.

public class SwitchExpressionExample {
    public static void main(String[] args) {
        String day = "MONDAY";
        int dayNumber = switch(day) {
            case "MONDAY" -> 1;
            case "TUESDAY" -> 2;
            default -> 0;
        };
        System.out.println(dayNumber);
    }
}

 

Record

Record는 데이터를 간편하게 표현할 수 있는 구조체 같은 역할을 한다. 주로 불변 객체를 생성하는 데 사용된다. 

Record 클래스의 특징

  • 간결한 구문: 클래스를 정의할 때 필요한 코드가 상당히 줄어든다
  • 불변성: Record 객체는 기본적으로 불변으로 처리된다. 즉, 생성 후에 필드 값을 변경할 수 없다
  • 자동 생성되는 메서드: equals(), hashCode(), toString() 메서드가 자동으로 생성된다
  • 구조화된 데이터 표현: 주로 데이터 전송 객체(DTO)와 같이 여러 속성을 포함하는 경우에 바로 사용할 수 있다
public record Person(String name, int age) {}

public class RecordExample {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.name());
    }
}

 

sealed 타입

Java 17에서 도입된 Sealed 클래스는 클래스의 상속을 제한할 수 있는 기능을 제공한다. 이를 통해 특정 클래스만 서브클래스로 정의할 수 있게 하여, 더욱 안전하고 예측 가능한 코드 구조를 형성할 수 있다.

Sealed 클래스의 특징

  • 상속 제한: Sealed 클래스를 어떻게 상속할 수 있는지를 명시적으로 정의할 수 있다
  • 유지 보수 용이: 클래스 구조를 명확히 하여 코드의 가독성과 유지보수성을 향상시킨다
  • 유형 안전성: 서브클래스가 제한되어 있기 때문에, switch 문과 같은 구조에서 타입 오류를 방지할 수 있다

특히 switch 문과 함께 사용하면 효과적으로 사용할 수 있는게 컴파일 타임에 구현체를 전부 알고 있기 때문이다.

// Sealed 클래스 정의
sealed class Shape permits Circle, Square, Rectangle { 
}

// Circle 서브클래스 정의
final class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double area() {
        return Math.PI * radius * radius;
    }
}

// Square 서브클래스 정의
final class Square extends Shape {
    private final double side;

    public Square(double side) {
        this.side = side;
    }

    public double area() {
        return side * side;
    }
}

// Rectangle 서브클래스 정의
final class Rectangle extends Shape {
    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }
}

public class SealedClassExample {
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        Shape square = new Square(4);
        Shape rectangle = new Rectangle(3, 6);

        System.out.println("Circle Area: " + circle.area()); // 원의 면적 출력
        System.out.println("Square Area: " + square.area()); // 정사각형의 면적 출력
        System.out.println("Rectangle Area: " + rectangle.area()); // 직사각형의 면적 출력
    }
}

// switch 와 함께 응용하면 default 를 설정해주지 않아도 된다
public static void printShape(Shape shape) {
    switch (shape) {
        case Circle c -> System.out.println("Circle with radius: " + c.radius);
        case Square s -> System.out.println("Square with side: " + s.side);
        case Rectangle r -> System.out.println("Rectangle with width: " + r.width + " and height: " + r.height);
    }
}

// Sealed 인터페이스 정의
sealed interface Vehicle { 
}

// Car 서브클래스 정의
final class Car implements Vehicle {
    private final String model;

    public Car(String model) {
        this.model = model;
    }

    public String getModel() {
        return model;
    }
}

// Bike 서브클래스 정의
final class Bike implements Vehicle {
    private final String type;

    public Bike(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}
728x90

'Java' 카테고리의 다른 글

Java 에서 다형성과 객체지향 프로그래밍  (1) 2025.01.29
Java 에서 상속  (0) 2025.01.29
프로젝트, 코드 품질 관리에 대해서  (0) 2025.01.26
Java 메모리 영역과 Overflow  (0) 2025.01.23
Modern Java 란 무엇일까?  (1) 2025.01.16