深刻理解Java泛型

泛型是什么

一说到泛型,大伙确定不会陌生,咱们代码里面有不少相似这样的语句:java

List<String> list=new ArrayList<>();
复制代码

ArrayList就是个泛型类,咱们经过设定不一样的类型,能够往集合里面存储不一样类型的数据类型(并且只能存储设定的数据类型,这是泛型的优点之一)。“泛型”简单的意思就是泛指的类型(参数化类型)。想象下这样的场景:若是咱们如今要写一个容器类(支持数据增删查询的),咱们写了支持String类型的,后面还须要写支持Integer类型的。而后呢?Doubel、Float、各类自定义类型?这样重复代码太多了,并且这些容器的算法都是一致的。咱们能够经过泛指一种类型T,来代替咱们以前须要的全部类型,把咱们须要的类型做为参数传递到容器里面,这样咱们算法只须要写一套就能够适应全部的类型。最典型的的例子就是ArrayList了,这个集合咱们不管传递什么数据类型,它都能很好的工做。
聪明的同窗看完上面的描述,灵机一动,写出了下面的代码:算法

class MyList{
    private Object[] elements=new Object[10];
    private int size;
    
    public void add(Object item) {
    	elements[size++]=item;
    }
    
    public Object get(int index) {
    	return elements[index];
    }
}
复制代码

这个代码灵活性很高,全部的类型均可以向上转型为Object类,这样咱们就能够往里面存储各类类型的数据了。的确Java在泛型出现以前,也是这么作的。可是这样的有一个问题:若是集合里面数据不少,某一个数据转型出现错误,在编译期是没法发现的。可是在运行期会发生java.lang.ClassCastException。例如:数组

MyList myList=new MyList();
myList.add("A");
myList.add(1);
System.out.println(myList.get(0));
System.out.println((String)myList.get(1));
复制代码

咱们在这个集合里面存储了多个类型(某些状况下容器可能会存储多种类型的数据),若是数据量较多,转型的时候不免会出现异常,而这些都是没法在编译期得知的。而泛型一方面让咱们只能往集合中添加一种类型的数据,同时可让咱们在编译期就发现这些错误,避免运行时异常的发生,提高代码的健壮性。bash

Java泛型介绍

下面咱们来介绍Java泛型的相关内容,下面会介绍如下几个方面:ide

  • Java泛型类
  • Java泛型方法
  • Java泛型接口
  • Java泛型擦除及其相关内容
  • Java泛型通配符

Java泛型类

类结构是面向对象中最基本的元素,若是咱们的类须要有很好的扩展性,那么咱们能够将其设置成泛型的。假设咱们须要一个数据的包装类,经过传入不一样类型的数据,能够存储相应类型的数据。咱们看看这个简单的泛型类的设计:函数

class DataHolder<T>{
    T item;
    
    public void setData(T t) {
    	this.item=t;
    }
    
    public T getData() {
    	return this.item;
    }
}
复制代码

泛型类定义时只须要在类名后面加上类型参数便可,固然你也能够添加多个参数,相似于<K,V>,<T,E,K>等。这样咱们就能够在类里面使用定义的类型参数。
泛型类最经常使用的使用场景就是“元组”的使用。咱们知道方法return返回值只能返回单个对象。若是咱们定义一个泛型类,定义2个甚至3个类型参数,这样咱们return对象的时候,构建这样一个“元组”数据,经过泛型传入多个对象,这样咱们就能够一次性方法多个数据了。ui

Java泛型方法

前面咱们介绍的泛型是做用于整个类的,如今咱们来介绍泛型方法。泛型方法既能够存在于泛型类中,也能够存在于普通的类中。若是使用泛型方法能够解决问题,那么应该尽可能使用泛型方法。下面咱们经过例子来看一下泛型方法的使用:this

class DataHolder<T>{
    T item;
    
    public void setData(T t) {
    	this.item=t;
    }
    
    public T getData() {
    	return this.item;
    }
    
    /**
     * 泛型方法
     * @param e
     */
    public <E> void PrinterInfo(E e) {
    	System.out.println(e);
    }
}
复制代码

咱们来看运行结果:spa

1
AAAAA
8.88
复制代码

