개인 프로젝트를 만들기 위해 Java LTS 버전 중 최신 버전이기도 한 21 버전을 사용하기로 했는데
그동안 Java 버전별로 어떤 특징이 있는지 알아보고 사용하자.
JDK 1.5
2004년 9월에 출시, 이때부터 버전 중 앞의 1을 빼고 표기하기 시작했다.
JDK 1.5 = Java 5
Autoboxing / Unboxing
- Java에는 Primitive(원시) 타입과 Reference(참조) 타입이 있다.
- Primitive type (int, long, double, float, boolean, byte, short, char)
int numA = 1;
int numB = 1;
System.out.println(numA == numB); // true
- Reference type (Integer, Long, Double, Float, Boolean, Byte, Short, Char) = Wrapper class
Integer numA = new Integer(1);
Integer numB = new Integer(1);
System.out.println(numA == numB); // false
System.out.println(numA.equals(numB)); // true
위 출력들을 보면 같은 값을 갖는 Primitive type과는 달리
Primitive type에 대한 Wrapper class는 Reference type이기 때문에 객체의 주소를 가진다.
Primitive type을 Wrapper Class로 변환하는 것을 Auto Boxing,
Wrapper class를 Primitive type으로 변환하는 것을 Auto Unboxing이라고 한다.
// Auto Boxing
// 컴파일러가 Integer integerNum = Integer.valueOf(1); 로 변환
Integer integerNum = 1;
// Auto Unboxing
// 컴파일러가 int intNum = integerNum.intValue(); 로 변환
int intNum = integerNum;
Generics
- 잘못된 타입이 사용될 수 있는 문제를 컴파일 시점에 제거할 수 있게 되었다.
- 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.
- 제네릭 사용 전
필드에 모든 종류의 객체를 저장하기 위해 Java에서 가장 상위 클래스인 Object 타입으로 선언
자식 객체 생성 시 자동 타입 변환이 발생하여 빈도가 빈번하다면 프로그램 성능에 좋지 않다.
public class Data {
private Object object;
public void set(Object object) {
this.object = object;
}
public Object get() {
return object;
}
}
- 제네릭 사용 후
더 이상 타입 변환이 일어나지 않는다.
public class Data<T> {
private T value;
public Data(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
- 타입을 제한한 제네릭 사용법
public class Generics {
public static void main(String[] args) {
Data<Integer> integerData = new Data<>(1);
Data<String> stringData = new Data<>("Number");
int intValue = integerData.getValue();
String stringValue = stringData.getValue();
System.out.println("Integer Value : " + intValue); // Integer Value : 1
System.out.println("String Value : " + stringValue); // String Value : Number
Intger[] integerArray = {1, 2, 3, 4, 5};
String[] stringArray = {"One", "Two", "Three", "Four", "Five"};
System.out.print("Integer Array : ");
printArray(integerArray); // Integer Array : 1 2 3 4 5
System.out.print("String Array : ");
printArray(stringArray); // String Array : One Two Three Four Five
}
public static <E> void printArray(E[] elements) {
for(E element : elements) {
System.out.print(element + " ");
}
System.out.println();
}
}
Enumeration
- 한정된 값만을 찾는 데이터 타입
- 열거 상수는 모두 대문자로 작성
enum Weekend {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public class Enumeration {
public static void main(String[] args) {
// 열거형 상수 사용
Weekend today = Weekend.FRIDAY;
System.out.println("Today : " + today);
// 열거형 상수 비교
if(today == Weekend.FRIDAY) {
System.out.println("Today is Friday");
} else {
System.out.println("Today is not Friday");
}
// 열거형 상수 순회
System.out.print("Weekend : ");
for(Weekend day : Weekend.values()) {
System.out.print(day + " ");
}
}
}
Enhanced for each loop
- JDK 1.5 이전
public class ForEach {
public static void main(String[] args) {
final String[] names = {"A", "B", "C"};
for(int i = 0; i < names.length; i++) {
System.out.print(names[i] + " ");
}
}
}
- JDK 1.5 이후
public class ForEach {
public static void main(String[] args) {
final String[] names = {"A", "B", "C"};
for(String name : names) {
System.out.print(name + " ");
}
}
}
Varargs
- variable arguments의 줄임말로 메서드의 인자로 받을 value의 갯수를 임의로 설정하는 방식이다.
- JDK 1.5 이전
class NotVarargs {
public int sum(int a, int b) {
return a + b;
}
public int sum(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
NotVarargs notVarargs = new NotVarargs();
System.out.println(notVarargs.sum(1, 2)); // 3
System.out.println(notVarargs.sum(1, 2, 3)); // 6
}
}
- JDK 1.5 이후
public class Varargs {
public int sum(int... args) {
System.out.println("args length : " + args.length);
int result = 0;
for(int num : args) {
result += num;
}
return result;
}
public static void main(String... args) {
Varargs varargs = new Varargs();
System.out.println("sum : " + varargs.sum(1, 2)); // sum : 3
System.out.println("sum : " + varargs.sum(1, 2, 3)); // sum : 6
System.out.println("sum : " + varargs.sum(1, 2, 3, 4)); // sum : 10
System.out.println("sum : " + varargs.sum()); // sum : 0
}
}
Multi-thread improvements
- java.util.concurrent 패키지가 도입되면서 다중 스레드 프로그램의 실행과 관련된 부분들이 개선되었다.
1. `volatile` 키워드
변수가 `volatile` 키워드가 붙어서 선언되면 항상 메모리에서 최신 값을 가져온다.
붙어있지 않으면 캐시를 이용하여 데이터를 사용하는데, 이는 멀티 스레드일 경우 문제가 될 수 있다.
public class Volatile {
private volatile boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
public boolean isFlag() {
return flag;
}
public static void main(String[] args) {
Volatile volatile = new Volatile();
Thread thread1 = new Thread(() -> {
while(!volatile.isFlag()) {
// flag 값이 변경될 때까지 대기
}
System.out.println("Thread 1 : Flag is true.");
});
Thread thread2 = new Thread(() -> {
volatile.toggleFlag(); // flag 값을 true로 설정
System.out.println("Thread 2 : Flag set to true.");
});
thread1.start();
thread2.start();
}
}
2. `Atomic` 키워드
`synchronized` 키워드가 성능에 좋지 않은 점 보완 가능
여러 스레드가 잠금을 획득하려고 시도하면 그중 하나가 승리하고 나머지 스레드는 차단되거나 일시 중단되는데, 이때 스레드를 일시 중지했다가 다시 시작하는 프로세스는 비용이 많이 들고 시스템의 전반적인 효율성에 영향을 미친다.
public class AtomicExample {
public static void main(String[] args) {
AtomicWithoutLock atomicWithoutLock = new AtomicWithoutLock();
atomicWithoutLock.increment();
System.out.println("atomicWithoutLock.getCount() : " + atomicWithoutLock.getCount());
SynchronizedWithLock synchronizedWithLock = new SynchronizedWithLock();
synchronizedWithLock.increment();
System.out.println("synchronizedWithLock.getCount() : " + synchronizedWithLock.getCount());
}
}
class AtomicWithoutLock {
private final AtomicInteger count = new AtomicInteger(0);
int getCount() {
return count.get();
}
void increment() {
count.incrementAndGet();
}
}
class SynchronizedWithLock {
private int count;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
3. `Lock` 인터페이스
Java에서 스레드 간의 동기화를 관리하기 위한 메커니즘을 제공하며, `synchronized` 키워드보다 더 유연한 동기화 제어를 제공한다.
public class LockExample {
private static int shardValue = 0;
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for(int i = 0; i < 5; i++) {
performTask();
}
});
Thread thread2 = new Thread(() -> {
for(int i = 0; i < 5; i++) {
performTask();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch(InterruptedException e) {
System.out.println("Final shared value : " + shardValue);
}
}
private static void performTask() {
lock.lock(); // 임계구역임을 명시
try {
int tempValue = shardValue;
tempValue++;
try {
Thread.sleep(100);
} catch(InterruptedException e) {
System.out.println("ERROR : " + e);
}
sharedValue = temp;
} finally {
lock.unlock();
}
}
}
4. `Executor` 인터페이스
스레드 풀을 관리한다.
public class Concurrent {
public static void main(String[] args) {
// 스레드 풀 생성
ExecutorService executor = Executors.newFixedThreadPool(2);
// 작업 제출
executor.execute(() -> {
for(int i = 0; i <= 5; i++) {
System.out.println("Thread 1 - Iteration " + i);
}
});
executor.execute(() -> {
for(int i = 0; i <= 5; i++) {
System.out.println("Thread 2 - Iteration " + i);
}
});
executor.execute(() -> {
for(int i = 0; i <= 5; i++) {
System.out.println("Thread 3 - Iteration " + i);
}
});
// 스레드 풀 종료
executor.shutdown();
}
}
JDK 1.6
이때부터 J2SE에서 Java SE로 표기가 변경되었다.
Annotation 기능 향상
1. 사용자 정의 어노테이션을 만들 시 적용 대상을 지정 가능
향상된 Annotation
사용자 정의 어노테이션 제공
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationName { }
ElementType 열거 상수 | 적응 요소 |
TYPE | 클래스, 인터페이스, 열거타입 |
ANNOTATION_TYPE | 어노테이션 |
FIELD | 필드 |
CONSTRUCTOR | 생성자 |
METHOD | 메서드 |
LOCAL_VARIABLE | 로컬 변수 |
PACKAGE | 패키지 |
2. 언제까지 유지할지 유지정책 지정 가능
@Target({ ... })
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationName { }
RetentionPolicy 열거 상수 | 어노테이션 적용 시점 | 어노테이션 제거 시점 |
SOURCE | 컴파일할 때 적용 | 컴파일된 후에 제거됨 |
CLASS | 메모리로 로딩할 때 적용 | 메모리로 로딩된 후에 제거됨 |
RUNTIME | 실행할 때 적용 | 계속 유지됨 |
3. 리플렉션을 이용해 어노테이션에서 값 추출
public class Annotation {
@MyAnnotation(value = "Custom Value", count = 10)
public void myAnnotation() {
System.out.println("Annotation.myAnnotatedMethod");
}
public static void main(String[] args) throws NoSuchMethodException {
// Annotation 클래스 지정
Class<Annotation> annotationClass = Annotation.class;
// 메서드 추출
Method method = annotationClass.getMethod("myAnnotatedMethod");
// 추출한 메서드로부터 어노테이션 추출
MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);
// 어노테이션 내용 검증
if(myAnnotation != null) {
String value = myAnnotation.value();
int count = myAnnotation.count();
System.out.println("Value : " + value); // Custom Value
System.out.println("Count : " + count); // 10
}
}
}
JVM 성능 개선
- 동기화 및 컴파일러 성능 최적화
- 새로운 알고리즘 및 기존 GC 알고리즘 업데이트
- 애플리케이션 시작 성능 향상
JDK 1.7
String in switch
- switch 문에서 문자열을 사용할 수 있도록 개선됨
public class StringInSwitch {
public static void main(String[] args) {
String fruit = "apple";
switch(fruit) {
case "apple":
System.out.println("Selected fruit is an apple.");
break;
case "banana":
System.out.println("Selected fruit is an banana.");
break;
default:
System.out.println("Unknown fruit.");
}
}
}
Diamond Operator
- 변수를 선언할 때 동일한 타입으로 호출하고 싶다면, 타입을 명시하지 않고 <>만 붙일 수 있다.
public class DiamondOperator {
public static void main(String[] args) {
// JDK 1.7 이전
List<String> list1 = new ArrayList<String>();
// JDK 1.7 이후, 다이아몬드 연산자를 사용하여 타입을 중복 기술할 필요가 없음
List<String> list2 = new ArrayList<>();
}
}
Binary integer literals
- 0b로 시작하는 이진 정수 리터럴을 사용할 수 있게 되었다.
public class BinaryIntegerLiterals {
public static void main(String[] args) {
int binaryValue = 0b2020;
System.out.println("Binary Value : " + binaryValue); // 10
}
}
Automatic resource management in try-statement
- try 문을 사용하여 자원 관리를 자동화하는 기능
- 자원을 열고 닫는 부분을 명시적으로 처리하는 번거로움을 줄일 수 있다.
- JDK 1.7 이전 : BufferedReader의 자원에 대해 닫는 코드를 직접 작성해줘야 한다.
public class ResourceManagementBeforeJdk7 {
public static void main(String[] args) {
try {
BufferedReader br = new BufferedReader(new FileReader("example.txt"));
String line = "";
while((line = br.readLine()) != null) {
System.out.println(line);
}
} catch(IOException e) {
System.err.println("An error occurred : " + e.getMessage());
} finally {
try {
if(br != null) {
br.close();
}
} catch(IOException e) {
System.err.println(e.getMessage());
}
}
}
}
- JDK 1.7 이후 : try 블록에서 자원을 열고 사용하면, 자원을 닫는 코드는 작성하지 않아도 된다.
public class ResourceManagementAfterJdk7 {
public static void main(String[] args) {
try(BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line = "";
while((line = br.readLine()) != null) {
System.out.println(line);
}
} catch(IOException e) {
System.err.println("An error occurred : " + e.getMessage);
}
}
}
JDK 1.8 (LTS)
2014년 3월 출시
오라클 인수 후 첫 번째 버전이며, 2개의 버전으로 나누어진다.
- Oracle JDK
- Open JDK
둘 다 Java Development Kit이며, 본질적으로 코드 변경이 거의 없어 기능적으로 매우 유사하다.
Java 11 이후부터는 Oracle JDK와 Open JDK의 빌드가 동일해진다.
Lambda Expression
- 람다식은 익명 함수를 생성하기 위한 식으로, 객체 지향 언어보다는 함수 지향 언어에 가깝다.
// 람다식 적용X
Comparator<Car> price = new Comparator<Car>() {
public int compare(Car car1, Car car2) {
return car1.getPrice().compareTo(car2.getPrice());
}
}
// 람다식 적용O
Comparator<Car> price = (Car car1, Car car2) -> car1.getPrice().compareTo(car2.getPrice());
람다식은 함수형 인터페이스라는 문맥에서 사용 가능하다.
Functional Interface
- 하나의 추상 메서드를 지정하는 인터페이스를 의미한다.
@FunctinalInterface
interface Calculator {
// 추상 메서드를 단 한개만 가져야 한다.
int calculate(int num1, int num2);
}
public class FunctionalInterfaceExample {
public static void main(String[] args) {
// 람다 표현식 사용X
final Calculator additionBeforeUsingLambda = new Calculator() {
@Override
public int calculate(int num1, int num2) {
return num1 + num2;
}
};
// 람다 표현식 사용O
final Calculator addition = (num1, num2) -> num1 + num2;
final Calculator subtraction = (num1, num2) -> num1 - num2;
final int result1 = calculation(5, 3, additionBeforeUsingLambda);
final int result2 = calculation(5, 3, addition);
final int result3 = calculation(8, 4, subtraction);
System.out.println(result1); // 8
System.out.println(result2); // 8
System.out.println(result3); // 4
}
public static int calculation(final int num1, final int num2, final Calculator calculator) {
return calculator.calculate(num1, num2);
}
}
아래는 Java8에 추가된 함수형 인터페이스들이다.
- Consumer
- Supplier
- Function
- Operator
- Predicate
Functional Interface - Consumer
매개변수를 받지만, 결과를 반환하지 않고 동작소비만 하는 경우 사용
public class ConsumerExample {
public static void main(String[] args) {
final List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
// 리스트의 각 요소를 매개변수로 받아, 출력하는 데에 사용하는 Consumer
final Consumer<String> printFruits = (fruit) -> System.out.println(fruit);
fruits.forEach(printFruits);
/*
* Apple
* Banana
* Orange
*/
}
}
Functional Interface - Supplier
매개변수를 받지 않고, 결과를 반환하는 경우 사용
public class SupplierExample {
public static void main(String[] args) {
// 매개변수를 받지 않고, 무작위 숫자를 생성(리턴)하는 Supplier
Supplier<Integer> randomValue = () -> (int) (Math.random() * 100);
int number = randomValue.get();
System.out.println("Random Number : " + number); // Random Number : [x * 100]
}
}
Functional Interface - Function
매개변수를 받아 결과를 반환하는 경우 사용 (Input 타입과 Return 타입을 지정하여 타입 변환 가능)
public class FunctionExample {
// 문자열을 대문자로 변환하는 Function
Function<String, String> toUpperCase = (str) -> str.toUpperCase();
String result1 = toUpperCase.apply("hello");
System.out.println("UpperCase : " + result1); // UpperCase : HELLO
// 문자열을 해당 문자열의 길이로 반환하는 Function
Function<String, Integer> toLength = (str) -> str.length();
Integer result2 = toLength.apply("hello");
System.out.println("Length : " + result2); // Length : 5
}
Operator
하나의 타입을 받아 같은 타입을 반환하는 경우 사용
public class OperatorExample {
public static void main(String[] args) {
// BinaryOperator를 사용하여 두 숫자를 더함
BinaryOperator<Integer> addition = (num1, num2) -> num1 + num2;
int result = addition(5, 3);
System.out.println("5 + 3 = " + result); // 5 + 3 = 8
}
}
Predicate
매개변수를 받아서 조건을 검사하고 Boolean 값을 반환하는 경우 사용
public class PredicateExample {
public static void main(String[] args) {
// Predicate를 사용하여 숫자가 짝수인지 검사
Predicate<Integer> isEven = (num) -> num % 2 == 0;
boolean result = isEven.test(6);
System.out.println("Is 6 even? " + result); // Is 6 even? true
}
}
Method Reference
- 메서드 참조는 람다 표현식을 더 간결하게 표현하기 위한 기능이다.
메서드 참조 사용(`참조할 클래스명::메서드명`)
public class MethodReferenceExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
// 메서드 참조 전
fruits.forEach(fruit -> System.out.println(fruit));
// 메서드 참조 후
fruits.forEach(System.out::println);
/*
* Apple
* Banana
* Orange
*/
}
}
Default Methods in Interface
- 기존의 인터페이스를 확장하고 구현체를 제공하지 않고도 메서드를 추가할 수 있게 해주는 기능이다.
interface Hello {
default void hello() {
System.out.println("Hello!");
}
}
class MyMessage implements Hello {
public void customMessage() {
System.out.println("This is a custom message.");
}
}
public class DefaultMethodExample {
public static void main(String[] args) {
final MyMessage message = new MyMessage();
message.hello(); // Hello!
message.customMessage(); // This is a custom message.
}
}
Parallel Array Sorting (병렬 배열 정렬)
- 배열의 크기가 큰 경우, 더 빠른 정렬을 위해 병렬로 동작하도록 `Arrays.parallelSort()`를 제공한다.
public class ParallelArrayExample {
public static void main(String[] args) {
// 정렬할 배열을 생성하고 랜덤 값으로 초기화
int[] numbers = new int[1000000];
// 랜덤 값 생성
Random random = new Random();
for(int i = 0; i < numbers.length; i++) {
numbers[i] = random.nextInt(1000000);
}
// 배열을 병렬로 정렬
Arrays.parallelSort(numbers);
// 정렬된 배열 출력(처음 10개 값만 출력)
for(int i = 0; i < 10; i++) {
System.out.print(numbers[i] + " ");
}
// 0 1 2 3 4 5 6 7 8 9
}
}
Base64 인코딩과 디코딩을 위한 표준 API
- Base64는 이진 데이터를 텍스트로 변환하거나, 텍스트를 이진 데이터로 디코딩할 때 주로 사용된다.
(문자열과 바이트 배열 사이의 Base64 변환을 수행할 수 있는 클래스 및 메서드 제공)
public class Base64Example {
public static void main(String[] args) {
// 인코딩할 문자열
String original = "Base64 Test";
// Base64 인코딩
String encoded = Base64.getEncoder().encodeToString(original.getBytes());
System.out.println("Encoded : " + encode); // Encoded : QmFzZTY0IFRlc3Q=
// Base64 디코딩
byte[] decodeBytes = Base64.getDecoder().decode(encoded);
String decode = new String(decodedBytes);
System.out.println("Decoded : " + decode); // Decoded : Base64 Test
}
}
Date & Tiime API
- java.time 패키지에 포함되어 있는 클래스 및 인터페이스들로, 이전에 제공되던 Date 및 Calendar 보다 연산을 수행하는 데에 적합하다.
LocalDate, LocalTime, LocalDateTime
// 현재 날짜 및 시간 가져오기
LocalDate currentDate = LocalDate.now(); // 2024-03-27
LocalTime currentTime = LocalTime.now(); // 02:09:45.729041
LocalDateTime currentDateTime = LocalDateTime.now(); // 2024-03-27T02:09:45.729041
LocalDate tomorrow = currentDate.plusDays(1); // 2024-03-28
Instant
// 시간 간격 계산
Instant start = Instant.now();
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.err.println("ERROR : " + e.getMessage);
}
Instant end = Instant.now();
// 차이 구하기
Duration elapsedTime = Duration.between(start, end);
System.out.println("Elapsed Time : " + elapsedTime.toMillis() + "ms");
// Elapsed Time : 1009 ms
ZoneId, ZonedDateTime
ZoneId zone = ZoneId.of("America/New_York");
ZonedDateTime newYorkTime = ZonedDateTime.now(zone);
System.out.println("New York Time : " + newYorkTime);
// New York Time : 2024-03-27T11:35:20.123456789-04:00[America/New_York]
null 대신 Optional
- Java에서 null 값을 다루는 방식을 개선하기 위해 도입되었다.
- null로 인한 NullPointerException을 방지하고, 코드의 가독성을 향상시키는 데 도움을 준다.
- Optional 클래스 동작 방식
- 값이 있으면 값을 감싸고
- 값이 없으면 Optional.empty 메서드로 Optional을 반환한다.
public class OptionalExample {
public static void main(String[] args) {
// 값이 존재하는 경우
Optional<String> notEmpty = Optional.of("Hello, Optional!");
// 값이 존재하지 않는 경우
Optional<String> empty = Optional.empty();
// 값이 존재하는 경우, 값을 출력
notEmpty.ifPresent(value -> System.out.println("Value : " + value); // Value : Hello, Optional!
// 값이 존재하지 않는 경우, 기본값 반환
String orElseValue = empty.orElse("Default Value");
System.out.println("Value : " + orElseValue); // Value : Default Value
// 값이 존재하지 않는 경우, 예외 발생
String value = empty.orElseThrow(
() -> new IllegalArgumentExcpetion("Value is not present"));
}
}
Stream API
- 스트림은 데이터 처리를 함수형 스타일로 선언하여 수행할 수 있게 한다.
- 병렬 처리와 지연 연산 등을 지원하며, 코드의 가독성과 성능을 향상시킨다.
위 선언형은 데이터를 처리하는 임시 구현 코드 대신 질의로 표현하는 방식을 말한다.
public class StreamExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
// 컬렉션에서 스트림 생성
Stream<String> stream = fruits.stream();
// 스트림에서 필터링하여 새로운 스트림 생성 (길이 제한)
Stream<String> filteredStream = stream.filter(fruit -> fruit.length() > 5);
// 매핑하여 스트림 변환 (대문자로)
Stream<String> mappedStream = filteredStream.map(fruit -> fruit.toUpperCase());
// 스트림을 리스트로 변환
List<String> resultList = mappedStream.collect(Collectors.toList());
// 결과 출력
resultList.forEach(System.out::println);
/*
* BANANA
* ORANGE
*/
}
}
- 병렬 스트림
public class ParallelExample {
public static void main(String[] args) {
Random random = new Random();
List<Integer> scores = new ArrayList<>();
for(int i = 0; i < 100000000; i++) {
scores.add(random.nextInt(101));
}
double avg = 0.0;
long startTime = 0;
long endTime = 0;
long time = 0;
Stream<Integer> stream = scores.stream();
startTime = System.nanoTime();
avg = stream.mapToInt(i -> i)
.average()
.getAsDouble();
endTime = System.nanoTime();
time = endTime - startTime;
System.out.println("avg : " + avg + ", 일반 스트림 처리 시간 : " + time + "ns");
// avg : 50.00269196, 일반 스트림 처리 시간 : 83008083ns
Stream<Integer> parallelStream = scores.parallelStream();
startTime = System.nanoTime();
avg = parallelStream.mapToInt(i -> i)
.average()
.getAsDouble();
endTime = System.nanoTime();
time = endTime - startTime;
System.out.println("avg : " + avg + ", 병렬 스트림 처리 시간 : " + time + "ns");
// avg : 50.00269196, 병렬 스트림 처리 시간 : 26553958ns
}
}
JDK 9
JPMS : Java 9 Platform Module System
Java 8 이전 버전까지는 응용 프로그램이 표준 라이브러리의 5%만 사용하는데도 불구하고, 응용 프로그램을 실행하려면 전체 표준 라이브러리가 갖추어진 자바 실행 환경(JRE)이 필요했다.
즉, 응용 프로그램을 실행하는 데 필요한 모듈만으로 구성된 작은 사이즈의 자바 실행 환경(JRE)을 만들기 위해 표준 라이브러리를 모듈화 한 것이다.
작은 사이즈의 자바 실행 환경이 필요한 경우는 아래와 같다.
- 독립 실행형(응용 프로그램 + 표준 라이브러리)으로 배포할 경우, 표준 라이브러리의 크기가 작을수록 배포 크기가 줄어든다.
- 제한된 자원만 가지고 있는 소형(임베디드) 기기에는 사이즈가 작은 자바 실행 환경이 필요하다.
모듈과 라이브러리의 차이
일반 라이브러리는 내부에 포함된 모든 패키지에 외부 프로그램에서의 접근이 가능하지만,
모듈은 일부 패키지를 은닉하여 접근할 수 없게 할 수 있다.
module-info.java
모듈의 또 다른 차이점은 자신이 실행할 때 필요로 하는 의존 모듈을 모듈 기술자(module-info.java)에 기술할 수 있기 때문에 모듈 간의 의존 관계를 쉽게 파악할 수 있다.
pakage-info.java
아래 두 가지 목적으로 사용된다.
- 패키지 단위의 문서 작성
- JDK 5 이전까지 사용되던 package.html의 대체자로 활용된다.
- 패키지 단위의 어노테이션 적용
- 패키지 단위의 어노테이션 적용이 필요한 상황이라면, 간단히 package-info.java에 어노테이션을 붙임으로써 문제를 해결한다.
응용 프로그램 모듈화
응용 프로그램은 하나의 프로젝트로도 개발이 가능하지만, 기능별로 서브 모듈로 쪼갠 후 조합해서 개발할 수 있다.
응용 프로그램의 규모가 커질수록 협업과 유지보수 측면에서 서브 모듈로 쪼개서 개발하는 것이 유리하다.
서브 모듈로 개발된 모듈들은 다른 응용 프로그램에서도 재사용이 가능하다.
Interface Private Method
- 인터페이스 내에 private 메서드 사용이 가능하다.
public class InterfacePrivateMethod {
public static void main(String[] args) {
CalculatorApp calculatorApp = new CalculatorApp();
int num1 = 3;
int num2 = 5;
int resultAdd = calculatorApp.add(num1, num2); // a = 3, b = 5
System.out.println(resultAdd); // 8
int resultSubtract = calculatorApp.subtract(num1, num2); // a = 3, b = 5
System.out.println(resultSubtract); // -12
}
}
interface Calculator {
default int add(int num1, int num2) {
printInputNum(num1, num2);
return num1 + num2;
}
default int subtract(int num1, int num2) {
printInputNum(num1, num2);
return num1 - num2;
}
private void printInput(int num1, int num2) {
System.out.println("a = " + a + ", b = " + b);
}
}
class CalculatorApp implements Calculator { }
JShell (대화형 쉘)
코드 스니펫을 바로 실행하고 테스트할 수 있어서 빠른 프로토타이핑 및 학습 도구로 사용될 수 있다.
Stream API 추가
takeWhile()
조건에 대해 참이 아닐 경우 바로 멈춤
이미 정렬되어 있다면 false가 등장한 위치부터 반복을 중단할 수 있기 때문에 크기가 큰 Stream의 경우 많은 시간 절약 가능
public class StreamApiExample {
public static void main(String[] args) {
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 조건에 대해 참이 아닐 경우 바로 멈춘다.
Stream<Integer> takeWhileStream = numbers.stream().takeWhile(n -> n < 6);
takeWhileStream.forEach(System.out::print); // 1 2 3 4 5
}
}
dropWhile()
`takeWhile()`과는 정반대 작업으로, 처음으로 false가 등장하는 시점까지의 요소를 모두 버리고 남은 요소 반환
public class StreamApiExample {
public static void main(String[] args) {
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 처음으로 false가 등장하는 시점까지의 요소를 모두 버리고 남은 요소를 반환한다.
Stream<Integer> dropWhileStream = numbers.stream().dropWhile(n -> n < 6);
dropWhileStream.forEach(System.out::print); // 6 7 8 9 10
}
}
Try-With-Resources 개선
try 블럭에 선언된 객체들에 대해서 AutoCloseable 인터페이스를 구현하는 자원을 try 문이 종료 될 때 자동으로 자원을 해제해 주는 기능이다.
Java 9 이후에는 try 블럭 밖에서 선언한 변수를 가져와 사용할 수 있다.
public class TryWhitResources {
public static void main(String[] args) {
try(BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line = br.readLine();
System.out.println(line);
} catch(IOException e) {
System.err.println("ERROR : " + e.getMessage);
}
}
}
JDK 10
Local-Variable Type Type Inference (var keyword)
- 로컬 변수 타입 추론에 사용되는 var 키워드가 등장했다.
- 메서드 내부의 변수에만 적용 가능하다.
- 초기화를 하지 않으면 컴파일 에러가 발생한다.
public class VarLocalVariable {
public static void main(String[] args) {
final String nickname = "yn3";
System.out.println(nickname.getClass()); // class java.lang.String
final var varNickname = "yn3";
System.out.println(varNickname.getClass()); // class java.lang.String
}
}
JDK 11 (LTS)
Oracle JDK와 OpenJDK가 통합되었으며, Oracle JDK가 유료 모델로 전환되었다.
String API 개선
public class ImprovedStringApi {
public static void main(String[] args) {
final String multilineString = "Improved Strign API \n in \n JDK 11.";
multilineString.lines()
.filter(line -> !line.isBlank())
.map(String::strip)
.forEach(line -> System.out.println("line = " + line));
}
}
File API 개선
public class ImprovedFileApi {
public static void main(String[] args) throws IOException {
String content = "Sample text";
// writeString() : 파일 생성 후 경로 출력
Path filePath = Files.writeString(
Files.createTempFile(
Path.of("."),
"JDK11-file-example",
".txt"),
content);
String fileContent = Files.readString(filePath);
System.out.println("content : " + content);
System.out.println("fileContent : " + fileContent);
}
}
Local-Variable Syntax for Lambda Parameters
- Lambda 파라미터로 var를 사용한다.
(var x, var y) -> x.process(y) => (x, y) -> x.process(y)
HTTP Client
- JDK 9에 도입되었던 java.net.http 패키지의 HTTP 클라이언트가 JDK 11에서 표준화되었다.
public class HttpClientExample {
public static void main(String[] args) throws IOException, InterruptedException {
final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(duration.ofSeconds(20))
.build();
final HttpRequest httpRequest = HttpRequest.newBuilder()
.GET()
.uri(URI.create("http://www.naver.com"))
.build();
final HttpResponse<String> httpResponse = httpClient.send(httpRequest.BodyHandlers.ofString());
final String body = httpResponse.body();
Sysmte.out.println("body : " + body);
}
}
JDK 12
Switch Expression 확장 (Preview)
public class ImprovedSwitchExpression {
public static void main(String[] args) {
int day = 3;
String dayName;
// before JDK 12
switch(day) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Not monday";
break;
case 3:
dayName = "Not monday";
break;
default:
dayName = "Unknown";
break;
}
System.out.println(dayName); // Not monday
// after JDK 12
dayName = switch(day) {
case 1 -> "Monday";
case 2, 3 -> "Not monday";
default -> "Unknown";
};
System.out.println(dayName); // Not monday
}
}
JDK 13
Switch Expression 개선 (Preview)
- switch 문이 값을 반환할 수 있도록 `yield` 키워드 추가
public class SwitchExpression {
public static void main(String[] args) {
int day = 3;
String dayType = switch(day) {
case 1, 2, 3, 4, 5 -> {
System.out.println("day : " + day);
yield "Weekday";
}
case 6, 7 -> {
System.out.println("day : " + day);
yield "Weekend";
}
default -> {
System.out.println("day : " + day);
yield "Invalid day";
}
};
System.out.println(dayType); // Weekday
}
}
Text Blocks : Multiline Strings (Preview)
- 줄 바꿈 문자가 자동으로 포함되는 문법이다.
public class TextBlock {
publick static void main(String[] args) {
// before JDK 13
String multiLineString = "Thist is a\n"
+ "multi-line\n"
+ "string.";
System.out.println(multiLineString);
// after JDK 13
String textBlock = """
This is a
multi-line
text block.""";
System.out.println(textBlock);
}
}
JDK 14
Records
- 변수의 타입과 이름을 이용해 private final 필드로 선언한다.
- 생성자, Getter, hashCode(), equals(), toString()도 자동 생성한다.
public class RecordsExample {
public static void main(String[] args) {
PeopleClass peopleClass = new PeopleClass(1, "yn3");
System.out.println("peopleClass : " + peopleClass); // peopleClass : PeopleClass[num : 1, nickname : yn3]
PeopleRecord peopleRecord = new PeopleRecord(1, "yn3");
System.out.println("peopleRecord : " + peopleRecord); // peopleRecord : PeopleRecord[num=1, nickname=yn3]
}
}
record PeopleRecord(
int num,
Stirng nickname
) { }
class PeopleClass {
int num;
String nickname;
public PeopleClass(int num, String nickname) {
this.num = num;
this.nickname = nickname;
}
public int getNum() {
return num;
}
public String getNickname() {
return nickname;
}
@Override
public String toString() {
return "PeopleClass[" +
"num : " + num +
", nickname : " + nickname +
"]";
}
@Override
public boolean equals(Obejct o) {
if(this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PeopleClass that = (PeopleClass) o;
return num == that.num && Object.equals(nickname, that.nickname);
}
@Override
public int hashCode() {
return Object.hash(num, nickname);
}
}
NullPointerExceptions 개선
- 정확히 어떤 변수가 null인지 자세히 나타낸다.
public class HelpfulNullPointerException {
public static void main(String[] args) {
PeopleRecord people = new PeopleRecord(null, "yn3");
int num = people.num(); // null
System.out.println("num : " + num); // NPE
}
}
/*
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()"
because the return value of "Conditional_Statement.PeopleRecord.num()" is null
at Conditional_Statement.HelpfulNullPointerException.main(HelpfulNullPointerException.java:6)
*/
Pattern Matching for instanceof
public class PatternMatchingInstanceof {
public static void main(String[] args) {
Object obj = "Hello, Pattern Matching!";
// instanceof를 사용한 기존 코드
if(obj instanceof String) {
// 형변환 필요
String str = (String) obj;
System.out.println("Length of the string : " = str.length());
} else {
System.out.println("Not a String");
}
// Pattern Matching을 사용한 코드
if(obj instanceof String str) {
// 형변환 필요없이 바로 str 변수 할당 후 사용
System.out.println("Length of the string : " + str.length());
} else {
System.out.println("Not a String");
}
}
}
JDK 15
Sealed Class
- Sealed Class 혹은 Interface는 무분별한 상속/구현을 막기 위해 특정 클래스에만 허용하도록 제한한다.
- `sealed` 키워드를 이용해 선언하고, `permits` 키워드로 상속받을 서브 클래스를 명시한다.
public sealed class Developer permits Yn3 { ... }
- 자식 클래스는 `sealed`, `final`, `non-sealed` 세 종류로 나뉜다.
- `sealed` : 자식도 마찬가지로 상속받을 서브 클래스를 명시할 수 있다.
- `final` : 더 이상 확장할 수 없다.
- `non-ssealed` : 모든 서브 클래스들에 의해 확장될 수 있다.
public sealed class Yn3Sub1 extends Yn3 permits Yn3Sub11 { ... }
public final class Yn3Sub2 extends Yn3 { ... }
public non-sealed class Yn3Sub3 extends Yn3 { ... }
public class SealedClassExample {
public static void main(String[] args) {
final Person employee = new Employee();
final Person manager = new Manager();
final Person director = new Director();
employee.work();
manager.work();
director.work();
}
}
sealed interface Person permits Employee, Manager, Director {
void work();
}
sealed class Employee implements Person permits Employee2 {
@Override
public void work() {
System.out.println("제품 생산");
}
}
non-sealed class Manager implements Person {
@Override
public void work() {
System.out.println("제품 관리");
}
}
final class Director implements Person {
@Override
public void work() {
System.out.println("제품 기획");
}
}
Pattern Matching for instanceof (개선)
- `instanceof`와 함께 새로운 변수를 동시에 선언하고 사용하는 기능 추가
public class InstanceofExample {
public static void main(String[] args) {
Object obj = "Hello, Java 15!";
if(obj instanceof String str && str.length() > 10) {
System.out.println("문자열 길이 10 초과");
} else {
System.out.println("문자열 길이 10 이하");
}
}
}
Records (개선)
1. Sealed Class와 함께 사용 가능
public sealed interface Shape permits Circle, Rectangle, Triangle {
record Circle(int radius) implements Shape { ... }
record Rectangle(int width, int height) implements Shape { ... }
record Triangle(int base, int height) implements Shape { ... }
}
2. 메서드 내부에서 Local Records를 정의하고 사용 가능
public class LocalRecordExample {
public static void main(String[] args) {
record Point(int x, int y) { ... }
Point point = new Point(10, 20);
System.out.println(point);
}
}
JDK 16
OpenJDK의 버전 관리가 Mercuria에서 Git으로 바뀌었다.
Record Class (정식 사용)
- JDK 14에서 처음 소개 후, Java 16 버전에서 정식으로 사용할 수 있게 되었다.
- `record` 클래스는 불변성의 데이터를 갖는 객체를 만들기 위해서 사용된다.
Stream toList
- 기존에는 Stream의 동작 결과를 List로 모으기 위해서 Collectors.toList를 사용해야 했지만, Stream에 직접 toList()가 추가되었다.
public class streamToListExample {
public static void main(String[] args) {
String[] strNums = {"1", "2", "3"};
List<Integer> beforeJdk16List = Arrays.stream(strNums)
.map(Integer::parseInt).collect(Collectors.toList());
List<Integer> afterJdk16List = Arrays.stream(strNums)
.map(Integer::parseInt).toList();
}
}
JDK 17 (LTS)
Pattern Matching for switch (Preview)
- 여러 타입에 대해 다르게 동작하고 싶을 때의 성능 개선을 위해 switch문이 개선되었다.
// before JDK 17
static String formatter(object obj) {
String formatted = "unknown";
if(obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if(obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if(obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if(obj instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
// after JDK 17
static String formatterPatternSwitch(Object obj) {
return switch(obj) {
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 -> obj.toString();
}
}
Sealed Class (표준화)
- `sealed` 클래스가 표준화되었다.
JDK 18
UTF-8 by Default
- before JDK 18 : 런타임 환경(운영체제, 사용자 로케일 및 기타 요인)을 기반으로 시작 시 Charset을 선택
- after JDK 18 : UTF-8이 기본으로 변경
Code Snippets in Java API Documentation
- API 문서에 예제 소스 코드 포함을 단순화하기 위해 JavaDoc의 표준 Doclet에 `@Snippet` 태그를 도입했다.
/**
* JDK 18에 추가된 Java Doc에서 코드 스니펫 작성 기능
* {@snippet :
* String example = "Java Doc에 예시 코드를 작성";
* System.out.println(example);
*}
*/
public class CodeSnippetsJavaDocExample {
}
Deprecate Finalization for Removal
- Java의 finalization은 객체가 garbage 수집될 때 `finalize()` 메서드를 호출하여 객체가 자원을 정리하거나 해제하는 데 사용된다.
- finalization은 언제 실행될지 확신할 수 없으며, 실행 시점은 불확실하고 보장되지 않는다.
- 메모리 누수 및 성능 저하와 같은 문제가 발생한다.
try-with-resources 구문을 사용하거나 `close()`를 명시적으로 호출하기를 권장한다.
JDK 19
Record Patterns (Preview)
- before JDK 19
record Point(int x, int y) { }
static void printSum(Object obj) {
if(obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x + y);
}
}
- after JDK 19
public class RecordsPatternExample {
record Point(int x, int y) { }
static void printSum(Object obj) {
if(obj instanceof Point(int x, int y)) {
System.out.println(x + y);
}
}
public static void main(String[] args) {
printSum(new Point(1, 2));
}
}
`instanceof`에서 `record`의 구성요소까지 추출 가능
Virtual Threads (Preview)
- 가상 스레드란 기존의 전통적인 Java 스레드에 더하여 새롭게 추가되는 경량 스레드이다.
- OS 스레드를 그대로 사용하지 않고, JVM 자체 내부 스케줄링을 통해서 사용할 수 있는 경량 스레드를 제공한다.
가상 스레드는 저렴하고 풍부하므로, 풀링 되어서는 안 된다.
모든 애플리케이션 작업에 대해 새로운 가상 스레드를 생성해야 한다.
따라서 대부분의 가상 스레드는 수명이 짧고 얕은 호출 스택을 가지며, 단일 HTTP 클라이언트 호출이나 단인 JDBC 쿼리만 수행한다.
이와 대조적으로 플랫폼 스레드는 무겁고 비용이 많이 들기 때문에 종종 풀링 되어야 한다.
수명이 길고, 호출 스택이 깊으며, 여러 작업에서 공유되는 경향이 있다.
플랫폼 스레드란
일반적으로 Java에서 스레드(java.lang.Thread)는 OS에서 생성한 스레드를 매핑해 JVM에서 사용하는 것을 말한다.
OS에 의해 스케줄링되기 때문에 스레드 간 전환을 위한 문맥 교환이 발생하기도 하고, 플랫폼 스레드를 생성하는 것이 OS에도 스레드를 하나 생성하는 것이기 때문에 동작이 매우 느려, 스레드 풀을 사용한다.
이 스레드 풀을 관리해야 하는데, 많은 양의 스레드를 만들다 보면 메모리에 문제가 생길 수도 있다.
가상 스레드란
가상 스레드는 OS의 스레드와 1:1로 대응되지 않는다.
플랫폼 스레드라고 부르던 것은 이제 캐리어 스레드라고 부른다.
캐리어 스레드는 Fork-Join 풀 안에 Worker Thread로 생성되어 스케줄링되고, 각 Worker Thread들은 Queue를 가진다.
이때 Task를 스케줄링하는데 가상 스레드 자체가 각각의 Task가 되어 Queue에 들어가게 된다.
가상 스레드는 OS의 스레드와 대응되는 개념도 아니고, JVM에서 직접 스레드를 생성하기 때문에 생성 비용이 비싸지도 않다. 또한 크기가 자동으로 조절되기 때문에 스레드 풀의 개수를 관리할 필요도 없다.
Structured Concurrency (Incubator)
- 구조화된 동시성을 위한 API를 도입하여 멀티스레드 프로그래밍을 단순화한다.
- 기본적으로 가상 스레드를 사용한다.
- 기존 동시성을 사용한 코드
Future<Shelter> shelter;
Future<List<Dog>> dogs;
try(ExecutorService executorService = Executors.newFixedThreadPool(3)) {
shelter = executorService.submit(this::getShelter);
dogs = executorService.submit(this::getDogs);
Shelter theShelter = shelter.get();
List<Dog> dogList = dogs.get();
Response response = new Response(theShelter, dogList);
} catch(ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
`getShelter()`가 실행되는 동안 `getDogs()`에서 예외가 발생한다면 `getDogs()`에서의 에러가 잡히지 않고 진행된다.
- Structured Concurrency 사용
try(var scrope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Shelter> shelter = scope.fork(this::getShelter);
Futer<List<Dogs>> dogs = sccope.fork(this::getDogs);
scope.join();
Response response = new Response(shelter.resultNow(), dogs.resultNow());
}
`StructuredTaskScope.ShutdownOnFailure()` 생성자로 해당 scope에서 문제 발생 시 하위 작업을 모두 종료한다.
JDK 20
가상 스레드 및 Structured Concurrenc에서의 성능이 개선되었다.
JDK 21 (LTS)
가상 스레드가 표준화되었다.
Structured Concurrency에서의 성능이 개선되었다.
String Templates
- Java의 새로운 종류의 표현식으로, 템플릿 표현식은 문자열 보간을 수행할 수 있다.
public class StringTemplatesExample {
public static void main(String[] args) {
String feelsLike = "Good";
String temperature = "0";
String unit = "c";
// before JDK 21
System.out.println(composeUsingFormatters(feelsLike, temperature, unit));
System.out.println(composeUsingMessageFormatter(feelsLike, temperature, unit));
// after JDK 21
System.out.println(usingStrProcessor(feelsLike, temperature, unit));
System.out.println(usingStrJsonBlock(feelsLike, temperature, unit));
}
static String composeUsingFormatters(String feelsLike, String temperature, String unit) {
return String.format("Today's weather is %s, with a temperature of %s degrees %s",
feelsLike, temperature, unit);
}
static String composeUsingMessageFormatter(String feelsLike, String temperature, String unit) {
return MessageFormat.format("Today's weather is {0}, with a temperature of {1} degrees {2}",
feelsLike, temperature, unit);
}
static String usingStrProcessor(String feelsLike, String temperature, String unit) {
return STR
. "Today's weather is \{ feelsLike }, with a temperature of \{ temperature } degrees \{ unit }";
}
static String usingStrJsonBlock(String feelsLike, String temperature, String unit) {
return STR
. """
{
"feelsLike": "\{ feelsLike }",
"temperature": "\{ temperature }",
"unit": "\{ unit }"
}""";
}
}
Sequenced Collections
- 정의된 만남 순서로 컬렉션을 나타내는 새로운 인터페이스를 도입했다.
- 각 컬렉션에는 잘 정의된 첫 번째 요소, 두 번째 요소 등을 거쳐 마지막 요소까지 포함한다.
- 첫 번째 요소와 마지막 요소에 액세스 하고, 해당 요소를 역순으로 처리하기 위한 통일된 API를 제공한다.
- before JDK 21
- after JDK 21
public class SequencedCollectionsExample {
public static void main(String[] args) {
List<String> numbers = List.of("1", "2", "3", "4");
List<String> reversedNumbers = strings.reversed();
System.out.println(strings); // [1, 2, 3 ,4]
System.out.println(reversedNumbers); // [4, 3, 2, 1]
}
}
`reversed()` 외에도 `addFirst(E)`, `addLast(E)`, `getFirst()`, `getLast()`, `removeFirst()`, `removeLast()` 메서드들이 있다.
Unnamed Patterns and Variables (Preview)
- 구성 요소의 이름이나 유형을 명시하지 않고 이름 없는 패턴 및 변수로 밑줄 문자(_)로 표시한다.
// before JDK 21
for(Order order : orders) {
if(total < LIMIT) {
...
}
}
// after JDK 21
for(Order _ : orders) {
if(total < LIMIT) {
...
}
}
Reference