java8 泛型理解

前言

  关于java的泛型一开始只学习了<T> 这一种写法。但是到了jdk8的时候由于函数式接口,接触到了很多很不一样的泛型格式,且不易理解,这对学习及使用产生了很大的困扰。
  以下将依次对<T>,<?>,<? extends T> ,<? super T>这些格式做详细说明,并提供一些常见的例子以做参考。

<T>


  中文名叫占位符,这是最简单的泛型格式。

  如下例1,我们定义了一个盘子类,内部维护着一个属性,但是它的类型是泛型,意味着你可以在使用的时候再定义它的具体类型;

  • 例1:
public class Plate<T> {
    private T t;

    public T get() {
        return this.t;
    }

    public void set(T t) {
        this.t = t;
    }
}

   如例2,我们在使用的时候再定义具体的类型。applePlate里面就可以放Apple实例,bananaPlate里面可以存放Banana实例。

  • 例2
// 装苹果的盘子
Plate<Apple> applePlate=new Plate<>();
// 装香蕉的盘子
Plate<Banana> bananaPlate=new Plate<>();

  所以(T)占位符的意思就是:

  在类定义的时候不确定具体类型,可以用一个T来代表一个未确定的类型,而在使用的时候可以指定为任意类型

  • 使用泛型的好处
      显而易见,如果没有泛型我就需要根据实际情况定义好多个类,装苹果的,装香蕉的,西瓜的。有了泛型我只需要定义一次盘子类就够了,把变化的属性定义成泛型在使用的时候自动生成。封装了不变的(盘子),抽离了变化的(具体的水果),符合开闭设计。

<?>


  • 定义:
       ?:通配符;它与占位符不是同一层次的概念,不出现在类定义中,应该说它说占位符的扩展,为了解决占位符无法解决的一些问题。

   在说明 <?> 的作用之前,我们要先看一下占位符的使用有哪些不完美的地方。

  • 例3
/*
* 假定我们有Fruit(水果类),及其派生类 Apple,Bnana
*/

// java的多态机制
Fruit fruit=new Apple();

Plate<Apple> applePlate = new Plate<>();
// 类型不同,编译直接不通过
Plate<Fruit>  fruitPlate = applePlate;
 

  如上第一行代码我们都知道,基于多态的设计,java允许将子类型的实例赋给父类型引用。但是第三行的代码编译不通过的是因为编译器的逻辑是:

  1. 苹果 IS-A 水果
  2. 装苹果的盘子 NOT-IS-A 装水果的盘子

  在编译器的逻辑里,苹果是水果的派生,但是装苹果的盘子与装水果的盘子毫无关系,是类型不一样的两个东西。但是在一些情况下我们的确有一些需求要求我们继续保持这种多态的变化。下面我们就看一下通配符是如何解决这些需求的。

  • 例4

public static void main(String[] args) {

        Plate<Apple> applePlate = new Plate<>();
        applePlate.set("apple");
        sendPlate(applePlate);

        Plate<Banana> bananaPlate = new Plate<>();
        bananaPlate.set("banana");
        sendPlate(bananaPlate);
    }

	/**
	* 使用通配符接受其他泛型实例
	*/
    public static void sendPlate(Plate<?> palte) {
        System.out.println("waiter saying plate has "+palte.get().toString());
    }

  如例4代码,我们可能有时候会有一些需求,只和盘子有关与具体的水果无关,如果只有占位符,当我们封装一些方法的时候,我们就得为所有的实际类型提供调用接口,如下

 sendPlate(Plate<apple> palte){...}
 sendPlate(Plate<Banana> palte){...}

  这样就和不合适,违背了开闭原则。这时候就是通配符大显神威的时候了,通配符的含义是就是允许

  含该通配符的引用可以被赋予该泛型类的任何实际类型的对象

Plate<Apple> applePlate = new Plate<>();
// success
Plate<?> plate = applatePlate;

  回到刚才在定义里说的,通配符与占位符不是同一层次的概念,通配符是占位符在使用上的扩展与补充。占位符是泛型的基础,而通配符是为了关联 同一个泛型类 的不同实际类型,在这个层面上实现 多态 。

  • 补充:
       <?> 这个在实际的使用中比较少 ,因为它还存在一些问题。这节主要是为了介绍通配符的概念。
      下面将介绍另外两种泛型的写法格式及说明 <?> 有什么问题。

