Ngày 15/09 vừa qua, các iFan đã được “thỏa mãn” với màn ra mắt của iPhone 13. Hòa chung không khí đó, các tín đồ Java cũng được “sung sướng” khi Oracle chính thức release JDK 17 LTS sau 3 năm thống trị của JDK 11 LTS release vào 09/2018.
Khi JDK 8 LTS release vào 03/2014, nó là một cuộc cách mạng siêu to khổng lồ khi có rất nhiều sự khác biệt với phiên bản tiền nhiệm JDK 7. Rất nhiều features nổi bật như stream API, lambda expression, method reference, functional interface, interface default method, Optional… Phải nói là cực kì đồ sộ, chả thế mà người người, nhà nhà, các công ty, các dự án update ầm ầm.
4 năm sau, Oracle release JDK 11 LTS nhưng không thực sự có sự dịch chuyển mạnh mẽ như JDK 8 đã làm được trước đó.
Đến thời điểm hiện tại cũng còn kha khá các công ty và project sử dụng JDK 8 hoặc kết hợp >cả 2 JDK 8 và JDK 11 (theo nguồn khảo sát tự tìm hiểu 😂).
Oracle vẫn support JDK 8 cho đến hết 03/2022. Tức là chỉ còn còn vài tháng ngắn ngủi (tính từ thời điểm viết bài này). Thực ra cũng không cần quan tâm lắm vì đa số sử dụng OpenJDK vì OracleJDK đã tính phí sử dụng doanh nghiệp từ lâu roài.<
JDK 11 vẫn ổn, thậm chí JDK 8 vẫn đang phổ biến. Vậy JDK 17 có những feature gì nổi bật, đáng để chuyển đổi hay không?
Cùng đi tìm hiểu những feature chính và điểm khác biệt hay được sử dụng từ JDK 8 lên JDK 17 qua bài viết này nhé. Let’s begin.
Những thứ nâng cấp hoặc bỏ đi ít được sử dụng như RMI activation, Applet API, Stack-Walking API, Process API improvement… mình sẽ không đề cập đến trong bài viết này.
Trước thời điểm Java 8 release, Interface và Abstract class có sự khác biệt khá lớn và gần như luôn xuất hiện trong các buổi phỏng vấn.
Sau đó, Java 8 cung cấp thêm default method biến một abstract method thành non-abstract method với 2 mục đích:
Tiếp tục đến Java 9, Oracle đã bổ sung thêm private method cho interface. Tất nhiên các private method với mục đích tăng tính re-used cho code.
Ví dụ, với Java 8:
public interface Human {
default void awake() {
System.out.println("Same code here");
}
default void sleep() {
System.out.println("Same code here");
}
}
Java 9 với private method:
public interface Human {
default void awake() {
doSomething();
}
default void sleep() {
doSomething();
}
private void doSomething() {
System.out.println("Same code here");
}
}
Thậm chí support luôn private static method:
public interface Human {
default void awake() {
doSomething();
}
default void sleep() {
doSomething();
}
private static void doSomething() {
System.out.println("Same code here");
}
}
Như vậy, interface đã có non-abstract method, private/public static method, public static variable. Về mặt implement đã không còn khác nhau quá nhiều giữa interface và abstract class.
Lưu ý rằng về ý nghĩa thì interface và abstract class vẫn khác nhau, và interface không có instance variable và private static variable.
CompletableFuture đã xuất hiện từ Java 8. Tuy nhiên, Oracle bổ sung thêm 4 method trong Java 9 liên quan đến delay và timeout để support cho việc lập trình bất đồng bộ dễ dàng hơn.
Với delay là 2 method:
static Executor delayedExecutor(long delay, TimeUnit unit); static Executor delayedExecutor(long delay, TimeUnit unit, Executor executor);
Method đầu tiên trả về executor sẽ thực thi task mà chúng ta submit sau một khoảng thời gian delay time.
Method thứ hai cần truyền thêm executor và chính executor đó sẽ thực thi task sau delay time.
Có thể chạy đoạn code sau để hiểu thêm về nó nhé:
public static void main(String[] args) throws InterruptedException {
var future = new CompletableFuture<>();
var delayExecutor = CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS);
future.completeAsync(() -> {
System.out.println("Processing data");
return "process success";
}, delayExecutor)
.thenAccept(r -> System.out.println("Result: " + r));
for (int i = 1; i <= 5; i++) {
Thread.sleep(1000);
System.out.println("Running outside... " + i + " s");
}
}
Tiếp theo cùng đến với 2 method timeout:
CompletableFuture<T> orTimeout(long timeout, TimeUnit unit); CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit);
Giống với tên gọi của nó, method đầu tiên support việc throw exception nếu process time quá khoảng thời gian chỉ định. Method thứ hai thay vì throw exception sẽ trả về giá trị default value.
Thực hiện run code dưới đây để hiểu thêm nếu cần.
public static void main(String[] args) throws Exception {
var task = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Finish";
});
BiConsumer<String, Throwable> onComplete = (result, error) -> {
if (error == null) {
System.out.println("The result is: " + result);
return;
}
System.out.println("Time is over");
};
var future = task
.orTimeout(3, TimeUnit.SECONDS)
.whenComplete(onComplete);
var content = future.get();
System.out.println("Result: " + content);
}
Java 9 đã bổ sung thêm 4 methods để tương tác với stream, trong đó có 2 instance methods và 2 static methods là:
Stream<T> takeWhile(Predicate<? super T> predicate); Stream<T> dropWhile(Predicate<? super T> predicate); static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next); static <T> Stream<T> ofNullable(T t);
Giống với tên gọi của nó, method takeWhile() trả về tất cả các element thỏa mãn điều kiện. Nghe thì có vẻ giống filter() nhưng có đôi chút khác biệt. Hãy chú ý vào chữ while(): có thể hiểu nôm na rằng sẽ duyệt stream từ đầu cho đến khi nào gặp element không thỏa mãn điều kiện thì sẽ dừng lại luôn.
Stream.of(1, 2, 3, 4, 1, 2, 3, 4) .filter(i -> i < 4) .forEach(System.out::print); Stream.of(1, 2, 3, 4, 1, 2, 3, 4) .takeWhile(i -> i < 4) .forEach(System.out::print);
Kết quả của dòng đầu tiên với filter() là 123123
. Với takeWhile() kết quả chỉ là 123
.
Idea tương tự như takeWhile() tuy nhiên logic của nó sẽ là loại bỏ tất cả các element thỏa mãn điều kiện và dừng lại cho đến khi gặp phần tử không thỏa mãn.
Stream.of(2, 4, 6, 8, 9, 10, 12) .dropWhile(n -> n % 2 == 0) .forEach(System.out::print);
Dễ dàng đoán được kết quả là 91012
.
Nếu muốn tạo ra một stream với element đầu tiên = 2, element sau gấp đôi element trước cho đến khi element cuối cùng < 30, có thể sử dụng iterate với Java 9 như sau:
IntStream.iterate(2, x -> x < 30, x -> 2 * x) .forEach(System.out::println);
Cuối cùng là ofNullable(), nó sẽ trả về stream chứa element đó nếu element not null, còn không sẽ trả về empty stream.
Stream.ofNullable(1).forEach(System.out::println); Stream.ofNullable(null).forEach(System.out::println);
Từ Java 8 trở về trước, việc tạo một collection (List, Set, Map…) khá dài dòng:
List<String> values = new ArrayList<>(); values.add("Hello"); values.add("World");
Vì lý do đó nên Google đã tung ra Guava collection giúp chúng ta thuận tiên hơn trong việc tạo ra một collection với cú pháp đơn giản:
List<String> values = ImmutableList.of("Hello", "World");
May mắn thay, Oracle đã nhận ra sự bất tiện này và đưa chúng vào luôn Java 9. Vậy là từ giờ không cần thêm common lib bên ngoài nữa:
List<String> values = List.of("Hello", "World");
Java Module system đem đến 3 lợi ích chính:
Thực tế mình cũng chưa thấy có dự án hoặc công ty nào sử dụng Modular Java platform để triển khai code base. Ngoài ra có rất nhiều thứ để nói về Java module, mình sẽ trình bày trong một bài khác.
Không cần bàn cãi nhiều, đây chính là cứu tinh của cuộc đời những lập trình viên Java.
Java là một ngôn ngữ đẹp, rõ ràng, nên nó muốn mọi thứ phải rõ ràng. Trước JDK 10, nếu muốn khai báo bất kì biến nào chúng ta cũng cần chỉ đích danh biến đó có type gì:
Object object = new Object();
Map<String, String> map = new HashMap<>();
BiConsumer<String, String> biConsumer = new BiConsumer<>() {
@Override
public void accept(String s1, String s2) {
}
};
Thank God! À không thank Oracle mới đúng, mọi thứ sẽ ngắn gọn hơn rất nhiều với var:
var object = new Object();
var map = new HashMap<>();
var biConsumer = new BiConsumer<>() {
@Override
public void accept(String s1, String s2) {
}
};
Lưu ý rằng Java vẫn là một ngôn ngữ rõ ràng, giàu tính thống nhất, scope của var là compile time chứ không phải run time giống như JavaScript.
Note: Oracle đã tăng thêm phạm vi sử dụng của var trong Java 11. Cụ thể là var có thể được sử dụng với lambda parameter.
Map.of("key", "value").forEach((var k, var v) -> {});
Nếu trước đây để check một string có blank hay không ta cần sử dụng 2 method là trim() và isEmpty() thì nay đã có isBlank().
boolean result = " ".trim().isEmpty();
boolean result = " ".isBlank();
Giống tên gọi của nó, method này thực hiện nối chuỗi với chính nó theo số lần chỉ định.
var string = "repeat ";
System.out.print(string.repeat(3)); // repeat repeat repeat
Method này thực hiện bỏ tất cả những khoảng trắng ở đầu và cuối string, giống như những gì trim() đang làm. Sự khác biệt đó là strip() xịn xò hơn trim() do sử dụng cách thức định nghĩa khoảng trắng xịn sò hơn, nhờ đó đem lại tốc độ tốt hơn và fix một bug đang tồn tại với trim().
Cuối cùng là lines(), method này giúp chúng ta chia string thành stream of lines phân cách nhau bằng \n
:
"hello\nworld".lines().forEach(System.out::println);
// hello
// world
Tiếp tục là sự cải tiến cho String API ở JDK 12 với 2 methods:
Với indent() method, chuỗi ban đầu được split thành nhiều line giống method lines(), sau đó mỗi line được indent (thêm khoảng trắng) dựa trên giá trị truyền vào.
var input = "Hello\nWorld";
System.out.println(input);
System.out.println(input.indent(2));
// Result
Hello
World
Hello
World
Không có gì phức tạp, transform() apply function lên string hiện tại để tạo ra một kết quả mới:
var result = "Hello".transform(input -> input + " world");
System.out.println(result); // Hello world
Nếu cần assign value với if else, ta đã có ternary operator. Nhưng với switch case thì.. chẳng có cách nào cho đến khi Java 14 xuất hiện.
// ternary operator
var isMonday = day == Day.Monday ? true : false;
// switch expression
var isMonday = switch (day) {
case MONDAY -> true;
default -> false;
};
Mình tin rằng bất kì dev nào cũng đã từng bị ám ảnh bởi Java NPE vì chẳng biết variable nào bị null ngoài cách đọc code và debug.
Tỉ dụ có đoạn code như sau:
var fullName = employee.getDetail().getFullName().toLowerCase();
Và đoạn log:
Exception in thread "main" java.lang.NullPointerException
at com.oracle.java14.Application.main(Application.java:7)
Có 3 trường hợp null: employee, employee.getDetail() hoặc employee.getDetail().getFullName(). Chỉ biết chửi đứa nào code như …, không check choác gì cả 🔨.
May mắn thay Oracle đã tung ra một JVM options mới giúp chúng ta thuận tiện hơn trong việc debug NPE:
-XX:+ShowCodeDetailsInExceptionMessages
Sau khi thêm option trên, nếu có lỗi xảy ra JVM sẽ show message thân thiện hơn rất nhiều:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.oracle.java14.Application$Employee.getName()" because "employee" is null
at com.oracle.java14.Application.main(Application.java:7)
hoặc
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.trim()" because the return value of "com.oracle.java14.Application$Employee.getName()" is null at com.oracle.java14.Application.main(Application.java:7)
tùy thuộc giá trị nào là null.
Với Java 17, tính năng này đã được tự động thêm vào, không cần enable bằng JVM option nữa.
Feature này đã có từ Java 13 tuy nhiên chỉ là phiên bản preview, nếu muốn sử dụng phải enable. Đến Java 15 mới được đưa vào chính thức.
Với Java 15 trở xuống, nếu muốn tạo một String và xuống dòng:
var string = "Line 1\n"
+ "Line 2\n"
+ "Line 3";
Hơi vất vả nhưng vẫn dùng được. Để đơn giản hóa việc khai báo này, Java 15 sử dụng cú pháp như sau:
var string = """
Line 1
Line 2
Line 3
""";
Những dòng code đã nói lên tất cả, rất tiện lợi. Thank Oracle.
instanceof
Nếu muốn check một object có type là String không và assign nó sang biến mới để sử dụng ta làm như sau:
Object object = "";
if (object instanceof String) {
var string = (String) object;
System.out.println(string);
}
Để đơn giản hóa quá trình này, Java 16 cung cấp pattern matching cho instanceof
với cách sử dụng mới như sau:
Object object = "";
if (object instanceof String string) {
System.out.println(string);
}
Chúng ta đã quá nhàm chán với việc tạo ra những Java bean chứa đầy rẫy boilerplate code với getter(), setter(), toString()…
Nếu bạn đã quen với Lombok thì không còn lạ gì những annotation @Getter
, @Setter
, @Data
. Chúng sẽ giúp giải quyết những boilerplate code kể trên.
Oracle biết điều đó nhưng họ muốn làm tốt hơn. Và keyword record ra đời, ngang hàng với class hoặc interface, đều là type.
Nếu trước đây nếu muốn tạo một Java bean Person với các thông số fullName và address, thực hiện như sau:
public class Person {
private String fullName;
private String address;
public Persion();
public Persion(String fullName, String address);
public getFullName();
public setFullName(String fullName);
public getAddress();
public setAddress(String fullName);
}
Với record:
public record Person(String fullName, String address) {
public Person() {
this("defaultValue", "defaultValue");
}
}
Nhẹ nhàng thư thái, code ngắn gọn hơn hẳn.
Vẫn là nhu cầu tăng tính encapsulation. Java đã mạnh OOP nay càng mạnh hơn.
Thực tế khi implement, sẽ có những lúc ta muốn class A chỉ được phép extends bởi class B và C. Một cách để có thể triển khai đó là đặt cả 3 class vào chung package để để class A có level access modifier là package. Tuy nhiên sẽ có những trường hợp không muốn đặt chung như vậy.. nhưng vẫn phải làm như vậy.
Sealed class sẽ giúp chúng ta xử lý vấn đề này.
sealed interface Runnable permits Dog {
void run();
}
final class Dog implements Runnable {
@Override
public void run() {
}
}
Sử dụng keyword sealed để thông báo đây là một class/interface giới hạn, chỉ được phép extend bởi class Dog. Nếu tạo một class khác implement interface Runnable sẽ báo lỗi.
Với mỗi feature mình chỉ lấy ví dụ đơn giản để dễ hình dung nhất có thể.
Ngoài ra còn rất nhiều feature/tool khác hoặc cả những thứ đã bị loại bỏ mà chưa đề cập đến như:
Từ những thứ kể trên có thể thấy rằng bản release lần này chất lượng thật sự. Còn chần chừ gì mà không nâng cấp lên JDK 17 và trải nghiệm thôi.. à nhầm code thôi.
Say goodbye to Java 8 or even Java 11.
You need to login in order to like this post: click here
YOU MIGHT ALSO LIKE