Generic trong Java
- 09-11-2023
- Toanngo92
- 0 Comments
Mục lục
Tổng quan về Generic
Genericity (chung/tổng quát) là cách mà các lập trình viên có thể xác định kiểu của các đối tượng mà một lớp có thể làm việc thông qua các tham số được truyền vào lúc khai báo và được đánh giá vào thời gian biên dịch. Loại Generic có thể được so sánh với các hàm có tham số là các biến kiểu và có thể được khởi tạo với các đối số kiểu khác nhau tùy thuộc vào ngữ cảnh.
Generics trong mã Java tạo ra một phiên bản biên dịch duy nhất của một lớp generic. Sự ra đời của Generics trong các lớp Java giúp loại bỏ việc phải ép kiểu tường minh cho đối tượng lớp, ngăn ngừa lỗi ClassCastException trong quá trình biên dịch. Generics giúp loại bỏ sự không nhất quán về kiểu trong thời gian biên dịch thay vì thời gian chạy. Generics được thêm vào ngôn ngữ lập trình Java vì chúng cho phép:
- Lấy thêm thông tin về kiểu của một tập hợp.
- Theo dõi kiểu của các phần tử một tập hợp chứa.
- Giảm cần sử dụng các lệnh ép kiểu trong toàn bộ chương trình.
Lưu ý: Generics được kiểm tra vào thời gian biên dịch để đảm bảo tính chính xác về kiểu. Trong Generics, một tập hợp kiểu không được xem xét là một danh sách các tham chiếu đến đối tượng. Bạn có thể phân biệt sự khác biệt giữa một tập hợp các tham chiếu đến số nguyên và byte. Một tập hợp kiểu generic yêu cầu một tham số kiểu xác định kiểu của phần tử được lưu trong tập hợp.
“Tổng hợp” là một thuật ngữ kỹ thuật trong Java chỉ các tính năng liên quan đến việc sử dụng phương thức và kiểu tổng quát. Việc biết kiểu phần tử của một tập hợp là động cơ chính cho việc thêm Generics vào ngôn ngữ lập trình Java. Trước đây, các tập hợp trình xử lý phần tử như một tập hợp các đối tượng. Để truy xuất một phần tử từ một tập hợp yêu cầu một lệnh ép kiểu tường minh, vì ép kiểu xuống không thể được kiểm tra bởi trình biên dịch. Do đó, luôn có nguy cơ xảy ra ngoại lệ thời gian chạy, ClassCastException, nếu bạn ép kiểu một kiểu không phải là kiểu cha của kiểu được trích xuất.
Generics cho phép người lập trình gửi thông tin về kiểu của một tập hợp đến trình biên dịch để nó kiểm tra. Do đó, việc sử dụng Generics là an toàn vì trong quá trình biên dịch của chương trình, trình biên dịch liên tục kiểm tra kiểu phần tử của tập hợp và chèn lệnh ép kiểu chính xác cho các phần tử được lấy ra khỏi tập hợp.
Xét: (cho mã không tổng quát)
LinkedList list = new LinkedList();
list.add(new Integer(1));
Integer num = (Integer) list.get(0);
Trong đoạn mã này, một phiên bản của LinkedList được tạo ra. Một phần tử kiểu Integer được thêm vào danh sách. Khi lấy giá trị từ danh sách, cần phải sử dụng lệnh ép kiểu tường minh.
Để lấy dữ liệu ra, chúng ta cần làm như sau:
LinkedList<Integer> list = new LinkedList<>();
list.add(new Integer(1));
Integer num = list.get(0);
Trong đoạn mã, LinkedList<> là một lớp tổng quát chấp nhận một Integer là kiểu tham số. Trình biên dịch kiểm tra tính chính xác về kiểu vào thời gian biên dịch, vì vậy không cần phải ép kiểu thành Integer bởi vì trình biên dịch chèn lệnh ép kiểu chính xác cho các phần tử được lấy ra từ danh sách bằng cách sử dụng phương thức get().
Ưu điểm và Hạn chế của Generics
Ưu điểm của Generics:
- Generics cho phép linh hoạt trong việc ràng buộc động.
- Loại tổng quát giúp trình biên dịch kiểm tra tính chính xác về kiểu của chương trình vào thời gian biên dịch.
- Trong Generics, các lỗi phát hiện bởi trình biên dịch dễ dàng sửa hơn so với lỗi thời gian chạy.
- Việc xem xét mã nguồn trở nên đơn giản hơn trong Generics vì sự mập mờ giữa các bộ chứa ít hơn.
Trong Generics, mã nguồn chứa ít lệnh ép kiểu hơn, từ đó giúp tăng tính đọc và tính mạnh mẽ.
Hạn chế của Generics:
- Trong Generics, bạn không thể tạo các hàm tạo tổng quát.
- Một biến cục bộ không thể được khai báo khi kiểu khóa và kiểu giá trị khác nhau.
Các Lớp Tổng Quát (Generic classes)
Lớp tổng quát cho phép một lập trình viên Java xác định một tập hợp các kiểu liên quan thông qua một lời khai báo lớp duy nhất. Một kiểu tổng quát được tham số hóa qua các kiểu. Đó có thể là một lớp tổng quát hoặc giao diện.
Lớp tổng quát là cách xác định mối quan hệ kiểu giữa một kiểu thành phần và kiểu đối tượng của nó. Trong quá trình tạo ra các trường hợp của lớp, kiểu cụ thể cho một tham số lớp sẽ được xác định. Vì vậy, kiểu thành phần được xác định bởi tham số lớp phụ thuộc vào một thể hiện cụ thể. Không thể thiết lập mối quan hệ con giữa một lớp tổng quát và các thể hiện của nó. Sự đa hình thời gian chạy trên một lớp tổng quát không thể thực hiện, do đó không thể xác định biến bằng lớp tổng quát.
Lời khai báo lớp tổng quát giống với lời khai báo lớp không tổng quát. Tuy nhiên, trong lời khai báo lớp tổng quát, tên lớp được theo sau bởi một phần tham số kiểu.
Cú pháp để khai báo lớp tổng quát giống với lớp thông thường, ngoại trừ rằng trong dấu ngoặc nhọn (<>) tham số kiểu được khai báo. Lời khai báo của tham số kiểu theo sau tên lớp. Tham số kiểu giống như biến và có thể có giá trị là một kiểu lớp, kiểu giao diện hoặc bất kỳ biến kiểu nào trừ kiểu dữ liệu nguyên thủy. Lời khai báo lớp như List<E> đề cập đến một lớp kiểu tổng quát.
Một lớp tổng quát cho phép một loạt các đối tượng của bất kỳ kiểu nào được lưu trữ trong lớp tổng quát và không có giả định nào về cấu trúc nội bộ của các đối tượng đó. Tham số của lớp tổng quát (ví dụ: Integer
trong Array<Integer>
) là lớp được chỉ định trong lời khai báo mảng và được ràng buộc vào thời gian biên dịch. Một lớp tổng quát có thể tạo ra nhiều kiểu, một cho mỗi loại tham số, ví dụ: Array<Tree>
, Array<String>
, và còn nhiều loại khác. Các lớp tổng quát có thể chấp nhận một hoặc nhiều tham số kiểu. Do đó, chúng được gọi là các lớp có tham số hoặc các loại có tham số. Phần tham số kiểu của một lớp tổng quát có thể bao gồm nhiều tham số kiểu tách biệt bằng dấu phẩy.
Khai báo và Tạo thể hiện của Lớp Tổng Quát
Để tạo một thể hiện của lớp tổng quát, từ khóa new được sử dụng cùng với tên lớp, nhưng tham số kiểu được truyền giữa tên lớp và dấu ngoặc đơn. Tham số kiểu sẽ được thay thế bằng kiểu thực tế khi một đối tượng được tạo ra từ lớp. Một lớp tổng quát được chia sẻ giữa tất cả các thể hiện của nó.
Cú pháp:
class NumberList<Element> {
// todo
}
Ví dụ:
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
public void printType() {
System.out.println("Type of content: " + content.getClass().getName());
}
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>(42);
Box<String> stringBox = new Box<>("Hello, World!");
System.out.println("Content of integerBox: " + integerBox.getContent());
integerBox.printType();
System.out.println("Content of stringBox: " + stringBox.getContent());
stringBox.printType();
Integer[] intArray = { 1, 2, 3, 4, 5 };
String[] strArray = { "one", "two", "three", "four", "five" };
System.out.print("Integer Array: ");
printArray(intArray);
System.out.print("String Array: ");
printArray(strArray);
}
}
Mã nguồn tạo ra một lời khai báo kiểu tổng quát với một biến kiểu, ví dụ như T, mà có thể được sử dụng ở bất kỳ đâu trong lớp. Để tham chiếu đến lớp tổng quát này, một thực hiện kiểu tổng quát được thực hiện để thay thế T bằng một giá trị như String.
Người dùng có thể chỉ định một biến kiểu như bất kỳ kiểu không phải là kiểu nguyên thủy nào, có thể là bất kỳ kiểu lớp nào, kiểu mảng, kiểu giao diện hoặc thậm chí là một biến kiểu khác.
Thường thì tên tham số kiểu là các chữ cái viết hoa đơn giản.
Dưới đây là các tên tham số kiểu thường được sử dụng:
- K – Key (Khóa)
- T – Type (Kiểu)
- V – Value (Giá trị)
- N – Number (Số)
- E – Element (Phần tử)
- S, U, V, và các tên tương tự.
Đoạn mã dưới minh họa cách khai báo và khởi tạo một lớp Generic cơ bản:
import java.util.*;
class TestQueue<DataType> {
private LinkedList<DataType> items = new LinkedList<DataType>();
public void enqueue(DataType item) {
items.addLast(item);
}
public DataType dequeue() {
return items.removeFirst();
}
public boolean isEmpty() {
return items.size() == 0;
}
public static void main(String[] args) {
TestQueue<String> testObj = new TestQueue<>();
testObj.enqueue("Hello");
testObj.enqueue("Java");
System.out.println(testObj.dequeue());
}
}
Trong Java SE 7 trở đi, các đối số kiểu cần thiết có thể được thay thế để gọi constructor của một lớp tổng quát với một tập hợp trống các đối số kiểu (<>) . Cặp dấu ngoặc nhọn (<>) được gọi là “diamond”. Quan trọng lưu ý rằng trình biên dịch cần xác định các đối số kiểu từ ngữ cảnh khi sử dụng tập hợp trống các đối số kiểu.
Trong đoạn mã trên, tạo một lớp tổng quát triển khai khái niệm hàng đợi (queue) và lấy ra phần tử khỏi hàng đợi trên bất kỳ kiểu dữ liệu nào như Integer, String hoặc Double. Biến kiểu hoặc tham số kiểu, <DataType>, được sử dụng cho kiểu đối số và kiểu trả về của hai phương thức. Tham số kiểu có thể có bất kỳ tên nào. Tham số kiểu có thể được so sánh với tham số hình thức trong các tiểu trình. Tên sẽ được thay thế bằng tên thực tế khi lớp được sử dụng để tạo một thể hiện. Ở đây, <DataType> đã được thay thế bằng String trong phương thức main() khi tạo thể hiện của lớp.
Output:
Hello
Phương thức Generic (Generic Methods)
Java cũng hỗ trợ các phương thức tổng quát (generic methods). Các phương thức tổng quát được định nghĩa cho một phương thức cụ thể và có cùng chức năng như tham số kiểu trong các lớp tổng quát. Các phương thức tổng quát có thể xuất hiện trong các lớp tổng quát cũng như trong các lớp không tổng quát. Phương thức tổng quát có thể được định nghĩa như là một phương thức có tham số kiểu. Các phương thức tổng quát phù hợp nhất cho các phương thức nạp chồng (overloaded methods) thực hiện các hoạt động tương tự hoặc với các kiểu đối số khác nhau. Việc sử dụng các phương thức tổng quát làm cho các phương thức nạp chồng trở nên gọn gàng hơn và dễ dàng hơn khi viết mã.
Phương thức tổng quát cho phép các tham số kiểu được sử dụng để thiết lập sự phụ thuộc giữa kiểu đối số của một phương thức và kiểu trả về của nó. Kiểu trả về không phụ thuộc vào tham số kiểu hoặc bất kỳ đối số nào của phương thức. Điều này cho thấy rằng đối số kiểu đang được sử dụng cho đa hình. Phạm vi của tham số kiểu của phương thức là khai báo của phương thức. Phạm vi của các tham số kiểu cũng có thể là các tham số kiểu của các tham số kiểu khác.
Cú pháp:
public <T> void display(T val) {
// Mã nguồn của phương thức
}
Ví dụ:
import java.util.List;
public class NumberList<T> {
public void display(T[] val) {
for (T element : val) {
System.out.printf("Value is as follows: %s %n", element);
}
}
public static void main(String[] args) {
Integer[] intValues = {1, 7, 9, 15};
NumberList<Integer> listObj = new NumberList<>();
listObj.display(intValues);
}
}
Mã nguồn này sử dụng một phương thức tổng quát, phương thức display(), mà chấp nhận một tham số mảng làm đối số của nó.
Output:
Value is as follows: 1
Value is as follows: 7
Value is as follows: 9
Value is as follows: 15
Ví dụ 2:
Tạo lớp TestQueue
import java.util.LinkedList;
class TestQueue<DataType1, DataType2> {
private final DataType1 num;
private LinkedList<DataType1> items = new LinkedList<>();
public TestQueue(DataType1 num) {
this.num = num;
}
public void enqueue(DataType1 item) {
items.addLast(item);
}
public DataType1 dequeue() {
return items.removeLast();
}
}
Tạo lớp MyTest
public class MyTest extends TestQueue<Integer, String> {
public MyTest(Integer num) {
super(num);
}
public static void main(String[] args) {
MyTest test = new MyTest(new Integer(10));
test.enqueue("Hello");
test.enqueue("Java");
System.out.println(test.dequeue());
}
}
Khai báo phương thức Genenric
Để tạo các phương thức và hàm khởi tạo tổng quát, tham số kiểu được khai báo trong chữ ký của phương thức và hàm tạo. Tham số kiểu được chỉ định trước kiểu trả về của phương thức và trong dấu ngoặc nhọn. Các tham số kiểu có thể được sử dụng như kiểu đối số, kiểu trả về và kiểu biến cục bộ trong khai báo phương thức tổng quát. Có thể có nhiều loại tham số kiểu, mỗi loại tham số kiểu được phân tách bằng dấu phẩy. Các tham số kiểu này hoạt động như giữ chỗ cho các kiểu dữ liệu thực tế của đối số kiểu, được truyền vào phương thức. Các kiểu dữ liệu nguyên thủy không thể được đại diện cho tham số kiểu.
Đoạn mã dưới mô tả generic method trong interface Colletion
public interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public boolean addAll(Collection<? extends E> c);
}
Trong các phương thức containsAll() và addAll() được thể hiện trong đoạn mã trên, tham số kiểu T được sử dụng chỉ một lần. Đối với các hàm tạo, các tham số kiểu không được khai báo trong hàm tạo mà được khai báo trong phần khai báo của lớp. Các tham số kiểu thực tế được truyền vào khi gọi hàm tạo.
Đoạn mã dưới mô tả cách khai báo một lớp tổng quát chứa một hàm khởi tạo tổng quát:
public class StudPair<T, U> {
private T name;
private U rollNumber;
public StudPair(T nameObj, U rollNo) {
this.name = nameObj;
this.rollNumber = rollNo;
}
public T displayName() {
return name;
}
public U displayNumber() {
return rollNumber;
}
public static void main(String[] args) {
StudPair<String, Integer> studObj = new StudPair<>("John", 2);
System.out.println("Student Name: " + studObj.displayName());
System.out.print("Student Number: " + studObj.displayNumber());
}
}
Output:
Student Name: John
Student Number: 2
Nhận tham số tổng quát (Generic parameters)
Một lời khai báo phương thức tổng quát có thể được gọi với đối số có các kiểu khác nhau. Dựa trên các kiểu của các đối số được truyền, trình biên dịch xử lý mỗi cuộc gọi phương thức. Các phương thức tổng quát có thể được định nghĩa dựa trên các quy tắc sau:
- Mỗi phần tham số kiểu bao gồm một hoặc nhiều tham số kiểu được phân tách bằng dấu phẩy. Một tham số kiểu là một tên định danh chỉ định một tên kiểu tổng quát.
- Tất cả các lời khai báo phương thức tổng quát có một phần tham số kiểu được bao quanh bởi dấu ngoặc nhọn đứng trước kiểu trả về của phương thức.
- Thân của phương thức tổng quát nên bao gồm các tham số kiểu chỉ đại diện cho kiểu tham chiếu (reference types).
- Phương thức tổng quát có thể được khai báo trong một lời khai báo phương thức khác.
- Các tham số kiểu có thể được sử dụng để khai báo kiểu trả về. Chúng hoạt động như giữ chỗ cho các kiểu của các đối số được truyền vào phương thức tổng quát. Những đối số này được gọi là đối số kiểu thực tế.
Ví dụ:
public class GenericConceptReturn {
// Generic method named displayArray
public static <E> void displayArray(E[] acceptedArray) {
// Display array elements
for (E element : acceptedArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
public static void main(String[] args) {
// Create arrays of Integer, Double, and Character
Integer[] intArrayObj = {100, 200, 300, 400, 500};
Double[] doubleArrayObj = {51.1, 52.2, 53.3, 54.4};
Character[] charArrayObj = {'J', 'A', 'V', 'A'};
System.out.print("Integer Array contains: ");
displayArray(intArrayObj);
System.out.print("\nDouble Array contains: ");
displayArray(doubleArrayObj);
System.out.print("Character Array contains: ");
displayArray(charArrayObj);
}
}
Output:
Integer Array contains: 100 200 300 400 500
Double Array contains: 51.1 52.2 53.3 54.4
Character Array contains: J A V A
Trả về kiểu Generic
Phương thức cũng có thể trả về kiểu tổng quát.
Ví dụ:
public class GenericReturnTest {
public static <T extends Comparable<T>> T maxValueDisplay(T val1, T val2, T val3) {
T maxValue = val1;
if (val2.compareTo(maxValue) > 0) {
maxValue = val2;
}
if (val3.compareTo(maxValue) > 0) {
maxValue = val3;
}
return maxValue;
}
public static void main(String[] args) {
System.out.println(maxValueDisplay(23, 42, 1));
System.out.println(maxValueDisplay("apples", "oranges", "pineapple"));
}
}
Output:
42
pineapple
Suy luận kiểu (Type Inference)
Trong các phương thức tổng quát, việc suy luận kiểu giúp gọi một phương thức tổng quát. Ở đây, không cần phải chỉ định kiểu giữa dấu ngoặc nhọn.
Suy luận kiểu cho phép trình biên dịch Java xác định các đối số kiểu phù hợp với cuộc gọi. Nó phân tích mỗi phương thức cuộc gọi và khai báo tương ứng để thực hiện điều này. Thuật toán suy luận xác định các điều sau:
- Kiểu của các đối số.
- Kiểu mà kết quả được trả về.
- Kiểu cụ thể nhất hoạt động với tất cả các đối số.
Các đối số kiểu cần thiết để gọi hàm khới tạo của một lớp tổng quát có thể được thay thế bằng một tập hợp trống các tham số kiểu (<>) miễn là trình biên dịch suy luận các đối số kiểu từ ngữ cảnh. Mã nguồn dưới minh họa điều này:
Map<String, List<String>> map = new HashMap<String, List<String>>();
Trong Java SE 7, kiểu có tham số của hàm khởi tạo có thể được thay thế bằng tập hợp trống các tham số kiểu như được hiển thị trong Mã nguồn dưới:
Map<String, List<String>> map = new HashMap<>();
Lưu ý: Tập hợp trống các tham số kiểu quan trọng để sử dụng suy luận kiểu tự động trong quá trình tạo thể hiện của lớp tổng quát.
Khi Mã nguồn dưới được chạy, trình biên dịch sẽ tạo ra cảnh báo “unchecked conversion” (chuyển đổi không kiểm tra).
Map<String, List<String>> myMap = new HashMap(); // cảnh báo unchecked conversion
Trong đoạn mã này, constructor HashMap() tham chiếu đến kiểu nguyên thủy (raw type) của HashMap.
Java SE 7 và các phiên bản sau hỗ trợ suy luận kiểu hạn chế cho việc tạo thể hiện tổng quát. Suy luận kiểu chỉ có thể được sử dụng nếu kiểu có tham số của constructor là rõ ràng từ ngữ cảnh. Mã nguồn dưới minh họa điều này.
public class TestArrayList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
// Following statement should fail since addAll expects
// Collection<? extends String>
// The ArrayList you're trying to add is empty.
List<String> emptyList = new ArrayList<>();
list.addAll(emptyList);
List<? extends String> list2 = new ArrayList<>();
list.addAll(list2);
for (String item : list){
System.out.println(item.toString());
}
}
}
Các hàm khởi tạo Tổng Quát cho Các Lớp Tổng Quát và Không Tổng Quát
Các hàm khởi tạo có thể khai báo riêng các tham số kiểu hình thức của mình trong cả các lớp tổng quát và không tổng quát. Ví dụ dưới minh họa điều này.
class MyClass<T> {
<T> MyClass(T t) {
// todo constructor
}
}
Xét đoạn mã dưới:
MyClass objMyClass = new MyClass<Integer>();
- Câu lệnh tạo một thể hiện của lớp tham số hóa MyClass<Integer>.
- Câu lệnh chỉ định kiểu Integer cho tham số kiểu hình thức X của lớp tổng quát MyClass<X>.
- Hàm khởi tạo của lớp tổng quát chứa một tham số kiểu hình thức.
- Trình biên dịch hiểu kiểu String cho tham số kiểu hình thức T của hàm tạo của lớp tổng quát này. Điều này xảy ra vì tham số kiểu thực tế của hàm khởi tạo là một đối tượng String.
rTong Java SE 7 trở lên, trình biên dịch hiểu tham số kiểu thực tế của lớp tổng quát được tạo thể hiện với dấu ngoặc nhọn. Trước Java SE 7, trình biên dịch hiểu tham số kiểu thực tế của các hàm khởi tạo tổng quát tương tự như cách trình biên dịch hiểu với các phương thức tổng quát.
Mã nguồn dưới chỉ có hiệu lực cho Java SE 7 và các phiên bản sau. Nó minh họa cách trình biên dịch hoạt động:
MyClass<Integer> myObject = new MyClass<>();
Trong đoạn mã này, trình biên dịch hiểu:
- Kiểu Integer là tham số kiểu hình thức X của lớp tổng quát MyClass<X>.
- Kiểu String là tham số kiểu hình thức T của hàm khởi tạo của lớp tổng quát.
Cải tiến từ Java SE 7 về sau
Dưới đây là những cải tiến trong Java SE 7 và các phiên bản sau:
- Dấu Gạch Dưới trong Ký Hiệu Số: Ký tự gạch dưới (_) có thể được thêm ở bất cứ đâu giữa các chữ số trong một ký hiệu số để phân tách các nhóm chữ số trong ký hiệu số. Điều này cải thiện tính đọc của mã nguồn.
- Câu lệnh switch sử dụng lớp String: Lớp String có thể được sử dụng trong biểu thức của câu lệnh switch.
- Ký Hiệu Nhị Phân: Trong Java SE 7 và các phiên bản cao hơn, các kiểu số nguyên có thể được định nghĩa bằng hệ thống số nhị phân.
Chú ý: Các kiểu số nguyên bao gồm kiểu byte, short, int và long.
Để chỉ định một ký hiệu nhị phân, thêm tiền tố “Ob” hoặc “OB” trước số.
Cảnh báo và Lỗi Trình Biên Dịch Tốt Hơn với Tham Số Hình Thức Không Thể Hiện Lại: Trong Java, một kiểu mà thông tin kiểu của nó hoàn toàn có sẵn tại thời gian chạy được gọi là kiểu có thể hiện (reifiable type). Điều này bao gồm kiểu nguyên thủy, kiểu không tổng quát, kiểu nguyên thủy, và các lời gọi của các dấu hỏi hoang (wildcards) không ràng buộc. Một kiểu không thể hiện (non-reifiable type), ngược lại, là một kiểu mà thông tin của nó đã bị loại bỏ tại thời gian biên dịch bởi việc xóa thông tin kiểu. Một kiểu không thể hiện không có tất cả thông tin của nó có sẵn tại thời gian chạy.
Trình biên dịch trong Java SE 7 trở đi tạo ra cảnh báo tại điểm khai báo của một phương thức hoặc hàm tạo sử dụng varargs với một tham số hình thức varargs không thể hiện (non-reifiable).
Java SE 7 và các phiên bản sau đã tắt cảnh báo này bằng cách sử dụng tùy chọn trình biên dịch -Xlint:varargs và các chú thích @SafeVarargs và @SuppressWarnings ({“unchecked”, “varargs”}).
Suy luận kiểu cho Việc Tạo Thể Hiện Tổng Quát: Các tham số kiểu yêu cầu có thể được thay thế để gọi hàm tạo của một lớp tổng quát bằng một tập hợp rỗng các tham số kiểu, miễn là trình biên dịch suy luận các tham số kiểu từ ngữ cảnh.
Xử lý Nhiều Loại Ngoại Lệ: Một khối catch duy nhất xử lý nhiều loại ngoại lệ. Người dùng có thể xác định cụ thể các loại ngoại lệ trong mệnh đề throws của khai báo phương thức vì trình biên dịch thực hiện phân tích chính xác các ngoại lệ được ném lại.
Câu lệnh try-with-resources: Câu lệnh này khai báo một hoặc nhiều tài nguyên, đó là các đối tượng cần được đóng sau khi chương trình đã hoàn thành công việc với chúng. Bất kỳ đối tượng nào triển khai giao diện java.lang.AutoCloseable hoặc giao diện java.io.Closeable đều có thể được sử dụng làm tài nguyên. Câu lệnh đảm bảo rằng mỗi tài nguyên được đóng vào cuối câu lệnh.
Lưu ý: Các lớp java.io. InputStream, OutputStream, Reader, Writer, java.sql.Connection, Statement và ResultSet có thể triển khai giao diện AutoCloseable và có thể được sử dụng làm tài nguyên trong câu lệnh try-with-resources.
Collection và Generics
Một đối tượng bộ sưu tập (collection object) trong Java là một đối tượng quản lý một nhóm các đối tượng. Giao diện Collection trong Java dựa trên tính tổng quát (generics) cho việc thực thi của nó.
Ví dụ:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericArrayListExample {
public static void main(String[] args) {
List<Integer> partObj = new ArrayList<>(3);
partObj.add(new Integer(1010));
partObj.add(new Integer(2020));
partObj.add(new Integer(3030));
System.out.print("Part Numbers are as follows: ");
Iterator<Integer> value = partObj.iterator();
while (value.hasNext()) {
Integer partNumberObj = value.next();
int partNumber = partNumberObj.intValue();
System.out.print(" " + partNumber);
}
}
}
Output:
Part Numbers are as follows: 1010 2020 3030
Wildcards với Generics
Ký tự đại diện (wildcards) được sử dụng để khai báo các kiểu tham số tổng quát không xác định. Wildcards được sử dụng như đối số cho các thể hiện của các kiểu tổng quát. Wildcards hữu ích trong trường hợp không có hoặc chỉ có một ít thông tin về kiểu tham số của một kiểu tổng quát.
Có 3 loại wildcards được sử dụng trong Generics.
?: Ký tự đại diện ‘?’ đại diện cho một kiểu không xác định trong Generics. Nó chỉ định tập hợp của tất cả các kiểu hoặc bất kỳ kiểu nào. Ví dụ, List có nghĩa là danh sách chứa các đối tượng kiểu không xác định. Wildcard không giới hạn (unbounded wildcard) được sử dụng như đối số cho việc tạo thể hiện của các kiểu tổng quát. Wildcard không giới hạn hữu ích trong các tình huống mà không cần biết gì về kiểu tham số của một kiểu tổng quát.
Ví dụ:
import java.util.List;
public class Paper {
public void draw(Shape shapeObj) {
shapeObj.draw(this);
}
public void displayAll(List<Shape> shapeList) {
for (Shape s : shapeList) {
s.draw(this);
}
}
}
Xem xét một tình huống trong đó lớp Paper
bao gồm một phương thức, displayAll(), được thiết kế để hiển thị tất cả các hình dạng được biểu diễn dưới dạng một danh sách. Nếu chữ ký phương thức của displayAll() được định nghĩa như trong mã nguồn, nó chỉ có thể được gọi trên danh sách kiểu Shape
. Do đó, phương thức không thể được gọi trên một List<Circle>.
Ký tự đại diện trói buộc ? extends Type biểu thị một kiểu không xác định là một kiểu con của lớp trói buộc được chỉ định, được biểu thị bởi thuật ngữ ‘Type.’ Thuật ngữ ‘Type’ chỉ định một giới hạn trên, ngụ ý rằng giá trị của tham số kiểu phải mở rộng từ lớp hoặc triển khai giao diện của lớp trói buộc. Điều này chỉ định một gia đình các kiểu con của kiểu Type. Ví dụ, List<? extends Number> ngụ ý rằng danh sách được cho chứa các đối tượng được xuất phát từ lớp Number. Nói cách khác, ký tự đại diện trói buộc, sử dụng từ khóa extends
, giới hạn phạm vi của các kiểu có thể được gán. Điều này chứng tỏ là ký tự đại diện hữu ích nhất trong nhiều tình huống khác nhau. Khái niệm này được gọi là Upper Bound Wildcard (ký tự đại diện giới hạn trên)
Ví dụ:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
import java.util.ArrayList;
import java.util.List;
public class UpperBoundedWildcardExample {
// Upper Bounded Wildcard: This method prints elements of a list of any type that extends Number.
public static void printNumbers(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.print(number + " ");
}
System.out.println();
}
public static void main(String[] args) {
// Create a list of integers and doubles, and print its elements.
List<Integer> integerList = new ArrayList<>(List.of(1, 2, 3));
List<Double> doubleList = new ArrayList<>(List.of(1.5, 2.5, 3.5));
System.out.println("Printing Integer List:");
printNumbers(integerList);
System.out.println("Printing Double List:");
printNumbers(doubleList);
}
}
Ký tự đại diện trói buộc ? super Type biểu thị một kiểu không xác định là một kiểu cha của lớp trói buộc. Thuật ngữ ‘Type’ chỉ định giới hạn dưới. Ký tự đại diện này chỉ ra một gia đình các kiểu cha của kiểu Type. Do đó, List<? super Number> ngụ ý rằng nó có thể là List<Number> hoặc List<Object>. Khái niệm này còn được gọi là (Lower Bound Wildcard (ký tự đại diện giới hạn dưới)
Ví dụ:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
import java.util.ArrayList;
import java.util.List;
public class LowerBoundedWildcardExample {
// Lower Bounded Wildcard: This method adds an Integer to a list of any type that is a supertype of Integer.
public static void addInteger(List<? super Integer> list) {
System.out.println(list.toString());
list.add(42);
}
public static void main(String[] args) {
// Create a list of objects and add an Integer to it.
List<Object> objectList = new ArrayList<>(List.of("hello", 3.14, true));
System.out.println("Original Object List:");
for (Object element : objectList) {
System.out.print(element + " ");
}
System.out.println();
// Add an Integer to the list using the addInteger method.
addInteger(objectList);
// Print the modified list.
System.out.println("Modified Object List:");
for (Object element : objectList) {
System.out.print(element + " ");
}
}
}
Ví dụ kết hợp:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
import java.util.ArrayList;
import java.util.List;
public class WildcardExample {
// Upper Bounded Wildcard: This method prints elements of a list of any type that extends Number.
public static void printNumbers(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.print(number + " ");
}
System.out.println();
}
// Lower Bounded Wildcard: This method adds an Integer to a list of any type that is a supertype of Integer.
public static void addInteger(List<? super Integer> list) {
list.add(42);
}
public static void main(String[] args) {
// Unbounded Wildcard: Create a list of unknown type and print its elements.
List<?> unknownList = new ArrayList<>(List.of(1, "two", 3.0)) ;
for (Object element : unknownList) {
System.out.print(element + " ");
}
System.out.println();
// Upper Bounded Wildcard: Create a list of integers and doubles, and print its elements.
List<Number> numberList = new ArrayList<>(List.of(1, 2.5, 3)) ;
printNumbers(numberList);
// Lower Bounded Wildcard: Create a list of objects and add an Integer to it.
List<Object> objectList = new ArrayList<>(List.of("hello", 42.0, true)) ;
addInteger(objectList);
// Print the modified list.
System.out.println("Modified Object List:");
for (Object element : objectList) {
System.out.print(element + " ");
}
}
}
Xử lý Ngoại lệ với Generics
Ngoại lệ cung cấp một cơ chế đáng tin cậy để xác định và phản ứng với điều kiện lỗi. Mệnh đề catch đi kèm với một câu lệnh try kiểm tra xem ngoại lệ được ném có khớp với loại đã cho hay không. Một trình biên dịch không thể đảm bảo rằng các tham số kiểu được chỉ định trong mệnh đề catch khớp với ngoại lệ không rõ nguồn gốc khi một ngoại lệ được ném và bắt tại thời điểm chạy. Do đó, mệnh đề catch không thể bao gồm biến kiểu hoặc ký tự đại diện. Một lớp con của lớp Throwable không thể được làm cho là kiểu tổng quát vì không thể bắt một ngoại lệ thời gian chạy với các tham số thời gian biên dịch vẫn nguyên vẹn. Trong Generics, biến kiểu có thể được sử dụng trong mệnh đề throws của chữ ký phương thức.
Hình dưới mô tả xử lý ngoại lệ với Generics.
Ví dụ:
Tạo giao diện Command
interface Command<X extends Throwable> {
void calculate(Integer arg) throws X;
}
Tạo lớp ExTest
public class ExTest implements Command<ArithmeticException> {
public void calculate(Integer num) throws ArithmeticException {
int no = num.intValue();
System.out.println("Value is: " + (no / 0));
}
}
Đoạn mã mô tả cách sử dụng tính chất tổng quát trong xử lý ngoại lệ. Mã sử dụng biến kiểu trong mệnh đề throws của chữ ký phương thức. Trong quá trình triển khai, tham số kiểu X được thay thế bằng ArithmeticException, cho biết rằng mã có thể ném một ngoại lệ toán học.
Kế thừa với Generics
Kế thừa là một cơ chế để tạo ra các lớp hoặc giao diện mới từ những cái đã tồn tại. Lập trình hướng đối tượng cho phép các lớp kế thừa trạng thái và hành vi thường được sử dụng từ các lớp khác. Các lớp có thể mở rộng từ các lớp tổng quát và cung cấp giá trị cho tham số kiểu hoặc thêm các tham số kiểu mới. Một lớp không thể kế thừa từ kiểu tham số. Hai sự thể hiện của cùng một kiểu tổng quát không thể được sử dụng trong kế thừa.
Tạo lớp Month
class Month<T> {
T monthObj;
Month(T obj) {
monthObj = obj;
}
T getObj() {
return monthObj;
}
}
Tạo lớp MonthArray
// Subclass of Month that defines a second type parameter, called V.
class MonthArray<T, V> extends Month<T> {
V valObj;
MonthArray(T obj, V obj2) {
super(obj);
valObj = obj2;
}
V getObj2() {
return valObj;
}
}
Tạo lớp GenericTest
// Test class for MonthArray
public class GenericTest {
public static void main(String[] args) {
MonthArray<String, Integer> month = new MonthArray<>("Value is:", 99);
System.out.print(month.getObj());
System.out.println(month.getObj2());
}
}
Tương thích ngược với Generics
Một đoạn mã hiện có có thể được sửa đổi để sử dụng Generics mà không cần thực hiện tất cả các thay đổi một lần. Thiết kế của Generics đảm bảo rằng thư viện Java mới vẫn có thể được sử dụng để biên dịch mã cũ(cổ điển) – (legacy). Nói cách khác, cùng một đoạn mã sẽ hoạt động với cả phiên bản cũ (legacy) và phiên bản tổng quát của thư viện. Điều này được biết đến là tương thích nền tảng. Tính tương thích này từ trên xuống cung cấp cho người lập trình tự do chuyển đổi từ phiên bản cũ (legacy) sang phiên bản tổng quát khi cần thiết.
Trong Java, tính tổng quát đảm bảo rằng cùng một tệp lớp được tạo ra bởi cả hai phiên bản cũ (legacy) và tổng quát với một số thông tin bổ sung về kiểu. Điều này được biết đến là tương thích nhị phân vì tệp lớp cũ có thể được thay thế bằng tệp lớp tổng quát mà không cần biên dịch lại.
Hình dưới mô tả sự chuyển đổi giữa mã cũ (legacy) và mã mới.
Mô tả ví dụ mã phiên bản cũ (Legacy Code với Legacy Client)
Tạo file NumStack:
interface NumStack {
boolean empty();
void push(Object elt);
Object retrieve();
}
Tạo file NumArrayStack:
import java.util.ArrayList;
import java.util.List;
class NumArrayStack implements NumStack {
private List<Object> listObj;
public NumArrayStack() {
listObj = new ArrayList<>();
}
@Override
public boolean empty() {
return listObj.size() == 0;
}
@Override
public void push(Object obj) {
listObj.add(obj);
}
@Override
public Object retrieve() {
Object value = listObj.remove(listObj.size() - 1);
return value;
}
@Override
public String toString() {
return "stack" + listObj.toString();
}
}
Tạo lớp Client
public class Client {
public static void main(String[] args) {
NumStack stackObj = new NumArrayStack();
for (int ctr = 0; ctr < 4; ctr++) {
stackObj.push(new Integer(ctr));
}
assert stackObj.toString().equals("stack[0, 1, 2, 3]");
int top = ((Integer) stackObj.retrieve()).intValue();
System.out.println("Value is: " + top);
}
}
hiển thị việc sử dụng mã cổ điển và một client cổ điển. Trong đoạn mã này, dữ liệu được thêm vào một ngăn xếp và được lấy từ ngăn xếp (mà không sử dụng lớp Stack, vì nó chỉ được giới thiệu từ Java 7 trở đi). Một phiên bản mới của mã này sẽ sử dụng lớp Stack và các tính năng mới khác.
Lưu ý: Việc viết một chương trình với mã hoàn toàn độc lập với nền tảng là khó khăn (không lỗi khi cập nhật). Do đó, khi một nền tảng được nâng cấp, mã nguồn trước đây trở thành mã cổ điển có thể không hoạt động nữa.
Thư viện Generic với Legacy Client
Trong mã tổng quát, các lớp đi kèm với một tham số kiểu. Khi một loại tổng quát như collection được sử dụng mà không có tham số kiểu, nó được gọi là kiểu nguyên thủy. Trong ví dụ trước, tham số hóa NumStack<E> tương ứng với kiểu nguyên thủy NumStack và tham số hóa NumArrayStack<E> tương ứng với kiểu nguyên thủy NumArrayStack. Một giá trị của kiểu tham số hóa có thể được truyền cho một kiểu nguyên thủy vì kiểu tham số hóa là một loại con của kiểu nguyên thủy. Java tạo ra cảnh báo chuyển đổi không kiểm tra khi một giá trị của kiểu nguyên thủy được truyền vào nơi một kiểu tham số hóa được mong đợi. Trong cùng ví dụ, một giá trị của kiểu tlumstack<E> có thể được gán cho một biến của kiểu Numstack. Tuy nhiên, khi thực hiện ngược lại, một cảnh báo chuyển đổi không kiểm tra sẽ được hiển thị.
Hình dưới mô tả thư viện tổng quát với client cổ điển.
Khi mã được biên dịch, cảnh báo chuyển đổi không được kiểm tra sẽ hiển thị như sau:
Client.java uses unchecked or unsafe operation
Recompile with -Xlint:unchecked for details.
Nếu mã được biên dịch bằng cách sử dụng switch (chuyển đổi) như đề xuất, thì thông báo sau sẽ xuất hiện:
Client.java:21: warning: [unchecked] unchecked call to add
the raw type java.util.List
as a member of
ListObj.add(obj);
1 warning
Tắt warning bằng cách:
javac -source 1.4 -Xlint:-options Client.java
Xóa bỏ (Erasure)
Khi bạn chèn một số nguyên vào một danh sách và cố gắng trích xuất một chuỗi, điều này là sai. Nếu bạn trích xuất một phần tử từ danh sách và cố gắng xử lý nó như một chuỗi bằng cách ép kiểu, bạn sẽ nhận được ClassCastException
. Lý do là Generics được thực hiện bởi trình biên dịch Java dưới dạng một quá trình chuyển đổi trước gọi là xóa bỏ. Xóa bỏ loại bỏ mọi thông tin kiểu chung. Tất cả thông tin kiểu giữa dấu ngoặc nhọn được loại bỏ, vì vậy một kiểu tham số như List<String>
được chuyển đổi thành List
. Quá trình xóa bỏ giữ tính tương thích với thư viện và ứng dụng Java được tạo ra trước generics.
Hình dưới mô tả quá trình xóa bỏ.
Generics trong Legacy Code
Đôi khi có thể cần cập nhật thư viện không ngay lập tức mà qua một khoảng thời gian. Trong những trường hợp như vậy, các chữ ký phương thức sẽ thay đổi và sẽ bao gồm các tham số kiểu. Thân phương thức sẽ không thay đổi. Sự thay đổi này trong chữ ký phương thức có thể được thực hiện bằng cách thực hiện số lượng thay đổi tối thiểu trong phương thức, hoặc bằng cách tạo ra một “stub” hoặc bằng cách sử dụng bao bọc. Những thay đổi tối thiểu cần phải được tích hợp như sau:
- Thêm tham số kiểu vào các khai báo lớp hoặc giao diện.
- Thêm tham số kiểu vào lớp hoặc giao diện đã được mở rộng hoặc triển khai.
- Thêm tham số kiểu vào chữ ký phương thức.
- Thêm lệnh ép kiểu khi kiểu trả về chứa một tham số kiểu.
import java.util.*;
interface NumStack<E> {
boolean empty();
void push(E elt);
E retrieve();
}
@SuppressWarnings("unchecked")
class NumArrayStack<E> implements NumStack<E> {
private List<E> listObj;
public NumArrayStack() {
listObj = new ArrayList<>();
}
@Override
public boolean empty() {
return listObj.size() == 0;
}
@Override
public void push(E obj) {
listObj.add(obj);
}
@Override
public E retrieve() {
E value = listObj.remove(listObj.size() - 1);
return value;
}
@Override
public String toString() {
return "stack" + listObj.toString();
}
}
public class ClientLegacy {
public static void main(String[] args) {
NumStack<Integer> stackObj = new NumArrayStack<>();
for (int ctr = 0; ctr < 4; ctr++) {
stackObj.push(ctr);
}
assert stackObj.toString().equals("stack[0, 1, 2, 3]");
int top = stackObj.retrieve();
System.out.println("Value is: " + top);
System.out.println("Stack contains: " + stackObj.toString());
}
}
Đoạn mã trên sẽ không cảnh báo unchecked khi biên dịch
Sử dụng Generic trong Legacy code
Thư viện tổng quát nên được tạo ra khi có truy cập vào mã nguồn. Cập nhật toàn bộ mã nguồn thư viện cũng như mã nguồn client code để loại bỏ các cảnh báo kiểm tra không kiểm soát có thể xuất hiện.
Ví dụ:
import java.util.*;
interface NumStack<E> {
void push(E elt);
E retrieve();
}
class NumArrayStack<E> implements NumStack<E> {
private List<E> listObj;
public NumArrayStack() {
listObj = new ArrayList<>();
}
public void push(E obj) {
listObj.add(obj);
}
public E retrieve() {
E value = listObj.remove(listObj.size() - 1);
return value;
}
public String toString() {
return "stack" + listObj.toString();
}
}
public class GenericClient {
public static void main(String[] args) {
NumStack<Integer> stackObj = new NumArrayStack<>();
for (int ctr = 0; ctr < 4; ctr++) {
stackObj.push(ctr);
}
int top = stackObj.retrieve();
System.out.println("Value is: " + top);
System.out.println("Stack contains: " + stackObj.toString());
assert stackObj.toString().equals("stack[0, 1, 2, 3]");
}
}
Interface và lớp triển khai sử dụng tham số kiểu. Tham số kiểu <E> thay thế cho kiểu đối tượng trong chữ ký và thân phương thức của push() và retrieve(). Tham số kiểu phù hợp được thêm vào client code.
Bài tập
1. Write a generic method to count the number of elements in a collection that have a specific property (for example, odd integers, prime numbers, palindromes).
2. Will the following class compile? If not, why?
public final class Algorithm { public static <T> T max(T x, T y) { return x > y ? x : y; } }
3. Write a generic method to exchange the positions of two different elements in an array.
4. If the compiler erases all type parameters at compile time, why should you use generics?
5. What is the following class converted to after type erasure?
public class Pair<K, V> { public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey(); { return key; } public V getValue(); { return value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } private K key; private V value; }
6. What is the following method converted to after type erasure?
public static <T extends Comparable<T>> int findFirstGreaterThan(T[] at, T elem) { // ... }
7. Will the following method compile? If not, why?
public static void print(List<? extends Number> list) { for (Number n : list) System.out.print(n + " "); System.out.println(); }
8. Write a generic method to find the maximal element in the range [begin, end) of a list.
9. Will the following class compile? If not, why?
public class Singleton<T> { public static T getInstance() { if (instance == null) instance = new Singleton<T>(); return instance; } private static T instance = null; }
10. Given the following classes:
class Shape { /* ... */ } class Circle extends Shape { /* ... */ } class Rectangle extends Shape { /* ... */ } class Node<T> { /* ... */ }
Will the following code compile? If not, why?
Node<Circle> nc = new Node<>(); Node<Shape> ns = nc;
11. Consider this class:
class Node<T> implements Comparable<T> { public int compareTo(T obj) { /* ... */ } // ... }
Will the following code compile? If not, why?
Node<String> node = new Node<>(); Comparable<String> comp = node;
12. How do you invoke the following method to find the first integer in a list that is relatively prime to a list of specified integers?
public static <T> int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
Note that two integers a and b are relatively prime if gcd(a, b) = 1, where gcd is short for greatest common divisor.