Record – keyword sắp ra mắt chính thức trong Java 16 có gì thú vị?

Record keyword là một Java preview feature xuất hiện từ phiên bản Java 14 và 15 – preview feature trong Java được xem là một tính năng được ra mắt để cộng đồng sử dụng, phát triển và đánh giá, nếu được ứng dụng phổ biến và nhận được nhiều phản hồi tích cực, sẽ trở thành tính năng chính thức trong phiên bản Java kế tiếp.

Và record sẽ được ra mắt quan viên hai họ trong phiên bản Java 16 sắp tới đây, bài viết sẽ tập trung tìm hiểu những điểm thú vị của từ khoá mới này.

Record – bản ghi sắp ra mắt chính thức trong Java 16

Immutable class

Record là một keyword định nghĩa cho một immutable class chỉ chứa kiểu dữ liệu và tên thuộc tính của các trường trong nó.

Immutable class là class có các object sau được khởi tạo từ nó sẽ có các trường (fields) là bất biến. Hay nói cách khác, sau khi khởi tạo object bằng constructor định nghĩa giá trị cho các trường, bạn không thể thay đổi giá trị của bất kì trường nào trong object đó được nữa.

Hoặc hiểu đơn giản hơn là giống như khi bạn làm bài thi trắc nghiệm xong rồi nộp bài liền mà không thay đổi gì cả – bài thi chứa tất cả thông tin và câu trả lời của bạn sẽ được sử dụng để vận chuyển, lưu trữ, chấm điểm, v.v… nhưng không ai có quyền thay đổi bất kì thông tin hay các câu trả lời mà bạn đã đánh dấu.

Trong Java, đặc trưng của immutable class được thể hiện qua các đặc điểm sau:

  • Định nghĩa class sẽ đi kèm từ khoá final – class sẽ không thể được kế thừa từ class khác.
  • Các thuộc tính sẽ được định nghĩa với khả năng truy cập là private, và đi kèm với final – được định nghĩa với một giá trị (value) bất biến.
  • Constructor hầu hết sẽ là constructor chứa tham số dùng định nghĩa tất cả giá trị các trường trong class (trừ một số trường hợp đặc biệt như định nghĩa sẵn giá trị hoặc giá trị được suy từ giá trị các trường khác).
  • Việc truy cập các thuộc tính sẽ thông qua các getter, và dĩ nhiên theo mô tả – class sẽ không có setter.
  • Ngoài ra cũng có thể định nghĩa các thuộc tính static hoặc phương thức khác, tuy nhiên vẫn phải tuân thủ theo quy tắc không được thay đổi giá trị các trường.

Dưới đây là một ví dụ minh hoạ cho một immutable class.

public final class Student {
    private final String id;
    private final String name;

    public Student(String id, String name) {
        this.id= id;
        this.name = name;
    }

    public String getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public String getFullInfo() {
        return String.format("Id = %s, Name = %s", this.id, this.name);
    }
}

Immutable class boilerplate

Với những đoạn code có logic giống nhau, tuy nhiên không thể (hoặc rất khó) để viết theo hướng tái sử dụng, buộc ta phải viết các đoạn code lặp đi lặp lại các phương thức hay cấu trúc nhất định… sẽ được xem là các boilerplate code (hoặc boilerplate).

Dĩ nhiên, không lập trình viên nào thích phải viết các đoạn code lặp đi lặp lại một logic cả.

Từ các đặc trưng đã nêu về immutable class, ta dễ thấy sẽ có những điểm trùng lặp dẫn đến sinh ra các boilerplate code:

  • Định nghĩa class phải đi với từ khoá final.
  • Các thuộc tính khi khai báo phải đi với cụm private final.
  • Constructor hầu như sẽ chứa toàn bộ tham số tương ứng với tất cả các thuộc tính.
public final class Student {
    private final String id;
    private final String name;

    public Student(String id, String name) {
        this.id= id;
        this.name = name;
    }

    public String getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }
}

Định nghĩa immutable class Student

public final class Book {
    private final String title;
    private final String author;

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public String getTitle() {
        return this.title;
    }

    public String getAuthor() {
        return this.author;
    }
}

Định nghĩa immutable class Book

Và đó cũng chính là lí do từ khoá record xuất hiện – nhằm giảm boilerplate code cho việc định nghĩa các immutable class

Record keyword

Hai đoạn code immutable class ở ví dụ trên có thể dễ dàng thay thế khi sử dụng từ khoá record như sau:

public record Student(String id, String name) {}

Định nghĩa record Student

public record Book(String title, String author) {}

Định nghĩa record Book

Chỉ một dòng code như trên, bạn đã có một immutable class bao gồm:

  • Ba đặc trưng đã mô tả về immutable class như trên.
  • Ba phương thức đặc trưng khác của class trong Java: equals, hashCode, toString.

