JavaRush /Blog Java /Random-VI /Thuốc generic dành cho mèo
Viacheslav
Mức độ

Thuốc generic dành cho mèo

Xuất bản trong nhóm
Thuốc generic dành cho mèo - 1

Giới thiệu

Hôm nay là một ngày tuyệt vời để ghi nhớ những gì chúng ta biết về Java. Theo tài liệu quan trọng nhất, tức là. Đặc tả ngôn ngữ Java (JLS - Java Language Specifiaction), Java là một ngôn ngữ được định kiểu mạnh, như được mô tả trong chương " Chương 4. Kiểu, Giá trị và Biến ". Điều đó có nghĩa là gì? Giả sử chúng ta có một phương thức chính:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
Việc gõ mạnh đảm bảo rằng khi mã này được biên dịch, trình biên dịch sẽ kiểm tra xem nếu chúng ta đã chỉ định loại biến văn bản là Chuỗi thì chúng ta sẽ không cố gắng sử dụng nó ở bất kỳ đâu dưới dạng một biến thuộc loại khác (ví dụ: dưới dạng Số nguyên) . Ví dụ: nếu chúng ta cố lưu một giá trị thay vì văn bản 2L(tức là dài thay vì Chuỗi), chúng ta sẽ gặp lỗi khi biên dịch:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
Những thứ kia. Gõ mạnh cho phép bạn đảm bảo rằng các thao tác trên đối tượng chỉ được thực hiện khi các thao tác đó hợp pháp đối với các đối tượng đó. Điều này còn được gọi là loại an toàn. Như đã nêu trong JLS, có hai loại loại trong Java: loại nguyên thủy và loại tham chiếu. Bạn có thể nhớ về các kiểu nguyên thủy từ bài viết đánh giá: “ Các kiểu nguyên thủy trong Java: Chúng không quá nguyên thủy ”. Các kiểu tham chiếu có thể được biểu diễn bằng một lớp, giao diện hoặc mảng. Và hôm nay chúng ta sẽ quan tâm đến các loại tài liệu tham khảo. Và hãy bắt đầu với mảng:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
Mã này chạy không có lỗi. Như chúng ta đã biết (ví dụ: từ " Oracle Java Tutorial: Arrays "), mảng là một vùng chứa chỉ lưu trữ dữ liệu của một loại. Trong trường hợp này - chỉ các dòng. Hãy thử thêm dài vào mảng thay vì Chuỗi:
text[1] = 4L;
Hãy chạy mã này (ví dụ: trong Trình biên dịch Java trực tuyến Repl.it ) và gặp lỗi:
error: incompatible types: long cannot be converted to String
Mảng và độ an toàn về kiểu của ngôn ngữ không cho phép chúng tôi lưu vào một mảng những gì không phù hợp với kiểu. Đây là một biểu hiện của loại an toàn. Chúng tôi được thông báo: “Hãy sửa lỗi, nhưng cho đến lúc đó tôi sẽ không biên dịch mã”. Và điều quan trọng nhất là điều này xảy ra tại thời điểm biên dịch chứ không phải khi chương trình được khởi chạy. Nghĩa là, chúng tôi thấy lỗi ngay lập tức chứ không phải “một ngày nào đó”. Và vì chúng ta đã nhớ về mảng, nên chúng ta cũng hãy nhớ về Khung công tác bộ sưu tập Java . Chúng tôi có những cấu trúc khác nhau ở đó. Ví dụ: danh sách. Hãy viết lại ví dụ:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
Khi biên dịch chúng ta sẽ gặp testlỗi ở dòng khởi tạo biến:
incompatible types: Object cannot be converted to String
Trong trường hợp của chúng tôi, Danh sách có thể lưu trữ bất kỳ đối tượng nào (tức là một đối tượng thuộc loại Đối tượng). Vì vậy, trình biên dịch nói rằng nó không thể đảm nhận gánh nặng trách nhiệm như vậy. Do đó, chúng ta cần chỉ định rõ ràng loại mà chúng ta sẽ nhận được từ danh sách:
String test = (String) text.get(0);
Dấu hiệu này được gọi là chuyển đổi kiểu hoặc ép kiểu. Và mọi thứ sẽ hoạt động tốt cho đến khi chúng tôi cố gắng lấy phần tử ở chỉ mục 1, bởi vì nó thuộc loại Dài. Và chúng tôi sẽ gặp một lỗi khá lớn, nhưng trong khi chương trình đang chạy (trong Thời gian chạy):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
Như chúng ta có thể thấy, có một số nhược điểm quan trọng ở đây. Đầu tiên, chúng ta buộc phải “truyền” giá trị thu được từ danh sách vào lớp String. Đồng ý, điều này thật xấu xí. Thứ hai, trong trường hợp có lỗi, chúng ta chỉ thấy lỗi khi chương trình được thực thi. Nếu mã của chúng tôi phức tạp hơn, chúng tôi có thể không phát hiện ra lỗi đó ngay lập tức. Và các nhà phát triển bắt đầu nghĩ cách làm cho công việc trong những tình huống như vậy trở nên dễ dàng hơn và mã rõ ràng hơn. Và họ đã ra đời - Generics.
Thuốc generic dành cho mèo - 2

