泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。使用范型可以减少大量的强制类型转换,在编译期检查类型,减少出错的可能。
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 var;
.....
}
}
范型接口和范型类使用方法类似,唯一要注意的是在实现该接口时范型的书写
//定义一个泛型接口
public interface Container<T> {
public T get();
}
实现该接口时,当不传入范型实参时,需要在该实现类声明范型参数
public class ContainerImpl implements Container<T>{
//未声明时,编译器会报错:"Unknown class"
@Override
public T get() {
return null;
}
}
//声明范型参数时
public class ContainerImpl implements Container<String>{
@Override
public String get() {
return null;
}
}
传入范型实参时
public class ContainerImpl<T> implements Container<T>{
@Override
public T get() {
return null;
}
}
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。在方法中使用类上定义的范型,我们暂且把它归为第一类,范型类中
public class StaticMethodr<T> {
....
....
/**
* 静态方法中先于类的初始化(new,而非类加载等初始化),因此无法使用类中定义的范型,使用时会报错
* "StaticGenerator cannot be refrenced from static context"
*/
public static <E> void get(E t){
}
}
使用泛型的过程中,经常出现一种很别扭的情况,比如我们有水果类,和它的派生类苹果
class Fruit {}
class Apple extends Fruit {}
然后我们有一个盘子,里面可以放各种东西
class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
现在家里来客人了,我需要把苹果放入水果盘子
Plate<Fruit> p=new Plate<Apple>(new Apple());
但实际上会出现编译错误,error: incompatible types: Plate cannot be converted to Plate,因为在编译器看来:
Plate<? extends Fruit>
那么此时我们的盘子可以放下水果以及它的一切派生类,或者说我们的果盘可以放所有的水果。但是这样的范型会给它带来一个副作用:只能取不能放
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
//不能存入任何元素
p.set(new Fruit()); //Error
p.set(new Apple()); //Error
//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get(); //Error
编译器只知道是这个盘子可以放水果的派生类,但是不知道具体的类型,可能是苹果也有可能是香蕉,所以没法进行存放,但是可以进行读取,因为盘子里的东西肯定是一个水果类型的。事实上,编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。
Plate<? super Fruit>
表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。那么它有什么用处呢?他可以解决刚才上界通配符所带来的弊端
Plate<? super Fruit> p=new Plate<Fruit>(new Fruit());
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get(); //Error
Fruit newFruit1=p.get(); //Error
Object newFruit2=p.get();
虽然我们的通配符表示水果的基类,但是存放内容是只能存放水果的派生类,这是因为编译器无法知道具体的类到底是水果的哪个基类,但是可以肯定可以存放比最小粒度水果还要小的派生类,可以把它强行提升为水果类,然后再提升为我们所要存放的那个特定的水果的基类。当然他也有一个弊端就是,只能存放,无法读取,因为无法判断它到底是那个类。
所以我们得出了一个结论:PECS(Producer Extends Consumer Super)原则:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}