1 điểm bởi GN⁺ 2024-08-04 | 1 bình luận | Chia sẻ qua WhatsApp
  • Java đang chuẩn bị một tính năng ngôn ngữ preview gắn dấu nullness vào kiểu: Foo! từ chối null, còn Foo? cho phép null một cách có chủ ý
  • Foo không có dấu vẫn có nullness chưa chỉ định, tương thích với mã hiện có; việc chuyển đổi giữa các kiểu có nullness khác nhau có thể đi kèm cảnh báo hoặc kiểm tra lúc chạy
  • Các trường và mảng từ chối null không thể dùng giá trị mặc định null, nên trường instance phải được gán xác định trước lời gọi super(...), còn mảng cần giá trị khởi tạo cho component
  • Khi null bị thu hẹp thành Foo!, NullPointerException sẽ xảy ra; trên đường lưu vào mảng/trường, ArrayStoreExceptionFieldStoreException ngăn chặn việc làm ô nhiễm
  • Đây là nền tảng giúp các tối ưu như làm phẳng value class của Valhalla có thể tin cậy việc loại trừ null; việc áp dụng cho thư viện chuẩn hay tự động diễn giải lại mã hiện có hiện không phải mục tiêu

Tính năng preview để chỉ rõ nullness trong kiểu Java

  • Gắn dấu nullness vào kiểu Java để biểu diễn liệu tập giá trị của kiểu có bao gồm null hay không
  • Foo! là kiểu null-restricted, loại trừ null khỏi tập giá trị
  • Foo? là kiểu nullable, chủ ý bao gồm null trong tập giá trị
  • Foo không có dấu có nullness unspecified; null có thể xuất hiện, nhưng không biết sự hiện diện đó có phải là có chủ ý hay không
  • Tính năng này là tính năng preview, được bật bằng cờ biên dịch và runtime --enable-preview

Mục tiêu và không phải mục tiêu

  • Biểu diễn liệu một kiểu tham chiếu Java có kỳ vọng null hay không, đồng thời cung cấp cảnh báo và kiểm tra cho các chuyển đổi giữa những kiểu có khác biệt về nullness
  • Tương thích với mã Java hiện có, và phải có thể được đưa vào dần dần mà không tạo ra bất tương thích source hoặc binary
  • Biến có kiểu từ chối null phải được khởi tạo trước khi được đọc lần đầu, và việc từ chối null được cưỡng chế lúc chạy ngay cả trong các lớp được biên dịch riêng
  • Cung cấp metadata và bảo đảm tính toàn vẹn để các tối ưu runtime như làm phẳng value class của Valhalla có thể tin cậy các kiểu giới hạn null
  • Những hạng mục hiện không phải mục tiêu gồm
    • Không tự động diễn giải lại mã hiện có
    • Không biến mọi trường hợp null có thể xảy ra thành lỗi biên dịch
    • Không thêm dạng nullable cho các kiểu nguyên thủy như int
    • Không áp dụng phần mở rộng ngôn ngữ này cho thư viện chuẩn ở giai đoạn hiện tại

Vì sao cần thiết

  • Một biến String trong Java có thể chứa tham chiếu tới đối tượng String hoặc null, nhưng ở cấp độ ngôn ngữ không có cách nào biểu diễn bên nào là chủ ý
  • Nhiều chương trình giả định không có null, nhưng cần thêm công việc để cưỡng chế điều đó nhất quán trong đặc tả Javadoc và mã triển khai
  • Khi kỳ vọng này bị phá vỡ, giá trị null có thể chảy trong mã triển khai rồi gây ngoại lệ ở một điểm rất xa lỗi gốc
  • Nếu lập trình viên biểu diễn ý định từ chối hoặc cho phép null như một phần của kiểu, phản hồi lúc biên dịch và kiểm tra lúc chạy sẽ giúp phát hiện null ngoài dự kiến sớm hơn
  • Trong Valhalla, biến kiểu value class có thể được tối ưu thành biểu diễn làm phẳng của giá trị, nhưng nếu cần thêm bit để mã hóa null, mức dùng bộ nhớ có thể tăng hoặc không thể tối ưu lưu trữ
  • Trong Amber, nullness của ứng viên pattern matching có thể ảnh hưởng tới việc đánh giá tính exhaustiveness của switch, và nullness của type pattern có thể ảnh hưởng tới việc có match null hay không

