Tính năng cú pháp mới trong Java 17

Tính năng cú pháp mới trong Java 17

Mục lục

  • Tính năng cú pháp mới trong Java 17
  • JEP và Project Amber
  • Giới thiệu
    1. Records [JEP 395]
    1. Biểu thức switch và yield [JEP 361]
    1. Phối hợp mẫu cho instanceof [JEP 394]
    1. Khối văn bản [JEP 378]
    1. Lớp bị niêm phong [JEP 409]
  • Những cải tiến chính khác của Java 17

Lưu ý: Mã nguồn chi tiết xem tại kho mã "Hướng dẫn thiết kế chương trình Java thực hành"

JEP và Project Amber

Trong quá trình phát triển ngôn ngữ Java, nếu cần thêm các tính năng cú pháp mới, người khởi xướng có thể đưa ra đề xuất thông qua tài liệu JEP (Java Enhancement Proposal). Sau đó trải qua các bước đánh giá sơ bộ, tạo JEP, phản hồi cộng đồng, thẩm định JEP, phê duyệt hoặc từ chối, thử nghiệm trước khi phát hành chính thức. Dự án Project Amber của Open JDK được sử dụng để khám phá và ấp ủ các đặc điểm nhỏ hơn của Java đã được chấp nhận bởi JEP. Các đặc điểm mới trong bài viết này thường sẽ kèm theo số JEP để chỉ rõ đặc điểm cú pháp thuộc về JEP nào.

Giới thiệu

Java 17 được phát hành vào ngày 14 tháng 9 năm 2021, với nhiều tính năng cú pháp và lớp mới. Trong số đó, những tính năng đáng chú ý bao gồm: Record, Khối Văn Bản, Phối hợp Mẫu cho instanceof, và Lớp Bị Niêm Phong. Bài viết này sẽ chi tiết hóa các tính năng mới này cùng với các ví dụ mã nguồn tương ứng.

  1. Records [JEP 395]

Từ phiên bản Java 16, kiểu Record được giới thiệu như một kiểu dữ liệu mới để biểu diễn dữ liệu bất biến. Nó giúp chúng ta tạo ra một kiểu dữ liệu bất biến chỉ chứa các trường dữ liệu và phương thức truy cập mà không có trạng thái thay đổi hoặc phương thức tùy chỉnh. Sử dụng từ khóa record, bạn có thể dễ dàng tạo đối tượng truyền tải dữ liệu bất biến (DTO). DTO là viết tắt của Data Transfer Object, dùng để chuyển dữ liệu đơn giản POJO (Plain Old Java Object). Kiểu Record phù hợp để lưu trữ các loại dữ liệu thuần túy, chẳng hạn như tọa độ điểm hoặc nhật ký chỉ đọc. Khi bạn cần một kiểu chỉ để lưu trữ dữ liệu, hãy sử dụng Record.

Ví dụ: Giả sử cần định nghĩa một kiểu record Point đại diện cho một điểm, bao gồm hai trường xy, cũng như các phương thức truy cập tương ứng x()y(). Có thể sử dụng đoạn mã sau:

public class PointRecordExample {
    public static void main(String[] args) {
        // Định nghĩa kiểu record, cũng có thể đặt riêng biệt trong một tệp
        record Point(int coordinateX, int coordinateY) {}

        // Sử dụng kiểu record
        Point origin = new Point(0, 0);
        Point destination = new Point(0, 0);
        System.out.println(origin); // Gọi phương thức toString() tự động sinh
        System.out.println(origin.coordinateX()); // Lấy giá trị thuộc tính x
        System.out.println(origin.coordinateY());
        System.out.println(origin.equals(destination)); // So sánh tất cả giá trị thuộc tính, trả về true
        System.out.println(origin == destination); // Trả về false vì so sánh địa chỉ tham chiếu
    }
}

Kết quả chạy chương trình:

Point[coordinateX=0, coordinateY=0]
0
0
true
false

Phân tích mã:

Point mặc dù không định nghĩa bất kỳ phương thức xây dựng, getter hay equals nào, nhưng biên dịch viên sẽ tự động sinh ra những phương thức này. toString() phương thức: Trả về chuỗi biểu diễn tất cả các thuộc tính. coordinateX(), coordinateY() phương thức truy cập: Trả về giá trị của thuộc tính tương ứng. equals phương thức: Sẽ so sánh tất cả giá trị thuộc tính có bằng nhau hay không, tương tự như việc sử dụng Objects.equals(). Điều này khiến cho các đối tượng kiểu record khi gọi equals sẽ so sánh tất cả giá trị thuộc tính thay vì địa chỉ tham chiếu.

Kiểu record có thể được định nghĩa trong một tệp riêng biệt hoặc trực tiếp trong phương thức cần sử dụng, cũng hỗ trợ định nghĩa nhiều phương thức xây dựng cũng như phương thức xây dựng mặc định.

