題外話: 泛型與通配符是Java語法中比較難懂的兩個語法,學習泛型和通配符的主要目的是能夠看懂源碼,實際使用的不多。 1.泛型 1.1泛型的用法 1.1.1泛型的概念 《 Java程式設計思想》上有這麼一句話:一般的類別和方法,只能使用具體的型別: 要嘛是基本型別,要嘛是自訂的類別。如果要編寫可以應用於多種類型的程式碼,這種刻板的限制對程式碼的束縛就會很大。所以從Java5開始引進了泛型機制,這個泛型又是什麼意思呢?由於一般的類別和方法只能使用一種具體得類型,這就使程式碼受到了很大的束縛,例如一個求三個數中最大值的方法,假設一開始方法中的參數列表的類型是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 表示ElementK 表示Key#V 表示ValueN 表示NumberT 表示TypeS, U, V 等等- 第二、第三、第四個型別//一个泛型类 class ClassName { }#使用泛型類別時,指定了這個類別的物件所持有的類型,則該物件只能接收該類型的對象,傳入其他類型對象,編譯器會報錯,並且接收泛型類別中泛型方法的返回值時,不需要進行強制型別轉換(向下轉型),而使用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的子类。