CodeGym/Blog Java/Ngẫu nhiên/Java Generics: cách sử dụng dấu ngoặc nhọn trong thực tế

Java Generics: cách sử dụng dấu ngoặc nhọn trong thực tế

Xuất bản trong nhóm

Giới thiệu

Bắt đầu với JSE 5.0, các khái quát đã được thêm vào kho vũ khí của ngôn ngữ Java.

Thuốc generic trong java là gì?

Generics là cơ chế đặc biệt của Java để triển khai lập trình chung — một cách để mô tả dữ liệu và thuật toán cho phép bạn làm việc với các kiểu dữ liệu khác nhau mà không thay đổi mô tả của thuật toán. Trang web của Oracle có một hướng dẫn riêng dành cho thuốc generic: " Bài học ". Để hiểu thuốc generic, trước tiên bạn cần tìm ra lý do tại sao chúng cần thiết và những gì chúng cung cấp. Phần " Tại sao nên sử dụng Generics? " của hướng dẫn nói rằng một vài mục đích là kiểm tra kiểu mạnh hơn tại thời điểm biên dịch và loại bỏ nhu cầu về các phép truyền rõ ràng. Generics trong Java: cách sử dụng dấu ngoặc nhọn trong thực tế - 1Hãy chuẩn bị cho một số thử nghiệm trong trình biên dịch java trực tuyến Tutorialspoint yêu thích của chúng tôi . Giả sử bạn có đoạn mã sau:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
Mã này sẽ chạy hoàn toàn tốt. Nhưng điều gì sẽ xảy ra nếu ông chủ đến gặp chúng tôi và nói rằng "Xin chào, thế giới!" là một cụm từ được sử dụng quá mức và bạn chỉ được trả lại "Xin chào"? Chúng tôi sẽ xóa mã nối ", world!" Điều này có vẻ đủ vô hại, phải không? Nhưng chúng tôi thực sự gặp lỗi TẠI THỜI GIAN BIÊN TẬP:
error: incompatible types: Object cannot be converted to String
Vấn đề là trong Danh sách của chúng tôi lưu trữ Đối tượng. String là hậu duệ của Object (vì tất cả các lớp Java đều kế thừa ngầm Object ), điều đó có nghĩa là chúng ta cần một phép ép kiểu rõ ràng, nhưng chúng ta đã không thêm một lớp nào. Trong quá trình nối, phương thức String.valueOf(obj) tĩnh sẽ được gọi bằng cách sử dụng đối tượng. Cuối cùng, nó sẽ gọi phương thức toString của lớp Đối tượng . Nói cách khác, Danh sách của chúng tôi chứa một Đối tượng . Điều này có nghĩa là bất cứ nơi nào chúng ta cần một loại cụ thể (không phải Object ), chúng ta sẽ phải tự mình thực hiện chuyển đổi loại:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
Tuy nhiên, trong trường hợp này, vì Danh sách lấy các đối tượng, nó có thể lưu trữ không chỉ các Chuỗi mà còn cả các Số nguyên . Nhưng điều tồi tệ nhất là trình biên dịch không thấy có gì sai ở đây. Và bây giờ chúng ta sẽ gặp lỗi AT RUN TIME (được gọi là "lỗi thời gian chạy"). Lỗi sẽ là:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Bạn phải đồng ý rằng điều này không tốt lắm. Và tất cả điều này bởi vì trình biên dịch không phải là một trí tuệ nhân tạo có khả năng đoán chính xác ý định của lập trình viên. Java SE 5 đã giới thiệu các khái quát để cho phép chúng tôi nói với trình biên dịch về ý định của chúng tôi - về những loại chúng tôi sẽ sử dụng. Chúng tôi sửa mã của mình bằng cách nói cho trình biên dịch biết những gì chúng tôi muốn:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
Như bạn có thể thấy, chúng ta không còn cần ép kiểu thành String nữa . Ngoài ra, chúng ta có các dấu ngoặc nhọn bao quanh đối số kiểu. Bây giờ trình biên dịch sẽ không cho phép chúng ta biên dịch lớp cho đến khi chúng ta xóa dòng thêm 123 vào danh sách, vì đây là một số nguyên . Và nó sẽ cho chúng ta biết như vậy. Nhiều người gọi thuốc generic là "đường cú pháp". Và họ đúng, vì sau khi tổng quát được biên soạn, chúng thực sự trở thành chuyển đổi cùng loại. Hãy xem mã byte của các lớp đã biên dịch: một lớp sử dụng kiểu diễn đạt rõ ràng và một lớp sử dụng kiểu chung: Generics trong Java: cách sử dụng dấu ngoặc nhọn trong thực tế - 2Sau khi biên dịch, tất cả các kiểu chung sẽ bị xóa. Điều này được gọi là " kiểu xóa". Xóa kiểu và kiểu chung được thiết kế để tương thích ngược với các phiên bản cũ hơn của JDK đồng thời cho phép trình biên dịch trợ giúp với các định nghĩa kiểu trong các phiên bản Java mới.