Tham khảo:

Đặc điểm mới hữu ích của JDK - Java Record

  1. Biểu thức switch và yield [JEP 361]

Java 17 giới thiệu biểu thức switch (Switch Expressions), từ khóa yield và dấu mũi tên ->. Bạn có thể sử dụng từ khóa yield trong nhánh case (label case) để trả về một giá trị. Mã ví dụ dưới đây minh họa điều này:

public class SwitchExpressionExample {
    public static void main(String[] args) {
        String dayOfWeek = "Monday";

        String result = switch (dayOfWeek) { // Biểu thức switch
            case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> {
                System.out.println("Đến nhánh 1");
                yield "Ngày làm việc"; // Tương đương return "Ngày làm việc"
            }
            case "Saturday", "Sunday" -> "Ngày nghỉ"; // Không cần break hay yield, trả về trực tiếp
            default -> throw new IllegalArgumentException("Đầu vào không hợp lệ");
        };

        System.out.println(dayOfWeek + " là " + result);
    }
}

Kết quả chạy chương trình:

Đến nhánh 1
Monday là Ngày làm việc

Trong đoạn mã trên, biểu thức switch trả về một chuỗi và gán nó cho biến result. Trong các nhánh case, từ khóa yield được sử dụng để trả về một giá trị. Như vậy, biểu thức switch có thể trả về một giá trị mà không cần sử dụng break để ngắt. Nếu -> không có dấu ngoặc nhọn, thì nó biểu thị trả về một biểu thức. Ví dụ đoạn mã case "Saturday", "Sunday" -> "Ngày nghỉ" không sử dụng từ khóa yield, nhưng vẫn trả về chuỗi "Ngày nghỉ".

Lưu ý: Bên phải -> của khối switch có thể là khối mã, biểu thức hoặc ngoại lệ được ném thủ công.

  1. Phối hợp mẫu cho instanceof [JEP 394]

Từ phiên bản Java 16, bạn có thể sử dụng phối hợp mẫu (Pattern Matching) và từ khóa instanceof để xử lý kiểm tra kiểu và khớp mà không cần chuyển đổi kiểu rõ ràng. Xem ví dụ mã sau:

record Individual(String fullName, int yearsOld) {}

public class PatternMatchingExample {
    public static void main(String[] args) {
        Object object = new Individual("Nguyễn Văn A", 25);

        if (object instanceof Individual person) { // Nếu đúng, object sẽ được chuyển thành kiểu Individual và gán cho biến mẫu person
            System.out.println(person.yearsOld());
        } else if (object instanceof String text) {
            System.out.println(text.toLowerCase());
        }

        // Kết hợp instanceof với điều kiện khác
        if (object instanceof Individual person && person.yearsOld() > 21) {
            System.out.println("yearsOld = " + person.yearsOld() + " > " + 21);
        }
    }
}

Kết quả chạy chương trình:

25
yearsOld = 25 > 21

Trong đoạn mã object instanceof Individual person, Individual person là mẫu kiểu (type pattern), và person là biến mẫu (pattern variable). Nếu object instanceof Individual trả về true, thì object sẽ được chuyển sang kiểu Individual và gán giá trị cho biến person.

Chú ý: Biến mẫu chỉ là một biến cục bộ đặc biệt, có phạm vi tác động, không thể sử dụng bên ngoài phạm vi đó. Để xác định phạm vi tác động, bạn có thể để Java Compiler tự động xác định cho bạn.

  1. Khối Văn Bản [JEP 378]