Cú pháp dấu nullness và cấu trúc kiểu

  • Nullness được xem là một phần nội tại của kiểu; vì Foo?Foo khác nullness nên chúng là các kiểu khác nhau
  • Cả kiểu mảng lẫn kiểu component của mảng đều có thể có dấu nullness
    • Foo?[]! là kiểu mà bản thân mảng là null-restricted, còn component là Foo nullable
    • Với mảng nhiều chiều, có thể gắn dấu sau từng cặp ngoặc vuông, và theo quy ước được diễn giải từ trái sang phải, từ ngoài vào trong
  • Kiểu được tham số hóa và đối số kiểu cũng có thể có dấu nullness
    • Predicate!<Foo?>Predicate null-restricted, còn đối số kiểu là Foo nullable
  • Để biểu diễn kiểu giới hạn null hoặc cho phép null, ! hoặc ? phải xuất hiện tường minh trong source
  • Trong tương lai, có thể xem xét cách diễn giải mặc định mọi kiểu của một lớp hoặc đơn vị biên dịch là null-restricted và chỉ dùng ? làm ngoại lệ, nhưng chi tiết là công việc riêng

Quy tắc khởi tạo trường và mảng

  • Trong Java hiện tại, giá trị mặc định của trường kiểu tham chiếu và component mảng là null, nhưng giá trị đó không phù hợp làm giá trị ban đầu cho trường hoặc component mảng null-restricted
  • Trường và mảng null-restricted luôn phải được chương trình khởi tạo trước khi được đọc
  • Nếu một trường instance null-restricted không có initializer, trường đó phải được gán xác định trước lời gọi super(...) tường minh hoặc ngầm định của mỗi constructor
    • Flexible Constructor Bodies JEP cho phép viết mã khởi tạo cần thiết ở phần đầu constructor
    • Trong early construction context này, không cho phép các hành động có nguy cơ tham chiếu this hoặc đọc trường chưa được khởi tạo
  • Nếu trường instance null-restricted có initializer, initializer đó được chạy ở đầu mỗi constructor trước lời gọi super(...)
    • Constructor gọi this(...) là trường hợp đặc biệt không chạy initializer, như các quy tắc hiện có
  • Trường static null-restricted phải được gán xác định trước khi tất cả static initializer và khối khởi tạo của lớp kết thúc
    • Nếu lớp khác cố đọc trường đó trong khi lớp đang khởi tạo, kiểm tra runtime sẽ phát hiện việc đọc sớm và ném ngoại lệ
  • Mảng có kiểu component null-restricted phải cung cấp giá trị khởi tạo cho từng component trong biểu thức tạo mảng
    • Có thể liệt kê toàn bộ giá trị bằng array initializer
    • Cú pháp rút gọn mới cũng có thể có, nhưng cú pháp hiện vẫn TBD

Nullness của biểu thức và chuyển đổi nullness

  • Trình biên dịch Java xác định nullness của mọi biểu thức trong quá trình kiểm tra kiểu
  • Nullness của tham chiếu biến đến từ khai báo biến, còn nullness của lời gọi phương thức đến từ kiểu trả về của phương thức được tham chiếu
  • Literal null là nullable
  • Hầu hết các biểu thức kiểu tham chiếu khác là null-restricted
    • Bao gồm literal, nối chuỗi, this, tạo instance lớp, tạo mảng, method reference và biểu thức lambda
  • Chuyển đổi nullness được cho phép trong ngữ cảnh gán, gọi và ép kiểu
  • Chuyển đổi nullness mở rộng bao gồm
    • Foo!Foo?
    • Foo!Foo chưa chỉ định
    • Foo?Foo chưa chỉ định
    • Foo chưa chỉ định → Foo?
  • Chuyển đổi nullness thu hẹp bao gồm
    • Foo?Foo!
    • Foo chưa chỉ định → Foo!
  • Chuyển đổi nullness thu hẹp được trình biên dịch tự động thực hiện tương tự chuyển đổi unboxing, nhưng lúc chạy sẽ thực hiện kiểm tra động và có thể gây NullPointerException
  • Cố trực tiếp chuyển literal null sang kiểu null-restricted là lỗi biên dịch

Kiểm tra runtime và ngoại lệ

  • Nếu lúc chạy một giá trị null được chuyển đổi nullness thu hẹp thành kiểu null-restricted, NullPointerException sẽ xảy ra
  • Chuyển đổi nullness thu hẹp không hiện tường minh trong source cũng có thể xảy ra trong khi thực thi runtime
  • Mảng được gán kiểu component null-restricted sẽ từ chối giá trị null trong kiểm tra lưu mảng thông thường, ngay cả khi trong source code nó được xử lý như một kiểu kém cụ thể hơn
    • Thất bại chuyển đổi này gây ArrayStoreException
  • Nếu một trường vốn không null-restricted tại thời điểm biên dịch sau đó trở thành null-restricted qua biên dịch riêng, một field store check mới sẽ từ chối việc lưu null
    • Thất bại chuyển đổi này gây FieldStoreException
  • Trong lời gọi phương thức thuộc quan hệ overriding, có thể có chuyển đổi sang kiểu lời gọi của tham số phương thức cấp trên, rồi tiếp tục chuyển đổi sang kiểu tham số của phương thức override
  • Giá trị trả về của phương thức cũng có thể được chuyển đổi sang kiểu trả về đã khai báo của phương thức, rồi chuyển tiếp sang kiểu trả về kỳ vọng tại điểm gọi