Nói cách khác, khi khai báo record Student, ta đã rút ngắn đoạn boilerplate code dưới đây:

public final class Student {
    private final String id;
    private final String name;

    public Person(String id, String name) {
        this.name = id;
        this.address = name;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof Student)) {
            return false;
        } else {
            Studentother = (Student) obj;
            return Objects.equals(id, other.id)
              && Objects.equals(name, other.name);
        }
    }

    @Override
    public String toString() {
        return "Student [id=" + id+ ", name=" + name + "]";
    }

    public String id() {
        return this.id;
    }

    public String name() {
        return this.name;
    }
}

Và đó cũng là khi mình nhận ra thay vì hì hục viết cả chục dòng boilerplate code, thì đã có một tính năng hay thư viện làm giúp việc đó rồi =)

Một vài ngoại lệ

Xét record Adult định nghĩa đối tượng người gồm 2 thuộc tính là tên và tuổi của các bạn thích đi uống “gụ” (tuổi của một Adult phải từ 18 trở lên mới được phép dùng đồ có cồn):

public record Adult(String name, Integer age) {}

Tuy nhiên, ta dễ dàng “lách luật” bằng cách tạo một người có tuổi là 16, dĩ nhiên Compiler sẽ chẳng hề than phiền gì cả.

Person person = new Person("Definitely An Adult", 16);

Dĩ nhiên, record cũng sẽ hỗ trợ một số “chốt chặn” để tránh các thanh niên như thế này bằng compact constructor.

 public record Adult(String name, Integer age) {
    public Adult {
        if (age < 18) {
            throw new IllegalArgumentException("Are you sure?");
        }
    }
}

Tuy nhiên, một số bạn không thích để lộ danh tính của mình, mà chỉ muốn khai báo “có tuổi mà không có tên”, ta vẫn có thể tạo static constructor cho trường hợp này.

public record Adult(String name, Integer age) {
    public Adult {
        if (age < 18) {
            throw new IllegalArgumentException("Are you sure?");
        }
    }

    public static Adult fromUnknownName(Integer age) {
        return new Adult("Secret Agent Name", age);
    }
}

Ứng dụng record vào Java Spring

Mình sẽ ứng dụng framework Java Spring nhằm tạo một Restful API đơn giản để log thông tin những người lớn uống “gụ” như ví dụ trên, cụ thể là:

  • Phương thức POST với URL endpoint là: “/adult”
  • Request body sẽ accept định dạng JSON theo format: {name: <tên>, age: <tuổi>}
  • Thông báo chào mừng khi thông tin hợp lệ, và tra hỏi với các thanh niên lách luật.
  • Data trong request body sẽ được đổ vào record Person.
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public record Adult(
    @JsonProperty("name") String name, 
    @JsonProperty("age") Integer age) {

    public Adult {
        if (age < 18) {
            throw new IllegalArgumentException("Are you sure?");
        }
    }
}

Record Adult dùng “hứng” dữ liệu

@RestController
@RequestMapping("/adult")
public class AdultValidateController {
    @PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> validateAdult(@RequestBody Adult adult) {
        System.out.println("New guest: " + adult.toString());
        return ResponseEntity.ok().body("Welcome, " + adult.name() + "!");
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> faultAdultHandler(IllegalArgumentException e) {
        System.out.println("New kid!");
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

Định nghĩa RestController theo mô tả

Toàn bộ source code demo, các bạn có thể tham khảo tại đây.

Lưu ý khi sử dụng record

  • Record được sinh ra nhằm giảm boilerplate code của các immutable class chuyên dùng để “vận chuyển” data như nhận dữ liệu từ request, trả response, data transfer object (DTO), caching data, đảm bảo thread safe,…
  • Record không thể kế thừa (extends) từ class khác, tuy nhiên vẫn có thể được cài đặt (implements) interface.
  • Record vẫn có thể sử dụng cho Dependency Injection trong Spring thay cho @Autowired, tuy nhiên đó là khi class sử dụng các thể hiện thay vì thông qua interface, tuy nhiên việc này không đúng với lí do mà record được tạo nên cũng như vi phạm DI, nên mình không khuyến khích sử dụng khả năng này.
  • Hiện tại, Java 16 vẫn chưa chính thức ra mắt, để sử dụng tính năng record trong Java 14 hoặc 15, cần tuỳ chỉnh khả năng sử dụng preview feature.
  • Ứng dụng record trong @RequestBody trong Spring với JSON và thư viện Jackson vẫn chưa chính thức hoàn thiện, tuy nhiên vẫn có cách workaround thông qua annotation @JsonAutoDetect.

Khi Java 16 ra mắt cũng như phiên bản LTS Java 17 vào tháng 9 năm nay, hi vọng record sẽ được đón nhận nhiều hơn và giảm thiểu nhiều “nỗi đau boilerplate” của các Java dev, đây cũng sẽ là lựa chọn của mình trong thời gian tới =)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s