从上面的例子中,咱们看到咱们是在一个泛型类里面定义了一个泛型方法printInfo。经过传入不一样的数据类型,咱们均可以打印出来。在这个方法里面,咱们定义了类型参数E。这个E和泛型类里面的T二者之间是没有关系的。哪怕咱们将泛型方法设置成这样:设计

//注意这个T是一种全新的类型,能够与泛型类中声明的T不是同一种类型。
public <T> void PrinterInfo(T e) {
    System.out.println(e);
}
//调用方法
DataHolder<String> dataHolder=new DataHolder<>();
dataHolder.PrinterInfo(1);
dataHolder.PrinterInfo("AAAAA");
dataHolder.PrinterInfo(8.88f);
复制代码

这个泛型方法依然能够传入Double、Float等类型的数据。泛型方法里面的类型参数T和泛型类里面的类型参数是不同的类型,从上面的调用方式,咱们也能够看出,泛型方法printInfo不受咱们DataHolder中泛型类型参数是String的影响。 咱们来总结下泛型方法的几个基本特征:

  • public与返回值中间很是重要,能够理解为声明此方法为泛型方法。
  • 只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并非泛型方法。
  • 代表该方法将使用泛型类型T,此时才能够在方法中使用泛型类型T。
  • 与泛型类的定义同样,此处T能够随便写为任意标识,常见的如T、E、K、V等形式的参数经常使用于表示泛型。

Java泛型接口

Java泛型接口的定义和Java泛型类基本相同,下面是一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}
复制代码

此处有两点须要注意:

  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一块儿加到类中。例子以下:
/* 即:class DataHolder implements Generator<T>{
 * 若是不声明泛型,如:class DataHolder implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
复制代码
  • 若是泛型接口传入类型参数时,实现该泛型接口的实现类,则全部使用泛型的地方都要替换成传入的实参类型。例子以下:
class DataHolder implements Generator<String>{
    @Override
    public String next() {
    	return null;
    }
}
复制代码

从这个例子咱们看到,实现类里面的全部T的地方都须要实现为String。

Java泛型擦除及其相关内容

咱们下面看一个例子:

Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1);		//class java.util.ArrayList
System.out.println(class2);		//class java.util.ArrayList
System.out.println(class1.equals(class2));	//true
复制代码

咱们看输出发现,class1和class2竟然是同一个类型ArrayList,在运行时咱们传入的类型变量String和Integer都被丢掉了。Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。咱们来看一个更完全的例子:

class Table {}
class Room {}
class House<Q> {}
class Particle<POSITION, MOMENTUM> {}
//调用代码及输出
List<Table> tableList = new ArrayList<Table>();
Map<Room, Table> maps = new HashMap<Room, Table>();
House<Room> house = new House<Room>();
Particle<Long, Double> particle = new Particle<Long, Double>();
System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
/** 
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
 */
复制代码

上面的代码里,咱们想在运行时获取类的类型参数,可是咱们看到返回的都是“形参”。在运行期咱们是获取不到任何已经声明的类型信息的。
注意:
编译器虽然会在编译过程当中移除参数的类型信息,可是会保证类或方法内部参数类型的一致性。
泛型参数将会被擦除到它的第一个边界(边界能够有多个,重用 extends 关键字,经过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。若是没有指明边界,那么类型参数将被擦除到Object。下面的例子中,能够把泛型参数T看成HasF类型来使用。

public interface HasF {
    void f();
}

public class Manipulator<T extends HasF> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
}
复制代码

extend关键字后后面的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HasF类型。

Java泛型擦除的原理

咱们经过例子来看一下,先看一个非泛型的版本:

// SimpleHolder.java
public class SimpleHolder {
    private Object obj;
    public Object getObj() {
        return obj;
    }
    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String s = (String) holder.getObj();
    }
}
// SimpleHolder.class
public class SimpleHolder {
  public SimpleHolder();
    Code:
       0: aload_0       
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return        

  public java.lang.Object getObj();
    Code:
       0: aload_0       
       1: getfield      #2 // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(java.lang.Object);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2 // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3 // class SimpleHolder
       3: dup           
       4: invokespecial #4 // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5 // String Item
      11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8 // class java/lang/String
      21: astore_2      
      22: return        
}
复制代码

