(还没有前言提要,后期补上:)
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);
}
}
诚然,以上两个好处其实无需使用泛型,手码也可以实现,但它本身就是一个方便的工具,提供更优雅的方便。
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具体类型各不相同,怎么可以去用一个静态字段表示。同理,静态方法也无意义。(但泛型方法可以?,后续详解)
//错误写法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是相同概念么?
先公布正确答案,再慢慢道来:
Shop
中的 T 与 AIShop
中的 T 不同!是不是有点绕?来梳理一下,首先Shop接口中的 T 实则也只是一个占位符,有效范围仅在于Shop接口 内的变量、方法中使用,它可以声明为T/R/任意不冲突名称。而此处实现了一个类型参数是 T 的AIShop接口,它继承于Shop接口,但AIShop接口声明的类型参数是T,有效范围在于**AIShop接口 **内,即Shop
中的 T 与 AIShop
中的 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);
}
上述例子的所谓两个不同的概率其实也就是Type parameter 和 Type argument的区别:泛型的创建和泛型的实例化 (形参 实参)
Type parameter
public class Shop
里面的 < T >,表示创建一个 Shop 类,它的内部会用到一个统一的类型,这个类型姑且称为 T ;(泛型的创建)Type argument
Shop appleShop
的 Apple 类型,表示「那个统⼀代号,在这里的类型我决定是这个」;(泛型的实例化)举一个最常见的例子就是HashMap,其特点就是**
public interface GenericMap<K, V> {
void put(K key, V value);
V get(K key);
}
这2个关键词的作用就是 限制边界(within its bound),指定实例化时传入的类型参数。
怎么使用?举个例子,还是以上述举过的接口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);
}
开头先一句话总结重点:< ? extends xxx > 放宽了声明对象类型时泛型的限制,代价就是:对象只能Get,不能Set。
关于 ? extends 的使用,如下先来看一个常用的(泛型)集合声明的例子:
第一种list声明是开发时唱使用到的,无异议,但看到第二种 objectList的声明出错,可能就会疑惑了:左边声明的List类型是Object,右边给出的实例化对象类型是String,是其子类,而根据Java三大特性之一的 多态(polymorphism),这个错误倒是有些不明不白,无奈之下只得根据错误提示改写,而内部原理究竟是为何?
【变量声明Define】
先别急,再结合业务场景来看一个实际的例子,首先还是来声明变量:
type argument will be replaced with <>
,因为在运行时会根据左边的类型声明进行类型推断。add()
任何继承于Fruit类型的对象,这叫多态。 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();
如上常用的数组示例,明显是有问题的,但是使用数组结构时却不报错,为何?因此泛型中存在有类型擦除,导致泛型使用有更严格的规则,在创建时就会检测报错。
再来一个集合的极端例子,验证泛型这样设计的好处:
好了,上述说了这么多【类型擦除】,它究竟拥有什么特点?
中间会有一个bridge method做中转
List
和 List
以及 List
其实都是⼀个类型;查看字节码逆向出来的源码,类型是T , 运行时没有对应类型
**extends易混点注意:**extends可修饰符号?
,也可以修饰T
,但是前者可是扩大声明对象类型时泛型的限制,而后者是缩小范围限制。
开头先一句话总结重点:< ? 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操作。
只能写在泛型声明的地方,表示「这个类型是什么都行,只要不超出 ? extends
或者 ? super
的 限制」。
扩展一下,声明变量时在等号赋值的右边<>
直接写?
是肯定错的(如下例子1),因为这并非是声明,而是方法 ----- 创建集合的构造方法被调用,但是在type argument里嵌套一层使用type parameter定义的Shop(如下例子2),这又是可以声明的地方。
有关于extends的嵌套还有一个类Enum定义,如下:
还有一个冷知识,?
可以如下直接声明集合对象,但这样的对象传什么类型都可以,但是它既不能Get,也不能Set,无实际意义:
定义:定义并使用了泛型的类型参数 的方法。
//非泛型方法举例
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
泛型的意义在于:泛型的创建者 让泛型的使⽤者 可以在使用时(实例化)细化类型信息,从⽽可以触及到「使用者所细化的子类」的 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);
}
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接口,传入的类型是类名本身,即同样表示对⽗类(父接口)的扩展。