các loại thô

Nói về thuốc generic, chúng ta luôn có hai loại: loại được tham số hóa và loại thô. Loại thô là loại bỏ qua phần "làm rõ loại" trong dấu ngoặc nhọn: Generics trong Java: cách sử dụng dấu ngoặc nhọn trong thực tế - 3Mặt khác, loại được tham số hóa bao gồm phần "làm rõ": Generics trong Java: cách sử dụng dấu ngoặc nhọn trong thực tế - 4Như bạn có thể thấy, chúng tôi đã sử dụng một cấu trúc khác thường, được đánh dấu bằng một mũi tên trong ảnh chụp màn hình. Đây là cú pháp đặc biệt đã được thêm vào Java SE 7. Nó được gọi là " kim cương ". Tại sao? Các dấu ngoặc nhọn tạo thành một viên kim cương: <> . Bạn cũng nên biết rằng cú pháp kim cương được liên kết với khái niệm " suy luận kiểu ". Rốt cuộc, trình biên dịch, nhìn thấy <>ở bên phải, nhìn vào phía bên trái của toán tử gán, nơi nó tìm thấy loại biến có giá trị đang được gán. Dựa trên những gì nó tìm thấy trong phần này, nó hiểu loại giá trị ở bên phải. Trong thực tế, nếu một loại chung được đưa ra ở bên trái, nhưng không phải ở bên phải, trình biên dịch có thể suy ra loại:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
Nhưng điều này kết hợp phong cách mới với thuốc generic và phong cách cũ không có chúng. Và điều này là rất không mong muốn. Khi biên dịch mã ở trên, chúng tôi nhận được thông báo sau:
Note: HelloWorld.java uses unchecked or unsafe operations
Trên thực tế, lý do tại sao bạn thậm chí cần thêm một viên kim cương ở đây có vẻ khó hiểu. Nhưng đây là một ví dụ:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
Bạn sẽ nhớ lại rằng ArrayList có một hàm tạo thứ hai lấy một tập hợp làm đối số. Và đây là nơi một cái gì đó độc ác ẩn giấu. Không có cú pháp kim cương, trình biên dịch không hiểu rằng nó đang bị đánh lừa. Với cú pháp kim cương, nó làm được. Vì vậy, Quy tắc số 1 là: luôn sử dụng cú pháp kim cương với các loại được tham số hóa. Nếu không, chúng tôi có nguy cơ bỏ lỡ nơi chúng tôi đang sử dụng các loại thô. Để loại bỏ các cảnh báo "sử dụng các thao tác không được kiểm tra hoặc không an toàn", chúng ta có thể sử dụng chú thích @SuppressWarnings("unchecked") trên một phương thức hoặc lớp. Nhưng hãy nghĩ về lý do tại sao bạn quyết định sử dụng nó. Hãy nhớ quy tắc số một. Có thể bạn cần thêm đối số kiểu.

Các phương thức chung của Java

Generics cho phép bạn tạo các phương thức có kiểu tham số và kiểu trả về được tham số hóa. Một phần riêng biệt được dành cho khả năng này trong hướng dẫn của Oracle: " Generic Methods ". Điều quan trọng là phải nhớ cú pháp được dạy trong hướng dẫn này:
  • nó bao gồm một danh sách các tham số kiểu bên trong dấu ngoặc nhọn;
  • danh sách các tham số kiểu đi trước kiểu trả về của phương thức.
Hãy xem xét một ví dụ:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Nếu bạn nhìn vào lớp Util , bạn sẽ thấy rằng nó có hai phương thức chung. Nhờ khả năng suy luận kiểu, chúng ta có thể chỉ định kiểu trực tiếp cho trình biên dịch hoặc chúng ta có thể tự chỉ định kiểu đó. Cả hai tùy chọn được trình bày trong ví dụ. Nhân tiện, cú pháp rất có ý nghĩa nếu bạn nghĩ về nó. Khi khai báo một phương thức chung, chúng ta chỉ định tham số kiểu TRƯỚC phương thức, bởi vì nếu chúng ta khai báo tham số kiểu sau phương thức, JVM sẽ không thể tìm ra kiểu nào sẽ sử dụng. Theo đó, trước tiên chúng tôi khai báo rằng chúng tôi sẽ sử dụng tham số loại T , sau đó chúng tôi nói rằng chúng tôi sẽ trả về loại này. Đương nhiên, Util.<Integer>getValue(element, String.class) sẽ không thành công với một lỗi:các loại không tương thích: Không thể chuyển đổi Class<String> thành Class<Integer> . Khi sử dụng các phương pháp chung, bạn phải luôn nhớ xóa kiểu. Hãy xem xét một ví dụ:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
Điều này sẽ chạy tốt. Nhưng chỉ khi trình biên dịch hiểu rằng kiểu trả về của phương thức được gọi là Integer . Thay thế câu lệnh đầu ra của bàn điều khiển bằng dòng sau:
System.out.println(Util.getValue(element) + 1);
Chúng tôi nhận được một lỗi:
bad operand types for binary operator '+', first type: Object, second type: int.
Nói cách khác, việc xóa kiểu đã xảy ra. Trình biên dịch thấy rằng không có ai chỉ định loại, vì vậy loại được chỉ định là Đối tượng và phương thức không thành công với một lỗi.