<? extends T >


  • 定义
       上界通配符(Upper Bounds Wildcards)

   该语法还是用于定义引用类型的,同 <?>是同一类东西,区别去就是为通配符设定了上界。如下例5,大白话就是:装苹果的盘子可以被赋给装水果的盘子。
  

  • 例5
Plate<Apple> applePlate= new Plate<>();
applePlate.set(new Apple());

Plate<? extends Fruit>  plate = applePlate;

System.out.println(plate.get().toString());

plate.set("apple2");

   通配符为我们提供了泛型类之间多态的功能,上界通配符就是规定了这个变量可赋值的对象的泛型的上界类型。如下例6,通过定义Plate内占位符的上界上为Fruit类,所以plate只可以被赋予装水果及水果子类的盘子,装对象的盘子就不可以被赋予了。

  • 例6
// success
Plate<Apple> applePlate= new Plate<>();
Plate<? extends Fruit>  plate = applePlate;

// 编译不通过
Plate<Object> objPlate = new Plate<>();
Plate<? extends Fruit>  plate = objPlate;
  • 特性
       上界通配符就像一颗树,允许被赋值从根节点类型往下的所有子节点类型的泛型对象。
       上界通配符修饰的变量允许执行get操作(返回T),但是不允许执行set操作(插入T)for why?
       我们来看例6,plate变量被修饰为可以赋值的对象的泛型的上界上Fruit,所以无论plate被赋予了什么对象,都一定能保证这个对象的泛型一定是Fruit的子类,所以我可以get出来赋给一个Fruit类型的变量。
       但是当我们执行set操作的时候,你不知道plate变量指向的对象的泛型是哪一个,它可能Apple,可能是BlackApple,Banana等等。那我们这时候怎么set值?插入一个Apple对象,但是实际类型可能是Banana。插入一个Fruit对象,父对象也不可以赋给子类型。如例7,编译器直接不通过,因为它无法身边plate的泛型类型是哪一个。
       当然想知道更具体的设计原理可以自行百度。

  • 例7

Plate<Apple> applePlate= new Plate<>();
Plate<? extends Fruit>  plate = applePlate;
// 编译不通过
plate.set(new Apple());

  • Tip:
       当时我就有一点想不通,如果不能set,那怎么有数据get呢?当然是先通过实际类型的变量插入好值,再赋给通配类型的变量,然后由通配类型的变量去实现一些通用的操作。

<? super T >


  • 定义
       下界通配符(Lower Bounds Wildcards)
       与上届通配符相对应的下届通配符,顾名思义就是该通配变量可以赋值的对象的泛型的实际类型的下届。如下例8。
       上界通配符限定的范围是T类型节点往下的一颗树,而下届通配符限定的范围就是从该T类型开始一直往上回溯父类型直到Object的一颗链。

  • 例8

// success
Plate<Fruit> fruitPlate= new Plate<>();
Plate<? super Apple>  applePlate = fruitPlate;

// success
Plate<Object> objPlate = new Plate<>();
applePlate = objPlate;

// 编译不通过
Plate<Banana> bananaPlate = new Plate<>();
applePlate = bananaPlate;

// 编译不通过
Plate<BlackApple> baPlate = new Plate<>();
applePlate = baPlate;
  • 特性
       相对应的,下届通配符的变量允许执行set操作(插入T),但是不允许执行get操作(返回T)
       下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。意思就是下届通配符保证了实际的类型一定是T类型或T类型的父类,如下例9,不管你是什么类型,反正一定是Apple类型或其父类,那我往里面插入Apple及其子类肯定是没问题的。当然你不能插入Apple的父类,如第5行代码,同样的道理,如果范型实际类型是Apple怎么办?
       所以下届允许set,只是允许插入T类型及其子类,不要看到下届两个字就以为是可以插入T类型及T类型的父类。血淋淋的教训,一不小心就会想歪的,引以为戒。
       不能get的原因是没有对象能接收它,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。有人说取出来再手动再强转?不推荐。

  • 例9

