JavaRush /Blog Java /Random-VI /Lý thuyết về generics trong Java hay cách đặt dấu ngoặc đ...
Viacheslav
Mức độ

Lý thuyết về generics trong Java hay cách đặt dấu ngoặc đơ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 generic đã được thêm vào kho ngôn ngữ Java.
Lý thuyết generics trong Java hay cách đặt dấu ngoặc đơn trong thực tế - 1

Generics trong Java là gì?

Generics (chung) là các phương tiện đặc biệt của ngôn ngữ Java để triển khai lập trình tổng quát: một cách tiếp cận đặc biệt để mô tả dữ liệu và thuật toán cho phép bạn làm việc với các loại dữ liệu khác nhau mà không thay đổi mô tả của chúng. Trên trang web của Oracle, một hướng dẫn riêng dành riêng cho thuốc generic: “ Bài học: Generics ”.

Đầu tiên, để hiểu thuốc generic, bạn cần hiểu tại sao chúng lại cần thiết và chúng cung cấp những gì. Trong phần hướng dẫn ở phần " Tại sao nên sử dụng Generics ?" Người ta nói rằng một trong những mục đích là kiểm tra kiểu thời gian biên dịch mạnh mẽ hơn và loại bỏ nhu cầu truyền rõ ràng.
Lý thuyết generics trong Java hay cách đặt dấu ngoặc đơn trong thực tế - 2
Hãy chuẩn bị trình biên dịch java trực tuyến tutorialspoint yêu thích của chúng tôi để thử nghiệm . Hãy tưởng tượng đoạn mã này:
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 tốt. Nhưng điều gì sẽ xảy ra nếu họ đến gặp chúng tôi và nói rằng cụm từ “Xin chào thế giới!” bị đánh và bạn chỉ có thể quay lại Xin chào? Hãy loại bỏ phần nối bằng chuỗi khỏi mã ", world!". Có vẻ như điều gì có thể vô hại hơn? Nhưng trên thực tế, chúng ta sẽ gặp lỗi TRONG KHI BIÊN DẪN : error: incompatible types: Object cannot be converted to String Vấn đề là trong trường hợp của chúng ta, List lưu trữ một danh sách các đối tượng thuộc loại Object. Vì String là hậu duệ của Object (vì tất cả các lớp đều được kế thừa ngầm từ Object trong Java), nên nó yêu cầu một kiểu truyền rõ ràng, điều mà chúng tôi đã không làm. Và khi nối, phương thức tĩnh String.valueOf(obj) sẽ được gọi trên đối tượng, cuối cùng phương thức này sẽ gọi phương thức toString trên Đối tượng. Tức là Danh sách của chúng tôi chứa Object. Hóa ra là khi chúng ta cần một loại cụ thể chứ không phải Object, chúng ta sẽ phải tự thực hiện việc truyền kiểu:
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 chấp nhận một danh sách các đối tượng, nó không chỉ lưu trữ Chuỗi mà còn cả Số nguyên. Nhưng điều tệ nhất là trong trường hợp này trình biên dịch sẽ không thấy có gì sai. Và ở đây chúng ta sẽ nhận được lỗi TRONG KHI THỰC HIỆN (họ cũng nói rằng đã nhận được lỗi “at Runtime”). Lỗi sẽ là: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String Đồng ý, không dễ chịu nhất. Và tất cả điều này là do trình biên dịch không phải là trí tuệ nhân tạo và nó không thể đoán được mọi thứ mà người lập trình muốn nói. Để cho trình biên dịch biết thêm về những loại chúng ta sẽ sử dụng, Java SE 5 đã giới thiệu các generics . Hãy sửa phiên bản của chúng tôi bằng cách 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ư chúng ta có thể thấy, chúng ta không cần chuyển sang String nữa. Ngoài ra, hiện nay chúng ta có các dấu ngoặc nhọn tạo khung cho các tổng quát. Bây giờ trình biên dịch sẽ không cho phép lớp được biên dịch cho đến khi chúng ta loại bỏ phần bổ sung 123 vào danh sách, bởi vì đây là số nguyên. Anh ấy sẽ nói với chúng tôi như vậy. Nhiều người gọi thuốc generic là "cú pháp đường". Và họ đã đúng, vì các generics thực sự sẽ trở thành các đẳng cấp tương tự khi được biên dịch. Chúng ta hãy xem mã byte của các lớp đã biên dịch: với việc truyền thủ công và sử dụng các tổng quát:
Lý thuyết generics trong Java hay cách đặt dấu ngoặc đơn trong thực tế - 3
Sau khi biên soạn, mọi thông tin về thuốc generic sẽ bị xóa. Đây được gọi là "Loại xóa" hoặc " Loại xóa ". Xóa kiểu và khái quát được thiết kế để cung cấp khả năng tương thích ngược với các phiên bản cũ hơn của JDK, đồng thời vẫn cho phép trình biên dịch hỗ trợ suy luận kiểu trong các phiên bản Java mới hơn.
Lý thuyết generics trong Java hay cách đặt dấu ngoặc đơn trong thực tế - 4

