Java笔记一问三不知------泛型的秘密
Posted 鸽一门
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java笔记一问三不知------泛型的秘密相关的知识,希望对你有一定的参考价值。
(还没有前言提要,后期补上:)
泛型的创建和实例化
1.常见创建使用
(1)创建一个泛型类
public class Wrapper<T>
private T instance;
public T getInstance()
return instance;
public void setInstance(T instance)
this.instance = instance;
//Wrapper Test
public class TestWrapper
public static void main(String[] args)
Wrapper<String> stringWrapper = new Wrapper<>();
stringWrapper.setInstance("hello world");//添加数据时自动检测类型
String name = stringWrapper.getInstance();
System.out.println("Aemon: "+name);
以上是一个简单的泛型类实现,无需多言…
泛型的诞生运用的最广的场景就是集合类Collection(List、Map),因此实现一个简单的List,可以根据位置index获取对应位置的数据。
public class GenericList<T>
// 不可这样初始化
// T[] array = new T[0];
private Object[] array = new Object[0];
public T get(int index)
return (T) array[index];
public void add(T instance)
array = Arrays.copyOf(array, array.length + 1);
array[array.length - 1] = instance;
//GenericList Test
public class TestGenericList
public static void main(String[] args)
GenericList<String> stringList = new GenericList<>();
stringList.add("hello world"); //添加数据时自动检测类型
String name = stringList.get(0);
System.out.println("Aemon: "+name);
「Point Ⅰ」 :泛型优势
- 自动处理强制转型;
- 在编译之前检查类型符合性,提高程序代码运行可靠性;
诚然,以上两个好处其实无需使用泛型,手码也可以实现,但它本身就是一个方便的工具,提供更优雅的方便。
(2)创建一个泛型接口
public interface Shop<T>
T buy();
float refund(T item);
class Apple
private String des;
private float amount;
//set...
//get...
public class AppleShop implements Shop<Apple>
@Override
public Apple buy()
return new Apple();
@Override
public float refund(Apple item)
return item.amount;
创建一个简单的泛型接口 Shop,再创建一个实现类 AppleShop,传入具体类型限制T,这样不同的实例拥有不同的方法参数、返回值、字段等等。
注意:"实例"这两个关键字强调泛型一定是针对具体对象的,反之意味着,泛型不支持静态字段、方法。
如下图所示:在泛型类中创建一个泛型静态字段,报错'Wrapper.this' cannot be referenced from a static context
。
你品,你细品,实例创建实现时的T具体类型各不相同,怎么可以去用一个静态字段表示。同理,静态方法也无意义。(但泛型方法可以?,后续详解)
2. 继承
(1)怎么写
//错误写法1: 根本未使用到泛型接口的好处
public interface AIShop extends Shop
//错误写法2: 直接报错!内部无法解析T
public interface AIShop extends Shop<T>
void autoPay(T item);
//正确写法
public interface AIShop<T> extends Shop<T>
void autoPay(T item);
来看上面这个接口继承接口例子,错误写法1倒是容易理解,重点是错误写法2,为何继承泛型接口的AIShop若不声明T则内部方法使用T就无效?且正确写法中的两个T是相同概念么?
先公布正确答案,再慢慢道来:
- 有效范围
- 父接口和子接口的T是不相同的,即
Shop<T>
中的 T 与AIShop<T>
中的 T 不同!
是不是有点绕?来梳理一下,首先Shop接口中的 T 实则也只是一个占位符,有效范围仅在于Shop接口 内的变量、方法中使用,它可以声明为T/R/任意不冲突名称。而此处实现了一个类型参数是 T 的AIShop接口,它继承于Shop接口,但AIShop接口声明的类型参数是T,有效范围在于**AIShop接口 **内,即Shop<T>
中的 T 与 AIShop<T>
中的 T 是两个不同的概念。
换言之,实现AIShop接口时声明类型为 R,也不会影响父接口Shop,写法如下:
public interface Shop<T>
T buy();
float refund(T item);
public interface AIShop<R> extends Shop<R>
void autoPay(R item);
「Point Ⅱ」:T(Type parameter)
- 写在类、接⼝名称右边的括号<>里,表示 Type parameter 的声明,表示「我要创建⼀个统一代号」;
- 写在类⾥的其他方,表示「这个类型就是我那个代号的类型」;
- 泛型类的有效范围仅限于这个类⾥,出了类就没用了;同理,泛型⽅法的有效范围仅限于这个⽅法里;
「Point Ⅲ」:Type parameter 和 Type argument 区别
上述例子的所谓两个不同的概率其实也就是Type parameter 和 Type argument的区别:泛型的创建和泛型的实例化 (形参 实参)
-
Type parameter
public class Shop<T>
里面的 < T >,表示创建一个 Shop 类,它的内部会用到一个统一的类型,这个类型姑且称为 T ;(泛型的创建)
-
Type argument
- 其它地⽅尖括号<>⾥的全是 Type argument,⽐如
Shop<Apple> appleShop
的 Apple 类型,表示「那个统⼀代号,在这里的类型我决定是这个」;(泛型的实例化)
- 其它地⽅尖括号<>⾥的全是 Type argument,⽐如
(2)多个类型参数
举一个最常见的例子就是HashMap,其特点就是**<Key, Value>**,天然特效,其内部数据结构实现就需要2个类型支持,如下一个简单的多类型泛型接口:
public interface GenericMap<K, V>
void put(K key, V value);
V get(K key);
extends 和 super
这2个关键词的作用就是 限制边界(within its bound),指定实例化时传入的类型参数。
1.extends
(1)< T extends xxx >
怎么使用?举个例子,还是以上述举过的接口Shop为前提,实现一个FruitShop接口,并制定Fruit类型:
(业务场景解释:商店接口,其泛型类型就是针对商品对象,再根据业务特征细化业务范围,因此创建水果商店接口,并限制其商品为水果)
public interface FruitShop<T extends Fruit> extends Shop<T>
Boolean isRefresh(T furit);
//指定继承类型,可以继承一个类,实现多个接口,用 & 连接
public interface FruitShop<T extends Fruit & XXXinterface> extends Shop<T>
Boolean isRefresh(T furit);
(2)< ? extends xxx >
开头先一句话总结重点:< ? extends xxx > 放宽了声明对象类型时泛型的限制,代价就是:对象只能Get,不能Set。
关于 ? extends 的使用,如下先来看一个常用的(泛型)集合声明的例子:
第一种list声明是开发时唱使用到的,无异议,但看到第二种 objectList的声明出错,可能就会疑惑了:左边声明的List类型是Object,右边给出的实例化对象类型是String,是其子类,而根据Java三大特性之一的 多态(polymorphism),这个错误倒是有些不明不白,无奈之下只得根据错误提示改写,而内部原理究竟是为何?
【变量声明Define】
先别急,再结合业务场景来看一个实际的例子,首先还是来声明变量:
- 例子1:声明了一个Fruit类型集合,最常见的集合声明使用方式,没毛病,且对fruitList 进行set、get操作都行。
- tips:右边的<>内也无需声明Fruit类型,会提示
type argument will be replaced with <>
,因为在运行时会根据左边的类型声明进行类型推断。
- tips:右边的<>内也无需声明Fruit类型,会提示
- **例子2:**声明了一个子类Apple类型集合,但声明依旧是报错的,来解释一波:集合的声明(泛型的使用)与多态特性无关,并非是所谓的“子类可以赋值给父类”特性,forget it。来看左边声明集合时传入的是Fruit类型,意味着声明的集合是可以存放所有水果的,可是右边却给了一个Apple具体类型的集合,这不符合左边声明的初衷:这个集合可以添加Apple or Banner or else(任何继承于Fruit类型的)。
- 再来看例子1,声明的是一个Fruit类型集合,可以添加
add()
任何继承于Fruit类型的对象,这叫多态。 - 再回到例子2,直接将子类Apple集合赋值给Fruit集合,将集合可容纳的类型范围缩小,这不叫多态,这叫**“类型不安全”。左边指定类型为Fruit,万一我去添加Banner了呢,必然报错,而泛型则是预先禁止了这种写法。【=备注1】**
- 再来看例子1,声明的是一个Fruit类型集合,可以添加
- 例子3:使用
<? extends xxx>
放宽了声明时对类型的限制,声明了一个只要是继承于Fruit的类型集合,那右边传入一个Apple类型的集合,似乎是ok了?(注意Apple类型的字体颜色是灰色)
【变量修改set、add】
续接例子3,使用了<? extends xxx>
后就一劳永逸了吗?其实也不然,再看下面这个例子:
这里建议更改后的代码使用方式,不又回到原点了么,为何使用<? extends xxx>
后,集合声明时没问题,修改数据时却又报错了?
其实例子3的变量声明,若你依旧打算将“子类Apple集合赋值给Fruit集合”,即使使用了<? extends xxx>
逃过了声明时的Check,再修改数据时仍会报错。因为还是看左边的声明,没错,指定的是继承于Fruit类型的集合,这时添加添加Apple or Banner 应该没问题的,但是!集合是不知道的,在修改变量时传入的类型是否是Fruit子类,只能等到运行时再去Check,这是很危险的一件事,所以泛型也是在这里就禁止掉了。【=备注2】
Attention⚠️:这里有2个备注点提示,实则背后的原理是【泛型的类型擦除】,但是在此处暂且不解释术语概念,后续道来,先留有印象
那又有什么办法可以解决 add()
时的报错呢?
没有,这是无解的!先别急着喊waht f… ,回想下,我们使用 <? extends xxx>
的确放宽了声明对象类型时泛型的限制,但是变量却无法修改,这限制是不是太大了?这还有什么用?
别急,思考下泛型这样设计的目的,你如果想要一个Fruit集合,就直接定义一个fruitList;想要一个Apple集合,也是如此,这一点泛型在设计时已经考虑到了。
//正确声明定义变量 示例:
ArrayList<Fruit> fruitList = new ArrayList<>();
ArrayList<Apple> appleList = new ArrayList<>();
一句话总结重点:< ? extends xxx > 放宽了声明对象类型时泛型的限制,代价就是:对象只能Get,不能Set。
那,这个 <? extends xxx>
奇怪的限制还有使用场景吗,有只需要调用集合get,不需要set的场景吗?当然有的!举个实际例子,计算不同水果类型集合中的全部重量:
又是这个报错,怎么办?上<? extends xxx>
:
float totalWeight(List<? extends Fruit> fruits)
float weight = 0;
for (Fruit fruit : fruits)
weight += fruit.getWeight(); //获取重量(注意这里只是Get)
return weight;
此方法中使用到了泛型<? extends xxx>
指定类型范围特点,方法参数fruits只是要求参数类型是 Fruit 水果即可,而具体哪一种类型的水果(苹果 or 香蕉 or else)是不在乎的,这里只是获取水果Fruit对象的属性weight重量,计算重量而已。(不涉及到Set)
【数组 VS 集合】
list集合如此,但是想一想数组有这个问题吗,只能get,不能set?上例子:
//数组
Fruit[] fruits = new Apple[10];
//定义ok,运行时出错!!!
fruits[0] = new Banner();
如上常用的数组示例,明显是有问题的,但是使用数组结构时却不报错,为何?因此泛型中存在有类型擦除,导致泛型使用有更严格的规则,在创建时就会检测报错。
再来一个集合的极端例子,验证泛型这样设计的好处:
- 错误变量声明的示例在前面已经举例多次,这就是典型的【类型擦除】现象,在声明时泛型就检查并禁止这种写法;
- 第二个示例用了一个极端方法,在右边赋值的苹果集合 指定类型强制转换,这样声明变量fruitList时就不会报错,不过IDE会提示 “uncheck assignment”,也就是**“未检查的赋值”。诚然,这样声明变量确实暂且逃过Check一劫,但是这种“类型不安全”**代码真的方便、安全吗?再看声明后对集合的存取,集合先存入一个Banner,再取出数据强转为Apple类型,程序在此时无法检测出其问题,但运行时定会报错! 由此可见泛型这样设计的目的:类型擦除在问题的源头就遏止住存在潜在错误的使用写法!
「Point Ⅳ」 :类型擦除
好了,上述说了这么多【类型擦除】,它究竟拥有什么特点?
中间会有一个bridge method做中转
- 运行时,所有的 T 以及尖括号里的东西都会被擦除(其实保留在class文件里),即
List
和List<String>
以及List<Integer>
其实都是⼀个类型; - 但是,所有代码中声明的变量、参数、类或接口,在运⾏时可以通过反射获取到泛型信息;
- 但但是,运行时创建的对象,在运⾏时通过反射也获取不到泛型信息(因为 class ⽂件⾥没有);
- 但但但是,有个绕弯的⽅法就是创建一个子类(哪怕⽤匿名类也行),⽤这个子类来生成对象,这样由于子类在 class ⽂件里存在,所以可以通过反射拿到运行时创建的对象的泛型信息。例如 Gson 的 TypeToken 就是这么干的;
- 但但是,运行时创建的对象,在运⾏时通过反射也获取不到泛型信息(因为 class ⽂件⾥没有);
查看字节码逆向出来的源码,类型是T , 运行时没有对应类型
**extends易混点注意:**extends可修饰符号?
,也可以修饰T
,但是前者可是扩大声明对象类型时泛型的限制,而后者是缩小范围限制。
2.super
开头先一句话总结重点:< ? extends xxx > 放宽了修改对象类型时泛型的限制,代价就是:对象只能Set,不能Get。
【变量声明Define】
如上第一个错误例子是常见的集合赋值错误,又是典型的“类型不安全”,这次我们知道是因为泛型的类型擦除特性,但这次的赋值是将父类Fruit类型集合赋值给子类Apple类型集合,场景有些不同,看到例子2采用了 <? super XXX>
便没有报错,其作用与 <? extend XXX>
相反,指定集合类型是Apple或Apple的父类都可。
而且apples集合可以进行数据更改操作,即add
、set
等方法调用,这正是 <? extend XXX>
的缺憾。分析其业务场景,定义的apples集合是要求该集合能够容纳Apple类型,即它只要可以装下Apple即可,另外集合再添加Banner or else也没问题,只要不对集合进行get操作就行(若获取到非Apple类型,则类型转换运行时必定报错),意味着集合兼容性很强。 因此该集合的命名为apples似乎有些不妥,只是苹果集合?不然,其实是一个可以容纳苹果的水果集合,命名为 fruitsCompatableWithApple。
那这种特性有实际运用场景的吗?有的,例如一个Apple类型中有个方法:将自己添加到集合中:
class Apple extends Fruit
String des;
float amount;
public void addMeToList(List<Apple> list)
list.add(this);
看着似乎没什么问题,但运用到实际业务逻辑中则会发现熟悉的报错又来了:
要想解决报错,则需要放宽对类型的限制范围,又想对类型采取一定的限制,这时就是<? super XXX>
的出场机会了,正确声明如下:
class Apple extends Fruit
String des;
float amount;
//放宽使用时的类型限制,且限制了一定范围:包含且不仅限于Apple的Fruit
public void addMeToList(List<? super Apple> list)
list.add(this);
不过!相信刚学习过extend用法的你们定然知道,世上没有两全的事情:放宽了类型使用时的限制,但是代价就是:对象只能Set,不能Get。
再白话点说,既然放宽了使用时的限制(添加数据类型的限制范围扩大了),无疑在获取Get集合数据时的类型转换产生不确定性,因此拒绝Get操作。
「Point Ⅴ」 :?
只能写在泛型声明的地方,表示「这个类型是什么都行,只要不超出 ? extends
或者 ? super
的 限制」。
- extends的限制:放宽了声明对象类型时泛型的限制,代价就是:对象只能Get,不能Set。
- super的限制:放宽了修改对象类型时泛型的限制,对象只能Set,不能Get。
扩展一下,声明变量时在等号赋值的右边<>
直接写?
是肯定错的(如下例子1),因为这并非是声明,而是方法 ----- 创建集合的构造方法被调用,但是在type argument里嵌套一层使用type parameter定义的Shop(如下例子2),这又是可以声明的地方。
有关于extends的嵌套还有一个类Enum定义,如下:
还有一个冷知识,?
可以如下直接声明集合对象,但这样的对象传什么类型都可以,但是它既不能Get,也不能Set,无实际意义:
扩展
1.泛型方法
定义:定义并使用了泛型的类型参数 的方法。
//非泛型方法举例
public interface Shop<T>
T buy();
float refund(T item);
看以上之前举例过的泛型接口Shop,那T buy()
是泛型方法吗?它的返回值是T使用到了泛型呀。不是的!返回值T对于buy()
方法而言是一个实际的类型,是依赖于泛型接口Shop的,并非是方法本身自己定义的。
public interface Shop<T>
......
//新增泛型方法
<E> E refurbish(E item, float money); //商店兼职维修工作
//test调用
Shop<Apple> shop = ....实例化
Phone phone = shop.refurbish(new Phone(), 500); //维修手机
那什么样子的方法是泛型方法?例如常见的 findViewById
、findViewTraversal
:
//泛型方法举例
public <T extends View> T findViewById(@IdRes int id)
return getWindow().findViewById(id);
protected <T extends View> T findViewTraversal(@IdRes int id)
if (id == mID)
return (T) this;
return null;
运行时才能确定T的具体类型(是TextView or ImageView or else)
实例化时确定传入的Type parameter类型
静态泛型方法 VS 非静态泛型方法 的区别:是否用到当前对象,例如 getViewById findViewById
「Point Ⅵ」 :泛型的意义
泛型的意义在于:泛型的创建者 让泛型的使⽤者 可以在使用时(实例化)细化类型信息,从⽽可以触及到「使用者所细化的子类」的 API ,也可以说泛型是「有远⻅的创造者」创造的「⽅便使用者」的⼯具。
举个例子大白话说明一下上述红字,如下Shop接口,bug()
方法的返回值使用到了泛型,便利了该接口使用者在调用该方法时,可以获取到具体返回参数类型,从而后续享受到便利:可以使用到自己调用该方法而传入的具体类它的API。如果你不需要所谓“具体类”,例如全是refund
方法,或者 buy
方法的返回值改成Object or Fruit 也能符合需求,你大可以不使用泛型,完全没问题!
public interface Shop<T>
T buy(); //返回调用者指定类型对象!!!
float refund(T item);
因此,一个判别需不需要使用或抽出泛型的小技巧:看使用者有没有 “要使用它所指定具体类的API” 的需求,怎么满足使用者的这个需求?通过泛型返回它所指定的对象!
那照上面所说,参数时泛型而返回值不是泛型的 float refund(T item)
的方法没多大意义?不尽然,它更像是一个辅助作用,参数可用来进行内部逻辑的类型转换Check,满足方法本身需求。
啊?还是觉得参数时泛型而返回值不是泛型的方法好像不值得用泛型?也不是,也有特例时作为 “调用主类的API”而存在,例如Comparable接口的int compareTo(T o)
方法(如下图),而String、Integer类实现了该接口的方法,且传入参数类型不同。其重点还是:Comparable接口是为了使用者方便,所以使用泛型
泛型参数 特征
因此 泛型参数 至少满足以下两个条件之一:
- 泛型参数要么是一个⽅法的返回值类型:
T buy()
; - 要么是放在⼀个接口的参数里,等着实现类去写出不同的实现:
public int compareTo(T o);
但是,泛型由于语法⾃身特性,所以也有⼀个延伸⽤途:用于限制方法的参数类型或参数关系。例子如下:
//限制参数类型必须实现Runnable、Serializable
public <E extends Runnable, Serializable> void someMethod(E param);
//限制参数、返回值类型
public <T> merge(T item, List<T> list)
list.add(item);
「Point Ⅶ」 :泛型类型创建的重复
- < T> 的重复
public class RefundableShop<T> extends Shop<T> float refund(T item);
如上例子,RefundableShop接口声明的T,是表示对⽗类(⽗接口)的扩展。
- 类名的重复
public class String implements Comparable<String>
public native int compareTo(String anotherString);
//👆相当于👇
pulic class String<T == String>
public int compareTo(String anotherString)
实现Comparable接口,传入的类型是类名本身,即同样表示对⽗类(父接口)的扩展。
以上是关于Java笔记一问三不知------泛型的秘密的主要内容,如果未能解决你的问题,请参考以下文章