Generic, đối số kiểu và overriding

  • Việc dùng biến kiểu cũng có thể gắn dấu nullness; T! là kiểu null-restricted, còn T? là kiểu nullable
  • Các kiểu biến kiểu null-restricted và nullable khẳng định một nullness cụ thể bên trong mã generic
  • Kiểu dùng làm đối số kiểu cũng có thể biểu diễn nullness; dấu nullness gắn trên kiểu biến kiểu sẽ ghi đè nullness đã khẳng định trong đối số kiểu
  • Bên trong triển khai erased của API generic không thể cưỡng chế giới hạn null
    • Tuy nhiên, các cast ngầm định thông thường xảy ra tại ranh giới API generic sẽ cưỡng chế đối số kiểu null-restricted lúc chạy
  • Để đảm bảo khả năng tương tác, nullness bên trong đối số kiểu không bị cưỡng chế mạnh
    • Predicate<String!> có thể được chuyển đổi thành Predicate<String> hoặc Predicate<String?>
    • unchecked nullness conversion như vậy có thể gây cảnh báo
  • Thay đổi nullness của kiểu component mảng cũng được cho phép dưới dạng unchecked nullness conversion, và điều kiện nào sẽ kích hoạt kiểm tra runtime hiện là TBD
  • Nullness bị bỏ qua khi xác định tính đồng nhất của chữ ký phương thức
    • Một phương thức có thể override phương thức khác dù nullness của tham số và kiểu trả về không khớp
    • Nếu các API khác nhau độc lập đưa vào dấu nullness, sự không khớp như vậy có thể phổ biến
  • Nullness không ảnh hưởng tới tính áp dụng của phương thức và không thể gây thất bại suy luận đối số kiểu, nhưng có thể ảnh hưởng tới nullness được suy luận cho kiểu trả về của phương thức generic
    • Chi tiết của thuật toán suy luận là TBD

Cảnh báo và lỗi của trình biên dịch

  • Khi biến một kiểu thành null-restricted, có thể phát sinh lỗi biên dịch mới
    • Khi trường hoặc mảng thuộc kiểu đó chưa được khởi tạo
    • Khi cố chuyển literal null sang kiểu đó
    • Việc so sánh literal null với biểu thức kiểu null-restricted cũng có thể trở thành lỗi biên dịch
  • Trong các tình huống khác, phân tích nullness chỉ mang tính hỗ trợ và không tạo lỗi biên dịch
  • javac cung cấp cảnh báo để tránh lỗi runtime, và IDE cùng các công cụ phân tích khác cũng được khuyến nghị đi theo hướng này
  • Các nguyên nhân cảnh báo có thể có gồm
    • Chuyển đổi nullness thu hẹp, bao gồm trường hợp đến từ kiểu ?
    • Dùng biểu thức kiểu ? cho truy cập member hoặc thao tác khác không thân thiện với null
    • Nullness của đối số kiểu không nhất quán với bound
    • Nullness của tham số hoặc kiểu trả về phương thức không khớp với phương thức bị override
    • Unchecked conversion làm thay đổi nullness của kiểu

Class file, reflection và thay đổi hỗ trợ

  • Hầu hết việc dùng dấu null bị xóa trong file class, còn các chuyển đổi runtime đi kèm được biểu diễn trực tiếp trong bytecode
  • Cú pháp thuộc tính Signature được cập nhật để cho phép !? bên trong kiểu
  • Nullness không được mã hóa trong descriptor của phương thức và trường
  • Để ngăn ô nhiễm trường, thuộc tính NullRestricted mới đánh dấu rằng trường không cho phép giá trị null
    • Trường này cũng phải được đánh dấu ACC_STRICT và phải được khởi tạo nghiêm ngặt
    • Verifier kiểm tra rằng tất cả trường instance khởi tạo nghiêm ngặt đã được gán tại thời điểm constructor gọi super(...)
    • Mọi nỗ lực ghi vào trường đều kiểm tra giá trị null và nếu phát hiện thì ném FieldStoreException
  • Tạo mảng null-restricted không được hỗ trợ bằng lệnh anewarray mà phải thực hiện qua lời gọi reflection API
  • Không có literal Foo!.class hay Foo?.class, và cũng không có instance java.lang.Class tương ứng
  • RuntimeType API mới mô tả tập kiểu được cưỡng chế lúc chạy trong các kiểm tra lưu mảng và trường, bao gồm các biến thể null-restricted của mọi kiểu class và interface
  • API Field hỗ trợ truy vấn RuntimeType của trường, và giá trị này có thể khác kết quả của getType
  • API Array hỗ trợ biến thể newInstance biểu diễn kiểu component bằng RuntimeType
    • Biến thể này cũng cho phép cung cấp giá trị khởi tạo cho component mảng
    • Từ chối nỗ lực tạo mảng null-restricted không có giá trị khởi tạo
  • Cơ chế deserialization truyền thống không tương thích với trường và mảng null-restricted; một JEP riêng dự kiến sẽ cung cấp cơ chế serialization không để lộ trường và mảng null-restricted chưa được khởi tạo
  • Tài liệu do javadoc tạo ra bao gồm dấu nullness
  • API java.lang.reflect.Typejavax.lang.model mã hóa nullness trong biểu diễn kiểu

Phương án thay thế và quan hệ phụ thuộc

  • Nhiều công cụ phát triển trong hệ sinh thái Java đã triển khai theo dõi null ở thời điểm biên dịch riêng, nhưng vì không thay đổi ngôn ngữ Java nên cú pháp chủ yếu bị giới hạn ở annotation và hành vi có thể tác động cũng bị giới hạn ở kiểm tra lúc biên dịch
  • Các ngôn ngữ khác theo dõi nullness trong hệ thống kiểu, và nhiều ngôn ngữ mặc định là null-restricted, xem việc gán vào kiểu null-restricted mà không kiểm tra null tường minh là lỗi
  • Trong Java, tính năng này phải là tùy chọn và có thể dùng dần dần mà không cần một đợt migration quy mô lớn duy nhất
  • Việc cưỡng chế nullness lúc chạy cũng có thể được triển khai bằng kiểm tra tường minh hoặc lời gọi Objects.requireNonNull
    • Tuy nhiên, việc áp dụng nhất quán rất phiền phức, cần thêm tài liệu hóa và làm giảm khả năng đọc của chương trình
    • Không có cách áp dụng trực tiếp cho nơi lưu biến như trường và mảng
  • Điều kiện tiên quyết là Flexible Constructor Bodies (Second Preview)
    • Constructor có thể thực thi câu lệnh trước lời gọi super(...) và gán vào trường instance, qua đó đáp ứng yêu cầu khởi tạo trường null-restricted
  • Công việc trong tương lai bao gồm
  • Các cải tiến tiềm năng khác trong tương lai bao gồm áp dụng dấu nullness cho một phần API chuẩn, biểu diễn null check ngắn gọn trong bytecode, cưỡng chế cấp thấp mạnh hơn cho tham số phương thức null-restricted, và cơ chế ngôn ngữ để ngầm khẳng định mọi kiểu trong một số ngữ cảnh là null-restricted

1 bình luận

 
GN⁺ 2024-08-04
Ý kiến trên Hacker News
  • Điều thú vị là cách tiếp cận này khác với cách C# đã đưa vào vài năm trước. Trong C#, nếu bật nullability trong dự án, mọi biến sẽ được khai báo là non-null trừ khi được đánh dấu rõ là nullable; còn trong đề xuất này, biến hiện có trên thực tế sẽ thuộc một trong ba loại: nullable, nullable tường minh, hoặc non-nullable tường minh
    Kotlin cũng là ngôn ngữ JVM nhưng giống C#, nếu không ghi rõ thì xem là non-null; chỉ là Kotlin không có gánh nặng tương thích ngược. Kotlin cũng cung cấp cách lách bằng lateinit var, cho phép để trống một kiểu non-nullable cho đến khi nó được khởi tạo trong một phương thức khác, và ném ra ngoại lệ riêng nếu truy cập trước khi khởi tạo
    Tôi thắc mắc vì sao lại để ba lựa chọn. Không thể giữ biến không có chú thích là nullable, và chỉ biến có chú thích mới là non-null tường minh sao? Nếu không ghi gì cũng tự động là nullable thì tôi không nghĩ ra lý do gì để muốn khai báo nullable riêng cả
    Tôi thích cách của C# hơn, nhưng cách này có ưu điểm là có thể dùng trong codebase legacy mà không cần giải quyết toàn bộ vấn đề nullability. Ngược lại, C# làm lộ ngay các vấn đề nullability, còn đề xuất này vẫn có thể giấu chúng như trước giờ
    Ngoài ra, phần “một phương thức có thể override phương thức khác ngay cả khi nullability của tham số và giá trị trả về không khớp” nghe rất lạ. Khi override/implement callback, nếu phương thức gốc chỉ định trả về non-null mà lại trả về null thì có vẻ sẽ thành một khẩu súng bắn vào chân

    • Ở công ty hiện tại, chúng tôi đang chuyển một codebase C# legacy sang nullable reference types, và nếu có dấu hiệu non-nullable tường minh thì sẽ rất tuyệt, vì có thể biết ngay phần nào đã được xem xét và chú thích
      Vì thời gian, hiện tại chúng tôi chọn chỉ bật chú thích ngoại trừ một vài vị trí cốt lõi, nhưng vẫn giữ NotNullAttributeCanBeNullAttribute của JetBrains làm dấu hiệu để nhìn ngay những nơi đã được quyết định có chủ ý. Cái sau có thể bỏ vì nullable đã có dấu hiệu tường minh, nhưng cái trước lại xung đột tên với tính năng của chính C#
      Theo nghĩa đó, ba lựa chọn là khá đáng mong muốn. Lý do là khi mã có hàng trăm nghìn dòng thì rất khó chuyển đổi nhanh và dễ dàng
      Ở các dự án nội bộ khác, chúng tôi đang rải #nullable enable quanh phần mã đang đụng tới để mở rộng dần phạm vi nullability. Cũng có điều kiện rằng mã mới phải nằm trong ngữ cảnh nullable. Cách này cũng ổn để chỉ rõ những phần đã được chú thích, nhưng chỉ khả thi với codebase và đội ngũ nhỏ hơn nhiều
    • Kotlin cũng có kiểu không thể tự định nghĩa trực tiếp nhưng chưa được chỉ định nullability. Kotlin gọi chúng là platform types, và trong lỗi cũng như chẩn đoán thì hiển thị bằng dấu chấm than như String!
      Thuật ngữ “platform type” và ký hiệu dấu chấm than có hơi gây nhầm lẫn, nhưng ngoài ra thì cách của Kotlin hoạt động khá tốt. Vì Kotlin đã có nullability ngay từ đầu nên lập trình viên không thể tự chỉ định platform type trực tiếp, nhưng chúng vẫn cần thiết để tương thích với các nền tảng cơ sở có nullability không rõ ràng như JVM hay JavaScript
      Trong cách tiếp cận này, giá trị mặc định vẫn hợp lý. Mặc định phải luôn là non-nullable, và ai nghĩ khác thì coi như chưa học được gì từ Tony Hoare. Đồng thời vẫn duy trì được tương thích ngược. Kotlin thì tương đối dễ, còn Java và C# còn phải giữ tương thích với mã nguồn hiện có
      Cả cách của Java lẫn C# đều không lý tưởng. Cách của C# thay đổi đáng kể hành vi mã tùy theo cờ compiler, còn cách của Java biến giá trị mặc định thành lựa chọn tệ nhất
      Dù vậy, tôi vẫn nghiêng về phía C# hơn. Vì nếu biến “có lẽ có thể null” thành lựa chọn dễ nhất thì đa số lập trình viên sẽ chọn nó làm mặc định. Đặc biệt với một ngôn ngữ thân thiện với doanh nghiệp như Java thì càng có vẻ như vậy. Linter và cảnh báo compiler sẽ giúp ích về lâu dài, nhưng có lẽ sẽ mất nhiều năm trước khi phần lớn mã Java được chú thích nullability đúng cách. Người dùng C# sẽ đau hơn trong ngắn hạn, nhưng có khả năng đạt mục tiêu nullability rõ ràng nhanh hơn nhiều
      https://kotlinlang.org/docs/java-interop.html#null-safety-an...
      https://www.infoq.com/presentations/Null-References-The-Bill...
    • Tôi đồng ý là lạ. Cách tôi kỳ vọng là ở cấp mã nguồn, T? nghĩa là nullable, T! nghĩa là non-nullable, còn mặc định của T thông thường trong mã nguồn mới biên dịch sẽ được chỉ định bằng một khai báo kiểu pragma theo từng file nguồn, từng file package-info, hoặc toàn cục qua compiler switch
      Sau đó, ở một bản Java LTS về sau có thể đổi mặc định toàn cục. Như vậy việc chuyển đổi dự án sẽ dễ hơn, có thể tự động chèn/cập nhật pragma, và nếu cần vẫn có thể giữ mặc định cũ
      Ngoài ra, nếu tận dụng chú thích @Nonnull JSR-305 hiện có làm cách biểu thị sự xuất hiện của kiểu non-nullable trong class file, thì cũng đã có thể cung cấp tương thích hai chiều với các JDK cũ
    • Ở phần cuối có mục “cung cấp một cơ chế trong ngôn ngữ để khẳng định rằng mọi kiểu trong một ngữ cảnh nhất định đều bị giới hạn null một cách ngầm định, dù lập trình viên không dùng ký hiệu ! tường minh”
      Chỉ cần cỡ compiler flag hoặc tag module thôi có lẽ cũng ổn
    • Lý do có lẽ là vì tương thích ngược của mã là điều Java coi trọng hơn nhiều so với các ngôn ngữ khác
      Dù vậy, tôi nghĩ rồi sẽ có một compiler flag giả định tự động là non-nullable nếu không có chỉ định riêng
  • Có vẻ ổn. Cuối cùng cũng có cách ở cấp độ ngôn ngữ để loại bỏ hàng nghìn ngoại lệ và kiểm tra null không cần thiết. Tuy vậy, việc tự động chuyển đổi để thu hẹp trạng thái null có vẻ sai sai
    Trong các ví dụ của đề xuất như String? id(String! arg) { return arg; }, String s = null;, Object! o1 = s; // NPE, Object o2 = id(s); // NPE, Object o3 = (String!) s; // NPE, ít nhất hai trường hợp đầu nên là lỗi biên dịch mới đúng chăng
    Trường hợp cuối thì là ép kiểu tường minh nên hơi khó nói, nhưng có lẽ tốt hơn là bên trong if (s != null), trình biên dịch hiểu kiểu hợp lệ là String! và cho phép String! ss = s;. Như vậy sẽ không có khả năng lỗi

    • Từ góc nhìn của người từng dùng Java trong ngành, kiểm tra động chỉ nên xảy ra khi được yêu cầu tường minh, còn lại nên xử lý tĩnh hết. Bean Validation hoạt động tốt ở chỗ dù đối tượng có thể tạm thời ở trạng thái sai, nó vẫn được xem là hợp lệ tại thời điểm kiểm chứng tường minh, hoặc tại thời điểm framework kiểm chứng trước khi đi vào mã của mình
      Thực ra so với phép cast ở trường hợp cuối, tôi thích dùng Objects.requireNonNull(s) hơn vì tường minh hơn. Nhưng cũng sẽ hay nếu có thứ như Objects.unsafeForceNonNull(s) để bỏ qua kiểm tra tường minh, ngoại trừ những trường hợp bị chặn vì tối ưu hóa. Nếu có phương thức unsafe, ta có thể tự triển khai trực tiếp requireNonNull mà không cần thêm phân tích tĩnh phức tạp
    • Có đoạn viết rằng “việc yêu cầu chương trình xử lý tường minh mọi giá trị null có thể phát sinh không phải là mục tiêu; các giá trị null chưa được xử lý có thể là cảnh báo khi biên dịch, nhưng không phải lỗi”
      Đáng tiếc là điều này sẽ chỉ được xác nhận ở runtime
    • Nếu ví dụ thứ nhất hoặc thứ hai trở thành lỗi biên dịch, điều đó có nghĩa là mọi điểm sử dụng thư viện đều phải được chú thích. Cho đến khi thư viện đó chuyển sang kiểu null, hoặc nếu nó không bao giờ chuyển, gần như dòng nào cũng sẽ đầy các phép cast. Vô lý
      Ví dụ, thư viện chuẩn đã nói rõ rằng ít nhất trong thời gian trước mắt họ sẽ không chuyển sang kiểu null
    • Nếu bản chất vấn đề là đưa null vào API, tôi không chắc đó có phải lỗi của ngôn ngữ không. Tôi hiểu là nó rườm rà và cần được dọn dẹp, nhưng rốt cuộc vấn đề chẳng phải nằm ở các lập trình viên viết mã theo cách đó sao
    • Nếu trường hợp thứ hai không phải lỗi biên dịch thì mã legacy có thể gọi hàm nhận đối số non-null, nhưng tôi không rõ điều đó có nhất thiết tốt hay không. Khó đánh giá trước khi thực sự dùng thử
  • Có vẻ rất cần một cách đánh dấu mọi biến mặc định là non-null ở cấp package, hoặc ít nhất là cấp file. Nếu không, vì lý do an toàn, lập luận rằng gần như mọi biến đều nên dùng cú pháp T! sẽ trở nên rất mạnh, và điều đó chỉ tạo ra nhiều nhiễu

    • Trong phần “các cải tiến khả dĩ trong tương lai” có đoạn “cung cấp một cơ chế trong ngôn ngữ để lập trình viên có thể khẳng định rằng mọi kiểu trong một ngữ cảnh nhất định đều bị giới hạn null một cách ngầm định, mà không cần dùng ký hiệu ! tường minh”
    • Có lẽ cũng không tệ đến vậy. Trong dự án của chúng tôi đã có chuẩn rằng mọi biến trong mã Java do chúng tôi sở hữu đều không phải null. Nếu cần biến nullable thì phải chú thích bằng @Null
      Vấn đề chỉ phát sinh ở ranh giới mã tương tác với thư viện. Cú pháp mới này có lẽ cũng tương tự
    • Một linter quyết liệt có thể coi các kiểu không đánh dấu là non-null, và chỉ yêu cầu ghi chú khả năng null ở những nơi cần thiết
  • Phần “hiện tại, mục tiêu không phải là áp dụng cải tiến ngôn ngữ này cho thư viện chuẩn” hơi đáng tiếc
    Theo kinh nghiệm bất đắc dĩ phải dùng PHP, việc cứ phải bóc bỏ hoặc dựng lại các thuộc tính đã được đảm bảo trước về dữ liệu mỗi khi tương tác với thư viện chuẩn khổng lồ là rất phiền
    Java cũng nên đưa tính biểu đạt như vậy vào thư viện chuẩn một cách tích cực hơn và biến nó thành công dân hạng nhất

    • Một trong những vấn đề lớn nhất của Java là do cải tiến dần dần, phần mã mới vừa được cải thiện đôi chút lại trở thành mã legacy trong tính năng gia tăng tiếp theo. Mã legacy dùng Optional sẽ là gánh nặng cho đề xuất nullable/non-nullable tường minh. Tôi nghĩ lẽ ra record type cũng đã có thể mặc định là non-nullable
    • Có vẻ bạn đang đọc “hiện tại” thành “tuyệt đối không bao giờ”, nhưng thực tế có lẽ là bản thân quá trình chuyển đổi đó là một việc khổng lồ nên họ muốn làm riêng
      Ngoài ra, nếu chia làm hai bước thì tính năng này có thể được phát hành dưới dạng preview dễ hơn, nhận phản hồi rồi mới chốt thiết kế. Nếu cố làm tất cả cùng lúc, gần như không còn chỗ để lặp lại và cải tiến tính năng trên thực tế
    • Cụm then chốt là “hiện tại”. Tôi nghĩ khi tính năng đã ổn định thì nó sẽ trở thành mục tiêu
    • “Bất đắc dĩ” gì chứ, PHP hiện đại khá dễ chịu đấy
  • Tôi mong tính năng này được đưa vào Java. Khả năng chọn lựa tường minh ở cấp độ ngôn ngữ như T? đã cải thiện đáng kể chất lượng sống của lập trình viên trong Kotlin và TypeScript. Java có các công cụ như NullAway, nhưng khá phiền
    Tôi cho rằng hỗ trợ ở cấp độ ngôn ngữ tốt hơn nhiều so với Optional/Maybe, vì nó giúp tập trung vào logic thực tế thay vì đặt mã lên đường ray map/flatMap
    https://github.com/uber/NullAway

    • Ý là muốn phá vỡ khả năng tương thích mã nguồn hơn 30 năm à? Tại sao phải làm vậy
      Chỉ cần dùng một ngôn ngữ JVM khác là được mà
  • Trang hiện đang sập nên để lại link lưu trữ: https://web.archive.org/web/20240802081039/https://bugs.open...

    • Cũng không ngạc nhiên khi họ đổi sang “jira” mà ai cũng yêu quý
  • “Việc yêu cầu chương trình xử lý tường minh mọi giá trị null có thể phát sinh không phải là mục tiêu; các giá trị null chưa được xử lý có thể trở thành cảnh báo khi biên dịch, nhưng không phải lỗi” là một quyết định tệ
    Java phần lớn là ngôn ngữ kiểu tĩnh, vậy tại sao lại đưa thêm một hành vi động nữa vào thì không hiểu nổi. Hy vọng có cách dễ dàng để nâng các cảnh báo kiểu này thành lỗi

    • Không thể làm vậy được. Nếu thế thì mọi chương trình Java hiện có sẽ vỡ. Nếu bắt buộc, mọi chương trình sẽ phải được viết lại để xử lý phản hồi null ở mọi nơi. Với tính năng bổ sung lần này, chỉ có thể áp dụng xử lý null bắt buộc cho các thao tác liên quan đến kiểu mới, vì vậy sẽ không làm hỏng gì cả
      Những ngôn ngữ mà hệ thống kiểu bắt buộc xử lý null/nil trong mọi trường hợp thì tốt hơn nhiều. Nhưng Java hiện tại không phải là ngôn ngữ như vậy. Dù thế, đây vẫn sẽ là một cải tiến lớn
  • Thật tiếc là họ học những bài học này quá muộn. Mặc định phải là non-nullable, mặc định bất biến, và mặc định là phạm vi hẹp nhất
    Trong thiết kế mới, quá thường xuyên người ta chọn sự tiện lợi tức thời thay vì “để rơi vào con đường an toàn”. Giá trị mặc định an toàn đòi hỏi thiết kế và trải nghiệm người dùng tinh tế hơn nhiều, nhưng kết quả là hầu như mọi ngôn ngữ, nền tảng và công nghệ đều đầy rẫy những khẩu súng tự bắn vào chân. Kỹ thuật xây dựng và kỹ thuật điện có quy chuẩn, còn phần mềm thì cứ khoảng 30 năm lại học lại cùng một bài học trong các ngôn ngữ và công nghệ mới

    • Đáng tiếc là Java có vẻ vẫn chưa học được quy tắc mặc định non-nullable. Đề xuất này đưa vào hai kiểu mới, nhưng nếu hỏi kiểu mặc định không có annotation là nullable hay không thì câu trả lời vẫn là “ừm, khó nói”
    • Điều đáng tiếc là Java học những thứ này muộn. “Chúng ta” và “Java” không giống nhau
      Java giống như một ngôi làng ở thế giới thứ ba vừa học được rằng muốn diệt vi khuẩn thì chỉ cần đun sôi nước
  • Phần lớn những gì tôi làm ở Facebook là dùng Hack. Tính nullable là một yếu tố cốt lõi của hệ thống kiểu Hack, và nó giải quyết được thật sự rất nhiều lỗi vô nghĩa
    Tất nhiên điều đó không có nghĩa là hoàn toàn không thể nhận được null ngoài dự kiến. Vì tính năng này cũng được thêm vào ngôn ngữ về sau, nên vẫn còn nhiều kiểu mixed legacy phản ánh gốc PHP, về cơ bản nghĩa là gì cũng được
    Trước hết tôi tò mò mảng nullable sẽ như thế nào. Ví dụ String![] cho thấy trường hợp đối tượng có thể là null, nhưng bản thân mảng thì sao? Trong Java, String labels[] = null; là hoàn toàn hợp lệ. Vậy có phải phải khai báo kiểu String![]! labels; không?
    Trong Hack, vec $foo nghĩa là cả foo lẫn phần tử đều không phải null, còn ?vec $foo nghĩa là phần tử là non-null nhưng foo có thể null. Thực tế gần như không có lý do để dùng mảng null, nên giá trị mặc định phải là không cho phép null. Tuy nhiên Java có vấn đề là mọi mã legacy đều giả định khả năng null
    Trong các ví dụ Object! o1 = s, Object o2 = id(s), Object o3 = (String!) s của đề xuất, tôi nghĩ số 2 và số 3 lẽ ra phải là lỗi biên dịch
    Cuối cùng, tôi thích toán tử ép buộc as của Hack hơn casting của Java. Ví dụ foo($b) là lỗi biên dịch, $b as A là lỗi runtime nếu là null, còn $a as ?B thì nếu là B sẽ cast, nếu không thì trả về null
    Rốt cuộc câu hỏi là liệu có thể phủ tính năng này lên Java SDK hay không, và nó sẽ trông như thế nào trong mã legacy
    https://docs.hhvm.com/hack/types/nullable-types

    • Có lẽ chỉ là String![]? labels = null;
  • Có vẻ những điểm hay của Kotlin giờ đang dần được đưa hết vào Java
    Dù vậy tôi vẫn muốn tiếp tục làm việc với Kotlin, nơi không phải đối phó với những thứ như Lombok. Java record thì đúng là ổn

    • Dù vậy các lập trình viên Java vẫn sẽ tiếp tục dùng các idiom cũ, và bị ràng buộc với các codebase cũ. Sẽ mất rất lâu để những thứ này được dùng rộng rãi, nên các lập trình viên Kotlin không cần lo
    • Java có lịch sử lâu dài lấy cảm hứng từ các ngôn ngữ khác. Thật tốt khi thấy nó vẫn tiếp tục phát triển với tốc độ khá ổn trong khi nhìn chung vẫn giữ được khả năng tương thích ngược