lớp học chung

Không chỉ các phương thức có thể được tham số hóa. Các lớp học cũng có thể. Phần "Các loại chung" trong hướng dẫn của Oracle được dành cho việc này. Hãy xem xét một ví dụ:
public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
Mọi thứ đều đơn giản ở đây. Nếu chúng ta sử dụng lớp chung, tham số kiểu được chỉ định sau tên lớp. Bây giờ hãy tạo một thể hiện của lớp này trong phương thức chính :
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Mã này sẽ chạy tốt. Trình biên dịch thấy rằng có một Danh sách các số và một Bộ sưu tập các Chuỗi . Nhưng điều gì sẽ xảy ra nếu chúng ta loại bỏ tham số kiểu và làm điều này:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Chúng tôi nhận được một lỗi:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Một lần nữa, đây là loại tẩy xóa. Vì lớp không còn sử dụng tham số kiểu nên trình biên dịch quyết định rằng, vì chúng ta đã truyền List , nên phương thức có List<Integer> là phù hợp nhất. Và chúng tôi thất bại với một lỗi. Do đó, chúng tôi có Quy tắc #2: Nếu bạn có một lớp chung, hãy luôn chỉ định các tham số loại.

Những hạn chế

Chúng ta có thể hạn chế các kiểu được chỉ định trong các phương thức và lớp chung. Ví dụ: giả sử chúng ta muốn một vùng chứa chỉ chấp nhận một Số làm đối số kiểu. Tính năng này được mô tả trong phần Tham số loại giới hạn trong hướng dẫn của Oracle. Hãy xem xét một ví dụ:
import java.util.*;
public class HelloWorld {

    public static class NumberContainer<T extends Number> {
        private T number;

        public NumberContainer(T number) { this.number = number; }

        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
Như bạn có thể thấy, chúng tôi đã giới hạn tham số loại đối với lớp/giao diện Số hoặc các phần tử con của nó. Lưu ý rằng bạn có thể chỉ định không chỉ một lớp mà cả các giao diện. Ví dụ:
public static class NumberContainer<T extends Number & Comparable> {
Generics cũng hỗ trợ ký tự đại diện. Chúng được chia thành ba loại: Việc sử dụng ký tự đại diện của bạn phải tuân thủ nguyên tắc Get-Put . Nó có thể được thể hiện như sau:
  • Sử dụng ký tự đại diện mở rộng khi bạn chỉ lấy các giá trị từ một cấu trúc.
  • Sử dụng siêu ký tự đại diện khi bạn chỉ đặt các giá trị vào một cấu trúc.
  • Và không sử dụng ký tự đại diện khi cả hai bạn đều muốn nhận và đặt từ/đến một cấu trúc.
Nguyên tắc này còn được gọi là nguyên tắc Nhà sản xuất Mở rộng Hưu bổng Người tiêu dùng (PECS). Đây là một ví dụ nhỏ từ mã nguồn cho phương thức Collections.copy của Java : Generics trong Java: cách sử dụng dấu ngoặc nhọn trong thực tế - 5Và đây là một ví dụ nhỏ về những gì KHÔNG hoạt động:
public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
Nhưng nếu bạn thay thế extends bằng super thì mọi thứ đều ổn. Bởi vì chúng tôi điền vào danh sách một giá trị trước khi hiển thị nội dung của nó, nên nó là một người tiêu dùng . Theo đó, chúng tôi sử dụng super.

Di sản

Generics có một tính năng thú vị khác: kế thừa. Cách kế thừa hoạt động đối với thuốc generic được mô tả trong phần " Generics, Inheritance, and Subtypes " trong hướng dẫn của Oracle. Điều quan trọng là phải ghi nhớ và nhận ra những điều sau đây. Chúng ta không thể làm điều này:
List<CharSequence> list1 = new ArrayList<String>();
Bởi vì tính kế thừa hoạt động khác với thuốc generic: Generics trong Java: cách sử dụng dấu ngoặc nhọn trong thực tế - 6Và đây là một ví dụ điển hình khác sẽ không thành công khi có lỗi:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Một lần nữa, mọi thứ đều đơn giản ở đây. List<String> không phải là hậu duệ của List<Object> , mặc dù String là hậu duệ của Object . Để củng cố những gì bạn đã học, chúng tôi khuyên bạn nên xem một video bài học từ Khóa học Java của chúng tôi
Bình luận
  • Phổ biến
  • Mới
Bạn phải đăng nhập để đăng nhận xet
Trang này chưa có bất kỳ bình luận nào