- JEP 401: Value Classes and Objects đã đạt đến giai đoạn được đưa vào JDK preview thực tế
- Mục tiêu cốt lõi là khiến đối tượng Java có thể “lập trình như class, vận hành như int”, qua đó giảm chi phí của header đối tượng, cấp phát heap, GC và tham chiếu gián tiếp qua con trỏ
- Value class trong JDK 28 hiện vẫn là kiểu tham chiếu có thể null, chưa bao gồm kiểu non-null, generic chuyên biệt hóa hay mã hóa 128-bit, và cần
--enable-preview
- JVM có thể scalar hóa value object hoặc làm phẳng trên heap trong field và mảng, nhưng trong các kiểu cha như generic đã xóa kiểu hoặc
Object, nó có thể được materialize thành đối tượng trên heap
- Lập trình viên Java cần phản ánh sự khác biệt giữa identity và value trong thiết kế mã nguồn; điều này ảnh hưởng đến
==, synchronized, primitive wrapper, hiệu năng mảng và cả chuyên biệt hóa generic trong tương lai
Phạm vi của Valhalla được đưa vào JDK 28
- Ngày 15 tháng 6, kỹ sư Oracle Lois Foltan xác nhận JEP 401: Value Classes and Objects đã được tích hợp vào kho chính của OpenJDK và nhắm tới JDK 28
- Pull request liên quan đã bổ sung hơn 197 nghìn dòng trên 1.816 tệp
- Do quy mô thay đổi quá lớn, trong lúc tích hợp đã có yêu cầu các committer khác tạm hoãn những commit lớn
- JEP 401 là một tính năng preview bị tắt mặc định
- Cần
--enable-preview để dùng cú pháp
- Brian Goetz nhấn mạnh đây là “phần đầu tiên của Valhalla”
- JDK 28 dự kiến phát hành vào tháng 3 năm 2027, còn việc tích hợp vào mainline được lên kế hoạch vào khoảng tháng 7 năm 2026
Chi phí của mô hình đối tượng Java mà Valhalla nhắm tới
- Khẩu hiệu của Valhalla là “codes like a class, works like an int”
- Mục tiêu là vẫn dùng class thông thường với method, kiểm tra constructor và tên field có ý nghĩa, nhưng để JVM xử lý hiệu quả như primitive
- Trong Java, ngoại trừ 8 primitive, gần như mọi thứ đều là kiểu tham chiếu
- Trong
Point p = new Point(1, 2), p không phải bản thân point mà là con trỏ trỏ tới đối tượng trên heap
- Mỗi lần đọc field, JVM phải lần theo con trỏ
- Khi số lượng đối tượng tăng lên, chi phí tăng rất nhanh
- Mỗi đối tượng có header đối tượng để lưu kiểu, trạng thái đồng bộ hóa và các thông tin khác
- Đối tượng được cấp phát trên heap và sau đó trở thành đối tượng của GC
- Một mảng gồm một triệu
Point thực tế được tạo thành từ một triệu con trỏ và một triệu đối tượng nằm rải rác trên heap
- “State of Valhalla” của Brian Goetz gọi cách bố trí bộ nhớ này là fluffy
- Điều Valhalla hướng tới là bố trí dense, nơi dữ liệu được đặt sát nhau
Khoảng cách phần cứng và giới hạn của escape analysis
- Lý do bố trí bộ nhớ dày đặc quan trọng là vì khoảng cách tốc độ giữa CPU và bộ nhớ
- Năm 1995, chi phí truy cập bộ nhớ gần tương đương phép tính trên CPU
- Hiện nay CPU nhanh hơn bộ nhớ chính cả một bậc độ lớn hai chữ số, và cache lấp đầy khoảng cách đó
- CPU thường đọc bộ nhớ theo đơn vị cache line 64 byte
- Nếu dữ liệu dày đặc và nằm tuần tự, một lần đọc sẽ mang về nhiều giá trị hữu ích
- Nếu phải lần theo con trỏ đến các đối tượng nằm rải rác, có thể xảy ra cache miss và chậm hơn rất nhiều so với hit
- Escape analysis của JVM có thể loại bỏ một số lần cấp phát đối tượng
- Nếu xác định được đối tượng không “escape” ra ngoài đoạn mã cục bộ, JVM có thể không cấp phát nó trên heap mà trải các field ra thành biến hoặc thanh ghi
- Tuy nhiên escape analysis kém ổn định và khá mong manh
- Nếu đối tượng đi vào field của class khác, được lưu vào mảng, được truyền qua method phức tạp hoặc vượt qua ranh giới mà JIT không phân tích được, tối ưu hóa có thể dừng lại
- Chỉ một lần refactor nhỏ, cập nhật JDK hoặc thay đổi cấu trúc mã cũng có thể khiến đối tượng lại bị đưa lên heap
- Nếu từ bỏ đối tượng để tự mã hóa trực tiếp thành các byte thô như
r, g, b nhằm lấy hiệu năng, bạn sẽ có tốc độ nhưng mất đi tính an toàn, khả năng đọc hiểu, kiểm tra và method
Khởi đầu năm 2014 và bước chuyển từ Q World sang L World
- Project Valhalla chính thức bắt đầu vào năm 2014
- James Gosling khi đó mô tả nó là “six PhDs tied into a single knot”
- Những người tạo ra Java đã muốn có value type từ thời Java 1.0, nhưng vào năm 1995 bài toán này quá khó nên họ đã bỏ qua
- Mục tiêu ban đầu là khôi phục sự căn chỉnh giữa mô hình lập trình và đặc tính hiệu năng của phần cứng hiện đại
- Hướng đi là cho phép người dùng tự khai báo kiểu phẳng và dày đặc như primitive, nhưng vẫn trông và hoạt động như class thông thường
- Prototype ban đầu đi theo hướng Q World
- Cách này xem value type là thực thể khác biệt về căn bản so với đối tượng, với type descriptor, bytecode và top type riêng
- Toàn bộ hệ thống kiểu của JVM vì thế phải mang hai biến thể, làm độ phức tạp tăng mạnh
- L World xuất hiện vào khoảng năm 2019 là bước ngoặt
- Value type chia sẻ cùng một “L carrier” với reference thông thường
- Nhóm phát triển từng dự đoán việc hợp nhất này sẽ khó, nhưng nó hoạt động mà không cần thỏa hiệp lớn và giải quyết được nhiều vấn đề của prototype trước đó
- Trong L World xuất hiện một sự tách biệt quan trọng
- Mô hình JVM và mô hình ngôn ngữ không nhất thiết phải trùng khớp 100%
- JVM có thể giữ mô hình L World, còn với lập trình viên thì cung cấp một mô hình ngôn ngữ thuận tiện hơn
- Sau đó, công việc được chia thành hai giai đoạn: value class và generic chuyên biệt hóa
Sự thay đổi của tên gọi và mô hình
- Thuật ngữ Valhalla đã thay đổi nhiều lần, và đây không chỉ là đổi tên mà còn phản ánh sự thay đổi về mô hình
- Thuật ngữ ban đầu là value types
- Khi đó vẫn chưa thực sự rõ các kiểu này chính xác là gì
- Khoảng 2019~2020, mô hình inline classes dần được định hình
- Class hiện có được xem là identity classes, còn class mới được phân biệt là inline classes không có identity
- inline class mặc định là final, các field là final, và có ràng buộc không thể đồng bộ hóa
- “State of Valhalla” năm 2021 đề cập đến primitive classes và mô hình hai projection
- Ý tưởng là một kiểu sẽ có biến thể value phẳng, không cho phép null, và biến thể reference cho phép null
- Các cú pháp như
Point.val / Point.ref, sau đó là Point! / Point?, cũng từng được thử nghiệm
- Mô hình này mạnh mẽ nhưng tạo ra gánh nặng nhận thức lớn
- Lập trình viên phải thường xuyên hiểu hai dạng của cùng một kiểu và thời điểm chuyển đổi giữa chúng
- Cuối cùng, để đơn giản hóa mô hình cho người dùng, tính lưỡng nguyên đã được thu hẹp lại
- Hiện tại, JEP 401 dùng các khái niệm value class và value object
- value class được khai báo bằng modifier
value
- instance là value object không có identity
- value class vẫn là một reference type
- Tính non-nullability được tách thành một JEP tùy chọn riêng là Null-Restricted Value Class Types
- Những bài viết cũ giải thích mô hình “primitive classes” trước đây có thể khác với tiêu chuẩn OpenJDK hiện tại
- Trong JEP 401 còn có JEP 402: Enhanced Primitive Boxing ở trạng thái preview
- Hướng đi là làm cho việc chuyển đổi giữa primitive và wrapper mượt mà hơn
- Không nên giả định rằng mọi thứ sẽ cùng đi vào JEP 401 ở trạng thái hoàn thiện
Mô hình value class trong JDK 28
- value class được khai báo bằng modifier
value
value class USDCurrency implements Comparable<USDCurrency> {
private int cents; // implicitly final
public USDCurrency(int dollars, int cents) {
this.cents = dollars * 100 + cents;
}
public USDCurrency plus(USDCurrency that) {
return new USDCurrency(0, this.cents + that.cents);
}
// dollars(), cents(), compareTo(), toString()...
}
- Cũng có thể có value record
- Các quy tắc chính như sau
- Tất cả instance field đều ngầm định là final
- method không thể là
synchronized
- class mặc định là final
- Có thể tạo hệ phân cấp gồm value class và abstract value class
- Không thể kế thừa class có identity
- Có thể implement interface
- Đặc tính cốt lõi là không có identity
- Với object thông thường, dù nội dung giống nhau, nếu tạo hai lần bằng
new Point(1, 2) thì đó vẫn là hai object khác nhau
- Với value object, không có identity, giống như với giá trị
int là 4 thì không tồn tại “hai số 4 khác nhau”
Thay đổi của ==, synchronized, và null
- Với value object,
== không còn là so sánh identity mà trở thành phép kiểm tra substitutability
- Nó so sánh đệ quy xem có cùng class và cùng giá trị các field hay không
- primitive field được so sánh ở mức bit, còn object field lại tiếp tục được so sánh bằng
==
new USDCurrency(3,95) == new USDCurrency(3,95) sẽ cho kết quả true
- Tuy vậy, vì
== nhìn vào trạng thái bên trong, nên để kiểm tra “có biểu diễn cùng một dữ liệu hay không” thì thường equals vẫn phù hợp hơn
- value object không có identity để đồng bộ hóa
- Nếu cố đồng bộ hóa sẽ phát sinh IdentityException
- Khi cần xác nhận bắt buộc về identity, có thể dùng
Objects.requireIdentity và Objects.hasIdentity
- value class trong JDK 28 vẫn có thể là null
USDCurrency d = null; là hợp lệ
- Kiểu cấm null sẽ là một JEP tương lai riêng, không có trong JDK 28
- non-nullability không chỉ là vấn đề cú pháp đơn thuần mà còn là một đòn bẩy hiệu năng mở đường cho việc làm phẳng các value class lớn hơn
Scalarization và heap flattening
- JEP 401 cho JVM quyền tự do tối ưu hóa value object
- Scalarization là kỹ thuật JIT phân rã reference của value object thành một tập field
- Thay vì truyền con trỏ
Color, có thể truyền các byte r, g, b và một cờ null hay không
- Chi phí cấp phát và GC có thể biến mất
- Nó giống escape analysis nhưng dễ dự đoán hơn và có thể áp dụng vượt qua ranh giới của các lời gọi method không được inline
- Scalarization có những giới hạn
- Nếu kiểu của biến là
Object, tức supertype của value class, hoặc là tham số generic đã bị erase, thì thường sẽ không hoạt động
- Khi đó object phải được materialize trên heap
- Heap flattening là cách mã hóa các giá trị field của value object thành một vector bit gọn và ghi trực tiếp vào field hoặc phần tử mảng
- Không cần con trỏ trỏ đến vị trí heap khác
- Điều này tạo ra mật độ dữ liệu và locality tốt hơn
- Dữ liệu đã được làm phẳng phải có thể được đọc và ghi atomic để tránh tearing khi truy cập đồng thời
- Trên các nền tảng phổ biến, kích thước “đủ nhỏ” có thể ở mức 64 bit, bao gồm cả cờ null
- Value class nhỏ có thể được làm phẳng tốt, nhưng chỉ cần có hai field
int hoặc một double thôi cũng có thể không khớp với kích thước ghi atomic và vì vậy trở thành object heap thông thường
- Trong tương lai, mã hóa 128 bit và kiểu null-restricted có thể cho phép làm phẳng các value class lớn hơn
Tác động trong boxing, wrapper, và mảng
- Khi bật preview, chính các primitive wrapper class như
Integer, Long, Double sẽ trở thành value class
- Box sẽ mất identity nên JVM có thể scalarize và flatten chúng
Integer[] sẽ tiến gần đến hiệu quả của int[], theo hướng giảm đáng kể boxing overhead
- JEP 402: Enhanced Primitive Boxing mở rộng thêm việc chuyển đổi giữa primitive và box
- Nó mở đường tới các biểu thức như
List<int>, nhưng hiện vẫn là một nhánh công việc riêng đang tiếp tục hoàn thiện
- Hiệu quả thể hiện rõ nhất trong mảng
Color[] trước đây có thể trở thành một triệu con trỏ và một triệu object nằm rải rác trên heap
Color[] của value class có thể trở thành một contiguous block lưu trực tiếp các giá trị màu liên tiếp
- CPU có thể đọc tuần tự nhiều giá trị theo đơn vị cache line
Khác biệt trước và sau qua ví dụ Point[]
- Ví dụ về class thông thường trước thời Valhalla như sau
final class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- Mảng này chứa một triệu con trỏ
- Mỗi con trỏ trỏ tới một đối tượng
Point riêng biệt ở đâu đó trên heap
- Mỗi đối tượng ngoài hai
int còn có cả object header
- Khi duyệt, phải đọc con trỏ, nhảy tới địa chỉ đó, rồi đọc các field
- Sau Valhalla, ví dụ về value class sẽ như sau
value class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- Khác biệt trong code chỉ là một từ
value, nhưng cách bố trí bộ nhớ sẽ thay đổi
- JVM có thể lưu giá trị của từng point một cách dày đặc ngay trong mảng
- Không có header cho từng element và cũng không có con trỏ
- Với hai
int là x, y, chúng có thể được sắp liên tiếp thành 8 byte cùng với null flag nếu có
- Tính dễ bảo trì cũng vẫn được giữ nguyên
Point vẫn là một class có tên, constructor, kiểm tra hợp lệ và method
- Có thể tránh cách tách thành
int[] xs, int[] ys rồi phải canh chỉ số cho khớp
Vì sao specialized generics vẫn còn ở phía trước
- Java generics được triển khai bằng type erasure
List<String> và List<Integer> là cùng một List ở runtime
- type parameter
T bị erasure thành Object
- Erasure là lựa chọn có chủ đích để đưa generics vào Java mà không phá vỡ codebase hiện có
- Ngay cả khi đổi class không generic thành generic, các source file và compiled class cũ vẫn không bị phá vỡ
- Valhalla và erasure xung đột với nhau về hiệu năng
- Nếu đưa value object vào
List<Point>, vì T bị erasure thành Object, đối tượng sẽ phải được materialize trên heap
- Lợi ích flattening có được từ
Point[] có thể biến mất trong ArrayList<Point>
- Kế hoạch khắc phục có hai bước
- Universal Generics: ở cấp độ ngôn ngữ, cho phép type variable xử lý cả value type
- Vẫn dùng erasure
- Vì field
T mặc định bắt đầu từ null, có thể xuất hiện compiler warning về “null pollution”
- Nếu xử lý được các cảnh báo này, API sẽ tiến gần tới trạng thái sẵn sàng cho specialization
- Specialized Generics: ở cấp độ JVM, tạo class layout chuyên biệt cho từng concrete type argument
- Trong thuật ngữ của dự án, species và type restriction có liên quan ở đây
- Chỉ tới bước này
ArrayList<Point> mới có thể thực sự dùng bộ nhớ phẳng
- JDK 28 không có full specialized generics
- Việc để collection, stream và API trở nên phẳng và không cần cấp phát trên value type là phần việc của các bản phát hành tương lai
Những gì có và không có trong JDK 28
- Những gì sẽ có trong JDK 28 gồm
- khai báo
value class và value record
- migration sang value class cho các value-based class hiện có trong JDK như primitive wrapper
- scalarization và flattening cho các class đáp ứng điều kiện
- boxing rẻ hơn
- Những gì không có trong JDK 28 gồm
- null-restricted types
- full specialized generics
- mã hóa 128 bit
- JEP 402 chưa hoàn thiện đầy đủ
- Vì là preview feature, syntax và hành vi có thể thay đổi qua từng bản phát hành dựa trên feedback
- JDK 28 không phải LTS
- LTS tiếp theo nhiều khả năng là JDK 29 vào tháng 9/2027
- Nhiều công ty có thể sẽ gặp Valhalla đã ổn định trong một bản LTS, nhưng bản preview của JDK 28 sẽ khởi động vòng phản hồi với code thực tế
Những thay đổi sẽ đến với hệ sinh thái và code
- Trong các lĩnh vực Java hiệu năng cao, Valhalla mở ra con đường xử lý dữ liệu dày đặc mà không phải từ bỏ abstraction
- Bao gồm các lĩnh vực như xử lý dữ liệu, tính toán vector, ML, phát triển game, tài chính và codec
- Framework và library có thể bắt đầu migration các value-based class
- Code phụ thuộc vào identity có thể gặp khác biệt về hành vi
== trên value object sẽ không còn là so sánh địa chỉ mà là so sánh substitutability
synchronized trên value object sẽ dẫn tới IdentityException
- Ngay cả khi
Integer trở thành value class, trong đa số trường hợp binary vẫn sẽ tiếp tục link được
- Lỗi biên dịch mới sẽ xuất hiện khi cố đồng bộ hóa trên các kiểu như vậy
== dựa vào identity của Integer hoặc synchronized(someInteger) có thể bị ảnh hưởng
- Bản dựng early-access có thể dùng tại jdk.java.net/valhalla
Tóm tắt các câu hỏi thường gặp
- value class khác với record
record là lựa chọn coi content là component
value là lựa chọn từ bỏ identity
- Có thể có đủ các tổ hợp: class thường, record, value class và value record
- Có thể so sánh value object bằng
==
- Nhưng ý nghĩa là substitutability chứ không phải so sánh địa chỉ
- Với tính tương đương của dữ liệu được biểu diễn,
equals thường phù hợp hơn
- value class trong JDK 28 vẫn có thể là null
- non-nullable type là một JEP trong tương lai
- Điều này cũng quan trọng với flattening của các value class lớn hơn
- Chưa có
ArrayList<Point> phẳng và nhanh
- Vì type erasure, đối tượng trong collection generic sẽ được materialize trên heap
- Trong JDK 28, các trường hợp tiêu biểu mà flattening hoạt động trực tiếp là field của value type và mảng như
Point[]
- escape analysis không thể thay thế tất cả
- Nếu đối tượng đi vào field, mảng hoặc vượt ra ngoài ranh giới phân tích, tối ưu hóa có thể bị phá vỡ
- Scalarization của value object dễ dự đoán hơn và có thể đi xa hơn qua các ranh giới gọi method
- Toàn bộ Valhalla sẽ được mở rộng qua nhiều bản phát hành
- JDK 28 là preview đầu tiên của value class
- specialized generics, null-restricted types và mã hóa 128 bit là các phần việc trải dài qua những bản phát hành tương lai
2 bình luận
Các luồng ảo của Project Loom, được phát hành sau một thời gian dài, khá tiện lợi và giải quyết được nhiều thứ ở cấp độ runtime của JVM, nên nhà phát triển có ít điều phải bận tâm hơn.
Hy vọng Project Valhalla khi được phát hành chính thức cũng sẽ mang lại cảm giác gần như một bữa trưa miễn phí như vậy.
Ý kiến trên Hacker News
Dù nói khác biệt về bộ nhớ là phần cốt lõi, tôi vẫn nghi ngờ bài này có được biên tập tử tế hay không
Chẳng phải ngay trước đó bài vừa giải thích rằng các đối tượng có biểu diễn vượt quá 64 bit thì không được làm phẳng trên heap sao?
Pointtrong ví dụ có 2 số nguyên 32 bit cộng thêm cờ null, vậy tối thiểu là 65 bitCụm “có thể có cờ null” cùng với các câu nhấn mạnh ngắn đi kèm phía sau khiến tôi có cảm giác AI đã tạo các câu nhấn mạnh rồi đi chệch hướng, còn khối
"[IMAGE: the same Point[] array in two variants..."ở giữa thì thật đáng tiếcDùng AI để hỗ trợ viết thì không sao, nhưng nếu không đưa tiếng nói của chính mình vào thì chẳng có lý do gì để đọc
https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#...
Nhưng chỉ sau vài đoạn là quá rõ đây là bài viết được tạo bằng cách cho qua LLM, hoặc còn tệ hơn thế
Dù là blog kỹ thuật hay gì khác, làm ơn đừng để AI viết thay. Không ai muốn đọc kiểu bài đó cả
Hôm nay tôi mới biết trong Rust có
NonZeroU64, và khi kết hợp vớiOptionalthì có thể đạt được hành vi cần thiết chỉ với 64 bit cho mỗi mụchttps://doc.rust-lang.org/std/num/type.NonZeroU64.html
Như JEP cũng nói rất rõ, đây chỉ là sản phẩm đầu tiên của một tính năng khổng lồ, và cũng như các tính năng Java gần đây, nó đang được chuyển giao theo từng mảnh
Mục tiêu dĩ nhiên là làm phẳng cả các giá trị lớn hơn, và cơ chế đó đã có sẵn trong JVM. Việc còn lại là bộc lộ ở cấp ngôn ngữ ý định “cho phép bị xé”
Tôi ghi nhận công sức bỏ vào Valhalla, nhưng khó mà đồng ý với cách diễn giải rằng “mô hình thì mạnh nhưng quá nặng về mặt tinh thần”
Việc nói một biến không thể là null không phải là một sự phân biệt gây gánh nặng nhận thức, nhất là khi mọi thứ đều được chú thích đầy đủ
Thái độ “hy sinh trần hiệu năng để đơn giản hóa mô hình cho người dùng” thực ra có khi lại chính là một cách đơn giản hóa cho người dùng
Hệ thống kiểu của ngôn ngữ lập trình tồn tại để cung cấp cho lập trình viên những bảo đảm tiện lợi trên một CPU chỉ xử lý số. Không cần phải giảm bớt các bảo đảm an toàn tùy chọn chỉ vì cho rằng chúng “quá phức tạp”
Thậm chí người ta còn đã đi tới nhận thức rằng “mô hình ngôn ngữ và mô hình JVM không nhất thiết phải trùng khít 100%” mà vẫn như vậy
Cơ chế quản trị Java có vẻ thiếu sót, và điều đó đặc biệt tương phản với phía .NET, nơi phần lớn đã đưa ra quyết định đúng ngay từ đầu
Gần đây tôi còn nghi ngờ không biết Java có còn giá trị hay nhận được sự quan tâm nào bên trong Oracle nữa không. Công ty giờ trông như một mảng kinh doanh datacenter/compute với hoạt động di sản và khoản nợ khổng lồ bám theo
Có lúc tôi tự hỏi liệu ở Oracle có phải các bộ phận còn kiếm ra tiền chỉ là pháp lý và cắt cỏ hay không
Và dấu chỉ null cũng sẽ đến: https://openjdk.org/jeps/8303099
Chỉ là phải phát hành dần dần thôi, mà riêng PR này để đưa value class/object vào đã ở quy mô 200 nghìn dòng rồi
Có vẻ không phải là nói nó bất khả thi, mà là không thể ăn cả con voi trong một miếng
Dù vậy thì cũng phải nói là họ đã gặm cái chân voi này khá lâu rồi
Nếu đó là khái niệm đã được giải quyết từ năm 2012 thì không có lý do gì phải đưa trực tiếp nhiều hương vị trạng thái vào ngôn ngữ. Đường ray chỉ đi tới A hoặc B, còn việc rẽ theo hướng nào thì phụ thuộc vào trạng thái tải của đoàn tàu
Nếu một khái niệm cứ xuất hiện rồi biến mất và chiến tranh ngôn ngữ lại nổ ra, đó là dấu hiệu cho thấy có nhu cầu nhưng ngôn ngữ либо không xử lý được đúng nhu cầu đó, либо có xử lý thì cũng tạo ra quá tải nhận thức
Chẳng hạn như
Value,Errorstates,Null,IoExceptions,WeirdOsStatesNeededToHandleUpstairshttps://fsharpforfunandprofit.com/rop/
Nói theo kiểu Monty Python thì giờ ta tiến lên tiếp đi
Integer/intThay vì có một phép chiếu có định danh và một phép chiếu không có định danh cho mỗi kiểu, nhóm Valhalla đã làm cho value type hoàn toàn không có định danh, và vì thế
Integervớiinttrở thành từ đồng nghĩaBố cục bộ nhớ được quyết định tự động theo ngữ cảnh và các quyết định tối ưu hóa. Vì vậy, ý nghĩa của
==đối với các wrapper nguyên thủy nhưIntegercũng đã thay đổi, và giờ không còn phụ thuộc vào việc bạn dùng “phép chiếu tham chiếu” hay “phép chiếu giá trị” nữaỞ đây không hề có chuyện giảm bớt các bảo đảm an toàn tùy chọn chỉ vì cho rằng chúng “gây gánh nặng tinh thần”
Trong các bình luận trên HN liên quan đến Java/JVM, điều lặp đi lặp lại là có nhiều người đến mức đáng ngạc nhiên vẫn giữ hình ảnh cũ về JVM hay Java, nhưng gần như không biết gì về diện mạo ngày nay của nó
JVM của năm 2026 là một kẻ săn mồi cực kỳ khỏe mạnh. Có khuyết điểm không? Tất nhiên là có, nhưng nền tảng thì cực kỳ vững chắc
Trong công việc tôi dùng Java 26 mới nhất cùng các tính năng preview, chủ yếu là
StructuredConcurrency, và nó rất tuyệt. Ngay cả với tư cách người từng dùng Haskell và Python ở các công ty trước, tôi cũng hoàn toàn không hối tiếcCá nhân tôi có biết các tính năng mới xuất hiện vài năm gần đây, nhưng trong công việc thực tế thì Java đúng nghĩa là vẫn bị mắc kẹt trong quá khứ
Khá nhiều bình luận ở đây có phần không công bằng so với những công việc tuyệt vời đang diễn ra hiện nay và các JEP còn thú vị hơn sẽ ra mắt sau này
Nếu ví Java như một đứa trẻ, thì vài năm đầu nó lớn lên với cha mẹ đầy yêu thương (Sun), rồi sau đó bị quăng vào gara cùng những đứa trẻ khác và bị bỏ mặc bởi một người giám hộ độc ác (Oracle)
Nó đã bị bỏ mặc và không được yêu thương cho đến tận JDK 8, nên về cơ bản suốt thời gian qua là chạy để bắt kịp
Nói rằng “đến giờ mới có struct hay value type” là đúng, nhưng đó là vì sự phát triển của nó bị kìm hãm bởi quy trình doanh nghiệp khổng lồ, quan liêu và thù địch. Giờ thì nó đã được tự do và đang được yêu thương thông qua gia đình OpenJDK
Chúng ta sẽ còn tiếp tục tận hưởng niềm vui viết một lần, triển khai mọi nơi
Gần với sự thật hơn là nó được cha mẹ yêu thương nuôi lớn, rồi vì vấn đề tài chính mà bị gửi vào gia đình nhận nuôi tạm thời, nơi nó bị bỏ mặc
Sau đó nó được nhận nuôi bởi cha mẹ mới đầy yêu thương là Oracle, và Java đã nở rộ thành một người trưởng thành khỏe mạnh, ổn định
Chính Oracle là bên biến OpenJDK thành bản triển khai tham chiếu và hoàn tất việc mã nguồn mở nền tảng, đồng thời cũng mã nguồn mở các công cụ trước đây là độc quyền như JFR và Mission Control
Họ cũng giữ lại nhiều thành viên đời đầu của nhóm ngôn ngữ, điều khá hiếm trong các vụ thâu tóm kiểu này, và Java đã được cải thiện lớn cả ở ngôn ngữ lẫn runtime
Oracle đã đẩy Java tiến lên với tốc độ chưa từng có trong khi vẫn giữ được phần lớn tính tương thích ngược
Người ta nói .NET là “làm đúng ngay từ đầu”, nhưng nếu điều đó đồng nghĩa với việc tách và viết lại thành .NET Framework/.NET Core/.NET thì ngay trong cuộc thảo luận này cũng không hợp lý. .NET đáng lẽ có thể học từ Java mà vẫn làm hỏng ở một số chỗ
MySQL cũng vậy. Trên trang này người ta bảo nó “đã chết”, nhưng với những người thực sự hiểu thì nó đã hồi sinh dưới Oracle
Phiên bản Java cuối cùng dưới thời Sun ra mắt năm 2006, Oracle mua Sun năm 2010, còn JDK 7 ra năm 2011 và JDK 8 ra năm 2014
Đội ngũ nhìn chung vẫn như cũ, và khác biệt lớn nhất là Oracle đã chấm dứt sự bỏ mặc đó và rót thêm tiền. Vì vậy sau vụ thâu tóm, tốc độ của Java đã tăng lên
Người ta nói là “bắt kịp”, nhưng cũng không rõ là bắt kịp ai. Chỉ có JS/TS và Python là những ngôn ngữ nổi tiếng ngang hoặc hơn Java
Những người bảo Java bị tụt hậu thường lại so với các ngôn ngữ đang làm kém hơn Java rất nhiều. Những người yêu một tính năng cụ thể thường bỏ qua việc ngôn ngữ có tính năng đó vẫn đang ì ạch không phải nhờ tính năng ấy mà là bất chấp nó
Các nhà quản lý thích phát hành nhanh, trong khi giới lãnh đạo kỹ thuật từ thời Sun lại chủ trương phải cẩn trọng, chậm mà chắc
Tôi hiểu cảm giác rằng Java không còn nổi như năm 2003, nhưng thời kỳ đó là giai đoạn hợp nhất hiếm có không chỉ với Java mà với cả hệ sinh thái phần mềm nói chung, và trước sau đó cũng chưa từng thống nhất như vậy
Công nghệ thực sự viết một lần, triển khai mọi nơi bây giờ là WebAssembly. JVM đã có thời của mình và đã thua
Dù vậy tôi cũng không gọi đó là bị “kìm hãm phát triển”. Nó đã đưa ra các lựa chọn, một số hợp lý, một số thì không, và những lựa chọn đó về sau cực kỳ khó sửa
Chỉ cần nhìn C++ cũng thấy, cá nhân tôi xem tính tương thích ngược một phần với C như một con albatross dài 150 foot không thể sửa nổi, và nhiều phiên bản từ C++11 trở đi chỉ là nỗ lực làm cho cái gánh nặng đó dễ chịu hơn một chút
Trên JVM, việc xử lý mọi value class như một L-type duy nhất giống các kiểu nguyên thủy là một lời giải khá gọn cho một bài toán khó
Cuối cùng thì tất cả chuyện này đều bắt nguồn từ quyết định của Java 2 là triển khai generics bằng type erasure để giữ tương thích ngược, và C3 đã nhìn vào kết quả đó rồi từ chối đi theo con đường ấy
Chỉ riêng quá trình tiến hóa của value type trong Java thôi cũng có thể viết thành cả một cuốn techno-thriller
Tôi đã đọc mailing list và xem hết các video liên quan, và thực sự ấn tượng với quá trình họ hợp nhất thiết kế thành một thứ lúc nào cũng trông rất đúng chất Java
Đồng thời họ cũng đào sâu hơn rất nhiều vào việc value type thực sự có nghĩa là gì, cũng như những tối ưu hóa nào có thể thực hiện ở đâu
valueVới value class,
==về cơ bản sẽ hoạt động giống nhưmemcmp()Điều này hơi đáng tiếc, vì nó phá vỡ tính đóng gói và làm lộ chi tiết triển khai
Mã phía client có thể rẽ nhánh tùy theo cách một giá trị được biểu diễn nội bộ. Ở một khía cạnh nào đó, điều này còn tệ hơn so với so sánh định danh, vì so sánh định danh ít nhất không làm lộ trạng thái bên trong
Đây không phải là làm hướng đối tượng cổ điển theo cách mới, mà là một ngôn ngữ sinh ra từ lý tưởng hướng đối tượng tiến thêm một bước vào thế giới hậu hướng đối tượng
Tôi nghĩ phía Java hẳn cũng đã cân nhắc đầy đủ chuyện loại trừ padding khỏi phép so sánh hoặc ép các byte padding về 0
Điều này cũng phải áp dụng được cho chuỗi. Chuỗi rõ ràng vẫn sẽ tiếp tục được cấp phát trên heap, và việc
memcmpcác con trỏ bên trong một “struct” mới chính xác là so sánh định danhJava tách biệt việc kiểm tra định danh của đối tượng và kiểm tra tính tương đương.
==về cơ bản dùng để xem hai con trỏ có giống nhau hay không, còn tính tương đương là một khái niệm chủ quan dựa trên các giao diện nhưequals/hashCodeVì vậy
new Integer(1000) == new Integer(1000)trước đây làfalsenhưng giờ sẽ thànhtrue, cònnew Integer(1000).equals(new Integer(1000))làtrue, vànew Integer(10) == new Long(10)trước đây làfalsenhưng giờ trở thành lỗi biên dịchTrong Java trước đây, các số nguyên dưới một ngưỡng nhất định thường được thay bằng kiểu đã được canonical hóa, hình như tôi nhớ là quanh mức 128. Vì thế mới có khác biệt giữa 10 và 1000
Bây giờ có vẻ các phép so sánh bên trên được unboxing ngầm. Việc so sánh
Integer/Longtrước đây làfalsemà giờ thành lỗi biên dịch cho thấy chắc chắn có sự can thiệp của unboxingCó lẽ với biến thì vẫn có thể lấy lại hành vi cũ
Dù sao đi nữa, khi value class mất đi định danh,
==sẽ chuyển từ tương đương con trỏ sang tương đương ở mức bit. Tôi hy vọng họ sẽ xử lý được các trường hợp góc cạnh như vậy, nhưng về mặt kỹ thuật thì đây là thay đổi phá vỡ tương thíchTôi hiểu mục đích của value class, nhưng cách triển khai có khuyết điểm
Đoạn mã sau sẽ in ra gì?
Point a = new Point(10, 10); Point b = a; a.x = 100; System.out.println(b.x);Cho tới nay câu trả lời luôn rõ ràng, nhưng khi thêm value class thì đáp án sẽ khác tùy
Pointlà value class hay reference class. Vì vậy thiết kế này làm hại tính dễ đọcĐây là vi phạm nguyên tắc tính đồng nhất. Trong 『The Psychology of Computer Programming』 của Weinberg, tính đồng nhất được mô tả là nguyên tắc tâm lý theo đó người dùng kỳ vọng những gì trông giống nhau sẽ hoạt động giống nhau, còn những gì trông khác nhau sẽ hoạt động khác nhau
Khi một ngôn ngữ lập trình cho phép hai cú pháp trông gần như giống hệt nhau tại điểm sử dụng lại có hành vi ngữ nghĩa khác nhau, gánh nặng nhận thức của người đọc sẽ tăng lên. Muốn biết phép gán, tính tương đương, định danh và thay đổi có vận hành như đối tượng tham chiếu thông thường hay như giá trị, bạn phải kiểm tra khai báo kiểu hoặc dựa vào công cụ
Có thể sửa điều này nếu bắt buộc dùng từ khóa
valuekhông chỉ lúc khai báo mà cả lúc sử dụng. Ví dụ viết nhưvalue Point a = new Point(10, 10);https://openjdk.org/jeps/401
finalTất nhiên chỉ nhìn bốn dòng đó thì không có cách nào biết chuyện này sẽ xảy ra, nhưng vấn đề đó hiện giờ cũng đã tồn tại. Nếu
Pointlà record thì tình huống tương tự vẫn xảy raa.x = 100;sẽ không hợp lệ. Record type là bất biếnVì vậy tình huống đang lo ngại lẽ ra là không thể xảy ra
Nếu viết như
value Point a = new Point(10, 10); value Point b copy= a; a.x uniq= 100; System.out.println(b.x);thì sẽ rõ ràng hơn nhiều rằng có xảy ra sao chép/nhân bản, và việc thay đổi một trường sẽ không ảnh hưởng đến trường của đối tượng khácTôi biết trong thế giới Java thì việc thừa nhận .NET tồn tại có thể bị xem là bất lịch sự, nhưng tôi tò mò không biết cái này khác struct của .NET như thế nào
Nếu nhìn lướt qua value type, chuyên biệt hóa generic và boxing thì có vẻ như họ đã đưa ra cùng một lựa chọn
Nếu C# về cơ bản sao chép C từ góc nhìn cấp thấp, thì phía Java tiếp cận từ cấp cao hơn và phân tích kỹ những ràng buộc nào mang lại lợi ích gì
Ở các ngôn ngữ khác, việc phân loại struct/class là nhị phân, còn Java cho phép kiểm soát tinh vi hơn để phản ánh ngữ nghĩa của miền cơ sở
Và người ta cũng nhận ra struct có nhiều khẩu súng chĩa vào chân khác nhau, đặc biệt trong ngữ cảnh song song
Cá nhân tôi xem struct của C/C# là có thể thay đổi và được truyền bằng bản sao, còn value class thì không thể thay đổi và được truyền bằng giá trị
Tôi nghĩ Java không thể cấp phát trên stack
Kiểu lưỡng phân giả tạo rằng “struct của C# có định danh và khả năng thay đổi nên phải định nghĩa chính xác ngữ nghĩa sao chép khi gán hay truyền, vì thế tạo ra mô hình nặng hơn cho lập trình viên và ít tự do hơn cho runtime” thực ra không khớp lắm với điều đang được giải thích
Nó có thể không có định danh theo nghĩa tham chiếu lớp của Java, nhưng theo nghĩa là một cấu trúc bộ nhớ duy nhất tại một địa chỉ nhất định thì hiển nhiên nó vẫn có định danh. Điều này gần như là bắt bẻ câu chữ quanh thuật ngữ của Java
Chú thích 6 “Nó khác struct của C# thế nào” là không chính xác
Nhìn vào việc bài có đầy hình ảnh do AI tạo thì tôi tự hỏi có phải trong quá trình viết, hoặc ít nhất là quá trình tìm hiểu, cũng có lẫn rất nhiều nội dung bịa/hallucination hay không
Bài viết hơi mơ hồ và kịch tính, nhưng may là các tài liệu gốc khá dễ đọc
Trang cấp cao nhất: https://openjdk.org/projects/jdk/28/spec/
Trạng thái JEP: https://bugs.openjdk.org/secure/Dashboard.jspa?selectPageId=...
Giá mà có ai đó theo dõi giúp các bước phát triển liên quan của C#, Swift, Java và Rust. Theo tôi, tất cả đều đang cạnh tranh để bắt kịp phần cứng và đồng thời ảnh hưởng lẫn nhau
Cá nhân tôi lo ngại những thay đổi này sẽ ảnh hưởng thế nào đến chia sẻ bộ nhớ FFI