Loại thô hoặc loại thô

Khi nói về generic, chúng ta luôn có 2 loại: typed type (GenericType) và type “raw” (Raw Type). Loại thô là loại không xác định “trình độ chuyên môn” trong ngoặc nhọn:
Lý thuyết generics trong Java hay cách đặt dấu ngoặc đơn trong thực tế - 5
Các loại được gõ thì ngược lại, có dấu hiệu "làm rõ":
Lý thuyết generics trong Java hay cách đặt dấu ngoặc đơn trong thực tế - 6
Như chúng ta có thể thấy, chúng tôi đã sử dụng một thiết kế khác thường, được đánh dấu bằng mũi tên trong ảnh chụp màn hình. Đây là một cú pháp đặc biệt đã được thêm vào trong Java SE 7 và nó được gọi là " kim cương ", có nghĩa là kim cương. Tại sao? Bạn có thể rút ra sự tương đồng giữa hình dạng của một viên kim cương và hình dạng của dấu ngoặc nhọn: <> Cú pháp hình kim cương cũng gắn liền với khái niệm " Type Inference ", hay suy luận kiểu. Rốt cuộc, trình biên dịch, nhìn thấy <> ở bên phải, sẽ nhìn vào phía bên trái, nơi chứa phần khai báo loại biến mà giá trị được gán. Và từ phần này anh ấy hiểu giá trị bên phải được gõ là loại gì. Trong thực tế, nếu một generic được chỉ định ở bên trái và không được chỉ định ở bên phải, trình biên dịch sẽ có thể suy ra kiểu:
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);
	}
}
Tuy nhiên, đây sẽ là sự kết hợp giữa 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 cực kỳ không mong muốn. Khi biên dịch đoạn code trên chúng ta sẽ nhận được thông báo: Note: HelloWorld.java uses unchecked or unsafe operations. Trên thực tế, có vẻ như không rõ tại sao chúng ta lại cần thêm kim cương vào đây. 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);
	}
}
Như chúng ta đã nhớ, ArrayList cũng có hàm tạo thứ hai lấy một tập hợp làm đầu vào. Và đây chính là nơi mà sự lừa dối nằm. Nếu không có cú pháp kim cương, trình biên dịch sẽ không hiểu rằng nó đang bị lừa, nhưng với cú pháp kim cương thì có. Do đó, quy tắc số 1 : luôn sử dụng cú pháp kim cương nếu chúng ta sử dụng kiểu gõ. Nếu không, chúng tôi có nguy cơ bỏ lỡ nơi chúng tôi sử dụng loại thô. Để tránh các cảnh báo trong nhật ký “sử dụng các thao tác không được kiểm tra hoặc không an toàn”, bạn có thể chỉ định một chú thích đặc biệt về phương thức hoặc lớp đang được sử dụng: @SuppressWarnings("unchecked") Ngăn chặn được dịch là ngăn chặn, nghĩa đen là ngăn chặn các cảnh báo. Nhưng hãy nghĩ xem tại sao bạn lại quyết định chỉ ra điều đó? Hãy nhớ quy tắc số một và có thể bạn cần thêm cách gõ.
Lý thuyết generics trong Java hay cách đặt dấu ngoặc đơn trong thực tế - 7

Phương pháp chung

Generics cho phép bạn gõ các phương thức. Có một phần riêng dành riêng cho tính năng này trong hướng dẫn của Oracle: “ Các phương thức chung ”. Từ hướng dẫn này, điều quan trọng là phải nhớ cú pháp:
  • bao gồm danh sách các tham số được nhập bên trong dấu ngoặc nhọn;
  • danh sách các tham số đã nhập nằm trước phương thức trả về.
