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>中的 TAIShop<T> 中的 T 不同!

是不是有点绕?来梳理一下,首先Shop接口中的 T 实则也只是一个占位符,有效范围仅在于Shop接口 内的变量、方法中使用,它可以声明为T/R/任意不冲突名称。而此处实现了一个类型参数是 T 的AIShop接口,它继承于Shop接口,但AIShop接口声明的类型参数是T,有效范围在于**AIShop接口 **内,即Shop<T>中的 TAIShop<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 parameterType argument的区别:泛型的创建和泛型的实例化 (形参 实参)

  • Type parameter

    • public class Shop<T>里面的 < T >,表示创建一个 Shop 类,它的内部会用到一个统一的类型,这个类型姑且称为 T(泛型的创建)
  • Type argument

    • 其它地⽅尖括号<>⾥的全是 Type argument,⽐如 Shop<Apple> appleShopApple 类型,表示「那个统⼀代号,在这里的类型我决定是这个」;(泛型的实例化)

(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 <>,因为在运行时会根据左边的类型声明进行类型推断
  • **例子2:**声明了一个子类Apple类型集合,但声明依旧是报错的,来解释一波:集合的声明(泛型的使用)与多态特性无关,并非是所谓的“子类可以赋值给父类”特性,forget it。来看左边声明集合时传入的是Fruit类型,意味着声明的集合是可以存放所有水果的,可是右边却给了一个Apple具体类型的集合,这不符合左边声明的初衷:这个集合可以添加Apple or Banner or else(任何继承于Fruit类型的)。
    • 再来看例子1,声明的是一个Fruit类型集合,可以添加add()任何继承于Fruit类型的对象,这叫多态
    • 再回到例子2,直接将子类Apple集合赋值给Fruit集合,将集合可容纳的类型范围缩小,这不叫多态,这叫**“类型不安全”。左边指定类型为Fruit,万一我去添加Banner了呢,必然报错,而泛型则是预先禁止了这种写法。【=备注1】**
  • 例子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文件里),即ListList<String> 以及 List<Integer> 其实都是⼀个类型;
  • 但是,所有代码中声明的变量、参数、类或接口,在运⾏时可以通过反射获取到泛型信息;
    • 但但是,运行时创建的对象,在运⾏时通过反射也获取不到泛型信息(因为 class ⽂件⾥没有)
      • 但但但是,有个绕弯的⽅法就是创建一个子类(哪怕⽤匿名类也行),⽤这个子类来生成对象,这样由于子类在 class ⽂件里存在,所以可以通过反射拿到运行时创建的对象的泛型信息。例如 Gson 的 TypeToken 就是这么干的;

查看字节码逆向出来的源码,类型是T , 运行时没有对应类型

**extends易混点注意:**extends可修饰符号?,也可以修饰T,但是前者可是扩大声明对象类型时泛型的限制,而后者是缩小范围限制。


2.super

开头先一句话总结重点:< ? extends xxx > 放宽了修改对象类型时泛型的限制,代价就是:对象只能Set,不能Get。

【变量声明Define】

如上第一个错误例子是常见的集合赋值错误,又是典型的“类型不安全”,这次我们知道是因为泛型的类型擦除特性,但这次的赋值是将父类Fruit类型集合赋值给子类Apple类型集合,场景有些不同,看到例子2采用了 <? super XXX> 便没有报错,其作用与 <? extend XXX> 相反,指定集合类型是Apple或Apple的父类都可。

而且apples集合可以进行数据更改操作,即addset等方法调用,这正是 <? 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); //维修手机

那什么样子的方法是泛型方法?例如常见的 findViewByIdfindViewTraversal

//泛型方法举例
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笔记一问三不知------泛型的秘密的主要内容,如果未能解决你的问题,请参考以下文章

Java讲课笔记35:初探泛型

Java泛型学习笔记 - 泛型的介绍

Java泛型学习笔记 - 泛型的继承

JAVA关于泛型的笔记

Java_泛型笔记

泛型的作用是什么?——Java系列学习笔记