Khối Văn Bản (Text Blocks) trong Java 17 là một dạng chuỗi ký tự mới, cho phép bạn viết chuỗi nhiều dòng một cách thuận tiện và dễ đọc hơn. Khối Văn Bản sử dụng ba dấu gạch ngang ngược (```) để bọc chuỗi và có thể chứa các ký tự đặc biệt như ký tự xuống dòng, tab. Ví dụ mã sau:

public class TextBlockExample {
    public static void main(String[] args) {
        String block = """
                Đây là "chuỗi nhiều dòng"
                Có thể chứa ký tự tab\tTôi đứng trước là tab
                Và có thể chứa ký tự xuống dòng\nTôi đứng trước là xuống dòng
                  Tôi đứng trước là 2 khoảng trắng.
                Tiếp theo là chuỗi JSON của đối tượng Person
                {
                  "fullName": "John Doe",
                  "yearsOld": 30,
                }
                                """;
        System.out.println(block);
    }
}

Kết quả chạy chương trình:

Đây là "chuỗi nhiều dòng"
Có thể chứa ký tự tab    Tôi đứng trước là tab
Và có thể chứa ký tự xuống dòng
Tôi đứng trước là xuống dòng
  Tôi đứng trước là 2 khoảng trắng.
Tiếp theo là chuỗi JSON của đối tượng Person
{
  "fullName": "John Doe",
  "yearsOld": 30,
}

Khối Văn Bản thường được sử dụng để viết chuỗi nhiều dòng, chuỗi tài liệu, câu lệnh SQL, chuỗi JSON,...

  1. Lớp Bị Niêm Phong [JEP 409]

Trong Java 17, lớp bị niêm phong (sealed class) cho phép chúng ta giới hạn phạm vi kế thừa của một lớp hoặc phạm vi thực hiện của một interface, đảm bảo rằng nó chỉ có thể được kế thừa hoặc thực hiện bởi các lớp được chỉ định. Lớp hoặc interface bị niêm phong có thể có một hoặc nhiều lớp con được phép, những lớp con này phải được chỉ định trước và khai báo trong cùng một mô-đun. Ví dụ, nếu bạn muốn một số lớp chỉ có thể được kế thừa bởi các lớp khác trong cùng mô-đun mà không muốn các lớp từ mô-đun khác kế thừa, bạn có thể sử dụng lớp bị niêm phong để giới hạn phạm vi kế thừa.

Các lớp kế thừa hoặc thực hiện trực tiếp từ một kiểu bị niêm phong chỉ có thể được khai báo là final, sealed hoặc non-sealed, tham khảo mã sau:

// Chỉ cho phép Bird, Aircraft, UFO thực hiện Flyable interface
sealed interface Flyable permits Bird, Aircraft, UFO {}

/* Các lớp kế thừa hoặc thực hiện trực tiếp từ kiểu bị niêm phong chỉ có thể được khai báo là final, sealed hoặc non-sealed */

// 1. Khai báo là final. Không thể có lớp con.
final class Bird implements Flyable {}

// 2. Khai báo là non-sealed. Không bị niêm phong, có thể có lớp con.
non-sealed class Aircraft implements Flyable {}

class Helicopter extends Aircraft {}

// 3. Khai báo là sealed. Bị niêm phong, chỉ có thể có các lớp con được phép.
sealed class UFO implements Flyable permits CircleUFO, RectUFO {}

final class CircleUFO extends UFO {}

final class RectUFO extends UFO {}

Trong đoạn mã trên, interface Flyable được khai báo là sealed, điều này có nghĩa là nó chỉ có thể được thực hiện bởi ba lớp Bird, Aircraft, UFO. Lớp Bird được khai báo là final, điều này có nghĩa là nó không thể có lớp con. Lớp Aircraft được khai báo là non-sealed, điều này có nghĩa là nó cho phép có lớp con. Lớp UFO được khai báo là sealed, điều này có nghĩa là nó chỉ có thể được kế thừa bởi hai lớp CircleUFO, RectUFO.

Những cải tiến chính khác của Java 17

Java 17 còn có một số cải tiến đáng chú ý khác:

Bộ thu gom rác ZGC ổn định

Bộ thu gom rác ZGC (Z Garbage Collector) là một bộ thu gom rác thời gian dừng thấp của JVM, được giới thiệu như một tính năng thí điểm từ Java 11. Trong Java 17, ZGC trở thành phiên bản ổn định, có nghĩa là nó sẽ được bao gồm trong JDK và có thể sử dụng trong môi trường sản xuất. ZGC có khả năng đạt mức dừng dưới miligây, nhưng cần bật thủ công.

Hỗ trợ từ các khung phát triển Java phổ biến

Spring Framework 6Spring Boot 3 yêu cầu dựa trên Java 17.

Thông báo lỗi NullPointerException rõ ràng hơn

Java 17 cải thiện thông báo lỗi NullPointerException, trước đây chỉ có thể xác định đến hàng, giờ đây có thể xác định cụ thể đến biến. Ví dụ mã sau:

public class NullPointerExceptionMessageExample {
    public static void main(String[] args) {
        String string = null;
        System.out.println(string.length());
    }
}

Khi chạy, sẽ nhận được thông báo lỗi sau: Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "string" is null Như vậy, thông báo lỗi đã chỉ rõ rằng string là null.

Giao diện Stream bổ sung phương thức toList()

Trong Java 17, giao diện Stream chính thức bổ sung phương thức toList(), cho phép chuyển Stream thành List một cách thuận tiện mà không cần sử dụng Collectors.toList().

Ví dụ mã sau:

import java.util.List;
import java.util.stream.Stream;

public class StreamToListExample {
    public static void main(String[] args) {
        Stream<Integer> scoresStream = Stream.of(98, 97, 90);
        List<Integer> scoresList = scoresStream.toList();
        
        for (Integer score : scoresList) {
            System.out.print(score + " ");
        }
    }
}

Thẻ: Java records SwitchExpressions PatternMatching TextBlocks

Đăng vào ngày 25 tháng 7 lúc 06:56