다론: 제네릭과 와일드카드는 Java 문법에서 상대적으로 어려운 두 가지 구문입니다. 제네릭과 와일드카드를 배우는 주요 목적은 소스 코드를 이해하는 것이지만 실제로는 많이 사용되지 않습니다. 1. 제네릭 1.1 제네릭의 사용법 1.1.1 제네릭의 개념 "Java 프로그래밍 사고"에는 일반 클래스와 메소드는 특정 유형만 사용할 수 있다는 말이 있습니다. 맞춤 수업. 여러 유형에 적용할 수 있는 코드를 작성하려는 경우 이러한 엄격한 제한은 코드를 매우 제한하게 됩니다. 그러면 Java5부터 일반 메커니즘이 도입되었습니다. 이 일반 메커니즘은 무엇을 의미합니까? 일반 클래스와 메소드는 특정 유형 하나만 사용할 수 있으므로 코드에 큰 제약을 가합니다. 예를 들어 세 숫자의 최대값을 찾는 메소드는 처음에 메소드의 매개변수 목록 유형이 정수, 세 개의 정수 데이터에서 최대값을 찾는 데 문제가 없습니다. 이 프로그램은 완벽하게 실행될 수 있지만 세 개의 부동 소수점 숫자 중에서 최대값을 찾으려는 경우 이 프로그램은 실패합니다. , 그러면 또 다른 오버로드된 메소드를 작성하고 Double을 기반으로 매개변수 목록과 구현 함수를 다시 구현하도록 선택할 수 있습니다. 그러나 문제가 발생하면 생각해 본 적이 있습니까? 세 가지 개체 중 가장 큰 개체가 필요한 1만 개 또는 심지어 100만 개의 유형, 백만 개의 오버로드된 메서드를 작성하려면 어떻게 해야 합니까? 이런 문제를 해결하기 위해 제네릭이 도입되었습니다. 제네릭은 매개변수화된 유형의 개념을 구현하여 코드에서 여러 유형을 적용할 수 있다는 것을 의미합니다. 제네릭을 사용하면 유형을 클래스, 인터페이스 및 메소드에 "매개변수"로 전달할 수 있으므로 클래스와 메소드는 가장 광범위한 표현 기능을 가질 수 있으며 매개변수가 다르다는 이유로 다른 유형을 만들 필요가 없습니다. Integer,你从三个整型数据中找出一个最大值没有任何问题,这个程序能够完美地运行,但是你要找三个浮点数中的最大值时,这个程序编译都通不过,这时你可以选择另写一个重载方法将参数列表和实现功能都基于Double再实现一遍,这样也可以解决问题,但是,你想过一个问题没有,万一有一万甚至一百万种类型需要求三个对象中最大那一个,那应该怎么办,写一百万个重载方法?这是不可能的,为了解决这种类型的问题,引入了泛型,泛型实现了参数化类型的概念,使代码可以应用多种类型,通俗说泛型就是“适用于许多许多类型”的意思。 使用泛型可以将类型作为“参数”传递至类,接口,方法中,这样类和方法就可以具有最广泛的表达能力,不需要因为一个参数不同而去另建类型。 注意:任意的基本类型都不能作为类型参数。 1.1.2泛型类 我们通过一段代码来认识泛型,首先看下面一段不使用代码的泛型的代码:/** * 不使用泛型 */ class A { } class Print { private A a; public Print(A a) { setA(a); System.out.println(this.a); } public void setA(A a) { this.a = a; } public A getA() { return this.a; } } public class Generic { public static void main(String[] args) { Print print = new Print(new A()); } }//output:A@1b6d3586 不使用泛型取创建一个类没有任何问题,但是这个类的重用性就不怎么样了,它只能持有类A的对象,不能持有其他任何类的对象,我们不希望为碰到的每种类型都编写成一个新的类,这是不现实的。我们学习类的时候,知道Object类是所有类的父类,所以Object类可以接受所有的类型引用,我们可以让Print类持有Object类型的对象。/** * 使用Object类 */ class B{ } class Print1 { private Object b; public Print1(Object b) { setB(b); System.out.println(this.b); } public void print(Object b) { setB(b); System.out.println(this.b); } public void setB(Object b) { this.b = b; } } public class Generic1 { public static void main(String[] args) { Print1 print1 = new Print1(new B());//打印B类型 int i = 2022; print1.print(i);//打印整型类型 print1.print("这是一个字符串对象!");//打印字符串类型 } }//output://B@1b6d3586//2022//这是一个字符串对象! Print1可以接收并打印任何类型,但是这并不是我们想要的结果,你想想如果实现的是一个顺序表类,里面是通过一个数组来实现,如果这个数组什么类型都可以接收,那就非常混乱了,取出数据的时候不能确定取出的到底是什么类型的数据,而且取出的数据是Object类,需要进行强制类型转换,那能不能实现指定类持有什么类型的对象并且编译器能够检查类型的正确性。 泛型就完美实现了这个目的,下面我们将上述代码改写成泛型类,那么首先得知道泛型的语法,泛型类创建语法如下: class 类名 {权限修饰 泛型参数 变量名;//泛型成员变量权限修饰 返回值类型 方法名 (参数列表){}//参数列表和返回值类型可以是泛型} 例如:class Print2 { private T c; public void print(T c) { setC(c); System.out.println(this.c); } public void setC(T c) { this.c = c; } }泛型类的使用语法如下: 泛型类 变量名; // 定义一个泛型类引用new 泛型类(构造方法实参); // 实例化一个泛型类对象 例如:Print2 print3 = new Print2();使用泛型实现一个类,并使用它:/** * 使用泛型 */ class C{ } class Print2 { private T c; public void print(T c) { setC(c); System.out.println(this.c); } public void setC(T c) { this.c = c; } } public class Generic2{ public static void main(String[] args) { Print2 print2 = new Print2<>();//打印C类型 print2.print(new C()); Print2 print3 = new Print2<>();//打印整型类型 print3.print(2022); Print2 print4 = new Print2<>();//打印字符串类型 print4.print("这是一个字符串对象!"); } }/*** output:*C@1b6d3586* 2022* 这是一个字符串对象!*/ 类名后的 代表占位符,表示当前类是一个泛型类。 【规范】类型形参一般使用一个大写字母表示,常用的名称有: E 表示 Element K 表示 Key V 表示 Value N 表示 Number T 表示 Type S, U, V 等等 - 第二、第三、第四个类型//一个泛型类 class ClassName { }使用泛型类时,指定了这个类的对象持有的类型,则该对象只能接收该类型的对象,传入其他类型对象,编译器会报错,并且接收泛型类中泛型方法的返回值时,不需要进行强制类型转换(向下转型),而使用Object참고: 기본 유형은 유형 매개변수로 사용할 수 없습니다. 🎜🎜1.1.2 일반 클래스🎜🎜코드를 통해 제네릭에 대해 알아봅니다. 먼저 제네릭을 사용하지 않는 다음 코드를 살펴보세요. 🎜Print2 print3 = new Print2<>();//后面尖括号内可省略🎜//output:A@1b6d3586🎜 blockquote> 🎜제네릭을 사용하지 않고 클래스를 생성하는 데는 문제가 없지만 이 클래스의 재사용성은 그다지 좋지 않습니다. A 클래스의 개체만 담을 수 있고 다른 클래스의 개체는 담을 수 없습니다. , 우리는 우리가 만나는 모든 유형에 대해 새로운 클래스를 작성하고 싶지 않습니다. 이는 비현실적입니다. 클래스를 연구할 때 Object 클래스가 모든 클래스의 상위 클래스라는 것을 알고 있으므로 Object 클래스는 모든 유형 참조를 허용할 수 있습니다. 클래스는 Object 유형의 개체를 보유합니다. 🎜public static void main(String[] args) { Print2 print2 = new Print2(); print2.print(2022); print2.print("字符串"); }🎜//output://B@1b6d3586//2022//이것은 문자열 개체입니다! 🎜🎜Print1은 모든 유형을 수신하고 인쇄할 수 있지만 이는 우리가 원하는 결과가 아닙니다. 배열을 통해 구현되는 시퀀스 테이블 클래스를 구현하는 경우입니다. 배열은 모든 유형을 수신할 수 있으므로 데이터를 가져올 때 어떤 유형의 데이터를 가져오는지 확신할 수 없으며 가져온 데이터는 Object 클래스이므로 강제로 가져와야 합니다. 변환은 클래스가 보유하는 객체의 유형을 지정하여 달성할 수 있으며 컴파일러는 유형의 정확성을 확인할 수 있습니다. 제네릭은 이 목적을 완벽하게 달성합니다. 이제 위의 코드를 제네릭 클래스로 다시 작성하겠습니다. 그러면 먼저 제네릭 클래스를 생성하는 구문을 알아야 합니다. 🎜🎜클래스 클래스 이름 {권한은 일반 매개변수 변수 이름을 수정합니다.//일반 멤버 변수권한은 반환 값 유형 메서드 이름(매개변수 목록)을 수정합니다.{}//매개변수 목록 및 반환 값 유형은 일반 }🎜 🎜예: 🎜public class MyArrayList { public T[] elem ; private int usedSize; public MyArrayList(int capacity) { this.elem = (T[])new Object[capacity]; } }🎜일반 클래스를 사용하는 구문은 다음과 같습니다.🎜🎜일반 클래스 // 일반 클래스 참조 정의; />새로운 일반 클래스(생성자 인수); // 일반 클래스 객체 인스턴스화🎜🎜예: 🎜public class MyArrayList { public T[] elem ; private int usedSize; public MyArrayList(Class clazz, int capacity) { this.elem = (T[]) Array.newInstance(clazz, capacity); } }🎜제네릭을 사용하여 클래스를 구현하고 사용: 🎜import java.lang.reflect.Array; public class MyArrayList { public T[] elem ; private int usedSize; public MyArrayList(int capacity) { this.elem = (T[])new Object[capacity]; } public MyArrayList(Class clazz, int capacity) { this.elem = (T[]) Array.newInstance(clazz, capacity); } }🎜 /*** 출력:*C@1b6d3586* 2022* 이것은 문자열 객체입니다! */🎜🎜클래스 이름 뒤의 는 현재 클래스가 일반 종류임을 나타내는 자리 표시자를 나타냅니다. 🎜🎜[사양] 유형 매개변수는 일반적으로 대문자로 표시됩니다. 일반적으로 사용되는 이름은 다음과 같습니다. 🎜🎜E는 Element를 나타냅니다. , V 등 - 두 번째, 세 번째, 네 번째 유형🎜public static void main(String[] args) { MyArrayList list1 = new MyArrayList<>(10); MyArrayList list2 = new MyArrayList<>(10); System.out.println(list1); System.out.println(list2); }🎜일반 클래스를 사용할 때 이 클래스의 객체가 보유한 유형을 지정하면 객체는 이 유형의 객체만 받을 수 있고 다른 유형은 전달됩니다. Object인 경우 컴파일러는 오류를 보고하며 제네릭 클래스에서 제네릭 메서드의 반환 값을 수신할 때 강제 유형 변환(다운캐스트)이 필요하지 않지만 Object 클래스를 사용하려면 강제 유형 변환이 필요합니다. . 🎜1.1.3类型推导 使用泛型类时,可以通过泛型类型中传入的类型来推导实例化该泛型类时所需的类型参数,换个说法,定义泛型对象时,前面的尖括号内必须指定类型,后面实例化时可以不指定。如:Print2 print3 = new Print2<>();//后面尖括号内可省略1.2裸类型 裸类型其实很好理解,就是一个泛型类,你不去指定泛型对象持有的类型,这样的一个类型就是裸类型。 比如:public static void main(String[] args) { Print2 print2 = new Print2(); print2.print(2022); print2.print("字符串"); }//output://2022//字符串 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制。 1.3擦除机制 1.3.1关于泛型数组 介绍泛型的擦除机制之前,我们先来了解泛型数组·,先说结论,在Java中不允许实例化泛型数组,如果一定要建立一个泛型数组,正确的做法只能通过反射来实现,当然有一个“捷径”可以不使用反射来创建泛型数组。创建的代码如下: 1.通过捷径创建,大部分情况下不会出错。public class MyArrayList { public T[] elem ; private int usedSize; public MyArrayList(int capacity) { this.elem = (T[])new Object[capacity]; } }2.通过反射创建,现在只给代码,具体为什么要这么做后续介绍反射再说。public class MyArrayList { public T[] elem ; private int usedSize; public MyArrayList(Class clazz, int capacity) { this.elem = (T[]) Array.newInstance(clazz, capacity); } }1.3.2泛型的编译与擦除 我们先来实现一个简单的泛型顺序表,不考虑扩容问题,只实现简单的增删操作,来看看构造方法部分编译后的反汇编。import java.lang.reflect.Array; public class MyArrayList { public T[] elem ; private int usedSize; public MyArrayList(int capacity) { this.elem = (T[])new Object[capacity]; } public MyArrayList(Class clazz, int capacity) { this.elem = (T[]) Array.newInstance(clazz, capacity); } } 我们发现所有的泛型占位符T都被擦除替换成Object了,这就说明Java的泛型机制是在编译期实现的,而泛型机制实现就是通过像这样的擦除机制实现的,并在编译期间完成类型的检查。 我们通过打印持有不同类型的MyArrayList类来看看,泛型机制到底是不是不会出现在运行期间,如果是的话,打印出的类型都应该是MyArrayList。public static void main(String[] args) { MyArrayList list1 = new MyArrayList<>(10); MyArrayList list2 = new MyArrayList<>(10); System.out.println(list1); System.out.println(list2); }/*** output:* MyArrayList@1b6d3586* MyArrayList@4554617c*/ 我们发现打印的类型是一样的,都是MyArrayList,所以可以得出一个结论,泛型是发生在编译期,泛型的类型检查是在编译期完成的,泛型的实现是通过擦除机制实现的,类后面的占位符都会被擦除,其他的占位符都会被替换成Object。当然,这是在泛型参数没有指定上界的情况下,如果存在上界,那占位符会擦除成上界的类型或接口,其实没有指定上界,上界默认为Object,什么是泛型上界,嘘,等一下再说。 根据擦除机制,也能解释为什么Java当中不能实例化泛型数组了,因为泛型数组前面的占位符会被擦除成Object,实际上是创建一个Object数组,而Object数组中什么类型都能放,这就导致取数据时不安全,因为你不能确定数组里面存放的元素全部都是你预期的类型,所以为了安全,Java不允许实例化泛型数组。 1.4泛型的上界 1.4.1泛型的上界 在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。 class 泛型类名称 {...} 例如:Number是Integer,Float,Double等相关数字类型的父类。public class MyArrayList { }那么这个MyArrayList泛型类只能指定持有Number类以及Number的子类,像这样就给泛型的类型传参做了约束,这个约束就是泛型的上界,泛型类被类型边界约束时,只能指定泛型类持有类型边界这个类及其子类。MyArrayList list1 = new MyArrayList<>(10);//正确 MyArrayList list2 = new MyArrayList<>(10);//正确 MyArrayList list3 = new MyArrayList<>(10);//错误,因为String不是Number的子类1.4.2特殊的泛型上界 假设需要设计一个泛型类,能够找出数组中最大的元素。class MaxVal> { public T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; } }由于引用类型的比较需要使用Comparable接口来判断大小,所以所传入的类需要实现Comparable接口,上面这个泛型的类型参数的上界是一个特殊的上界,表示所传入的类型必须实现Comparable接口,不过实现了Comparable接口的类,那也就是Comparable的子类了,综上,像这样类似需要通过实现某一个接口来达到预期功能的类型,使用泛型时需指定泛型的上界,并且该传入的类型必须实现该上界接口。 1.4.3泛型方法 有泛型类,那么就一定有泛型接口,泛型方法,其中泛型接口与泛型类的创建和使用是一样的,所以我们重点介绍泛型方法的创建与使用。 创建泛型方法的基本语法: 方法限定符 返回值类型 方法名称(形参列表) { ... } 例如上面实现求数组中最大元素泛型版的方法如下:class MaxVal> { public > T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; } }对于非static修饰的静态方法, 可以省略,上述代码可以变成:class MaxVal> { public T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; } }但是,如果是一个static修饰的静态方法,不可以省略,因为静态方法不依赖与对象,它的使用不用实例化对象,所以必须有单独的类型参数列表来指定持有的对象类型。class MaxVal> { public static > T max(T[] data) { T max = data[0]; for (int i = 0; i < data.length; i++) { if (max.compareTo(data[i]) < 0) max = data[i]; } return max; } }1.4.4类型推导 和泛型类一样,泛型方法也有类型推导的机制,如果不使用类型推导,那么泛型方法是这么使用的: 使用类型推导图中画圆圈部分可以省略。 在泛型类中没有如下的父子类关系:public class MyArrayList { ... } // MyArrayList 不是 MyArrayList 的父类型 // MyArrayList 也不是 MyArrayList 的父类型但是使用通配符这两种类是有符子类关系的。 2.通配符 2.1通配符的概念 ?就是一个通配符,用与泛型的使用,与泛型不同的是,泛型T是确定的类型,传入类型实参后,它就确定下来了,而通配符更像是一种规定,规定一个范围,表示你能够传哪些参数。 一个泛型类名尖括号之内仅含有一个?,就会限制这个泛型类传入的类型为Object,相当于没有限制,但是获取元素时由于不能确定具体类型,只能使用Object引用接收,所以>也被称为无界通配符。//使用泛型打印顺序表 public static void printList1(ArrayList list) { for (T x:list) { System.out.println(x); } } //使用通配符打印顺序表 public static void printList2(ArrayList> list) { for (Object x:list) { System.out.println(x); } }使用泛型T能够确定传入的类型就是T类型,所以使用T类型的变量接收,而通配符?没有设置边界的情况下,默认上界是Object没有下界,为了保证安全,只能使用Object类型的变量接收。 通配符是用来解决泛型无法协变的问题的,协变指的就是如果Student是Person的子类,那么List也应该是List的子类。但是泛型是不支持这样的父子类关系的。 2.2通配符的上界 通配符也有上界,可以限制传入的类型必须是上界这个类或者是这个类的子类。 基本语法: extends 上界> extends Number>//可以传入的实参类型是Number或者Number的子类 例如:public static void printAll(ArrayList extends Number> list) { for (Number n: list) { System.out.println(n); } }我们对printAll方法的一个形参限制了类型的上界Number,所以在遍历这个顺序表的时候,需要使用Number来接收顺序表中的对象,并且使用该方法时,只能遍历输出Number及其子类的对象。public static void main(String[] args) { printAll(new ArrayList());//ok printAll(new ArrayList());//ok printAll(new ArrayList());//ok printAll(new ArrayList());//error } 假设有如下几个类:class Animal{} class Cat extends Animal{} class Dog extends Animal{} class Bird extends Animal{}Animal是Cat,Dog,Bird类的父类,我们来看一看使用泛型和使用通配符在打印对象结果上会有什么区别?我们对这两者都设置了上界,当打印不同的对象时,到底会调用谁的toString方法。 //泛型 public static void printAnimal1(ArrayList list) { for (T animal: list) { System.out.println(animal); } } //通配符 public static void printAnimal2(ArrayList extends Animal> list) { for (Animal animal: list) { System.out.println(animal); } }我们先来看泛型,使用泛型指定类型后,那么指定什么类型,那它就会输出什么类型的对象,比如你指定顺序表中放的类型是Cat,那么它调用的就是Cat对象的toString方法。public static void main(String[] args) { Cat cat = new Cat(); Dog dog = new Dog(); Bird bird = new Bird(); //泛型 ArrayList list1 = new ArrayList<>(); ArrayList list2 = new ArrayList<>(); ArrayList list3 = new ArrayList<>(); list1.add(cat); list2.add(dog); list3.add(bird); printAnimal1(list1);//Cat printAnimal1(list2);//Dog printAnimal1(list3);//Bird } 再来看一看通配符,使用通配符是规定能够使用Animal及其子类,不伦你传入哪一个子类对象,都是父类的引用接收,但是具体哪一个子类,并不清楚。public static void main(String[] args) { Cat cat = new Cat(); Dog dog = new Dog(); Bird bird = new Bird(); //通配符 ArrayList list1 = new ArrayList<>(); ArrayList list2 = new ArrayList<>(); ArrayList list3 = new ArrayList<>(); list1.add(cat); list2.add(dog); list3.add(bird); printAnimal2(list1);//Cat printAnimal2(list2);//Dog printAnimal2(list3);//Bird } 父类引用接收子类对象发生了向上转型,当打印父类引用的子类对象时,会优先使用子类的toString方法,在介绍多态的时候也讲过这个问题,所以输出结果与使用泛型是一样的,但是泛型和通配符的效果是不一样的,泛型是你传入什么类型,那这个类就会持有什么类型的对象,而通配符是规定一个范围,规定你能够传哪一些类型。 通配符的上界是支持如下的父子类关系的,而泛型的上界不支持: MyArrayList extends Number> 是 MyArrayList 或者 MyArrayList的父类类型MyArrayList> 是 MyArrayList extends Number> 的父类型 对于通配符的上界有个特点,先说结论,使用通配符上界可以读取数据,但是并不适合写入数据,因为不能确定类所持有的对象具体是什么。public static void main(String[] args) { ArrayList arrayList1 = new ArrayList<>(); ArrayList arrayList2 = new ArrayList<>(); arrayList1.add(10); List extends Number> list = arrayList1; System.out.println(list.get(0));//ok Integer = list.get(0);//error因为不能确定list所持有的对象具体是什么 list.add(2);//error因为不能确定list所持有的对象具体是什么,为了安全,这种情况Java不允许插入元素 } 因为从list获取的对象类型一定Number或者Number的子类,所以可以使用Number引用来获取元素,但是插入元素时你并不能确定它到底是哪一种类型,为了安全,使用通配符上界的list不允许插入元素。 2.3通配符的下界 与泛型不同,通配符可以拥有下界,语法层面上与通配符的上界的区别是讲关键字extends改为super。 super 下界> super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型 既然是下界那么通配符下界与上界对传入类的规定是相反的,即规定一个泛型类只能传入下界的这个类类型或者这个类的父类类型。比如 super Integer>代表 可以传入的实参的类型是Integer或者Integer的父类类型(如Number,Object)public static void printAll(ArrayList super Number> list) { for (Object n: list) { //此处只能使用Object接收,因为传入的类是Number或者是Number的父类 System.out.println(n); } }public static void main(String[] args) { printAll(new ArrayList());//ok printAll(new ArrayList());//ok printAll(new ArrayList());//error printAll(new ArrayList());//error printAll(new ArrayList());//error } 同理通配符的下界也是满足像下面这种父子类关系的。 MyArrayList super Integer> 是 MyArrayList的父类类型MyArrayList> 是 MyArrayList super Integer>的父类类型 总结: ?是? extends ....和? super ....的父类,看通配符之间的父子类关系,最关键的是看通配符所“规定的”范围,判断父子类是根据这个范围来判断的。 通配符的下界也有一个特点,那就是它能够允许写入数据,当然能够写入的数据对象是下界以及下界的子类,但是并不擅长读数据,与通配符的上界相反。public static void main(String[] args) { ArrayList super Animal> list = new ArrayList(); ArrayList super Animal> list2 = new ArrayList();//编译报错,list2只能引用Animal或者Animal父类类型的list list.add(new Animal());//添加元素时,只要添加的元素的类型是Animal或者Animal的子类就可以 list.add(new Cat()); Object s2 = list.get(0);//可以 ArrayList super Animal> list3 = new ArrayList(); Cat s1 = list3.get(0);//error因为构造对象时可以构造Animal父类类型的ArrayList,取出的对象不一定是Animal或者Animal的子类 } 对于这个栗子添加元素时,只要添加的元素的类型是Animal或者Animal的子类就可以,获取元素时,只能使用Object引用接收,不能使用其他的引用接收,因为因为构造对象时可以构造Animal父类类型的ArrayList,虽然可以插入Animal以及其子类对象,但取出的对象不能保证是Animal或者Animal的子类。