下面咱们给出一个泛型的版本,从字节码的角度来看看:

//GenericHolder.java
public class GenericHolder<T> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setObj("Item");
        String s = holder.getObj();
    }
}

//GenericHolder.class
public class GenericHolder<T> {
  T obj;

  public GenericHolder();
    Code:
       0: aload_0       
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return        

  public T getObj();
    Code:
       0: aload_0       
       1: getfield      #2 // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(T);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2 // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3 // class GenericHolder
       3: dup           
       4: invokespecial #4 // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5 // String Item
      11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8 // class java/lang/String
      21: astore_2      
      22: return        
}
复制代码

在编译过程当中,类型变量的信息是能拿到的。因此,set方法在编译器能够作类型检查,非法类型不能经过编译。可是对于get方法,因为擦除机制,运行时的实际引用类型为Object类型。为了“还原”返回结果的类型,编译器在get以后添加了类型转换。因此,在GenericHolder.class文件main方法主体第18行有一处类型转换的逻辑。它是编译器自动帮咱们加进去的。
因此在泛型类对象读取和写入的位置为咱们作了处理,为代码添加约束。

Java泛型擦除的缺陷及补救措施

泛型类型不能显式地运用在运行时类型的操做当中,例如:转型、instanceof 和 new。由于在运行时,全部参数的类型信息都丢失了。相似下面的代码都是没法经过编译的:

public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
        //编译不经过
        if (arg instanceof T) {
        }
        //编译不经过
        T var = new T();
        //编译不经过
        T[] array = new T[SIZE];
        //编译不经过
        T[] array = (T) new Object[SIZE];
    }
}
复制代码

那咱们有什么办法来补救呢?下面介绍几种方法来一一解决上面出现的问题。

类型判断问题

咱们能够经过下面的代码来解决泛型的类型信息因为擦除没法进行类型判断的问题:

/**
 * 泛型类型判断封装类
 * @param <T>
 */
class GenericType<T>{
    Class<?> classType;
    
    public GenericType(Class<?> type) {
        classType=type;
    }
    
    public boolean isInstance(Object object) {
        return classType.isInstance(object);
    }
}
复制代码

在main方法咱们能够这样调用:

GenericType<A> genericType=new GenericType<>(A.class);
System.out.println("------------");
System.out.println(genericType.isInstance(new A()));
System.out.println(genericType.isInstance(new B()));
复制代码

咱们经过记录类型参数的Class对象,而后经过这个Class对象进行类型判断。

建立类型实例

泛型代码中不能new T()的缘由有两个,一是由于擦除,不能肯定类型;而是没法肯定T是否包含无参构造函数。
为了不这两个问题,咱们使用显式的工厂模式:

/**
 * 使用工厂方法来建立实例
 *
 * @param <T>
 */
interface Factory<T>{
    T create();
}

class Creater<T>{
    T instance;
    public <F extends Factory<T>> T newInstance(F f) {
    	instance=f.create();
    	return instance;
    }
}

class IntegerFactory implements Factory<Integer>{
    @Override
    public Integer create() {
    	Integer integer=new Integer(9);
    	return integer;
    }
}
复制代码

咱们经过工厂模式+泛型方法来建立实例对象,上面代码中咱们建立了一个IntegerFactory工厂,用来建立Integer实例,之后代码有变更的话,咱们能够添加新的工厂类型便可。
调用代码以下:

Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));
复制代码
建立泛型数组

通常不建议建立泛型数组。尽可能使用ArrayList来代替泛型数组。可是在这里仍是给出一种建立泛型数组的方法。

public class GenericArrayWithTypeToken<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[]) Array.newInstance(type, sz);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        
    }
}
复制代码

这里咱们使用的仍是传参数类型,利用类型的newInstance方法建立实例的方式。

Java泛型的通配符

上界通配符<? extends T>

咱们先来看一个例子:

class Fruit {}
class Apple extends Fruit {}
复制代码

如今咱们定义一个盘子类:

class Plate<T>{
    T item;
    public Plate(T t){
        item=t;
    }
    
    public void set(T t) {
        item=t;
    }
    
    public T get() {
        return item;
    }
}
复制代码

下面,咱们定义一个水果盘子,理论上水果盘子里,固然能够存在苹果

Plate<Fruit> p=new Plate<Apple>(new Apple());
复制代码

你会发现这段代码没法进行编译。装苹果的盘子”没法转换成“装水果的盘子:

cannot convert from Plate<Apple> to Plate<Fruit>
复制代码

从上面代码咱们知道,就算容器中的类型之间存在继承关系,可是Plate和Plate两个容器之间是不存在继承关系的。 在这种状况下,Java就设计成Plate<? extend Fruit>来让两个容器之间存在继承关系。咱们上面的代码就能够进行赋值了

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
复制代码

Plate<? extend Fruit>是Plate< Fruit >和Plate< Apple >的基类。
咱们经过一个更加详细的例子来看一下上界的界限:

class Food{}

class Fruit extends Food {}
class Meat extends Food {}

class Apple extends Fruit {}
class Banana extends Fruit {}
class Pork extends Meat{}
class Beef extends Meat{}

class RedApple extends Apple {}
class GreenApple extends Apple {}
复制代码

在上面这个类层次中,Plate<? extend Fruit>,覆盖下面的蓝色部分:

若是咱们往盘子里面添加数据,例如:

p.set(new Fruit());
p.set(new Apple());
复制代码

你会发现没法往里面设置数据,按道理说咱们将泛型类型设置为? extend Fruit。按理说咱们往里面添加Fruit的子类应该是能够的。可是Java编译器不容许这样操做。<? extends Fruit>会使往盘子里放东西的set()方法失效。但取东西get()方法还有效
缘由是:
Java编译期只知道容器里面存放的是Fruit和它的派生类,具体是什么类型不知道,多是Fruit?多是Apple?也多是Banana,RedApple,GreenApple?编译器在后面看到Plate< Apple >赋值之后,盘子里面没有标记为“苹果”。只是标记了一个占位符“CAP#1”,来表示捕获一个Fruit或者Fruit的派生类,具体是什么类型不知道。全部调用代码不管往容器里面插入Apple或者Meat或者Fruit编译器都不知道能不能和这个“CAP#1”匹配,因此这些操做都不容许。
最新理解:
一个Plate<? extends Fruit>的引用,指向的多是一个Plate类型的盘子,要往这个盘子里放Banana固然是不被容许的。个人一个理解是:Plate<? extends Fruit>表明某个只能放某种类型水果的盘子,而不是什么水果都能往里放的盘子
可是上界通配符是容许读取操做的。例如代码:

Fruit fruit=p.get();
Object object=p.get();
复制代码

这个咱们很好理解,因为上界通配符设定容器中只能存放Fruit及其派生类,那么获取出来的咱们均可以隐式的转为其基类(或者Object基类)。因此上界描述符Extends适合频繁读取的场景。

下界通配符<? super T>

下界通配符的意思是容器中只能存放T及其T的基类类型的数据。咱们仍是以上面类层次的来看,<? super Fruit>覆盖下面的红色部分:

下界通配符<? super T>不影响往里面存储,可是读取出来的数据只能是Object类型。
缘由是:
下界通配符规定了元素最小的粒度,必须是T及其基类,那么我往里面存储T及其派生类都是能够的,由于它均可以隐式的转化为T类型。可是往外读就很差控制了,里面存储的都是T及其基类,没法转型为任何一种类型,只有Object基类才能装下。

PECS原则

最后简单介绍下Effective Java这本书里面介绍的PECS原则。

  • 上界<? extends T>不能往里存,只能往外取,适合频繁往外面读取内容的场景。
  • 下界<? super T>不影响往里存,但往外取只能放在Object对象里,适合常常往里面插入数据的场景。

<?>无限通配符

无界通配符 意味着可使用任何对象,所以使用它相似于使用原生类型。但它是有做用的,原生类型能够持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型。举个例子,在List<\?>类型的引用中,不能向其中添加Object, 而List类型的引用就能够添加Object类型的变量。
最后提醒一下的就是,List<\Object>与List<?>并不等同,List<\Object>是List<?>的子类。还有不能往List<?> list里添加任意对象,除了null。

相关文章
相关标签/搜索