Thuốc gốc

Vì vậy, thuốc generic. Nó là gì? Khái quát là một cách đặc biệt để mô tả các kiểu được sử dụng mà trình biên dịch mã có thể sử dụng trong công việc của mình để đảm bảo an toàn cho kiểu. Nó trông giống như thế này:
Thuốc generic dành cho mèo - 3
Đây là một ví dụ ngắn và giải thích:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
Trong ví dụ này, chúng tôi nói rằng chúng tôi không chỉ có List, mà Listcòn CHỈ hoạt động với các đối tượng thuộc loại Chuỗi. Và không có người khác. Những gì chỉ được chỉ định trong ngoặc, chúng ta có thể lưu trữ nó. Những "dấu ngoặc" như vậy được gọi là "dấu ngoặc nhọn", tức là dấu ngoặc nhọn. Trình biên dịch sẽ vui lòng kiểm tra xem chúng tôi có mắc lỗi nào khi làm việc với danh sách các chuỗi hay không (danh sách được đặt tên là văn bản). Trình biên dịch sẽ thấy rằng chúng ta đang cố gắng đưa Long vào danh sách Chuỗi một cách trắng trợn. Và lúc biên dịch nó sẽ báo lỗi:
error: no suitable method found for add(long)
Có thể bạn đã nhớ rằng String là hậu duệ của CharSequence. Và quyết định làm một cái gì đó như:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
Nhưng điều này là không thể và chúng ta sẽ gặp lỗi: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> It có vẻ lạ, bởi vì. dòng này CharSequence sec = "test";không có lỗi. Hãy tìm ra nó. Họ nói về hành vi này: “Generics là bất biến.” "bất biến" là gì? Tôi thích cách nói về điều này trên Wikipedia trong bài viết “ Hiệp phương sai và contravariance ”:
Thuốc generic dành cho mèo - 4
Vì vậy, Tính bất biến là sự vắng mặt của sự kế thừa giữa các kiểu dẫn xuất. Nếu Mèo là kiểu con của Động vật thì Set<Cats> không phải là kiểu con của Set<Animals> và Set<Animals> không phải là kiểu con của Set<Cats>. Nhân tiện, điều đáng nói là bắt đầu từ Java SE 7, cái gọi là “ Toán tử kim cương ” đã xuất hiện. Bởi vì hai dấu ngoặc nhọn <> giống như một viên kim cương. Điều này cho phép chúng tôi sử dụng thuốc generic như thế này:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
Dựa trên mã này, trình biên dịch hiểu rằng nếu chúng ta chỉ ra ở phía bên trái rằng nó Listsẽ chứa các đối tượng kiểu String, thì ở phía bên phải chúng ta muốn nói rằng chúng ta muốn lineslưu một ArrayList mới vào một biến, biến này cũng sẽ lưu trữ một đối tượng thuộc loại được chỉ định ở phía bên trái. Vì vậy, trình biên dịch ở bên trái sẽ hiểu hoặc suy ra kiểu cho bên phải. Đây là lý do tại sao hành vi này được gọi là suy luận kiểu hoặc "Suy luận kiểu" trong tiếng Anh. Một điều thú vị khác đáng chú ý là Loại RAW hay “loại thô”. Bởi vì Generic không phải lúc nào cũng xuất hiện và Java cố gắng duy trì khả năng tương thích ngược bất cứ khi nào có thể, sau đó generic buộc phải bằng cách nào đó làm việc với mã không được chỉ định chung. Hãy xem một ví dụ:
List<CharSequence> lines = new ArrayList<String>();
Như chúng ta nhớ, một dòng như vậy sẽ không được biên dịch do tính bất biến của generics.
List<Object> lines = new ArrayList<String>();
Và cái này cũng sẽ không được biên dịch vì lý do tương tự.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
Những dòng như vậy sẽ biên dịch và hoạt động. Trong đó, các Loại thô được sử dụng, tức là. các loại không xác định. Một lần nữa, cần chỉ ra rằng Loại thô KHÔNG NÊN được sử dụng trong mã hiện đại.
Thuốc generic cho mèo - 5

