대부분 Java 버전 8이나 11을 사용할 것이다. 그러나 가장 최근(2022년 9월)에 릴리즈된 버전은 19이다. 버전 6부터 8까지는 릴리즈 주기가 3 ~ 4년이었지만, 버전 9부터는 6개월 단위로 릴리즈된다. 버전 20은 2023년 3월 릴리즈 예정이다.
버전 별 릴리즈 날짜는 오라클 공식 문서에서 확인할 수 있다. 각 버전 별 특징을 알아보기 전 몇 가지 용어를 정리하자.
LTS는 Long Term Support의 약어로 출시 후 8년이라는 긴 기간 동안 보안 업데이트와 버그 수정을 지원할 것임을 선언한 버전이다. 아래 표에서 볼 수 있듯 Java 7, 8, 11, 17, 21이 LTS 릴리즈 버전이다. Java 8에 대한 지원은 2030년 12월까지로 연장되었다.
JEP는 Java Enhancement Proposal의 약어로 자바 코어 기술을 강화하기 위한 제안 문서이다. 말그대로 제안이기 때문에 모든 JEP가 승인되어 업데이트에 포함되는 것이 아니며 일부가 채택되어 특정 JDK 버전에 반영된다.
Oracle에서는 새로운 기능을 아래와 같이 3가지 카테고리로 분류해 개발자가 우선적으로 새로운 기능을 확인하고 이에 대한 피드백을 원활히 이루어질 수 있도록 한다.
Preview feature: 설계, 명세, 구현이 완료되었지만 평가가 필요한 기능이다. 아래는 버전 10부터 15까지 Preview 단계를 거쳐 표준으로 도입된 일부 기능들을 나타낸다.
Experimental feature: 주로 HotSpot JVM의 새로운 기능을 의미한다.
Incubating feature (Incubator modules): 새로 추가할 가능성이 있는 기능이나 JDK 툴을 의미한다.
Preview, Experimental, Incubating feature와 관련해서 Oracle 블로그에 세세하게 정리된 글이 있다. 더 자세히 알고 싶다면 여기를 참고하면 된다.
이제부터 Java 10에서 14까지 특징을 알아보자!
지역 변수 선언 시 아래와 같이 변수의 타입 대신 var로 선언할 수 있다.
String str = "Hello";
var str = "Hello";
int i = 10;
var i = 10;
HttpClient httpClient = HttpClient.newBuilder().build();
var httpClient = HttpClient.newBuilder().build();
var로 변수를 선언할 때 초기화를 함께 해줘야 한다. 그렇지 않으면 컴파일 에러가 발생한다. 또한, var은 예약어가 아니기 때문에 변수를 var로 선언해도 문제가 발생하지 않는다.
var i; // Invalid Declaration - Cannot use 'var' on variable without initializer
var var = 10; // Valid Declaration
var을 모든 곳에서 사용할 수 있는 것은 아니다. 아래와 같이 사용할 수 있는 경우가 있고, 없는 경우가 있다.
// Allowed Usage
var blog = "mangoo";
for (var data : dataList) { ... }
for (var i = 0; i < dataList.size(); i++) { ... }
// Not Allowed Usage
public class Application {
var firstName;
public Application(var param) { ... }
public var demoMethod1() {
try { ... }
catch (var exception) { ...}
}
public Integer demoMethod2(var input) { ... }
}
var은 버전 10에서 새로 도입된 특징이기 때문에 10보다 낮은 버전의 JDK에서는 컴파일되지 않는다. 또한, 자바에서 타입은 런타임이 아니라 컴파일 타임에 추론되기 때문에 var로 변수를 선언한 코드를 컴파일하면 타입을 명시적으로 표현한 코드와 동일한 바이트코드가 생성된다. 즉, var를 사용한다고 해서 런타임에 추가 처리가 발생하지 않는다.
// 왼쪽
public class Main {
public static void main(String[] args) {
var s = "Hello";
System.out.println(s);
}
}
// 오른쪽
public class Main {
public static void main(String[] args) {
String s = "Hello";
System.out.println(s);
}
}
람다 표현식에서는 아래와 같이 타입을 명시적으로 선언할 수도 있지만 버전 8에서 도입된 향상된 타입 추론 기능이 있기 때문에 타입을 선언하지 않아도 된다.
(List<String> l, String s) -> l.add(s);
(l, s) -> l.add(s);
버전 11부터는 람다식의 매개변수를 var로 선언할 수 있다.
(var l, var s) -> l.add(s);
타입을 명시적으로 선언한 코드와 비교했을 때 var 사용에서의 장점이 있는지 잘 모르겠다. 그럼 언제 필요한걸까?
변수에 애노테이션이 붙이는 경우이다. 변수에 애노테이션을 붙이려면 명시적인 타입 선언이나 var 앞에만 붙일 수 있다. 따라서 var을 사용했을 때 코드를 좀 더 간결하게 표현할 수 있다는 점이 장점인 것 같다.
// Valid Declaration
(@Nonnull List<String> l, @Nullable String s) -> l.add(s);
(@Nonnull var l, @Nullable var s) -> l.add(s);
// Invalid Declaration
(@Nonnull l, @Nullable s) -> l.add(s);
1997년 공개된 JDK 1.1 버전에서부터 자바는 HTTP 요청/응답 처리를 위한 HttpURLConnection 클래스를 가지고 있었다. 그러나 시간이 지날수록 복잡하고 많은 요구사항들이 생기면서 HttpURLConnection이 제공하는 기능만으로 충분하지 않았기 때문에 개발자는 Apache HttpComponents나 OkHttp와 같은 라이브러리에 의존해야 했다. 따라서 이를 해소하기 위해 버전 9에서 HttpClient API가 Incubator(JEP 110)로 포함되었고, 버전 10에서 업데이트가 된 후 최종적으로 버전 11에 표준으로 도입되었다.
HttpURLConnection 클래스를 사용해 HTTP POST 요청을 보내고 응답을 읽어들이려면 아래와 같이 코드를 작성해야 했다.
public String post(String url, String data) throws IOException {
URL urlObj = new URL(url);
HttpURLConnection con = (HttpURLConnection) urlObj.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
// Send data
con.setDoOutput(true);
try (OutputStream os = con.getOutputStream()) {
byte[] input = data.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
// Handle HTTP errors
if (con.getResponseCode() != 200) {
con.disconnect();
throw new IOException("HTTP response status: " + con.getResponseCode());
}
// Read response
String body;
try (InputStreamReader isr = new InputStreamReader(con.getInputStream());
BufferedReader br = new BufferedReader(isr)) {
body = br.lines().collect(Collectors.joining("n"));
}
con.disconnect();
return body;
}
그러나 버전 11에서 HttpClient 클래스가 포함되며 아래와 같이 간결하게 작성할 수 있다.
public String post(String url, String data) throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(data))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP response status: " + response.statusCode());
}
return response.body();
}
HTTPClient은 비동기적 모델도 지원한다.
public void postAsync(String url, String data, Consumer<String> consumer, IntConsumer errorHandler) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(data))
.build();
client
.sendAsync(request, BodyHandlers.ofString())
.thenAccept(
response -> {
if (response.statusCode() == 200) {
consumer.accept(response.body());
} else {
errorHandler.accept(response.statusCode());
}
});
}
버전 11 이전까지는 Collection 인터페이스가 컬렉션을 배열로 변환하는 두 가지 toArray() 메서드를 제공했다.
List<String> list = Arrays.asList("foo", "bar", "baz");
Object[] strings1 = list.toArray();
String[] strings2 = list.toArray(new String[0]);
버전 11에서는 새로운 toArray() 메서드를 추가로 제공한다. 아래는 버전 11의 Collection 인터페이스 중 toArray() 메서드 부분을 캡쳐한 것이다. 제일 마지막에 정의된 toArray() 메서드가 새로 추가된 메서드이다.
List<String> list = Arrays.asList("foo", "bar", "baz");
String[] strings3 = list.toArray(String[]::new);
버전 11에서는 Optional.isEmpty() 메서드가 추가되었다. 이전에 존재하던 isPresnet() 메서드는 값이 존재한다면 true를, 그렇지 않다면 false를 반환하지만 isEmpty() 메서드는 그 반대이다.
String currentTime = null;
assertTrue(!Optional.ofNullable(currentTime).isPresent());
assertTrue(Optional.ofNullable(currentTime).isEmpty());
currentTime = "12:00 PM";
assertFalse(!Optional.ofNullable(currentTime).isPresent());
assertFalse(Optional.ofNullable(currentTime).isEmpty());
버전 11에서는 String 클래스에 여러 메서드가 추가되었다.
아래는 repeat() 메서드와 isBlank() 메서드를 사용한 예제이다.
/* String.repeat() */
String str = "1".repeat(5);
System.out.println(str); // 11111
/* String.isBlank() */
"1".isBlank(); // false
"".isBlank(); // true
" ".isBlank(); // true
버전 11과 마찬가지로 버전 12도 새로운 String API들이 추가되었다.
각 메서드 사용 예제는 여기에서 확인할 수 있다.
NumberFormat.getCompactNumberInstance() 메서드를 사용하면 "2K", "2 billion"과 같이 숫자를 읽기 쉬운 형태로 변경할 수 있다.
NumberFormat nfShort = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.SHORT);
System.out.println("1,000 short -> " + nfShort.format(1000)); // 1천
System.out.println("2,000,000 short -> " + nfShort.format(2000000)); // 200만
여러 경우를 콤마로 구분하고 화살표를 사용해 switch 문을 더욱 간결하게 작성할 수 있다. 기존에는 아래와 같이 break를 명시해야 switch 문에서 빠져나올 수 있었다.
switch (day) {
case MONDAY:
case THRUSDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
버전 12에서는 아래처럼 여러 경우를 콤마로 구분해 하나의 동일한 라벨로 묶을 수 있고, 라벨이 일치하는 경우 break 없이 화살표로 나타낼 수 있는 swtich 문을 preview feature로 제공한다. 이 switch 문이 표준으로 도입되는 버전은 14이다.
switch (day) {
case MONDAY, THRUSDAY -> System.out.println(6);
case TUESDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
또한, 아래처럼 break 키워드 뒤에 값을 명시하면 해당 값을 리턴하도록 할 수 있다.
int dayInNum = switch (day) {
case MONDAY, THRUSDAY:
break 6;
case TUESDAY:
break 8;
case WEDNESDAY:
break 9;
}
버전 12에서 살펴본 switch 문에서 변경된 점이 하나 있다. 개발자 커뮤니티의 피드백을 바탕으로 break 키워드를 yield라는 새로운 키워드로 대체했다. yield 키워드는 switch 문 안에서만 예약어로 인식되기 때문에 변수명이나 함수명으로 yield를 사용해도 무관하다.
int dayInNum = switch (day) {
case MONDAY, THRUSDAY:
yield 6;
case TUESDAY:
yield 8;
case WEDNESDAY:
yield 9;
}
버전 12와 13에서 Preview feature로 분류되었던 Switch 문이 버전 14에서는 표준으로 도입되었다. 앞서 설명했던 내용에 덧붙이자면 enum의 경우 default case를 생략할 수 있지만 다른 타입(ex. String, int 등)은 default case를 명시해야 한다.
정리하자면 아래와 같이 switch 문을 작성할 수 있다. switch 문에 들어오는 day 값은 enum 타입이기 때문에 default case를 명시하지 않아도 된다.
Boolean result = switch(day) {
case MON, TUE, WED, THUR, FRI -> true;
case SAT, SUN -> false;
};
Boolean result = switch(day) {
case MON, TUE, WED, THUR, FRI : yield true;
case SAT, SUN : yield false;
};
Boolean result = switch(day) {
case MON, TUE, WED, THUR, FRI ->
{
System.out.println("It is WeekDay");
yield true;
}
case SAT, SUN ->
{
System.out.println("It is Weekend");
yield false;
}
};
버전 14 이전에는 아래와 같이 instanceof로 타입 체크를 한 뒤 타입 변환을 한 번 더 해줘야 했다.
if (obj instanceof String) {
String s = (String) obj;
if (s.length() > 5) {
System.out.println(s.toUpperCase());
}
} else if (obj instanceof Integer) {
Integer i = (Integer) obj;
System.out.println(i * i);
}
버전 14에서는 instanceof 다음에 변수명을 작성할 수 있다. 만약 obj가 명시된 타입에 해당한다면 obj를 해당 타입으로 캐스팅한 뒤 변수에 할당한다.
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase());
} else if (obj instanceof Integer i) {
System.out.println(i * i);
}
이후 버전 16에서 표준으로 도입된다.
record 타입은 enum과 같이 Java의 특별한 클래스 타입 중 하나이다. record는 불변 데이터 클래스로 클래스나 애플리케이션 간 데이터 교환을 위해 사용될 수 있다. 또한, record는 객체의 데이터를 최초로 set(생성자)하고 가져오기 위해 작성해야 하는 보일러 플레이트를 없애는 것을 목표로 하고, 이 역할을 Java 컴파일러에게 넘긴다. 따라서 Java 컴파일러는 record 클래스에 대해 생성자, 필드 getter, hashCode(), equals(), toString() 메서드를 자동으로 생성해준다.
public record Point(int x, int y) {}
Point p = new Point(3, 5);
int x = p.x();
int y = p.y();
record의 구체적인 문법은 여기에서 확인할 수 있으며, 버전 16에서 표준으로 도입된다.