一、背景
最近在封装一些通用组件的时候,发现自己对于泛型的理解并不深刻,因此开始网上冲浪查找资料。很多文章讲的一知半解,因此特地记录一下,也供后面自己翻阅。
二、泛型的由来
以这个类为例,如果使用object作为参数类型, 确实可以编译通过。但是在取值的时候一不小心就会报错,如果非要不报错就得每个item判断一下类型再强转,可读性非常差。
有没有什么办法在保持object所具有的对象重用功能,同时保证可读性,在编辑阶段就能报错提示?泛型就由此诞生了。
因此,我理解上泛型实际上是在一些使用场景上弥补了object的不足,但是不表示泛型可以替代object,因为泛型实际上也有自己的一些使用规则束缚,没有object简单粗暴。
public class Orange {
private List list;
public List get() {
return list;
}
public void set(Object object) {
list.add(object);
}
public static void main(String[] args) {
Orange orange = new Orange();
orange.set("1");
orange.set(Boolean.FALSE);
List resultList = orange.get();
resultList.forEach(item->{
//运行报错
String result = (String)item;
});
}
}
三、泛型的使用标准
类型参数(又称类型变量)用作占位符,指示在运行时为类分配类型。根据需要,可能有一个或多个类型参数,并且可以用于整个类。根据惯例,类型参数是单个大写字母,该字母用于指示所定义的参数类型。下面列出每个用例的标准类型参数:
E:元素
K:键
V:值
N:数字
T:类型
?:表示不确定的类型(无限制通配符)
可以看得出来,命名规则是取的英文首字母,这是一个规范,就好比方法的命名需要见名知意一样。
四、泛型的分类及使用
4.1、 泛型类
泛型类就是在类的后面添加<T>
标识,来指定当前类是一个泛型类。
在类new的时候需要指定T的类型,比如String。那么类型就会关联到使用的方法上,setType的时候入参就只能是String类型,如果换成其他类型就会编译报错。
public class Orange<T> {
// type的类型由T指定,即:由外部指定
private T type;
// 返回值的类型由外部决定
public T getType() {
return type;
}
// 设置的类型也由外部决定
public void setType(T type) {
this.type = type;
}
public static void main(String[] args) {
Orange<String> stringOrange = new Orange<>();
stringOrange.setType("or");
stringOrange.setType(1); //会编译报错
System.out.println(stringOrange.getType());
}
}
new的时候也可以不指定类型,那么默认就是object类型,这样使用泛型的意义就不大了。
public static void main(String[] args) {
Orange objectOrange = new Orange<>();
objectOrange.setType(1);
//换成其他类型会报错,需要强转
Object result = objectOrange.getType();
}
同时也支持多种类型的泛型,实际上使用场景并不多
public class Orange<T,K> {
// type的类型由T指定,即:由外部指定
private T type;
private K key;
// 返回值的类型由外部决定
public T getType() {
return type;
}
// 设置的类型也由外部决定
public void setType(T type) {
this.type = type;
}
public K getKey() {
return key;
}
public void setKey(K key){
this.key = key;
}
public static void main(String[] args) {
//定义两种不同类型的泛型
Orange<Integer, Boolean> integerBooleanOrange = new Orange<>();
//设置第一个具体值
integerBooleanOrange.setType(100);
//设置第二个具体值
integerBooleanOrange.setKey(Boolean.FALSE);
System.out.println(integerBooleanOrange.getKey());
}
}
可以发现,泛型类有以下特征(以下用T举例):
- 类后面都伴随着
<T>
,作用是申明这个类是一个泛型类 - 泛型可以用在入参、返回值以及成员变量上
- 只要使用到泛型,就必需在类后面申明这是一个泛型类,否则编译失败
- 泛型在使用上都会受到类后
<T>
的制约,比如申明的是<Integer>
,那么类中使用T的地方默认都是Integer类型 - 如果要传基本数据类型的泛型,必须是包装类
4.3、泛型接口
泛型接口和泛型类实际上类型,接口上申明了是什么泛型,实现类也要使用相同的泛型,两者几乎就是套娃一样的。
public interface AppleInterface<T> {
public T getAppleKind();
}
class GreenApple implements AppleInterface<String>{
@Override
public String getAppleKind() {
return "Green colour";
}
}
class ToyApple implements AppleInterface<Boolean>{
@Override
public Boolean getAppleKind() {
return false;
}
}
4.4、泛型方法
泛型类实际上有个缺点:每次针对不同类型的参数,都必须创建不同类型的对象。这使得方法依赖于创建对象时候申明的类型,它们之间的耦合度太高了,而泛型方法的出现,就是为了解决这个问题。
从以下代码就可以看出,每次水果店要卖一种水果,都需要定义具体的类型,耦合度非常高。
public class FruitShop<T> {
public T sale(T param){
return param;
}
public static void main(String[] args) {
FruitShop<Orange> orangeFruitShop = new FruitShop<>();
Orange orange = orangeFruitShop.sale(new Orange());
FruitShop<Apple> appleFruitShop = new FruitShop<>();
Apple sale = appleFruitShop.sale(new Apple());
}
}
基于上面这个例子改造,感受一下泛型方法的魅力
public class FruitShop {
//泛型方法
public <T> T sale(T param){
return param;
}
public static void main(String[] args) {
FruitShop FruitShop = new FruitShop();
Orange orange = FruitShop.sale(new Orange());
Apple apple = FruitShop.sale(new Apple());
}
}
可以看到使用泛型方法后,完全不需要多次定义FruitShop
,使得方法和类解耦。方法具体的泛型只在方法本身上申明,调用的时候确定具体参数类型。
泛型类的四大特征:
- 必须在方法后面加上
<T>
,作用是申明这是一个泛型方法 - 泛型可以使用在方法的入参、返回值
- 泛型方法支持多个泛型
<T,V>
,可以用一个表示入参,一个表示返回值 - 严格的调用方法
对象.<T>method()
,如FruitShop.<Orange>sale(new Orange())
,也可省略成对象.method()
,如例子
4.5、泛型类和泛型方法(泛型接口)的混用
最近在写封装项目的支付方式的时候发现,A、B两种支付方式的入参一样,但是返回值是不一样的。于是就针对入参定义了泛型接口,出参在具体的pay()
方法上使用了泛型方法。当然这是一个例子,实际上并不严谨,完全可以把返回值用一个大的对象包裹起来,在类上申明两个泛型<T,R>
。
public interface TradeInterface<T> {
/**
* 支付
* @param requestParam
* @return
* @param <R>
*/
<R> R pay(T requestParam);
/**
* 退款
* @param requestParam
* @return
* @param <R>
*/
<R> R refund(T requestParam);
}
4.6、泛型的边界
有些情况下,需要我们对泛型进行限制,比如<T extends Orange>
就限制了这个具体的类型,Orange就是T的上限,T可可以是Orange的任意子类,换做其他类型就会编译报错
public class FruitShop{
public <T extends Orange> T sale(T param){
return param;
}
public static void main(String[] args) {
FruitShop fruitShop = new FruitShop();
fruitShop.sale(new Orange());
// 编译报错
fruitShop.sale(new Apple<>());
}
}
多重限定
如果有多个限定,则可以使用&符号,但是如果其中限定包含类需要写在最前面,且只能存在一个类
public <T extends Orange & AppleInterface> T sale(T param){
return param;
}
4.7、通配符
1. 无边界通配符:?
很多时候会搞不清T、?的关系,为什么有了T还需要?
当代码不依赖具体的参数类型时,那就可以使用无边界通配符。如下,如果使用Apple类里面的setParam方法,因为通配符无法表示具体含义,所以即使调用了setParam方法也会报错。但是不涉及指定通配符相关的方法依旧可以调用。
public class Apple<T>{
private T param;
public void setParam(T param){
this.param = param;
}
public static void main(String[] args) {
Apple<?> apple = new Apple<>();
apple.hashCode();
}
}
看起来通配符和object有点类似,实际上还是有一些区别。
1.一个方法的入参是List<?>
,调用这个方法的时候可以是任意List
。而如果是List<Object>
,则限制了指定类型。
2.可以向 List<Object>
中插入 Object
对象,或者任何其子类对象,但是你只能向 List<?>
中插入 null 值。
2. 上界通配符: ? extends 上界类型
和4.6类型,只不过具体的泛型T替换成了?
3. 下界通配符:? super 子类
使用的时候通配符必须是子类或者其父类
public class FruitShop {
public AppleInterface sale(AppleInterface<? super ReadApple> param) {
return param;
}
public static void main(String[] args) {
FruitShop fruitShop = new FruitShop();
fruitShop.sale(()-> new ReadApple());
//GreenApple是AppleInterface的另一个子类,编译报错
fruitShop.sale(()-> new GreenApple());
}
}
4.8、泛型擦除
关于泛型擦除,实际上赘述起来可以单开一篇,后续会专门更新一篇来讲