Plate<Fruit> fruitPlate= new Plate<>();
Plate<? super Apple>  applePlate = fruitPlate;
// success
applePlate.set(new Apple());
applePlate.set(new BlackApple());

// 编译不通过
applePlate.set(new Fruit());

// 编译不通过
Apple apple=applePlate.get();

总结


PECS(Producer Extends Consumer Super原则

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。

   对不同的操作选取不同的通配符类型。说回小节2中的 <?> ,它像不像 <? extends Object > 的效果?所以个人不建议使用<?>,范围太管了,要遵循单一原则,越简单越好,目前见到的代码中似乎也没见到直接使用通配符的 。

   打脸了,之前我说的 <?> 几乎不用是不对的,其实还是有地方用到的。如下例10,如果不做set,get操作,使用与实际类型无关其实直接使用 <?> 就好了,理解更通顺,没必要使用上下届通配符。

  • 例10
Apple apple=new Apple();

// 写法1 getClass 方法返回的就是通配类型
Class<?> clazz=apple.getClass();
// 写法2
// Class clazz=apple.getClass();
// 写法3 三种写法等价
// Class<? extends Object> clazz=apple.getClass();

// 使用与实际类型无关
Field[] field1 = clazz1.getDeclaredFields();

Tip2

   另外如上的例子,会发现真正使用上下界通配符的时候,占位符都是实际的类型了。对!

  在运行期间使用泛型时,占位符都必须转化为实际的类型才能使用

   为什么要说这段话,肯定是看到一些写法,脑袋懵了。
   首先占位占位,就说明它只是一个代替的标示,所以在运行期间要赋值或使用时肯定是要变成实际类型的。如例11,编译肯定不通过,这算什么?

  • 例11
Plate<Fruit> fruitPlate= new Plate<>();
// 编译不通过
Plate<? super T>  applePlate = fruitPlate;

   所以我们先定下一个基调,super和extends后面肯定是要一个确定的类型的。

   如下例12的exec方法,第一张破坏方式。但是乍一看,也没问题,因为当Plate实际类型确定的时候,exec的里面的占位符也被替换了。
  再看例11的execSec方法,第二种破坏方式,这时候R占位符依赖调用时入参的显示类型决定的。好像也没有什么问题。ps:没怎么样验证过不同界的泛型传入使用时会有什么效果。有兴趣可以试试,反正好像都可以传入。

  • 例12

public class Plate<T> {
    private T t;

    public T get() {
        return this.t;
    }

    public void set(T t) {
        this.t = t;
    }
    public void exec(Plate<? extends T> plate){
    }

    public <R> void execSec(Plate<? extends R> plate){
    }

}

   就是以上两者破坏方式,一个通过初始化类定义,一个通过传入参数定义,根据参数类型定义。没有其他的了,如下例12,你会发现把execSec改成这样写也没问题,但是由于R在运行时没发知道它的类型,它不会报错,但是plate你也无法使用,也为无法确定类型。
   所以上面的说法应该是你可以在代码里定义 <? super T > ,但是在使用时不能确定实际类型你就无法使用它。

  • 例13
public <R> void execSec(){
  Plate<? extends R> plate;
  }

Tip3

   关于占位符的数量,如上我们只描述了一个占位符的例子,但是占位符数量不限制唯一,你在一个类里面定义两个,三个,都行。至多开源定义几个?没验证过。
   同一个域(<…>)内,多个占位符用逗号隔开,如例14

  • 例14
/*
*类域的定义写在类后面
*/
public class Plate<T,V> {
    private T t;
    private. V v;

    public void set(T t) {
        this.t = t;
    }
	
	public void set(V v){
		this.v=v;
	}

	/*
	* 方法域的定义写返回值前面
	* 域类域不同的是,方法域的占位符确定是由该方法被调用时传入的参数类
	* 型的泛型决定的
	* /
	public <W,R> void trans(Plate<W,R> plate2){
		....
	}

	/*
	* 但是先对的如果传入参数没有使用该泛型,也不会报
	* 错,代码也能正常执行,具体会怎么样我也不太清楚。
	* 占时未找出错误的案例
	*/
	public <W,R> void trans2(Object obj){
		....
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值