Các lớp đánh máy

Vì vậy, gõ các lớp. Hãy xem làm thế nào chúng ta có thể viết lớp được đánh máy của riêng mình. Ví dụ: chúng ta có hệ thống phân cấp lớp:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
Chúng tôi muốn tạo một lớp triển khai vùng chứa động vật. Có thể viết một lớp có chứa bất kỳ tệp Animal. Điều này đơn giản, dễ hiểu, NHƯNG... trộn lẫn chó và mèo với nhau là xấu, chúng không phải là bạn của nhau. Ngoài ra, nếu ai đó nhận được một chiếc hộp như vậy, anh ta có thể ném nhầm mèo từ hộp vào một bầy chó... và điều này sẽ không dẫn đến điều gì tốt đẹp. Và ở đây thuốc generic sẽ giúp chúng ta. Ví dụ: hãy viết cách triển khai như thế này:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
Lớp của chúng ta sẽ làm việc với các đối tượng thuộc loại được chỉ định bởi một generic có tên T. Đây là một loại bí danh. Bởi vì Generic được chỉ định trong tên lớp, sau đó chúng ta sẽ nhận được nó khi khai báo lớp:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
Như chúng ta có thể thấy, chúng ta đã chỉ ra rằng chúng ta có Box, chỉ hoạt động với Cat. Trình biên dịch nhận ra rằng catBoxthay vì kiểu chung, Tbạn cần thay thế kiểu Catở bất cứ nơi nào tên của kiểu chung được chỉ định T:
Thuốc generic dành cho mèo - 6
Những thứ kia. chính nhờ trình Box<Cat>biên dịch mà nó hiểu được slotsnó thực sự là gì List<Cat>. Vì Box<Dog>bên trong sẽ có slots, chứa List<Dog>. Có thể có một số generic trong một khai báo kiểu, ví dụ:
public static class Box<T, V> {
Tên chung có thể là bất cứ thứ gì, mặc dù nên tuân thủ một số quy tắc bất thành văn - "Quy ước đặt tên tham số loại": Loại phần tử - E, loại khóa - K, loại số - N, T - cho loại, V - cho loại giá trị. Nhân tiện, hãy nhớ rằng chúng tôi đã nói rằng thuốc generic là bất biến, tức là không bảo toàn hệ thống phân cấp thừa kế. Trên thực tế, chúng ta có thể tác động đến điều này. Nghĩa là, chúng ta có cơ hội tạo ra biến thể COvariant, tức là. giữ di sản theo cùng một thứ tự. Hành vi này được gọi là "Loại bị ràng buộc", tức là. loại hạn chế. Ví dụ, lớp của chúng ta Boxcó thể chứa tất cả các loài động vật thì chúng ta sẽ khai báo một generic như sau:
public static class Box<T extends Animal> {
Tức là chúng ta đặt giới hạn trên cho lớp Animal. Chúng ta cũng có thể chỉ định một số loại sau từ khóa extends. Điều này có nghĩa là kiểu mà chúng ta sẽ làm việc phải là hậu duệ của một số lớp và đồng thời triển khai một số giao diện. Ví dụ:
public static class Box<T extends Animal & Comparable> {
Trong trường hợp này, nếu chúng ta cố gắng đặt Boxthứ gì đó không phải là phần kế thừa Animalvà không triển khai Comparablethì trong quá trình biên dịch, chúng ta sẽ gặp lỗi:
error: type argument Cat is not within bounds of type-variable T
Thuốc generic cho mèo - 7

Phương pháp gõ

Generics không chỉ được sử dụng trong các loại, mà còn trong các phương pháp riêng lẻ. Việc áp dụng các phương pháp này có thể được xem trong hướng dẫn chính thức: " Generics Methods ".

Lý lịch:

Thuốc generic dành cho mèo - 8
Chúng ta hãy nhìn vào bức tranh này. Như bạn có thể thấy, trình biên dịch nhìn vào chữ ký phương thức và thấy rằng chúng ta đang lấy một số lớp không xác định làm đầu vào. Nó không xác định bằng chữ ký rằng chúng tôi đang trả lại một loại đối tượng nào đó, tức là. Sự vật. Do đó, nếu chúng ta muốn tạo một ArrayList, thì chúng ta cần thực hiện điều này:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
Bạn phải viết rõ ràng rằng đầu ra sẽ là một ArrayList, điều này xấu và tạo thêm khả năng mắc lỗi. Ví dụ: chúng ta có thể viết những điều vô nghĩa như vậy và nó sẽ biên dịch:
ArrayList object = (ArrayList) createObject(LinkedList.class);
Chúng tôi có thể giúp trình biên dịch không? Có, thuốc generic cho phép chúng tôi làm điều này. Hãy xem ví dụ tương tự:
Thuốc generic cho mèo - 9
Sau đó, chúng ta có thể tạo một đối tượng đơn giản như thế này:
ArrayList<String> object = createObject(ArrayList.class);
Thuốc generic dành cho mèo - 10

WildCard

Theo Hướng dẫn về Generics của Oracle, cụ thể là phần " Ký tự đại diện ", chúng ta có thể mô tả một "loại không xác định" bằng dấu chấm hỏi. Wildcard là một công cụ hữu ích để giảm thiểu một số hạn chế của thuốc generic. Ví dụ, như chúng ta đã thảo luận trước đó, generics là bất biến. Điều này có nghĩa là mặc dù tất cả các lớp đều là hậu duệ (kiểu con) của kiểu Đối tượng, nhưng nó List<любой тип>không phải là kiểu con List<Object>. NHƯNG, List<любой тип>nó là một kiểu con List<?>. Vì vậy chúng ta có thể viết đoạn mã sau:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
Giống như thuốc generic thông thường (tức là không sử dụng ký tự đại diện), thuốc generic có ký tự đại diện có thể bị hạn chế. Ký tự đại diện giới hạn trên trông quen thuộc:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
Nhưng bạn cũng có thể giới hạn nó bằng ký tự đại diện giới hạn dưới:
public static void printCatList(List<? super Cat> list) {
Do đó, phương thức sẽ bắt đầu chấp nhận tất cả các con mèo, cũng như cao hơn trong hệ thống phân cấp (tối đa Object).
Thuốc generic dành cho mèo - 11

Loại Xóa

Nói về thuốc generic, cần biết về “Xóa kiểu”. Trong thực tế, việc xóa kiểu có nghĩa là các generics là thông tin dành cho trình biên dịch. Trong quá trình thực hiện chương trình, không còn thông tin nào về generics nữa, việc này gọi là “xóa”. Việc xóa này có tác dụng là loại chung được thay thế bằng loại cụ thể. Nếu loại chung không có ranh giới thì loại Đối tượng sẽ được thay thế. Nếu đường viền được chỉ định (ví dụ <T extends Comparable>), thì đường viền đó sẽ được thay thế. Đây là một ví dụ từ Hướng dẫn của Oracle: " Xóa các loại chung ":
Thuốc generic dành cho mèo - 12
Như đã nói ở trên, trong ví dụ này, tên chung Tsẽ bị xóa đến đường viền của nó, tức là. trước Comparable.
Thuốc generic dành cho mèo - 13

Phần kết luận

Generics là một chủ đề rất thú vị. Tôi hy vọng chủ đề này được bạn quan tâm. Tóm lại, generics là một công cụ tuyệt vời mà các nhà phát triển được cung cấp để cung cấp thông tin bổ sung cho trình biên dịch nhằm đảm bảo một mặt là an toàn kiểu và mặt khác là tính linh hoạt. Và nếu bạn quan tâm, thì tôi khuyên bạn nên xem các tài nguyên mà tôi thích: #Viacheslav
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION