参考:
- https://blog.youkuaiyun.com/weixin_50635856/article/details/124768787
- https://zhuanlan.zhihu.com/p/154595559
- https://www.bilibili.com/video/BV1ds4y1E7uW
泛型的简介
泛型是 JDK5 中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
泛型的英文是 generrics,一系列和泛型相关的名词都是以 generic 为前缀的,如:Generic Method、Generic Types。
在方法中定义泛型,即 Generic Methods。
在类型中定义泛型,即 Generic Types。类型可以是类,也可以是接口。
泛型的格式
1. 占位符
<数据类型>
(注意:泛型只能支持引用数据类型)
<数据类型1,数据类型2>
注意:定义中的数据类型的命名可以是任意的,因为它起到占位符的作用,在使用时,才会根据使用的数据类型去取代占位符所在位置,形成对代码的完整意义表达。即定义时写 <A>
,使用时使用的是 String,那么就会取代之前定义中出现的所有的 <A>
,全部变为 <String>
。
// List
public interface List<E> extends Collection<E>{ // 关注此处即可,泛型接口
Iterator<E> iterator();
<T> T[] toArray(T[] a); // 泛型方法,之后会介绍
boolean add(E e);
}
// Map
public interface Map<K,V> { // 关注此处即可
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
void putAll(Map<? extends K, ? extends V> m);
}
// 使用
List<String> = new ArrayList<>(); // 那么 List 里所有除泛型方法的 <E> 都会被替换为 <String>
/*
即 Iterator<E> iterator(); 会被替换为 Iterator<String> iterator();
boolean add(E e); 会被替换为 boolean add(String e);
另外 Java 5 时,是要写成 List<String> = new ArrayList<String>(); 的。即两边都得写明泛型的类型,但 Java 7 (含)以后,右边的泛型就可以留空省略,因此可以直接写成 List<String> = new ArrayList<>();
*/
2. 泛型的分类
注意:定义时使用的泛型实际只是“占位符”,具体数据类型取决于使用时赋予的数据类型,使用时被赋予数据类型后,会取代定义中的“占位符”。
1. 泛型类:类定义时使用泛型
作用:编译阶段可以指定数据类型,类似于集合的作用
格式说明
// 泛型类的格式:访问修饰符 class 类名<泛型变量> { }
public class TestClass<E> { // 类定义时使用泛型
}
具体案例
// 定义
public class TestClass<E> { // 类定义时使用泛型
}
public class TestClass2<T,E> { // 类定义时使用泛型
}
// 使用
// 需要在创建实例时,传入类型(非必要,但若传入类型,则定义多少个泛型就得传入多少个类型)
TestClass<String> foo = new TestClass<>();
TestClass2<String,Integer> foo2 = new TestClass2<>();
泛型类使用时可以不传入类型
泛型类使用的时候(创建实例时),并不是必须要对这个泛型传入一个类型的,若不传入类型,则默认这个泛型为 Object 类,因此可以传入所有类型。
- 如果泛型信息缺失(没有对泛型传入一个类型),虽然不会报错,但由于编译器也无法帮忙检查出类型是否匹配,所以会给出编译警告(但不影响正常编译与运行),在 IEDA 中会对该部分代码背景色标黄,并用小黄色灯泡提示
raw use of parameterized class 'xxx'(泛型类名字)
。 - 实际上不传入类型时,默认也不一定为 Object 类,而要看这个泛型类是怎么定义的,如果设置了有界泛型(如
<T extends Animal>
),则默认为这个上界类型(Animal
),如果没有设置有界泛型,则默认为 Object。(其实没有设置有界,如<T>
,实际就等价于<T extends Object>
)
但若在泛型类使用的时候(创建实例时),对泛型传入指定的类型,那么除了泛型类中的泛型方法外,类中所有与该泛型占位符同名的泛型占位符都会被传入该指定类型。
而对于非泛型类,使用的时候(创建实例时),是不允许使用泛型的。即其类定义本身就没有允许泛型,你就无法强行去传入一个类型。如:TestClass{}
,你是不能在使用时 TestClass<String> test = new TestClass<>()
的,除非他定义的时候是泛型类(如 TestClass<T>{}
)。
泛型类若传入类型需要如数传入
另外,若泛型类需要传入多个类型,如 public class TestClass<T,E>{}
,那么使用时(创建实例时)要么就完全不传入类型(那么默认用到 T 和 E 的地方都为 Object 类),要传,就得传两个,哪怕你将其中一个显式地传入为 Object,如 TestClass<String,Object>
,若传入类型数少于要求传入的类型数,则会报错,比如:TestClass<String>
或 TestClass<String,>
或 TestClass<,String>
。
2. 泛型接口:接口定义时使用泛型
作用:泛型接口可以让实现类选择当前功能需要操作的数据类型
格式说明
// 泛型接口的格式:访问修饰符 interface 接口名<泛型变量> { }
public interface TestInter<E>{
}
具体案例
// List
public interface List<E> extends Collection<E>{
}
3. 泛型方法:在方法声明中使用泛型
实例方法与静态方法都可以使用泛型。
作用:方法中可以使用泛型接收一切实际类型的参数,方法更具备通用性。
此小节对泛型方法只作概念性简介,下文会另外重点介绍泛型方法。
格式说明
// 泛型方法的格式:修饰符 <泛型变量> 返回值类型 方法名(形参列表){ }`
// 使用泛型,可以用在参数中
public <E> void testMethod(E e){}
// 也可以用在方法体中(包括返回值中)
public <E> E testMethod2(Object val){return (E)val;} // 若返回值是 E 类型,则方法名前的返回值类型也要用 E 来占位。
具体案例
public static void main(String[] args){
sayHello("abc");
sayHello(12.3);
}
public static <T> void sayHello(T words){
}
private <T> T sayHi(T words){
return words;
}
泛型方法 vs. 泛型类里的方法
要注意泛型类里的方法与泛型方法的区别
public class TestPrint<T>{ // 泛型类
// 泛型类中的 T 会在使用时被传入相应的类型,传入后,除了泛型方法外,泛型类里所有包含同名 T 的方法、属性都会被替换为该类型。
T thingToPrint;
public TestPrint(T thingToPrint){
this.thingToPrint = thingToPrint;
}
// 泛型类里的方法,形参使用了泛型
public void print(T something){
System.out.println(something);
}
// 泛型类里的方法,形参使用了泛型
public void printAgain(T something){
System.out.println(something);
}
// 这也是泛型类里的方法,没有涉及泛型
public void sayHello(){
System.out.println("Hello world!");
}
// 这是泛型方法
public <T> void sayHi(T otherthing){ // 这里的 T 虽然命名为了 T 但不会受到影响,可以传入任意类型。这是因为方法中标注 <T> 说明此方法中的 T 是泛型,而非类中的 T 的类型,可以理解成这种定义覆盖了类中传入的类型。当然,为了避免混淆,你也可以不命名为 <T> 而命名为任意名称,如 public <whatever> void sayHi(whatever otherthing){},这里故意命名为 <T> 只是想说明这里的 T 不会向泛型类中的其他方法一样受到使用该类时传入的 T 的类型的影响。
System.out.println(otherthing);
}
}
// 调用类
public class TestUse{
public static void main(String[] args){
TestPrint<Integer> integerPrinter = new TestPrint<>(123);
/*
此时 TestPrint 类中的 print、printAgain 的 T 也一并替换为 Integer,
因此,若传入 String 会报错,比如:
integerPrinter.print("abcd");
此时只能继续传入 Integer,如:
*/
integerPrinter.print(100); // 100
integerPrinter.sayHello(); // Hello world!
integerPrinter.sayHi("Hi!"); // Hi!
TestPrint printer = new TestPrint("test"); // 并非必须要对泛型类传入类型
/*
由于没有传入类型,所以不存在对 T 的全局替换。
比如此时 TestPrint 类中的 print 里的 T 就不会要求一定是 String。
*/
printer.print(1234); // 1234
/*
虽然调用 print 时传入了整数,但仍然不是全局替换 T,
因此 printAgain 里的 T 仍然可以是任意类型。
甚至 print 里可以再次传入不同类型的实参。
*/
printer.printAgain("abc"); // abc
printer.print(3.21); // 3.21
printer.print("wow"); // wow
}
}
3. ETKVN? 是什么
泛型在定义时,数据类型仅仅是临时的占位符,所以可以任意命名。但在浏览 java 自带的类与方法时,经常会看到泛型在定义时数据类型命名为:ETKVN?
其含义如下(其中ETKVN只是约定,非强制,你可以命名为 ABCD也没有问题):
- E:Element(在集合中使用,因为集合中存放的是元素)
- T:Type
- K:Key(键)
- V:Value(值)
- N:Number(数值类型)
- ?:表示不确定的 Java 类型,这个是通配符,和 ETKVN 都不同,下文会单独介绍。
4. 有界泛型
可以使用 extends
来表示泛型类的泛型上限。(但并不允许使用 super
来表示泛型类的泛型下限,要注意与后文中通配符的上下界的使用区别)
当然,除了在泛型类、泛型接口中引入有界泛型,也可以在泛型方法的声明时引入有界泛型。
extends
// 泛型
package genericexample;
public class GenericPrinter<T extends Animal> { // 即 T 类只能是 Animal 或 Animal 的子类
T thingToPrint;
public GenericPrinter(T thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
System.out.println(thingToPrint);
}
}
public class Animal {}
public class Cat extends Animal{}
public class Dog extends Animal{}
// 调用类
package genericexample;
public class GenericExample {
public static void main(String[] args) {
GenericPrinter<Cat> catPrinter = new GenericPrinter<>(new Cat());
catPrinter.print(); // genericexample.Cat@1b6d3586
GenericPrinter<Animal> animalPrinter = new GenericPrinter<>(new Animal());
animalPrinter.print(); // genericexample.Animal@4554617c
// GenericPrinter<Integer> printer9 = new GenericPrinter<>(3); // 报错:java: 类型参数java.lang.Integer不在类型变量T的范围内
// printer9.print();
}
}
// 可见,传入 Animal 与 Cat(Animal 的子类)都是被允许的,但传递非 Animal 及其子类,比如 Integer,则会报错。
另外,由于 T 已经明确是 extends Animal
(则 T 必然是 Animal 或 Animal 的子类),所以就可以直接使用 Animal 的方法。(如果不写明 extends Animal
,则 java 并不确定 T 是什么类型,所以是无法调用 Animal 中定义的方法的)
但要注意的是,是 T 的实例可以调用 Animal 的实例方法,而非使用了 泛型 T 的类(GenericPrinter)的实例可以调用 Animal 方法。
public class Animal {
public void eat(){
System.out.println("It's fun to eat!");
}
}
// 泛型
package genericexample;
public class GenericPrinter<T extends Animal> {
T thingToPrint;
public GenericPrinter(T thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
thingToPrint.eat(); // 因为 thingToPrint 是 T 类实例,不管是 Animal 还是Animal 子类,都一定可以调用 Animal 中的实例方法 eat(),所以这里可以直接调用 eat()。
System.out.println(thingToPrint);
}
}
// 调用类
package genericexample;
public class GenericExample {
public static void main(String[] args) {
GenericPrinter<Animal> animalPrinter = new GenericPrinter<>(new Animal());
animalPrinter.print();
/*输出:
It's fun to eat!
genericexample.Animal@1b6d3586
*/
// 但是 animalPrinter 这个实例本身并没有 eat() 方法,要注意区分概念。
// animalPrinter.eat(); // 报错:java: 找不到符号 符号: 方法 eat() 位置: 类型为genericexample.GenericPrinter<genericexample.Animal>的变量 animalPrinter
GenericPrinter<Cat> catPrinter = new GenericPrinter<>(new Cat());
catPrinter.print();
/*输出:
It's fun to eat!
genericexample.Cat@4554617c
*/
}
}
extends 多个接口
泛型的 extends 不仅可以是 extends 类
也可以是 extends 接口
(对的,不是使用 implements 接口
),并且可以同时 extends 多个
(比如 <T extends A & B & C>
,多个之间使用 &
来分隔)但由于 java 不支持多继承,所以只能同时继承一个类,但可以同时 extends 多个接口,并且同时 extends 类和接口时,要把类写在最前面,如:<T extends A类 & B接口 & C接口>
。若将接口写在了类前面,则会报错。如:<T extends B接口 & A类>
。
注意,当上例中若<T extends Animal & Serializable>
(Serializable 是一个接口),下面两句语句都会报错:
GenericPrinter<Animal> animalPrinter = new GenericPrinter<>(new Animal());
GenericPrinter<Cat> catPrinter = new GenericPrinter<>(new Cat());
这是因为无论是传入的 Animal 还是 Cat 都没有 implements Serializable
,也就是说想要不报错,需要修改为:
package genericexample;
import java.io.Serializable;
public class Animal implements Serializable { // 你得在 Cat 或 Animal 处把这个接口实现了
public void eat(){
System.out.println("It's fun to eat!");
}
}
public class Cat extends Animal {
}
// 泛型
package genericexample;
import java.io.Serializable;
public class GenericPrinter<T extends Animal & Serializable> {
T thingToPrint;
public GenericPrinter(T thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
thingToPrint.eat();
System.out.println(thingToPrint);
}
}
// 调用类
public class GenericExample {
public static void main(String[] args) {
GenericPrinter<Animal> animalPrinter = new GenericPrinter<>(new Animal());
animalPrinter.print();
GenericPrinter<Cat> catPrinter = new GenericPrinter<>(new Cat());
catPrinter.print();
}
}
// 输出结果:
/*
It's fun to eat!
genericexample.Animal@1b6d3586
It's fun to eat!
genericexample.Cat@4554617c
Process finished with exit code 0
*/
也就是说泛型的 extends 是要同时满足的,而非“或”。
泛型的作用
1. 参数化引用类型
泛型的本质是参数化引用类型。即,所操作的引用数据类型被指定为一个参数。
假设现在要写一个打印 Integer、Double、String 的方法,不使用泛型的情况下,代码应该如下:
// 打印 Integer 的类
package genericexample;
public class IntegerPrinter {
Integer thingToPrint;
public IntegerPrinter(Integer thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
System.out.println(thingToPrint);
}
}
// 打印 Double 的类
package genericexample;
public class DoublePrinter {
Double thingToPrint;
public DoublePrinter(Double thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
System.out.println(thingToPrint);
}
}
// 打印 String 的类
package genericexample;
public class StringPrinter {
String thingToPrint;
public StringPrinter(String thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
System.out.println(thingToPrint);
}
}
// 调用类
package genericexample;
public class GenericExample {
public static void main(String[] args) {
IntegerPrinter printer = new IntegerPrinter(3);
printer.print(); // 3
DoublePrinter printer1 = new DoublePrinter(3.3);
printer1.print(); // 3.3
StringPrinter printer2 = new StringPrinter("abc");
printer2.print(); // abc
}
}
即,如果想打印不同类型的数据,需要分别去定义该类的打印类与方法。如果还要打印各种自定义类如 Cat、Dog等类,那也要相应地为每个类去进行重新方法的方法定义,而这些方法的实现代码都是高度相似的。
而泛型,就能解决这个问题。使用泛型,代码可以如下:
package genericexample;
// 使用泛型,需要用尖括号将泛型标出,否则(如此处不标注<T>),类中直接提到 T 时,java 并不会知道这是个泛型,而是会去找名为 T 的类,如果找不到,就会报错。
public class GenericPrinter<T> {
T thingToPrint; // T 只是一个占位符的作用,实际类型是在使用时传入,所以起到一个类型参数的作用。另外 T 不一定是 T,可以是任意名称,只需要保证上下文的名称一致即可。即之前尖括号内写 T,后面就也写 T,尖括号内写 W,后面也写 W。
public GenericPrinter(T thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
System.out.println(thingToPrint);
}
}
// 调用类
package genericexample;
public class GenericExample {
public static void main(String[] args) {
// IntegerPrinter printer = new IntegerPrinter(3);
// printer.print();
//
// DoublePrinter printer1 = new DoublePrinter(3.3);
// printer1.print();
//
// StringPrinter printer2 = new StringPrinter("abc");
// printer2.print();
// 尖括号内的 Integer 将会被传递给 T
GenericPrinter<Integer> printer3 = new GenericPrinter<>(33);
printer3.print(); // 33
// 早前版本中,等号右侧的尖括号内要写上和左侧一样的类型不能省略,但 java 7 及以后版本右侧的尖括号内是可以留空省略的
GenericPrinter<Double> printer4 = new GenericPrinter<>(33.3);
printer4.print(); // 33.3
GenericPrinter<String> printer5 = new GenericPrinter<>("abcd");
printer5.print(); // abcd
}
}
如此一来,使用泛型,就可以做到只定义一个泛型打印类,却可以适用于任何类的打印。
但要注意的是,泛型不适用于基本类型,只能使用引用类型,即,<>
内不能是 int
,而要是对应的 Integer
。
GenericPrinter<int> printer8 = new GenericPrinter<>(3);
// 会报错:
// java: 意外的类型
// 需要: 引用
// 找到: int
2. 编译阶段的类型检查
泛型会在编译阶段进行类型检查。
下面以 ArrayList 举例说明泛型在编译阶段的类型检查。要注意,虽然引用类型(比如自定义类型 Cat)都是 Object 类的子类,但是 ArrayList<Cat>
并不是 ArrayList<Object>
的子类。
public class Animal {}
public class Cat extends Animal{}
public class Dog extends Animal{}
// 调用类
package genericexample;
import java.util.ArrayList;
public class GenericExample {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
animals.add(new Animal());
animals.add(new Object()); // 在未运行前,IDEA 就会对这一行标红报错
System.out.println(animals.size());
for (int i=0;i<animals.size();i++) {
System.out.println(animals.get(i));
}
}
}
// 由于 ArrayList<Animal> 指定了 ArrayList 链表中的元素只能为 Animal 类的实例,所以允许 Animal 及其子类,但并不允许往链表中加入其他类型。
// 若使用泛型,java 会在编译期间(非运行期间)就对类型进行检查,并不需要等到实际运行才报错。
package genericexample;
public class GenericPrinter<T> {
T thingToPrint;
public GenericPrinter(T thingToPrint){
this.thingToPrint = thingToPrint;
}
public void print(){
System.out.println(thingToPrint);
}
}
// 调用类
package genericexample;
public class GenericExample {
public static void main(String[] args) {
GenericPrinter<Integer> printer6 = new GenericPrinter<>("abcd");
// 报错。因为这里泛型要求是 Integer,但传入的是 String 类型。
}
}
类型的检查的作用
1. 无须强制类型转换即可调用该类型方法
不使用泛型时:List、Collection 元素的类型是 Object,而 Object 是所有类型的父类,所以其中的实际类型可以是不同的。(如在同一个 List 内包含不同类型的元素)
由于 ArrayList 默认存放的是 Object 类型,也就是可以往里面放任意的引用类型,你将别的类型放进去后,java 编译期间并不知道这是什么类型,而仅仅是将其视作 Object 类,因此你无法去调用该实际类型的方法,若想要调用该类的方法,需要进行类型的强制转换。
package genericexample;
public class Dog extends Animal {
public void sleep(){
System.out.println("Dog is sleeping.");
}
}
// 调用类
package genericexample;
import java.util.ArrayList;
public class GenericExample {
public static void main(String[] args) {
// 不使用泛型,需要进行强制类型转换才能调用该类型的方法
ArrayList dogs = new ArrayList();
dogs.add(new Dog());
System.out.println(dogs.get(0)); // genericexample.Dog@1b6d3586
System.out.println(dogs.get(0).getClass()); // class genericexample.Dog
Class clazz = dogs.get(0).getClass();
System.out.println(clazz); // class genericexample.Dog
// Dog myDog = dogs.get(0); // 报错。java: 不兼容的类型: java.lang.Object无法转换为genericexample.Dog
Dog myDog = (Dog)dogs.get(0);
// dogs.get(0).sleep(); // 报错:java: 找不到符号。符号: 方法 sleep() 位置: 类 java.lang.Object
((Dog) dogs.get(0)).sleep(); // Dog is sleeping.
myDog.sleep(); // Dog is sleeping.
}
}
可以看到,在虽然使用 .getClass()
能得到 ArrayList 里的元素是 Dog,也就是里面的元素的确是 Dog 类型的,但你直接去调用 Dog 类型的方法 .sleep()
就是不行,因为 java 在编译期间,会认为 ArrayList 里的元素就是 Object 类的,而 Object 类的实例是没有 .sleep()
方法的,所以无法调用。(这的确很蠢…)java 得真正运行才能获取到 ArrayList 里元素的类型,但编译阶段因为无法找到 .sleep()
方法,所以不会让你运行,强行运行只会报错。所以需要在代码中进行显式的强制类型转换为 Dog,才能调用 .sleep()
。
泛型最初的出发点就是为了解决这个问题——让这种通用的容器类里的元素有统一的规定的类型。
而使用泛型,明确告知 java 这个 ArrayList<Dog>
就只会放 Dog(及其子类),那么就可以直接将元素取出后直接调用 Dog 类的方法(.sleep()
),而无须再先进行显式的强制类型转换。
// 使用泛型,则无须在显式进行强制类型转换,即可调用该实际类型的方法
ArrayList<Dog> dogs2 = new ArrayList<>();
dogs2.add(new Dog());
System.out.println(dogs2.get(0)); // genericexample.Dog@4554617c
dogs2.get(0).sleep(); // Dog is sleeping.
Dog myDog2 = dogs2.get(0);
myDog2.sleep(); // Dog is sleeping.
[!tips] 集合(Collection)体系的全部接口和实现类都支持泛型的使用。
2. 类型安全
在 java 5 (不含)之前,没有泛型的情况下,通过对类型 Object 的引用来实现参数的“任意化”,带来的缺点是要做显式的类型转换后才能调用该数据类型的方法(如显式转换成 String 后才可以调用 String 方法)。
这种转换要求开发者对实际参数类型可预知,对于错误的预判,导致强制转换过程中报错,编译阶段是不会报错的,只有在运行阶段会抛出异常。
ArrayList 里的元素默认为 Object 类,因此可以往里面同时存放各种引用类型,即便你只往里面放一种类型,比如 Cat,也需要将 ArrayList 的元素强制类型转换为 Cat,之后才能调用 Cat 独有的方法。
但这个过程中因为 ArrayList 本身里面的元素类型并不强制为 Cat,比如说偶然地放进了一个 Dog 类的实例,你就无法进行强制类型转换为 Cat,并且在编译阶段是不会报错的,只会在运行期间报错,这导致类型安全无法得到保证,从而带来了安全隐患。
而泛型则可以明确地告诉 java 这个 ArrayList<Cat>
里只能放 Cat,若你偶然放了一个 Dog,在编译阶段就会报错,而不用等到运行阶段。这就避免了出现类型不安全的情况,编译期间只要不报错,运行期间就不会因为类型而出问题。
ArrayList<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Dog()); // 编译期间就报错,在 IDEA 中会直接对代码标红报错。
// 不使用泛型的话,以下代码编译期间并不会报错。
ArrayList cats = new ArrayList();
cats.add(new Cat());
cats.add(new Dog());
[!tips] 泛型的优点
- 统一数据类型。
- 把运行的问题提前到编译期间,避免了强制类型转换可能出现的问题,因为编译阶段类型便可以确定下来。
泛型方法
泛型也可以不标注在类名后,而只用于方法声明中,即只对一个类(可以是非泛型类)的某个方法使用泛型。
在方法中标注为泛型,需要在方法签名前的返回值前标注泛型,格式如下:
访问修饰符 <泛型占位符> 返回值类型 方法名(形参){}
public <T> void sayHello(T words){
System.out.println(words + "!");
}
// 返回值也可以是泛型
public static <whatever> whatever sayHi(whatever words){
return words;
}
// 还可以使用多个泛型
public <T,V> void sayGoodBye(T words, V voice){
System.out.println(words + "!!! with " + voice);
}
对比一下仅在某个方法中使用泛型,与在类定义中使用泛型的区别:
你会发现,所谓的泛型类、泛型接口、泛型方法,其实没有什么本质的区别,都是需要告诉 java 这个类型是泛型(<T>
),否则 java 不会知道你用的这个 T 类是泛型,而只会觉得这就是个普通的类,就会去尝试去寻找这个类,找不到的话,就会报错。因此需要明确地告知 java 这个 T 代表的是泛型。而“告知”,可以是在类名后、接口名后,也可以在方法的返回值前。位置的不同,使用了不同的称谓罢了(泛型类/泛型接口/泛型方法)。
// 仅在某个方法中使用泛型
public class GenericsExample{
public static void main(String[] args){
shout("John","gentle");
shout(123,"sweet");
shout(3.3,123);
shout(new Cat(),"funny");
}
private static <T,V> void shout(T thingToShout, V voice){
System.out.println(thingToShout + "!!! with " + voice);
}
}
// 类定义中使用泛型
public class GenericsExample<T,V>{
T thingToPrint;
V voice;
public GenericsExample(T thingToPrint, V voice){
this.thingToPrint = thingToPrint;
this.voice = voice;
}
public static void main(String[] args){
}
private static void shout(T thingToShout, V voice){ // 其实只是把泛型声明的占位符挪动到了类名之后罢了
System.out.println(thingToShout + "!!! with" + voice);
}
}
通配符
泛型还可以使用通配符 <?>
。
比如下面的场景中,printList()
方法的形参使用了泛型接口 List 的实例。但现在有个需求,就是我希望这个 printList()
方法可以传入任何元素类型的 List 实例,它都能帮我打印出来。
但问题是 printList()
的形参写 List<Animal>
就只能接收 List<Animal>
,甚至连传入 List<Cat>
都会报错(这是因为 Cat 虽然是 Animal 的子类,但 List<Cat>
并不是 List<Animal>
的子类)。
package genericexample;
import java.util.ArrayList;
import java.util.List;
public class GenericExample {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(3);
printList(intList); // java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.List<genericexample.Animal>
List<Cat> catList = new ArrayList<>();
catList.add(new Cat());
printList(catList); // java: 不兼容的类型: java.util.List<genericexample.Cat>无法转换为java.util.List<genericexample.Animal>
List<Animal> animalList = new ArrayList<>();
animalList.add(new Animal());
printList(animalList);
}
private static void printList(List<Animal> myList){
System.out.println(myList);
}
}
想要实现需求,让 printList()
可以接受所有类型的 List,就需要使用到通配符 <?>
private static void printList(List<?> myList){
System.out.println(myList);
}
这样写, printList()
即可以接受所有类型的 List。
通配符配合上下界
但如果想进一步只想接受 Animal 及其子类,就可以附加使用 extends
,如果只想接受 Cat 及其父类,则附加使用 super
。
// 只接受 Animal 及其子类
private static void printList(List<? extends Animal> myList){
System.out.println(myList);
}
// 或
// 只接受 Cat 及其父类
private static void printList(List<? super Cat> myList){
System.out.println(myList);
}
// 上面两种写法都会导致无法接收 List<Integer>。
若只是想对 <?>
有所了解,知道其“通配符”作用以及使用方式,如何设置上下界,则了解到这个小节就可以了。但进一步细究的话,<? extends XXX>
其实是叫“协变”,<? super XXX>
其实是叫“逆变”,详见后面的小节介绍。
通配符和占位符的区别
对比一下
private static void printList(List<?> myList){
System.out.println(myList);
}
// 若将 ? 换为 T 或者其他字符则会报错
private static void printList(List<T> myList){
System.out.println(myList);
}
// 若想 ? 换为 T 不报错,则需要在返回值前将 T 标注为泛型
private static <T> void printList(List<T> myList){
System.out.println(myList);
} // 这样写的效果,与使用通配符的效果是一样的
// 但这种写法并不能配合 extends、super 使用,会报错。
private static <T> void printList(List<T extends Animal> myList){
System.out.println(myList);
} // 报错
private static <T> void printList(List<T super Cat> myList){
System.out.println(myList);
} // 报错
// 对比占位符
private static <T> void printList(T myList){
System.out.println(myList);
}
细心观察可得,泛型的通配符的使用场景是使用带有泛型的类时,而非用作占位符来表示当前类不确定为什么类,而是确定是当前使用类(比如确定使用 List),但不确定具体使用当前使用类的哪个泛型类(当前使用类被定义为泛型类 class 类名<T>{}
)。
注意对比 有界泛型
与 通配符的上下界
,通配符是可以定义上界(extends)和下界(super)的,但有界泛型(无论是泛型类、泛型接口还是泛型方法的声明中)只支持 extends 并不支持 super,并且即便是在泛型方法声明中也是不支持 super 的。如:
private static <T super Cat> void printOhterThing(T otherThing){ // 不允许这种语法
}
// 当然也不允许下面两种语法
private static <? super Cat> void printOtherThing(? otherThing){
}
private static <? extends Animal> void printOtherThing(? otherThing){
}
// 只可以这样
private static <T extends Animal> void printOtherThing(T otherThing){
}
其实还可以留意到,有界泛型
的声明是分成两部分(占位符的声明、占位符的使用),而通配符
只需在一处使用。
通配符只能用在类型的声明中,不能用在实例的创建中。
- 变量的类型声明
- 参数的类型声明
TestClass<?> animal = new TestClass<>(new Cat()); // 合法
TestClass<?> animal = new TestClass<?>(new Cat()); // 非法
TestClass<Animal> animal = new TestClass<?>(new Cat()); // 非法
TestClass<Animal> animal = new TestClass<Animal>(new Cat()); // 合法
TestClass<? extends Animal> animal = new TestClass<>(new Cat()); // 合法
TestClass<? extends Animal> animal = new TestClass<? extends Animal>(new Cat()); // 非法
// 留意到在形参定义中使用,其实也只是对类型的声明
private static void printList(List<?> myList){
System.out.println(myList);
}
协变与逆变
Java 泛型对协变和逆变的支持是为了支持范围更广的参数类型,让参数和返回值等引用类型的泛型类型更灵活。
协变和逆变是针对引用类型而言的,可以用在返回值类型、参数类型(形参)等引用的类型上。创建对象的时候,不能使用协变和逆变。
- 即,
ArrayList<String> strList = new ArrayList<String>();
, 协变和逆变针对的是等号左边的“引用”里面的 String 类型,而非等号右边创建对象里尖括号里的类型。后者是没有协变和逆变的。
ArrayList<Parent> a1 = new ArrayList<>(); // 正常执行
ArrayList<Parent> b1 = new ArrayList<Parent>(); // 正常执行
ArrayList<? extends Parent> a2 = new ArrayList<Parent>(); // 正常执行
ArrayList<? extends Parent> a22 = new ArrayList<Children>(); // 正常执行
ArrayList<? extends Parent> b2 = new ArrayList<? extends Parent>(); // 报错
ArrayList<? super Parent> a3 = new ArrayList<Parent>(); // 正常执行
ArrayList<? super Parent> a32 = new ArrayList<GrandParent>(); // 正常执行
ArrayList<? super Parent> b3 = new ArrayList<? super Parent>(); // 报错
协变:允许类型为指定类型及其子类(<? extends XXX>
)
// 协变:形参中使用
public static void fun1(ArrayList<? extends GrandParent>){}
/*
若不使用协变,只定义为
public static void fun1(ArrayList<GrandParent>){}
则以下是非法的:
fun1(ArrayList<Children>的引用);
fun1(ArrayList<Parent>的引用);
如:
ArrayList<Children> childrenList = new ArrayList<>();
fun1(childrenList); // 报错
ArrayList<Parent> parentList = new ArrayList<>();
fun1(parentList); // 报错
这是因为虽然 Children、Parent 是 GrandParent 的子类,但 ArrayList<Children>、ArrayList<Parent> 并非是 ArrayList<GrandParent> 的子类。
使用协变后,形参可以接受的参数类型范围就变广了。(这就是 java 的一种语法规定,不用纠结太多)
ArrayList<GrandParent> 只能接受 ArrayList<GrandParent>,而
ArrayList<? extends GrandParent> 可以接受 ArrayList<GrandParent> 及其泛型的子类(如:ArrayList<Parent>、ArrayList<Children>)
*/
// 协变:也可在返回值、成员变量、局部变量中使用
ArrayList<? extends GrandParent> exList = null;
exList = new ArrayList<GrandParent>();
exList = new ArrayList<Parent>();
exList = new ArrayList<Children>();
exList = new ArrayList<Object>(); // 报错
但是带协变泛型的引用,无法让具体的类型满足其参数要求。(即实际为协变泛型的形参,无法接受任何类型的参数)
另外只有引用可以带协变信息,而带协变泛型的引用所指向的具体对象实例是不带有任何泛型信息的。(类型擦除)
// 泛型类
public class TestClass<T> {
public void fun1(T something){ // 泛型作为形参
}
}
public class GrandParent {}
public class Parent extends GrandParent{}
public class Children extends Parent{}
// 调用类
package covariance;
import java.util.ArrayList;
public class TestUse {
public static void main(String[] args) {
TestClass<Parent> a1 = new TestClass<>();
a1.fun1(new Parent());
a1.fun1(new Children());
// a1.fun1(new GrandParent());
// /*
// 编译期间报错:
// Required type: Parent
// Provided: GrandParent
// */
TestClass<? extends Parent> a2 = new TestClass<Parent>();
// a2.fun1(new Parent()); // 编译期间报错:java: 不兼容的类型: covariance.Parent无法转换为capture#1, 共 ? extends covariance.Parent
// a2.fun1(new Children()); // 编译期间报错:java: 不兼容的类型: covariance.Children无法转换为capture#1, 共 ? extends covariance.Parent
// a2.fun1(new GrandParent()); // 编译期间报错:java: 不兼容的类型: covariance.GrandParent无法转换为capture#1, 共 ? extends covariance.Parent
// 这是因为引用类型为协变时(<? extends Parent>), fun1(T something) 中的 T 无法确定到底是什么类型,所以所有类型的实参都无法接收。
TestClass<Parent> a11 = a1;
a11 = (TestClass<Parent>)a2;
a11.fun1(new Parent());
a11.fun1(new Children());
// a11.fun1(new GrandParent());
// /*
// 编译期间报错:
// Required type: Parent
// Provided: GrandParent
// */
// 由于类型擦除,实际带协变泛型的对象也是没有记录泛型信息的,用非协变的泛型引用 a11 指向该对象后,a11.fun1() 的传参类型要求和 a1.fun1() 一致。
a2 = a1;
// a2.fun1(new Parent()); // 编译期间报错:java: 不兼容的类型: covariance.Parent无法转换为capture#1, 共 ? extends covariance.Parent
// a2.fun1(new Children()); // 编译期间报错:java: 不兼容的类型: covariance.Children无法转换为capture#1, 共 ? extends covariance.Parent
// a2.fun1(new GrandParent()); // 编译期间报错:java: 不兼容的类型: covariance.GrandParent无法转换为capture#1, 共 ? extends covariance.Parent
// 由于类型擦除,所以其实 a1 原来指向的对象是不带泛型类型信息的,让带协变泛型的引用 a2 指向该对象后,a2.fun1() 仍旧是无法传入任何类型参数。
// 再用 ArrayList 来验证一下。
ArrayList<Parent> b1 = new ArrayList<>();
b1.add(new Parent());
b1.add(new Children());
b1.add(new GrandParent());
/*
编译期间报错:
Required type: Parent Provided: GrandParent */
ArrayList<? extends Parent> b2 = new ArrayList<Parent>();
// b2.add(new GrandParent()); // 编译期间报错
// b2.add(new Parent()); // 编译期间报错
// b2.add(new Children()); // 编译期间报错
}
}
// 总结:由于引用的泛型类型为 <? extends XXX>,则方法的泛型形参(fun1(T t))无法确定是哪种类型,因此不能接收任何类型的实参传入。
// 因为 <? extends GrandParent> ,那 T 可能是 GrandParent,Parent,Children,...(甚至Children之下有更多层子类),若 T 是 GrandParent,自然可以传入所有的子类对象。但若 T 是 Children,那么 fun1(Children children) 就不能传入 new Parent() 或 new GrandParent() 这类的实参。
// 所以带协变的泛型 <? extends XXX> 你是无法确定有哪个类型的实参是一定能传入进去而发生问题的,所以不允许传入任何类型的参数。
为什么 java 设计协变时,允许 ArrayList<? extends Parent> pList = new ArrayList<Children>();
,却不顺带让 fun1(T)
在使用带协变泛型时传入为 fun1(? extends Parent)
,然后让其接收 Parent 及其子类对象作为实参呢?
可以看一下例子:
// 创建一个 ArrayList 对象,让其元素必须为 Children 类型
ArrayList<Children> cList = new ArrayList<>();
// 而由于泛型其实在对象中是类型擦除的,也就是以下是合法的:
ArrayList<? extends Parent> pList = cList;
// 也就是现在, cList 和 pList 都指向了同一个 ArrayList 对象。但这个对象我们是设计为只接受 Children 类型的,如果以下语法是合法的,则会导致可以往这个 ArrayList 对象里插入 Parent 对象(即 Children 以外的对象),这就会对泛型构成污染。
pList.add(new Parent); // 这似乎很合理,因为 pList 是 <? extends Parent>,类型包含了 Parent,但若真的允许,就会造成泛型污染。
/*
备注:
ArrayList<E> 的 .add(E e) 形参为泛型
*/
// 所以这其实是出于语言的严谨性来考虑的,如果带协变的泛型引用,不设计成“无法让具体的类型满足其参数要求”,则即便让一个容器对象里混入不符合泛型要求的元素类型,编译期间也不会报错,只在运行期间报错。那么“泛型”(本身设计的初衷就是想在编译期间就做好类型检查)的作用就大打折扣了。
而带协变泛型的引用,泛型作为返回值,则是可以有明确的接收类型的。
// 泛型类
public class TestClass<T> {
public T fun1(Object val){ // 泛型作为返回值
return (T)val;
}
}
public class GrandParent {}
public class Parent extends GrandParent{}
public class Children extends Parent{}
// 调用类
public class TestUse {
public static void main(String[] args) {
TestClass<? extends Parent> a1 = new TestClass<Parent>();
Parent ret = a1.fun1();
GrandParent ret2 = a1.fun1();
// Children ret3 = a1.fun1(); // 编译期间报错:java: 不兼容的类型: capture#1, 共 ? extends covariance.Parent无法转换为covariance.Children
TestClass<? extends Parent> a2 = new TestClass<Children>();
Parent ret4 = a2.fun1();
GrandParent ret5 = a2.fun1();
// Children ret6 =a2.fun1(); // 编译期间报错:java: 不兼容的类型: capture#1, 共 ? extends covariance.Parent无法转换为covariance.Children
// 这是由于对象其实是类型擦除的,并不会记录自己是 Children 类,不会将 T 换成 Children,然后就刚好返回值可以被 Children 类型的引用接收,而还是要看实际的引用类型(调用函数的那个引用)。
// 实际的引用类型是带协变的泛型,那么返回值就一定是协变上界的子类,因此必然可以被上界的类型(或上界的父类)的引用所接收。(注意:这个接收返回值的类型和泛型没有关系,和泛型有关系的是调用方法的引用:a2)
// 也就是说,对于带协变泛型的引用,泛型为返回值时,可以用协变上界类型及其父类来接收返回值。
}
}
协变小结:
- 不允许该泛型作为方法的形参;
- 该泛型作为方法的返回值时,可以用协变的上界类型及其父类的引用来接收此返回值。
逆变:允许类型为指定类型及其父类(<? super XXX>
)
// 逆变:形参中使用
public static void fun1(ArrayList<? super Parent>){}
/*
使用逆变后,形参可以接受的参数类型范围就变广了。(这就是 java 的一种语法规定,不用纠结太多)
ArrayList<Parent> 只能接受 ArrayList<Parent>,而
ArrayList<? super Parent> 可以接受 ArrayList<Parent> 及其泛型的父类(如:ArrayList<GrandParent>、ArrayList<Object>)
*/
// 逆变:也可在返回值、成员变量、局部变量中使用
ArrayList<? super Parent> exList = null;
exList = new ArrayList<GrandParent>();
exList = new ArrayList<Parent>();
exList = new ArrayList<Children>(); // 报错
exList = new ArrayList<Object>(); // 由于 Object 也是 Parent 的父类,因此也不会报错
与协变不同,带逆变泛型的引用,可以让具体的类型(下界及其子类)满足其参数要求。
// 带逆变泛型的引用,可以让具体的类型(下界及其子类)满足其参数要求。
// 这是因为,由于引用的泛型类型为 <? super XXX>,则方法的泛型形参(fun1(T t))虽然无法确定是哪种类型,但至少可以确定的是一定可以接收 XXX 及其子类的对象作为实参传入。
// 因为 <? extends Parent> ,那 T 可能是 Parent,GrandParent,...(甚至可以假设GrandParent与Object之间还可以有多层父类),Object。但无论 T 是 Parent 还是 GrandParent,都必然可以传入 Parent 及其子类(Children、甚至是 Children 的子类)的实例对象作为实参。因为这就只是将子类对象赋值给父类引用罢了,java 是必然允许的。
// 所以带逆变的泛型 <? super XXX> 你是可以确定下界类及其子类的实参是一定能传入进去而不发生问题的,所以允许传入下界类及其子类。
// 泛型类
public class TestClass<T> {
public void fun1(T something){ // 泛型作为形参
}
}
public class GrandParent {}
public class Parent extends GrandParent{}
public class Children extends Parent{}
// 调用类
import java.util.ArrayList;
public class TestUse {
public static void main(String[] args) {
TestClass<? super Parent> a2 = new TestClass<Parent>();
a2.fun1(new Parent());
a2.fun1(new Children());
// a2.fun1(new GrandParent()); // 编译期间报错:java: 不兼容的类型: covariance.GrandParent无法转换为capture#1, 共 ? super covariance.Parent
// a2.fun1(new Object()); // 编译期间报错:java: 不兼容的类型: java.lang.Object无法转换为capture#1, 共 ? super covariance.Parent
// 这是因为引用类型为逆变时(<? super Parent>), fun1(T something) 中的 T 无论到底是什么类型,它都是 Parent 或其父类,所以必然可以接收 Parent 及其子类作为实参。
// 但<? super Parent> 也就是 T 可以是Parent、GrandParent、Object,若是传入的实参为 Parent 的父类,则因为除Parent外无法保证能接收,所以除Parent外都不允许传入。
// 另外,java 这样设计,也是为了让泛型不被破坏。
ArrayList<Parent> c1 = new ArrayList<>();
ArrayList<? super Parent> c2 = null;
c2 = c1; // 由于对象是类型擦除的,你可以让一个带逆变的泛型应用指向它。
// c2.add(new GrandParent()); // 编译期间报错
/*
如果 java 允许这种语法,则会导致原先你设计的 c1 是打算用来只放 Parent 元素的,现在由于可以用 <? super Parent> 指向这个 ArrayList 对象,导致你可以往这个 ArrayList 放入
GrandParent 甚至 Object 类对象,若语法允许(不给你编译期间报错),则破坏了 c1 的泛型(Parent)约束。那么 java 的泛型语法的设计就变得大打折扣了。
*/
}
}
而带逆变泛型的引用,泛型作为返回值,则是无法让具体的类型(除 Object 外)满足其返回值的接收类型的。
// 泛型类
public class TestClass<T> {
public T fun1(){
return null;
}
}
public class GrandParent {}
public class Parent extends GrandParent{}
public class Children extends Parent{}
// 调用类
package covariance;
import java.util.ArrayList;
public class TestUse {
public static void main(String[] args) {
TestClass<? super Parent> a1 = new TestClass<>();
// Parent ret = a1.fun1(); // 编译期间报错:java: 不兼容的类型: capture#1, 共 ? super covariance.Parent无法转换为covariance.Parent
// GrandParent ret2 = a1.fun1(); // 编译期间报错:java: 不兼容的类型: capture#1, 共 ? super covariance.Parent无法转换为covariance.GrandParent
// Children ret3 = a1.fun1(); // 编译期间报错:java: 不兼容的类型: capture#1, 共 ? super covariance.Parent无法转换为covariance.Children
Object ret4 = a1.fun1(); // 正常运行
// 这是因为返回值类型是看当前泛型引用 a1 的类型,带逆变的泛型 <? super Parent> 其返回值 T 就可能是 Parent、GrandParent、...(甚至GrandParent与Object期间还有多层父类)、Object,
// 所以只有 Object 类引用能保证可以接收(指向)这个返回值
}
}
逆变小结:
- 该泛型作为方法的形参时,只能传入逆变的下界类型及其子类作为实参;
- 该泛型作为方法的返回值时,除 Object 类引用外,无法让具体类型的引用来接收此返回值。
[!tips] producer-extends, consumer-super(PECS)
写入(泛型作为形参)使用逆变(用其下界类型及下界类型的子类来作为实参传入);
读取(泛型作为返回值)使用协变(用其上界类型及其上界类型的父类来接收返回值);
若要求这个泛型又能写入,又能读取,就不能带协变或逆变。
接收引用 <------ <? xxx T> <-------赋值引用
协变:<? xxx T> 一定能被 T 接收(返回值考虑被接收)
接收引用 <------ <? extends T>
逆变:<? xxx T> 一定能被 T 赋值(形参考虑被赋值)
<? super T> <-------赋值引用
写参逆,读返协!写参逆,读返协!写参逆,读返协!
(血掺腻,毒翻蟹:在血液里掺了腻子,毒翻了螃蟹)
泛型方法的重载问题
java 的方法重载是基于方法签名的,那么泛型这种比较特殊的方法签名到底如何定性是否发生重载?
实际测试代码如下:
// 泛型类
public class TestClass<T> {
// 泛型类里的普通方法
public void fun1(){
System.out.println("fun1");
}
// 泛型类里形参为泛型的方法
public void iter(T t){ // 编译期间报错:'iter(T)' clashes with 'iter(E)'; both methods have same erasure
System.out.println("T iter");
}
// 若调换 iter(T t) 与 <E> iter(E e) 的前后关系,
// 则 IDEA 编译期间划红色波浪底线(报错)的会是 <E> iter(E e),
// 所以并非是说 “泛型里的泛型方法”比“泛型里的参数为泛型的方法”地位更高,两者地位没有哪个更高。
// 泛型类里的泛型方法
public <E> void iter(E e){
System.out.println("E iter");
}
// 泛型类里普通方法
public void iter(String n){
System.out.println("String iter");
}
}
/*
经过测试,允许同时存在 iter(T t) 与 iter(String n)或
<E> iter(E e) 与 iter(String n)
即这种参数为泛型的,只能看作是同一个方法签名(哪怕占位符不同),并且使用“泛型参数” 与使用任何其他类型都视作不同的方法签名。
*/
下面在分别对 iter(T t) 与 iter(String n)
、<E> iter(E e) 与 iter(String n)
来进行一次方法调用的测试,看到底有什么表现。
// <E> iter(E e) 与 iter(String n)
// 泛型类
public class TestClass<T> {
// 泛型类里的普通方法
public void fun1(){
System.out.println("fun1");
}
// // 泛型类里形参为泛型的方法
// public void iter(T t){ // 编译期间报错:'iter(T)' clashes with 'iter(E)'; both methods have same erasure
// System.out.println("T iter");
// }
// 泛型类里的泛型方法
public <E> void iter(E e){
System.out.println("E iter");
}
// 泛型类里普通方法
public void iter(String n){
System.out.println("String iter");
}
public void iter(int n){
System.out.println("int iter");
}
public void iter(double n){
System.out.println("double iter");
}
}
// 调用类
public class TestUse {
public static void main(String[] args) {
TestClass<Parent> a1 = new TestClass<>();
a1.iter(123); // int iter ,即使将 iter(int n) 与 iter(double n) 方法注释,此处也不会报错,而是输出变为 E iter。
a1.iter("abc"); // String iter
a1.iter(new Parent()); // E iter
TestClass<Parent> a4 = new TestClass<>();
a4.iter(13); // int iter
a4.iter(13L); // double iter,如果存在 float 类型的重载方法,则调用的是 float iter,由于现在没有,所以往更高精度靠,long 类型找到的是 double 的重载方法。
a4.iter(13.12); // double iter
a4.iter(13.5f); // double iter
}
}
// 即,传入 String 类型时,优先会匹配 iter(String n) 而非 <E> iter(E e),匹配不到 String 类型的形参,才会去找泛型方法。
// iter(T t) 与 iter(String n)
// 泛型类
public class TestClass<T> {
// 泛型类里的普通方法
public void fun1(){
System.out.println("fun1");
}
// 泛型类里形参为泛型的方法
public void iter(T t){ // 编译期间报错:'iter(T)' clashes with 'iter(E)'; both methods have same erasure
System.out.println("T iter");
}
// // 泛型类里的泛型方法
// public <E> void iter(E e){
// System.out.println("E iter");
// }
// 泛型类里普通方法
public void iter(String n){
System.out.println("String iter");
}
public void iter(int n){
System.out.println("int iter");
}
public void iter(double n){
System.out.println("double iter");
}
}
// 调用类
public class TestUse {
public static void main(String[] args) {
TestClass<Parent> a1 = new TestClass<>();
a1.iter(123); // int iter ,若将 iter(int n) 与 iter(double n) 方法注释,则此处会报错:java: 对于iter(int), 找不到合适的方法
a1.iter("abc"); // String iter
a1.iter(new Parent()); // T iter
TestClass a2 = new TestClass();
a2.iter(123); // int iter
a2.iter("abc"); // String iter
a2.iter(new Parent()); // T iter
TestClass<Parent> a3 = a2;
a3.iter(123); // int iter
a3.iter("abc"); // String iter
a3.iter(new Parent()); // T iter
TestClass<Parent> a4 = new TestClass<>();
a4.iter(13); // int iter
a4.iter(13L); // double iter,如果存在 float 类型的重载方法,则调用的是 float iter,由于现在没有,所以往更高精度靠,long 类型找到的是 double 的重载方法。
a4.iter(13.12); // double iter
a4.iter(13.5f); // double iter
}
}
// 即泛型类里形参为该泛型同名占位符的方法,由于用该泛型类型的引用来调用方法,所以参数的泛型由当前调用它的引用类型决定(在实际执行该方法前前传入),因此会受到该泛型类型的约束,不能随意传入其他类型作为实参。
// 这一点也是 <E> iter(E e) 和 iter(T t) 的最重要的区别。发生重载时,前者是可以为任意类型兜底的,而后者并不能兜底。
// 而调用方法时,重载的优先级还是依然选择了 iter(String n) 而非 iter(T t),匹配不到 String 类型的形参,才会去找泛型方法。
带泛型参数的方法重载的总结:
- 同名方法参数为泛型的,只能看作是同一个方法签名(哪怕占位符不同),并且使用“泛型参数” 与使用任何其他类型都视作不同的方法签名。 (即,E 和 T 作为泛型,java 会认为是同一个方法签名,不能重复出现,而无论是 E 或 T ,对具体类型 String、Integer、或其他自定义类型来说,都是不同的类型)
- 当方法发生重载时,调用优先级是先去找类中形参为该类型(或靠近该类型的确定类型:byte、short、int、long、float、double,往右靠)的方法,找不到的话再去找参数带泛型的方法。
泛型方法的覆盖问题
其实从泛型方法的重载就可以知道带泛型形参的方法与带确定类型形参的同名方法是属于不同的方法签名,所以是无法互相覆盖的。
package overridetest;
public abstract class Father {
public abstract void sayHi(int num);
}
package overridetest;
public class Children extends Father{
// 即便把 @Override 注释,编译期间也会报错:java: overridetest.Children不是抽象的, 并且未覆盖overridetest.Father中的抽象方法sayHi(int)
// @Override
public <T> void sayHi(T num) {
System.out.println("hi "+num);
}
}
而因带泛型形参的两个同名方法仅仅占位符名字不同,却不会被视作是不同的方法签名,所以不同占位符不会影响到方法的覆盖。
package overridetest;
public abstract class Father {
public abstract <T> void sayHi(T num);
}
package overridetest;
public class Children extends Father{
@Override
public <E> void sayHi(E num) {
System.out.println("hi "+num);
}
}
// 调用类
public class TestUse {
public static void main(String[] args) {
Children children = new Children();
children.sayHi(123); // hi 123
children.sayHi("abc"); // hi abc
}
}
注意,若不是泛型方法,而是出现泛型类里的形参为泛型的方法,则覆盖的时候必须要使用 Object 类来作为形参类型来进行覆盖。
public abstract class Father <T>{
public abstract void sayHi(T t);
}
public class Children extends Father{
@Override
public void sayHi(int num) { // 报错:java: Children不是抽象的, 并且未覆盖Father中的抽象方法sayHi(java.lang.Object)
System.out.println("hi "+num);
}
}
public class Children extends Father{
@Override
public <T> void sayHi(T num) { // 报错:java: Children不是抽象的, 并且未覆盖Father中的抽象方法sayHi(java.lang.Object)
System.out.println("hi "+num);
}
}
public class Children<T> extends Father{ // 报错:java: Children不是抽象的, 并且未覆盖Father中的抽象方法sayHi(java.lang.Object)
@Override
public void sayHi(T num) {
System.out.println("hi "+num);
}
}
public class Children extends Father{
@Override
public void sayHi(Object num) {
System.out.println("hi "+num);
}
}
// 调用类
public class TestUse {
public static void main(String[] args) {
Children children = new Children();
children.sayHi(123); // hi 123
children.sayHi("abc"); // hi abc
}
}
另外要注意,即便 Father是泛型类(Father<T>
),其子类不一定是泛型类,只有当 Children 定义为 public class Children<T> extends Father
,才能在调用时 Children<Integer> children = new Children<>()
对类型参入参数(如Integer),若定义为 public class Children extends Father
,那么调用时是不能对 Children 传入参数的,只能 Children children = new Children<>()
。
泛型的本质
泛型的本质是参数化类型,即,所操作的数据类型被指定为一个参数。可以以此来理解“泛型在定义时类型名只是起到‘占位符’的作用”,使用时,才会将真正的数据类型传递给该泛型类、泛型接口、泛型方法。
泛型可以在编译阶段就检查类型安全,并且所有的强制类型转换都是自动且隐式进行的。
使用泛型时,java 到底做了些什么工作
在编译阶段告诉编译器帮助我们检查类型是否匹配(类型是什么不重要,类型一样才重要);并且在使用的地方悄悄帮我们做类型强制转换。
对于泛型,java 只在编译阶段做检查,但信息并没有被带到生成的 .class 文件中去。即,在 java 执行时,并不包含这些信息,java并不知道这个元素是什么类型。
- 比如 java 运行期间并不知道 ArrayList 里放的都是 String 类型,信息会被抹除掉。
具体实现方式是在编译阶段帮我们实现了强制类型转换,即生成 .class 文件的时候插入了强制转换的命令。
简单来说,可以理解为:泛型,并不会让 java 记住这个引用类(缺省为 Object 类)被传递的类型,java 仍旧会认为它是个 Object 类型。比如你定义 Object obj = new Object();
,然后对这个泛型传入了一个类型 String,java 并不会编译后帮你改为 String obj = new String();
,而是在你使用这个 obj 时,在编译后的 .class 文件中使用 obj 的语句后自动插入一句强制类型转换(java 自己做的,不需要程序员来写,程序要只需要将其指定为泛型),如 (String)obj
将其强制类型转换成 String (假设不会导致报错)。
下面验证一下,java 有没有记住“类型”。
类型擦除
public class TestClass<T> {
T something;
public TestClass(T something){
this.something = something;
}
}
// 调用类
import java.lang.reflect.Field;
public class TestUse {
public static void main(String[] args) throws NoSuchFieldException {
TestClass<String> test = new TestClass<>("abc");
Field field1 = test.getClass().getDeclaredField("something");
System.out.println(field1.getType()); // class java.lang.Object
}
}
// 可见,something 仍然是 Object 类,而非泛型指定的 String 类,这就是所谓的“类型擦除/信息抹除”,java 执行时实际上并不知道 something 是什么类型的(String 或者自定义类型)。
public class TestClass<T extends Animal> {
T something;
public TestClass(T something){
this.something = something;
}
}
// 调用类
import java.lang.reflect.Field;
public class TestUse {
public static void main(String[] args) throws NoSuchFieldException {
TestClass<Cat> test = new TestClass<>(new Cat());
Field field1 = test.getClass().getDeclaredField("something");
System.out.println(field1.getType()); // class genericexample.Animal
}
}
// 若设置为有界泛型,则 java 会将其默认类从 Object 变为上界类型(这里是 Animal)。
类型擦除,会导致“绕过泛型约束”成为可能。比如 ArrayList<String> strList = new ArrayList<>()
,创建的对象,由于对象本身并没有记录 String 类型的信息,所以可以用另外一个不带泛型类型约束的引用指向它(如:ArrayList list = strList
),对 list
操作,发现可以往里面添加非 String 类型的元素。
// 泛型类
public class GenericPrinter<T extends Animal> {
T thingToPrint;
public GenericPrinter(T thingToPrint) {
this.thingToPrint = thingToPrint;
}
public void sayHi(T name) {
System.out.println("Hi, " + name);
}
}
// 定义 3 个有继承关系的类
public class Animal {
public void eat(){
System.out.println("It's fun to eat!");
}
}
public class Dog extends Animal {
public void sleep(){
System.out.println("Dog is sleeping.");
}
}
public class Cat extends Animal {
@Override
public String toString() {
return "meow";
}
}
// 调用类
import java.util.ArrayList;
public class GenericExample {
public static void main(String[] args) {
// 用不带泛型类型信息的引用指向一个带泛型类型信息创建的对象
GenericPrinter catPrinter = new GenericPrinter<Cat>(new Cat());
catPrinter.sayHi(new Animal()); // Hi, genericexample.Animal@74a14482
catPrinter.sayHi(new Dog()); // Hi, genericexample.Dog@1540e19d
catPrinter.sayHi(new Cat()); // Hi, meow
// 也就是即便让这个对象传入了 Cat 类型,但是用这个泛型类不传入泛型类型信息的引用来指向它,实际这个对象的泛型信息是被擦除掉的,否则这里应该只能调用成功 .sayHi(new Cat()),而.sayHi(new Dog()) 应该是要报错的,但现在都能正常传入执行。
// 在 IDEA 中,使用 catPrinter.sayHi() 时,提示要传入的类型也是 Animal,即为这个泛型的默认类型。
// 用带泛型类型信息的引用指向一个不带泛型类型信息创建的对象
GenericPrinter<Dog> dogPrinter = new GenericPrinter(new Cat());
dogPrinter.sayHi(new Dog()); // Hi, genericexample.Dog@4554617c
// dogPrinter.sayHi(new Cat()); // 编译阶段就直接报错:java: 不兼容的类型: genericexample.Cat无法转换为genericexample.Dog
// 在 IDEA 中,使用 dogPrinter.sayHi() 时,提示要传入的类型是 Dog。也就是说,这个泛型类中的方法(其形参类型与泛型类的泛型一致)到底可以传入什么类型的参数,要看是用哪个类型的引用指向了这个类的实例对象,
// 而这个实例对象本身并没有记录类型信息。
// GenericPrinter<Dog> dogPrinter2 = new GenericPrinter<>(new Cat());
// /*
// 编译期间就报错:
// Required type: GenericPrinter<Dog>
// Provided: GenericPrinter<Cat>
// */
// GenericPrinter<Dog> dogPrinter3 = new GenericPrinter<Dog>(new Cat());
// /*
// 编译期间就报错:
// Required type: Dog
// Provided: Cat
// */
// GenericPrinter dogPrinter4 = new GenericPrinter<Dog>(new Cat());
// /*
// 编译期间就报错:
// Required type: Dog
// Provided: Cat
// */
GenericPrinter dogPrinter5 = new GenericPrinter<Dog>(new Dog()); // 正常执行
dogPrinter5.sayHi(new Animal()); // Hi, genericexample.Animal@677327b6。在 IDEA 中,使用 dogPrinter5.sayHi() 时,提示传入的是默认的 Animal 类型。
// GenericPrinter dogPrinter6 = new GeneralRenderer<>(new Dog());
// /*
// 编译期间就报错:
// Diamond operator is not applicable for non-parameterized types.(即这种写法是非法的)
// */
// GenericPrinter<Dog> dogPrinter7 = new GenericPrinter<Cat>(new Dog());
// /*
// 编译期间就报错:
// Required type: Cat
// Provided: Dog
// */
// GenericPrinter<Dog> dogPrinter8 = new GenericPrinter<Cat>(new Cat());
// /*
// 编译期间就报错:
// Required type: GenericPrinter<Dog>
// Provided: GenericPrinter<Cat>
// */
// 也就是说,对象本身并不记录(包含)泛型的类型信息,泛型类里含该泛型类型形参的方法最终能传入什么类型的参数,取决于是当前是哪个泛型类型的引用指向这个对象。则调用这些方法前会将泛型类型传给该方法。
// 然后 java 再检查实际传入参数,是否和泛型指定的类型相符。
// 如果是 GenericPrinter<Dog> dogPrinter2 = new GenericPrinter<>(new Cat()) 这种语句,
// java 会首先检查等号的右边的泛型,如上述右边允许尖括号(意味着等号左边的引用也是带传入类型的),那么泛型就生效,需要检查这个传入参数 new Cat() 的类型是否与泛型指定的类型相符,
// 如果此时尖括号是空的,并不意味这和左边传入相同的类型,而是 java 会觉得你可以传入 new Cat(),所以尖括号内会是 Cat。
// 如果右边符合泛型类型的约束要求,此时再执行赋值,由于是赋值给带泛型类型的引用,所以赋值前 java 会做强制类型转换,因此
// GenericPrinter<Dog> dogPrinter8 = new GenericPrinter<Cat>(new Cat());
// 等价于:GenericPrinter<Dog> dogPrinter8 = (GenericPrinter<Dog>)new GenericPrinter<Cat>(new Cat());
ArrayList<String> strList = new ArrayList<>();
strList.add("abc"); // 正常执行
// strList.add(123); // 编译阶段就报错,并且在 IDEA 中,使用 strList.add() 时,提示要传入的类型为 String
ArrayList list = strList;
list.add(123); // 正常执行,并且在 IDEA 中,使用 list.add() 时,提示要传入的类型为 Object,即 ArrayList 中元素的默认类型
// 也就是说因为这个类型擦除的机制,实际上 java 并没有在泛型类的对象信息中记录其类型(要求必须是哪个类型),而是在使用时默默地添加了强制类型转换的语句。
// 这导致可以绕过类型检查,方式是另外让一个没有传入泛型类型信息的引用(list)指向这个对象(有传入泛型类型的引用 strList 所指向的 new ArrayList<>() 对象),然后用这个没有泛型类型信息的引用去调用方法。
// 如上,就能往原本打算只能放 String 的 ArrayList,放入其他任何类型。
}
}
总结:
- 对象本身并不记录(包含)泛型的类型信息,泛型类里含该泛型类型形参的方法最终能传入什么类型的参数,取决于是当前是哪个泛型类型的引用指向这个对象。则调用这些方法前会将泛型类型传给该方法。
new XXX<Cat>(new Dog())
创建泛型类的实例对象,也仍旧没有将 Cat 这个类型信息记录到对象中,只是仅仅是在这个语句(new XXX<Cat>(new Dog())
)执行时,去检查传入的形参类型(Dog)是否与泛型类型(Cat)相符,这个案例中就是不相符的,因此会在编译阶段就报错。- 对所有的泛型类型引用赋值(如
XXX<String> = YYYY
),实际上都不会往对象里写入泛型信息,而是在执行这个赋值前,做一个隐式的强制类型转换(如XXX<String> = (String)YYYY
)。
泛型方法传入类型的时机(发生强制类型转换的时机)
上面提到过,泛型方法有两种形式,一种是在形参中使用泛型参数,一种是在返回值中返回泛型。
形参为泛型
public <T> void fun1(T something){
}
此时,对泛型进行类型传递发生在调用该方法,传入实参时。
fun1(new Cat()); // 此时 T 就被传入为 Cat 类型
返回值为泛型
public <T> T fun2(Object val){
return (T)val;
}
此时,直到调用该方法返回时,都没有进行类型传递。T 到底是要传入什么类型,取决于接收这个返回值的变量的类型。
String str = fun2(new Cat()); // 则 T 被传入为 String
但要注意,所谓的“类型传入”,其实并不是发生在 return (T)val;
或者 fun2()
函数中,而是 fun2(new Cat())
已经调用返回了,还没进行“类型传递”。这个实际的类型传递只传进了后续 java 在 .class 文件中自动加入的强制类型转换中,告诉 java 要强制转换成什么类型。即如下:
String str = (String)fun2()返回的Object对象;
也就是说如果你不赋值给一个类型的变量(引用),它压根就不会去做强制类型转换。
fun2(new Cat()); // 只是调用,而没有赋值给某个类型的引用的话,java 不会帮你做无谓的强制类型转换。
// 这就导致,比如将 Object 类强制转为 String 类是非法的,String str = fun2(new Cat()) 就会报错,而仅仅是调用 fun2(new Cat()) 的话,因为实际没有进行类型转换,所以并不会报错。
形参为泛型,返回值也为泛型
public <T> T fun3(T something){
Object obj = new Object();
return (T)obj;
}
这种情况就是调用这个 fun3()
方法时,先将传入的实参的类型传递给 T,方法调用返回 obj 后,视接收 obj 的引用的类型来作强制类型转换。由于是存在先后顺序的,实参的类型和实际接收返回值的变量的类型甚至可以是不同的。
具体案例如下:
public class TestClass<T> {
T something;
public TestClass(T something){
this.something = something;
}
// 泛型方法:返回值为泛型
public <Another> Another getAnother(Object val){
return (Another)val;
}
// 泛型方法:返回值为泛型,无形参
public <Another> Another getAnother2(){
Object val = new Animal();
return (Another) val;
}
public Cat getAnother3(){
Object val = new Animal();
return (Cat) val;
}
// 泛型方法:形参为泛型
public <Another> void getAnother4(Another another){
System.out.println(another.getClass());
}
// 泛型方法:形参为泛型,返回值也为泛型
public <Another> Another getAnother5(Another another){
Object val = new Animal();
return (Another) val;
}
public <Another> Another getAnother6(){
// return new Another(); // 语法非法,因为并不确定这个类型是否有不带参数的构造方法
// /*
// java: 意外的类型
// 需要: 类
// 找到: 类型参数Another
// */
return null; // 这样写则不会有语法问题
}
}
// 调用类
import java.lang.reflect.Field;
public class TestUse {
public static void main(String[] args) throws NoSuchFieldException {
TestClass<String> test = new TestClass<>("abc");
Field field1 = test.getClass().getDeclaredField("something");
System.out.println(field1.getType()); // class java.lang.Object
// 调用泛型方法:返回值为泛型
System.out.println(new Dog()); // genericexample.Dog@4554617c
// System.out.println(test.getAnother(new Dog())); // 报错:
// /*
// java: 对println的引用不明确
// java.io.PrintStream 中的方法 println(char[]) 和 java.io.PrintStream 中的方法 println(java.lang.String) 都匹配
// */
// 这说明,若不指定泛型返回值的类型,对返回值进行操作会报错类型不明确。
// 调用泛型方法:返回值为泛型,无形参
// Cat cat = test.getAnother2();
// /*
// Exception in thread "main" java.lang.ClassCastException: genericexample.Animal cannot be cast to genericexample.Cat
// at genericexample.TestUse.main(TestUse.java:46)
// */
// 调用普通方法,在返回值中作明确的类型强制转换
// Cat cat2 = test.getAnother3();
// /*
// Exception in thread "main" java.lang.ClassCastException: genericexample.Animal cannot be cast to genericexample.Cat
// at genericexample.TestClass.getAnother3(TestClass.java:22)
// at genericexample.TestUse.main(TestUse.java:51)
// */
// 注意到,这两次调用,虽然都是把 Animal 转换成 Cat 报错,但报错的位置是不同的。
// 非泛型方法报错是在被调用的方法内部(准确来说是return语句,即发生强制类型转换处)。
// 泛型方法报错则是只有在调用语句里报错,而不会在泛型方法内部报错。这说明类型转换并没有发生在泛型方法的返回语句中,而是要回到调用语句中时,根据等号左边的具体类型来确定要转换成的类型,才开始进行强制类型转换。
// 也就是返回值为泛型的泛型方法,泛型类型的指定是取决于使用时接收返回值的变量的数据类型。而且可以理解成这等同于不认为它是泛型,而是按它原本的类型来处理,最后返回后,才添加一步按指定类型进行强制类型转换的操作。
// 即 Cat cat = test.getAnother2();
// 等价于: Cat cat = (Cat)test.getAnother2();
// 调用泛型方法:形参为泛型
// 而形参类型为泛型的泛型方法,泛型类型是在传入实参时指定的,实参是什么类型,泛型类型就是什么类型。
test.getAnother4(new Cat()); // class genericexample.Cat
test.getAnother4(new Animal()); // class genericexample.Animal
Object animal = test.getAnother5(new Dog());
System.out.println(animal.getClass()); // class genericexample.Animal
String str = "bbc";
Object obj = str;
Object obj2 = (Object)str;
System.out.println(obj.getClass()); // class java.lang.String
System.out.println(obj2.getClass()); // class java.lang.String
// Cat cat3 = test.getAnother5(new Dog());
// /*
// java: 不兼容的类型: 推断类型不符合上限
// 推断: genericexample.Dog
// 上限: genericexample.Cat,java.lang.Object
// */
Animal cat3 = test.getAnother5(new Dog());
System.out.println(cat3.getClass()); // class genericexample.Animal
// Cat cat4 = test.getAnother5((Cat)null);
// /*
// Exception in thread "main" java.lang.ClassCastException: genericexample.Animal cannot be cast to genericexample.Cat
// at genericexample.TestUse.main(TestUse.java:73)
// */
// 可以看出,若在形参与返回值中同时使用泛型,那么泛型方法内部会先按实参的具体类型来指定泛型,最后返回之后,再按接收返回值的变量的具体类型来做强制类型转换。
}
}
总结:
对于泛型方法
-
返回值为泛型:
- 不传入泛型类型,直接对泛型返回值操作,会报错类型不明确;
- 不传入泛型类型,仅仅执行该泛型方法,不对返回值处理,则不会不错;
- 传入类型取决于接收泛型返回值的引用的类型。
- 强制类型转换是发生在该泛型方法返回后。
-
形参为泛型:
- 泛型类型是在泛型方法调用时随实参传入而传入,实参类型即为泛型类型
-
形参为泛型,返回值也为泛型:
- 泛型方法内部会先按实参的具体类型来指定泛型,最后返回之后,再按接收返回值的变量的具体类型来做强制类型转换。
泛型总结
泛型的实际行为:
- 编译期间检查(类型),而对象是类型擦除的(没有记录类型,只有在使用时,根据指向这个对象的具体泛型引用来隐式插入强制类型转换语句,已达到“类型约束”的效果)
泛型使用建议
虽然泛型给 java 类库增加了很多功能(比如说限制 ArrayList 元素为 String 后,使用时就不需要先手动强制类型转换为 String 再调用 String 的方法),但自己写代码时(自己写类库),不建议使用泛型。
因为使用泛型的话,你得想好整个类型的设计是否能在现在与将来都满足泛型约束的需求,否则泛型用得太多,为了节省这个“类型强制转换”语句,却可能会把代码搞得非常混乱复杂。