본 글은 이전 팀에서 팀 내부 Java 표준 가이드를 위해 필자가 작성했던 문서에 해당한다.
해당 팀의 표준은 Java 17을 사용하고 있어 해당 버전까지의 내용을 간단하게 정리한다.
- 본 글에선 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();
};
}