Hãy xem 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, chúng ta sẽ thấy hai phương thức được gõ trong đó. Với suy luận kiểu, chúng ta có thể cung cấp định nghĩa kiểu trực tiếp cho trình biên dịch hoặc chúng ta có thể tự xác định nó. Cả hai tùy chọn đều được trình bày trong ví dụ. Nhân tiện, cú pháp khá logic nếu bạn nghĩ về nó. Khi gõ một phương thức, chúng ta chỉ định kiểu chung TRƯỚC phương thức đó vì nếu chúng ta sử dụng kiểu chung sau phương thức, Java sẽ không thể tìm ra loại nào sẽ sử dụng. Do đó, trước tiên chúng tôi thông báo rằng chúng tôi sẽ sử dụng tên chung T và sau đó chúng tôi nói rằng chúng tôi sẽ trả lại tên chung này. Đương nhiên là Util.<Integer>getValue(element, String.class)nó sẽ bị lỗi incompatible types: Class<String> cannot be converted to Class<Integer>. Khi sử dụng các phương pháp gõ, bạn phải luôn nhớ về việc xóa kiểu. Hãy xem 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);
		}
    }
}
Nó sẽ hoạt động rất tốt. Nhưng chỉ khi trình biên dịch hiểu rằng phương thức được gọi có kiểu Số nguyên. Hãy thay thế đầu ra của bàn điều khiển bằng dòng sau: System.out.println(Util.getValue(element) + 1); Và chúng ta gặp lỗi: các loại toán hạng xấu cho toán tử nhị phân '+', loại đầu tiên: Object , loại thứ hai: int Tức là các loại đã bị xóa. Trình biên dịch thấy rằng không có ai chỉ định loại, loại được chỉ định là Đối tượng và việc thực thi mã không thành công và có lỗi.
Теория дженериков в Java or How на практике ставить скобки - 8

Các loại chung

Bạn có thể gõ không chỉ các phương thức mà còn cả các lớp. Oracle có phần “ Các loại chung ” dành riêng cho vấn đề này trong hướng dẫn của họ. Hãy xem 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 một lớp, tên chung sẽ được liệt kê sau tên lớp. Bây giờ chúng ta 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);
}
Nó sẽ hoạt động tốt. Trình biên dịch thấy rằng có Danh sách các số và Bộ sưu tập kiểu Chuỗi. Nhưng điều gì sẽ xảy ra nếu chúng ta xóa các generics và làm điều này:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Chúng ta sẽ gặp lỗi: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer Gõ erasure lần nữa. Vì lớp không còn có tên chung nên trình biên dịch quyết định rằng vì chúng ta đã chuyển một Danh sách nên một phương thức có List<Integer> sẽ phù hợp hơn. Và chúng ta mắc sai lầm. Do đó, quy tắc số 2: Nếu một lớp được nhập, hãy luôn chỉ định loại đó trong tệp .

Những hạn chế

Chúng ta có thể áp dụng hạn chế đối với các loại được chỉ định trong generics. Ví dụ: chúng tôi muốn vùng chứa chỉ chấp nhận Số làm đầu vào. Tính năng này được mô tả trong Hướng dẫn Oracle ở phần Tham số loại giới hạn . Hãy xem 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 loại chung là lớp/giao diện Số và các lớp con của nó. Thật thú vị, bạn có thể chỉ định không chỉ một lớp mà còn cả các giao diện. Ví dụ: public static class NumberContainer<T extends Number & Comparable> { Generics cũng có khái niệm Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html Chúng lần lượt được chia thành ba loại: Cái gọi là nguyên tắc Get Put áp dụng cho Ký tự đại diện . Chúng có thể được thể hiện dưới dạng sau:
Теория дженериков в Java or How на практике ставить скобки - 9
Nguyên tắc này còn được gọi là nguyên tắc PECS (Nhà sản xuất mở rộng siêu tiêu dùng). Bạn có thể đọc thêm về Habré trong bài viết “ Sử dụng ký tự đại diện chung để cải thiện khả năng sử dụng của API Java ”, cũng như trong cuộc thảo luận tuyệt vời về stackoverflow: “ Sử dụng ký tự đại diện trong Generics Java ”. Đây là một ví dụ nhỏ từ nguồn Java - phương thức Collections.copy:
Теория дженериков в Java or How на практике ставить скобки - 10
Chà, một ví dụ nhỏ về cách nó sẽ 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ế kéo dài bằng siêu, mọi thứ sẽ ổn. Vì chúng tôi điền vào danh sách một giá trị trước khi xuất nó, nên nó là một người tiêu dùng đối với chúng tôi, tức là một người tiêu dùng. Vì vậy, chúng tôi sử dụng super.

Di sản

Có một đặc điểm khác thường của thuốc generic - tính kế thừa của chúng. Tính kế thừa của generics được mô tả trong hướng dẫn của Oracle trong phần " Generics, Inheritance và Subtypes ". Điều chính là phải nhớ và nhận ra những điều sau đây. Chúng tôi 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:
Теория дженериков в Java or How на практике ставить скобки - 11
Và đây là một ví dụ điển hình khác sẽ thất bại do có lỗi:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Mọi thứ ở đây cũng đơn giản. 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.

Cuối cùng

Vì vậy, chúng tôi đã làm mới trí nhớ của mình về thuốc generic. Nếu chúng hiếm khi được sử dụng hết công suất, một số chi tiết sẽ bị xóa khỏi bộ nhớ. Tôi hy vọng bài đánh giá ngắn này sẽ giúp làm mới trí nhớ của bạn. Và để có kết quả tốt hơn, tôi thực sự khuyên bạn nên đọc các tài liệu sau: #Viacheslav
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION