Hiểu sâu hơn về Java Generics
Tại sao cần Generics
Bản chất của Generics là tham số hóa kiểu dữ liệu (cho phép kiểm soát kiểu dữ liệu cụ thể của tham số mà không cần tạo ra kiểu dữ liệu mới). Nghĩa là trong quá trình sử dụng Generics, kiểu dữ liệu được chỉ định là một tham số, tham số này có thể được sử dụng trong lớp, giao diện và phương thức, tương ứng được gọi là lớp Generics, giao diện Generics và Generics method.
Ý nghĩa của Generics là:
- Thực thi cùng một mã cho nhiều kiểu dữ liệu (tái sử dụng mã)
Chúng ta sẽ giải thích bằng một ví dụ, trước hết hãy xem đoạn mã sau:
Nếu không sử dụng Generics, để thực hiện phép cộng với các kiểu dữ liệu khác nhau, mỗi kiểu dữ liệu đều cần phải tạo ra một phương thức add riêng biệt. Thông qua Generics, chúng ta có thể tái sử dụng một phương thức duy nhất:
- Trong Generics, kiểu dữ liệu được chỉ định khi sử dụng, không cần chuyển đổi kiểu dữ liệu (an toàn về kiểu dữ liệu, trình biên dịch sẽ kiểm tra kiểu dữ liệu).
Hãy xem ví dụ sau:
Khi sử dụng danh sách trên, các phần tử trong danh sách đều là kiểu Object (không giới hạn kiểu dữ liệu), vì vậy khi lấy phần tử ra khỏi danh sách, chúng ta cần chuyển đổi kiểu dữ liệu mục tiêu một cách rõ ràng và dễ dàng gây ra ngoại lệ java.lang.ClassCastException
.
Với Generics, nó cung cấp ràng buộc kiểu dữ liệu, cung cấp kiểm tra trước khi biên dịch:
Chức năng cơ bản Generic
Generics Class
- Bắt đầu với một generics class đơn giản:
- Generics class đa kiểu dữ liệu
Generics Interface
- Generic Interface đơn giản
Generics Method
Generics Method là phương thức mà kiểu Generics được chỉ định khi gọi phương thức.
Cú pháp định nghĩa Generics method:
Cú pháp gọi Generics method:
Khi định nghĩa Generics method, chúng ta phải thêm trước kiểu trả về để khai báo rằng đây là một Generics method, sử dụng kiểu Generics T
làm kiểu trả về của phương thức.
Class<T>
được sử dụng để chỉ định kiểu cụ thể của Generics, và biến c
có kiểu Class<T>
có thể được sử dụng để tạo đối tượng của lớp Generics.
Tại sao lại sử dụng biến c
để tạo đối tượng? Vì đây là một Generics Method, điều đó có nghĩa là chúng ta không biết kiểu cụ thể là gì và cũng không biết cách tạo đối tượng, vì vậy không thể sử dụng từ khóa new
để tạo đối tượng. Tuy nhiên, chúng ta có thể sử dụng phương thức newInstance()
của biến c
để tạo đối tượng bằng cách sử dụng reflection
.
Generics method yêu cầu tham số là kiểu Class<T>
, và giá trị trả về của phương thức Class.forName()
cũng là kiểu Class<T>
, vì vậy có thể sử dụng Class.forName()
làm tham số. Trong đó, tham số của phương thức forName()
là kiểu dữ liệu nào thì giá trị trả về của Class<T>
cũng là kiểu dữ liệu đó. Trong ví dụ này, tham số truyền vào phương thức forName()
là đường dẫn đầy đủ của lớp User
, do đó giá trị trả về là đối tượng kiểu Class<User>
, vì vậy khi gọi Generics method, kiểu của biến c
là Class<T>
, do đó kiểu Generics T
trong phương thức được chỉ định là User
, và kiểu của biến obj
là User
.
Tất nhiên, Generics method không chỉ có thể có một tham số Class, mà còn có thể thêm các tham số khác theo nhu cầu.
Tại sao lại sử dụng Generics method? Vì lớp Generics yêu cầu chỉ định kiểu khi khởi tạo, nếu muốn thay đổi kiểu, phải tạo một đối tượng mới, điều này có thể không linh hoạt. trong khi Generics method có thể chỉ định kiểu khi gọi, linh hoạt hơn.
Giới hạn trên và dưới Generics
- Trước tiên hãy nhìn vào đoạn mã sau, rõ ràng là sẽ có lỗi (vui lòng tham khảo văn bản sau để biết lý do lỗi cụ thể).
Làm thế nào để giải quyết vấn đề này?
Để giải quyết vấn đề chuyển đổi ẩn trong Generics, Java Generics đã thêm cơ chế giới hạn cho tham số kiểu. <? extends A>
chỉ định rằng tham số kiểu có thể là A (giới hạn trên) hoặc một loại con của A. Khi biên dịch, kiểu sẽ được xóa thành kiểu A, tức là sử dụng kiểu A thay thế cho tham số kiểu. Phương pháp này có thể giải quyết vấn đề ban đầu, trình biên dịch biết phạm vi của tham số kiểu, nếu loại thực thể B được truyền vào nằm trong phạm vi này, cho phép chuyển đổi. Khi đó chỉ cần một lần chuyển đổi kiểu, trong run time, đối tượng sẽ được coi là một thực thể của A.
- Giới thiệu các giới hạn trên và dưới Generics
Giới hạn trên và dưới của Generics được sử dụng khi sử dụng Generics, chúng ta có thể giới hạn kiểu tham số kiểu được truyền vào, ví dụ: chỉ cho phép tham số kiểu được truyền vào là một lớp cha nào đó hoặc một lớp con của một lớp nào đó.
Giới hạn trên:
Giơi hạn dưới:
Tóm tắt
<?>
là ký tự đại diện không giới hạn.<? extends E>
sử dụng từ khóaextends
để chỉ định giới hạn trên, đại diện cho kiểu tham số hóa có thể là kiểu được chỉ định hoặc một lớp con của kiểu đó.<? super E>
sử dụng từ khóasuper
để chỉ định giới hạn dưới, đại diện cho kiểu tham số hóa có thể là kiểu được chỉ định hoặc một lớp cha của kiểu đó.
Nguyên tắc sử dụng:《Effictive Java》
Để có sự linh hoạt tối đa, hãy sử dụng ký tự đại diện cho tham số đầu vào của lớp sản xuất hoặc lớp tiêu thụ.
- Nếu tham số hóa kiểu đại diện cho lớp sản xuất, hãy sử dụng
<? extends T>
. - Nếu tham số hóa kiểu đại diện cho lớp tiêu thụ, hãy sử dụng
<? super T>
. - Nếu cần cả hai, việc sử dụng ký tự đại diện không có ý nghĩa, vì bạn cần kiểu tham số chính xác.
- Nhìn vào một ví dụ thực tế khác để hiểu làm rõ hơn
Phạm vi của tham số kiểu E trong đoạn mã trên là <E extends Comparable<? super E>>
. Hãy xem từng bước:
- Để thực hiện so sánh, E cần là một lớp có thể so sánh, do đó cần
extends Comparable<…>
(lưu ý rằng đây không phải là từ khóa extends trong việc kế thừa, nó khác nhau). Comparable<? super E>
đại diện cho việc so sánh E, tức là E là lớp tiêu thụ, vì vậy cần sử dụng từ khóasuper
.- Tham số
List<? extends E>
đại diện cho danh sách các lớp con của E mà chúng ta muốn thao tác, giới hạn trên, điều này cho phép chứa đủ các hạn chế sử dụng dấu&
.
Nhiều hạn chế
Sử dụng dấu &
Ví dụ thực tế khác để làm rõ hơn:
Trong đoạn mã trên, tham số kiểu là <T extends Staff & Passenger>
.
Generics Array
Đầu tiên, chúng ta xem các khai báo liên quan đến Generics array:
Vậy chúng ta thường sử dụng nó như thế nào?
- Một ví dụ sử dụng thông minh
- Sử dụng một cách hợp lý:
Để biết chi tiết, xin vui lòng tham khảo giải thích dưới đây.
Hiểu sâu sắc về Generics
Để hiểu sâu về Generics yêu cầu chúng ta tìm hiểu về khái niệm “type erasure” (loại bỏ kiểu dữ liệu) và các vấn đề liên quan.
Làm thế nào để hiểu rằng Generics trong Java là “pseudo-generics”?
Type erasure trong Generics của Java là một tính năng được thêm vào từ JDK 1.5. Do đó, để tương thích với các phiên bản trước đó, cài đặt Generics của Java sử dụng một chiến lược được gọi là “pseudo-generics” (giả Generics). Điều này có nghĩa là Java hỗ trợ Generics trong cú pháp, nhưng trong quá trình biên dịch, sẽ thực hiện “type erasure” (loại bỏ kiểu dữ liệu), thay thế tất cả các biểu thức Generics (nằm trong dấu ngoặc nhọn) bằng các kiểu dữ liệu cụ thể (kiểu nguyên thủy tương ứng), giống như không sử dụng Generics hoàn toàn. Hiểu rõ về “type erasure” là rất hữu ích để sử dụng Generics một cách hiệu quả, đặc biệt là khi gặp phải các vấn đề phức tạp.
Nguyên tắc của “type erasure” trong Generics là:
- Loại bỏ khai báo tham số kiểu, tức là xóa bỏ phần tử
<>
và các phần kèm theo. - Dựa vào giới hạn trên và dưới của tham số kiểu để suy ra và thay thế tất cả các tham số kiểu bằng kiểu nguyên thủy tương ứng: nếu tham số kiểu là một wildcard không giới hạn hoặc không có giới hạn, thì thay thế bằng Object; nếu có giới hạn trên và dưới, thì thay thế bằng giới hạn trên hoặc giới hạn dưới của tham số kiểu (ví dụ: được thay thế bằng Number, được thay thế bằng Object).
- Chèn mã chuyển đổi kiểu tường minh để đảm bảo an toàn kiểu dữ liệu.
- Tự động tạo ra “bridge method” (phương thức cầu nối) để đảm bảo tính “polymorphism” (đa hình) của Generics sau khi loại bỏ kiểu dữ liệu.
Vậy làm thế nào để thực hiện “type erasure”?
Tham khảo từ: http://softlab.sdut.edu.cn/blog/subaochen/2017/01/generics-type-erasure/
- Loại bỏ tham số kiểu trong định nghĩa lớp - Loại bỏ kiểu không giới hạn
Khi không có bất kỳ giới hạn nào cho tham số kiểu trong định nghĩa lớp, nó được thay thế trực tiếp bằng Object, tức là các tham số kiểu như và đều được thay thế bằng Object.
- Loại bỏ tham số kiểu trong định nghĩa lớp - Loại bỏ kiểu bị hạn chế
Khi có giới hạn cho tham số kiểu trong định nghĩa lớp, nó được thay thế bằng giới hạn trên hoặc dưới của tham số kiểu, ví dụ như và <? extends Number>
được thay thế bằng Number
, <? super Number>
được thay thế bằng Object
.
- Loại bỏ tham số kiểu trong định nghĩa phương thức
Nguyên tắc loại bỏ tham số kiểu trong định nghĩa phương thức tương tự như loại bỏ tham số kiểu trong định nghĩa lớp, ở đây chỉ trình bày ví dụ về loại bỏ tham số kiểu có giới hạn.
Làm thế nào để chứng minh việc kiểu dữ liệu bị loại bỏ?
Chúng ta sẽ sử dụng hai ví dụ để chứng minh rằng các kiểu dữ liệu trong Java bị loại bỏ và các kiểu dữ liệu nguyên thủy là bình đẳng.
Trong ví dụ này, chúng ta định nghĩa hai mảng ArrayList
, một là ArrayList
với kiểu dữ liệu Generics là String
và chỉ có thể chứa chuỗi, một là ArrayList
với kiểu dữ liệu Generics là Integer
và chỉ có thể chứa số nguyên. Cuối cùng, chúng ta sử dụng phương thức getClass()
của đối tượng list1
và list2
để lấy thông tin về lớp của chúng, kết quả cuối cùng là true
. Điều này cho thấy kiểu dữ liệu Generics String
và Integer
đã bị loại bỏ và chỉ còn lại kiểu dữ liệu nguyên thủy.
Trong chương trình này, chúng ta định nghĩa một đối tượng ArrayList
với kiểu dữ liệu Generics là Integer
. Nếu gọi phương thức add()
trực tiếp, nó chỉ có thể lưu trữ dữ liệu số nguyên. Tuy nhiên, khi chúng ta sử dụng reflection
để gọi phương thức add()
, chúng ta có thể lưu trữ một chuỗi. Điều này cho thấy rằng kiểu dữ liệu Generics Integer
đã bị loại bỏ và chỉ còn lại kiểu dữ liệu nguyên thủy.
Làm thế nào để hiểu về loại bỏ thông tin generic sau khi loại bỏ?
Trong đó, đã đề cập đến kiểu nguyên thủy hai lần, kiểu nguyên thủy là gì?
Kiểu nguyên thủy là kiểu biến thực sự của biến trong bytecode
sau khi thông tin generic đã bị loại bỏ, bất kể khi nào định nghĩa một generic, kiểu nguyên thuỷ tương ứng sẽ được tự động cung cấp, kiểu biến sẽ bị loại bỏ và được thay thế bằng kiểu giới hạn của nó (biến không giới hạn được thay thế bằng Object
).
- Kiểu nguyên thuỷ của T là Object
Kiểu nguyên thủy của Pair là:
Vì trong Pair<T>
, T
là một biến kiểu không giới hạn, nên nó được thay thế bằng Object
, kết quả là một lớp thông thường, giống như trước khi generic được thêm vào ngôn ngữ Java. Trong chương trình, có thể chứa các cặp khác nhau, như Pair<String>
hoặc Pair<Integer>
, nhưng sau khi loại bỏ thông tin kiểu, chúng trở thành kiểu nguyên thuỷ của Pair, và kiểu nguyên thuỷ đều là Object
.
Từ phần trên, chúng ta cũng có thể hiểu rằng sau khi loại bỏ thông tin kiểu của ArrayList, kiểu nguyên thuỷ cũng trở thành Object
, vì vậy chúng ta có thể lưu trữ chuỗi bằng reflection
.
Nếu biến kiểu có giới hạn, thì kiểu nguyên thuỷ sẽ được thay thế bằng lớp biến kiểu đầu tiên của giới hạn.
Ví dụ: Nếu khai báo Pair như sau:
Thì kiểu nguyên thủy là Comparable
.
Cần phân biệt giữa kiểu nguyên thủy và kiểu biến generic. Khi gọi phương thức generic, có thể chỉ định generic hoặc không chỉ định generic:
- Khi không chỉ định generic, kiểu biến generic sẽ là kiểu cha chung nhất của các kiểu trong phương thức, cho đến Object.
- Khi chỉ định generic, các kiểu trong phương thức phải là loại cụ thể của generic đó hoặc là lớp con của nó.
Thực tế trong lớp generic, khi không chỉ định kiểu generic thì nó cũng tương tự, chỉ khác là lúc đó kiểu generic là Object. Ví dụ trong ArrayList
, nếu không chỉ định kiểu generic, thì ArrayList đó có thể chứa bất kỳ đối tượng nào.
- Kiểu generic là Object:
Làm thế nào để hiểu về kiểm tra tại thời điểm biên dịch của generic?
Nếu thông tin về biến loại bị loại bỏ trong quá trình biên dịch, tại sao khi chúng ta thêm một số nguyên vào đối tượng được tạo trong ArrayList lại bị lỗi? Không phải kiểu biến generic String đã được biến thành loại Object trong quá trình biên dịch sao? Tại sao không thể lưu trữ các loại khác? Với việc loại bỏ thông tin loại, làm thế nào để đảm bảo rằng chúng ta chỉ có thể sử dụng các kiểu được giới hạn bởi biến generic?
Trình biên dịch Java kiểm tra kiểu của generic trước khi loại bỏ thông tin loại và biên dịch. Ví dụ:
Trong chương trình trên, khi chúng ta thêm một số nguyên bằng phương thức add
, IDE sẽ báo lỗi ngay lập tức. Điều này chứng tỏ đây là kiểm tra trước khi biên dịch, vì nếu kiểm tra sau khi loại bỏ thông tin kiểu, kiểu nguyên thủy sẽ là Object và sẽ cho phép thêm bất kỳ kiểu tham chiếu nào. Tuy nhiên, thực tế lại không như vậy, điều này chứng tỏ việc sử dụng biến generic được kiểm tra trước khi biên dịch.
Vậy kiểm tra kiểu này áp dụng cho ai? Hãy xem sự tương thích giữa loại tham số hóa và nguyên thủy.
Ví dụ với ArrayList
, cách viết trước đây là:
Cách viết hiện tại:
Nếu nó tương thích với mã cũ, sẽ có các trường hợp sau đây:
Điều này không gây lỗi, nhưng sẽ có cảnh báo biên dịch.
Tuy nhiên, trong trường hợp thứ nhất, chúng ta có thể đạt được hiệu quả tương tự như việc sử dụng tham số hóa đầy đủ, trong khi trường hợp thứ hai thì không có tác dụng.
Vì kiểm tra kiểu được thực hiện trong quá trình biên dịch, việc tạo ra ArrayList
mà không chỉ định generic chỉ là việc cấp phát một không gian lưu trữ trong bộ nhớ, có thể chứa bất kỳ loại nào. Quan trọng là việc sử dụng tham chiếu đó, vì chúng ta sử dụng tham chiếu list1
để gọi các phương thức của nó, ví dụ như gọi phương thức add
, vì vậy tham chiếu list1
có thể kiểm tra kiểu generic. Trong khi tham chiếu list2
không sử dụng generic, vì vậy không thể kiểm tra kiểu.
Ví dụ:
Từ ví dụ trên, chúng ta có thể hiểu rằng kiểm tra kiểu chỉ áp dụng cho tham chiếu. Tham chiếu là gì? Đó là khi chúng ta sử dụng tham chiếu để gọi phương thức generic, nó sẽ kiểm tra kiểu của tham chiếu đó, không quan tâm đến đối tượng thực sự mà tham chiếu đó đang tham chiếu đến.
Tại sao không xem xét mối quan hệ kế thừa trong tham số hóa kiểu?
Trong Java, việc truyền tham số hóa kiểu như sau không được phép:
- Trước tiên, hãy giả sử trường hợp thứ nhất được mở rộng thành:
Thực tế là ở dòng mã thứ 4, sẽ có lỗi biên dịch. Nếu nó không có lỗi, khi chúng ta sử dụng tham chiếu list2
để lấy giá trị, nó sẽ trả về các đối tượng kiểu String
(như đã đề cập, kiểm tra kiểu dựa trên tham chiếu), nhưng thực tế đã lưu các đối tượng kiểu Object
. Điều này sẽ gây ra lỗi ClassCastException
. Vì vậy, để tránh lỗi phổ biến này, Java không cho phép việc truyền tham số hóa kiểu như vậy. (Đây cũng là lý do tại sao generic được tạo ra, để giải quyết vấn đề chuyển đổi kiểu, chúng ta không thể vi phạm mục đích ban đầu của nó).
- Trong trường hợp thứ hai, mở rộng trường hợp đó thành:
Đúng vậy, trong trường hợp này tốt hơn so với trường hợp thứ nhất, ít nhất khi chúng ta lấy giá trị từ list2
, không xảy ra lỗi ClassCastException
vì chúng ta chuyển đổi từ String
sang Object
. Tuy nhiên, việc làm này có ý nghĩa gì? Lý do generic xuất hiện là để giải quyết vấn đề chuyển đổi kiểu.
Chúng ta đã sử dụng generic, nhưng cuối cùng lại phải tự ép kiểu, vi phạm mục đích thiết kế generic ban đầu. Vì vậy, Java không cho phép làm như vậy. Hơn nữa, nếu chúng ta sử dụng list2
để thêm đối tượng mới vào, khi lấy giá trị, làm sao chúng ta biết rằng chúng ta đang lấy ra kiểu String
hay kiểu Object
?
Vì vậy, hãy chú ý đặc biệt đến vấn đề truyền tham chiếu trong generic.
Làm thế nào để hiểu về đa hình trong generic? Bridge Method
Việc xóa thông tin kiểu của generic gây ra xung đột đa hình, và cách giải quyết của JVM là sử dụng phương thức cầu nối (bridge method).
Giả sử chúng ta có một lớp generic như sau:
Sau đó, chúng ta muốn một lớp con kế thừa nó:
Trong lớp con này, chúng ta đặt giới hạn kiểu generic của lớp cha là Pair<Date>
. Trong lớp con, chúng ta ghi đè hai phương thức của lớp cha. Ý định ban đầu của chúng ta là giới hạn kiểu generic của lớp cha là Date
, vì vậy cả hai tham số của phương thức trong lớp cha đều là kiểu Date
.
Vì vậy, chúng ta không gặp vấn đề khi ghi đè hai phương thức này trong lớp con, và thậm chí chú thích @Override
của chúng ta cũng có thể thấy rằng không có vấn đề nào.
Nhưng thực tế là như thế nào?
Phân tích: Sau khi xóa thông tin kiểu, kiểu generic của lớp cha trở thành kiểu nguyên thủy Object
, vì vậy lớp cha sau khi biên dịch trở thành:
Tiếp theo, xem xét kiểu của hai phương thức trong lớp con:
Hãy phân tích phương thức setValue
trước, kiểu của lớp cha là Object
, trong khi kiểu của lớp con là Date
, kiểu tham số không giống nhau, nếu đây là một quan hệ kế thừa thông thường, thì đây không phải là việc ghi đè (override) mà là việc nạp chồng (overload). Hãy kiểm tra bằng cách sử dụng một phương thức main
:
Nếu đây là việc nạp chồng, thì trong lớp con có hai phương thức setValue
, một với tham số kiểu Object
và một với tham số kiểu Date
, nhưng chúng ta thấy rằng không có phương thức với tham số kiểu Object
trong lớp con. Vì vậy, đây là việc ghi đè, không phải việc nạp chồng.
Vì sao lại như vậy?
Lý do là, chúng ta truyền vào kiểu generic của lớp cha là Date
, Pair<Date>
, ý định ban đầu của chúng ta là biến lớp generic thành:
Sau đó, chúng ta ghi đè hai phương thức với tham số kiểu Date
trong lớp con, để thực hiện đa hình trong kế thừa.
Nhưng vì một số lý do, máy ảo không thể biến kiểu generic thành Date
, chỉ có thể xóa thông tin loại, trở thành loại nguyên thủy Object
. Vì vậy, ý định ban đầu của chúng ta là thực hiện ghi đè, để thực hiện đa hình, nhưng sau khi xóa thông tin loại, chỉ có thể thực hiện nạp chồng. Điều này tạo ra một xung đột giữa xóa thông tin kiểu và đa hình. JVM có hiểu ý định của bạn không? Có! Nhưng nó không thể thực hiện trực tiếp, Không!!!! Nếu không, chúng ta sẽ làm thế nào để ghi đè phương thức với tham số kiểu Date
mà chúng ta muốn.
JVM sử dụng một phương thức đặc biệt để hoàn thành chức năng này, đó là phương thức cầu nối (bridge method).
Đầu tiên, chúng ta sẽ sử dụng lệnh javap -c className
để giải mã lại mã byte của lớp con DateInter
, kết quả như sau:
Từ kết quả biên dịch, chúng ta có thể thấy rằng lớp con mà chúng ta ghi đè setValue
và getValue
thực sự có 4 phương thức. Thực tế, hai phương thức cuối cùng là các phương thức cầu nối được tạo bởi trình biên dịch. Có thể thấy rằng tham số của các phương thức cầu nối đều là Object
, tức là kiểu của lớp con thực sự ghi đè hai phương thức của lớp cha. Nhưng chú thích @Override
được đặt trên phương thức setValue
và getValue
mà chúng ta tự định nghĩa chỉ là ảo tưởng. Các phương thức cầu nối thực chất chỉ gọi phương thức mà chúng ta ghi đè.
Vì vậy, JVM thông minh sử dụng phương pháp cầu nối để giải quyết xung đột giữa việc xóa thông tin kiểu và đa hình.
Tuy nhiên, cần lưu ý rằng ý nghĩa của hai phương thức setValue
và getValue
này khác nhau.
Phương thức setValue
được tạo ra để giải quyết xung đột giữa xóa thông tin kiểu và đa hình.
Trong khi đó, phương thức getValue
có ý nghĩa phổ quát, nếu đây là một mối quan hệ thừa kế bình thường:
Khi đó phương thức getValue
của lớp cha như sau:
Và phương thức được ghi đè bởi lớp con là:
Thực tế, việc này cũng tồn tại trong việc kế thừa thông thường, đó là sự chuyển đổi kiểu (covariant).
Một điều thú vị là trong trường hợp này, phương thức cầu nối Object getValue()
và Date getValue()
tồn tại đồng thời. Tuy nhiên, nếu đây là hai phương thức thông thường, cùng có chữ ký phương thức, JVM sẽ không thể phân biệt hai phương thức này. Nếu chúng ta tự viết mã Java, mã như vậy sẽ không vượt qua kiểm tra của trình biên dịch, nhưng máy ảo lại cho phép điều này xảy ra, vì máy ảo xác định một phương thức dựa trên kiểu tham số và kiểu trả về, vì vậy trình biên dịch cho phép thực hiện việc “không hợp lệ” này và để máy ảo phân biệt.
Làm thế nào để hiểu rằng kiểu cơ bản không thể được sử dụng làm kiểu Generics?
Ví dụ, chúng ta không có
ArrayList<int>
, chỉ cóArrayList<Integer>
, tại sao?
Điều này là do sau khi loại bỏ kiểu, kiểu gốc của ArrayList
trở thành Object
, nhưng kiểu Object
không thể lưu trữ giá trị int
, chỉ có thể tham chiếu đến giá trị Integer
. Ngoài ra, chúng ta cũng cần lưu ý rằng chúng ta có thể sử dụng list.add(1)
là do sự tự động đóng gói (autoboxing) và mở gói (unboxing) kiểu dữ liệu cơ bản trong Java.
Làm thế nào để hiểu rằng kiểu Generic không thể được khởi tạo?
Không thể khởi tạo kiểu generic, điều này căn bản là do quyết định của việc loại bỏ kiểu:
Chúng ta có thể thấy mã sau sẽ báo lỗi trong trình biên dịch Java:
Vì trong quá trình biên dịch Java không thể xác định được kiểu tham số hóa của kiểu thông thường, cũng không thể tìm thấy tệp mã bytecode
của lớp tương ứng, vì vậy tự nhiên không thể khởi tạo. Ngoài ra, vì T
bị loại bỏ thành Object
, nếu có thể new T()
thì nó sẽ trở thành new Object()
, mất đi ý nghĩa ban đầu. Nếu chúng ta thực sự cần khởi tạo một kiểu generic, chúng ta có thể sử dụng reflection
để thực hiện:
Mảng Generic: Có thể sử dụng kiểu cụ của kiểu generic thể để khởi tạo không?
Hãy xem ví dụ được cung cấp bởi Oracle:
Do cơ chế loại bỏ kiểu của JVM, vì vậy mã trên có thể gán giá trị cho oa[1]
là ArrayList
mà không gây ra ngoại lệ, nhưng khi lấy dữ liệu ra thì phải thực hiện một lần chuyển đổi kiểu, vì vậy sẽ xảy ra ClassCastException
. Nếu khai báo mảng chung new List[10]
bỏ <String>
, thì tình huống trên sẽ không có bất kỳ cảnh báo hoặc lỗi nào trong quá trình biên dịch, chỉ có lỗi xảy ra trong runtime, nhưng mục đích của việc sử dụng kiểu generic là để loại bỏ ClassCastException
tức là Type-Safe, vì vậy nếu Java hỗ trợ khởi tạo mảng qua kiểu generic thì chính là tự bắn vào chân mình.
Đối với đoạn mã dưới đây thì nó là hợp lệ:
Vì vậy, việc khởi tạo mảng generic qua cách sử dụng ký tự đại diện là được cho phép, vì khi lấy dữ liệu ra, chúng ta phải thực hiện chuyển đổi kiểu rõ ràng, phù hợp với logic dự kiến. Tóm lại, khởi tạo mảng generic trong Java không thể là kiểu cụ thể của kiểu genertic, chỉ có thể là dạng ký tự đại diện, vì kiểu cụ thể sẽ cho phép lưu trữ bất kỳ đối tượng nào, và khi lấy ra sẽ xảy ra ngoại lệ chuyển đổi kiểu, xung đột với thiết kế của kiểu generic, trong khi dạng ký tự đại diện ban đầu đã cần phải tự chuyển đổi kiểu, phù hợp với logic dự kiến.
Tài liệu chính thức của Oracle: https://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html
Để hiểu sâu hơn, chúng ta hãy xem đoạn mã sau:
Vì trong Java không thể tạo một mảng của một kiểu cụ thể của kiểu generic, trừ khi sử dụng ký tự đại diện và thực hiện chuyển đổi kiểu rõ ràng.
Mảng Generic: Làm thế nào để khởi tạo một phiên bản mảng generic một cách chính xác?
Dù chúng ta khởi tạo một phiên bản mảng generic bằng cách sử dụng
new ArrayList[10]
hoặc bằng cách sử dụng ký tự đại diện của kiểu generic, cả hai đều gây ra cảnh báo, điều đó có nghĩa là chúng chỉ đạt được cú pháp hợp lệ, nhưng chúng ta phải tự chịu rủi ro tiềm ẩn khi chạy. Do đó, cách khởi tạo mảng kiểu thông thường đó không phải là cách tối ưu và tinh tế nhất.
Trong các tình huống sử dụng mảng generic, chúng ta nên cố gắng sử dụng danh sách List
để thay cho Array
. Ngoài ra, chúng ta cũng có thể sử dụng phương thức Array.newInstance(Class<T> componentType, int length)
trong java.lang.reflect.Array
để tạo một mảng có kiểu và kích thước được chỉ định, như sau:
Vì vậy, sử dụng reflection
để khởi tạo một mảng generic được coi là một cách triển khai tinh tế, vì kiểu generic T
chỉ được xác định vào thời điểm chạy, và chúng ta chỉ có thể tạo một mảng generic trong runtime bằng cách sử dụng kỹ thuật reflection
.
Làm thế nào để hiểu về phương thức tĩnh và biến tĩnh trong lớp generic?
Phương thức tĩnh và biến tĩnh trong lớp generic không thể sử dụng các tham số kiểu thông thường được khai báo trong lớp chứa.
Ví dụ minh họa:
Vì việc khởi tạo các tham số kiểu generic trong lớp chứa xảy ra khi đối tượng được khai báo, trong khi biến tĩnh và phương thức tĩnh không cần sử dụng đối tượng để gọi. Vì chưa có đối tượng nào được tạo ra, làm thế nào để xác định kiểu tham số kiểu thông thường này? Vì vậy, tất nhiên là sai.
Tuy nhiên, hãy lưu ý sự khác biệt sau đây:
Vì đây là một phương thức generic, trong phương thức generic này, T
được định nghĩa trong phương thức chính nó, không phải là T
trong lớp generic.
Làm thế nào để hiểu về việc sử dụng kiểu generic trong các ngoại lệ?
- Không thể ném hoặc bắt các đối tượng của lớp generic. Thực tế là mở rộng lớp
Throwable
trong kiểu generic là không hợp lệ. Ví dụ: Định nghĩa dưới đây sẽ không được biên dịch:
Tại sao không thể mở rộng Throwable
? Vì các ngoại lệ được ném và bắt trong runtime, trong khi thông tin kiểu generic sẽ bị xóa đi trong quá trình biên dịch. Vì vậy, giả sử việc biên dịch trên là hợp lệ, hãy xem định nghĩa dưới đây:
Sau khi thông tin kiểu bị xóa, cả hai khối catch đều trở thành kiểu nguyên thủy Object
, điều đó có nghĩa là cả hai khối catch trở nên giống nhau, tương đương với:
Điều này tất nhiên là không thể.
- Không thể sử dụng biến kiểu generic trong khối catch:
Vì thông tin kiểu đã trở thành kiểu nguyên thủy trong quá trình biên dịch, điều đó có nghĩa là T
ở trên sẽ trở thành Throwable
nguyên thủy. Vậy nếu có thể sử dụng biến kiểu thông thường trong khối catch, thì định nghĩa dưới đây sẽ như thế nào:
Theo nguyên tắc bắt ngoại lệ, lớp con phải đứng trước, lớp cha đứng sau, vì vậy định nghĩa trên vi phạm nguyên tắc bắt ngoại lệ. Ngay cả khi bạn sử dụng T
là ArrayIndexOutofBounds
khi sử dụng phương thức tĩnh này, sau khi biên dịch, nó vẫn trở thành Throwable
, trong khi ArrayIndexOutofBounds
là lớp con của IndexOutofBounds
, vi phạm nguyên tắc bắt ngoại lệ. Vì vậy, để tránh tình huống như vậy, Java cấm sử dụng biến kiểu generic trong khối catch.
- Tuy nhiên, bạn có thể sử dụng biến kiểu generic trong khai báo ngoại lệ. Phương thức dưới đây là hợp lệ:
Việc sử dụng như trên là hợp lệ.
Làm thế nào để lấy được kiểu tham số của kiểu generic?
Vì kiểu generic đã bị xóa, vậy làm thế nào để lấy được kiểu tham số của kiểu generic? Chúng ta có thể sử dụng phản chiếu (
java.lang.reflect.Type
) để lấy kiểu tham số.
java.lang.reflect.Type
là một giao diện cao cấp chung cho tất cả các kiểu trong Java, đại diện cho tất cả các kiểu trong Java. Các kiểu trong hệ thống Type
bao gồm: GenericArrayType
(kiểu mảng chung), ParameterizedType
(kiểu tham số hóa), TypeVariable
(biến kiểu), WildcardType
(kiểu đại diện), Class
(kiểu nguyên thủy), Class
(kiểu cơ bản), tất cả các loại này đều triển khai giao diện Type
.
Trong đó, ParameterizedType
là một giao diện được định nghĩa như sau:
Ràng buộc của Generics
Thực hành tốt nhất với Generics
Đặt tên cho Generics
Có một số quy ước đặt tên thông thường cho generics:
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V v.v. - Thứ tự thứ 2, thứ 3, thứ 4 của các kiểu
Lời khuyên sử dụng Generics
- Loại bỏ cảnh báo kiểm tra kiểu
- Ưu tiên sử dụng List thay vì mảng
- Ưu tiên sử dụng generics để tăng tính tổng quát của mã
- Ưu tiên sử dụng phương thức generics để giới hạn phạm vi của generics
- Sử dụng wildcard giới hạn để tăng tính linh hoạt của API
- Ưu tiên sử dụng các bộ chứa không đồng nhất an toàn về kiểu