Java-泛型详解

1,132 阅读17分钟

概述

什么是泛型(Generic)

实现了参数化类型,适用于非特定类型的代码

普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。

多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。

接口突破了类的单一继承体系,如果方法以接口而不是类作为参数,限制就宽松很多了。

即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用非特定的类型,而不是一个具体的接口或类。这就是泛型的概念,是 Java 5 的重大变化之一。泛型实现了参数化类型。“泛型”这个术语的含义是**“适用于很多类型”**。

如果你了解其他语言(例如 C++ )的参数化机制,你会发现,Java 泛型并不能满足所有的预期。这并不是说 Java 泛型毫无用处。在很多情况下,它可以使代码更直接更优雅。

本质:参数化类型

参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递该类型实参。
参数化类型怎么理解?就是将**类型指定为参数**。类似于方法中的变量参数,此时类型也定义成参数形式(类型形参),然后在使用时传入具体的类型(类型实参)。

类型形参、类型实参

重点理解:类型作为形参、实参

//类型形参: T、V extends Teach
public class School<T>{
    public void manage(T t){}
    public <V extends Teach> void hasTeacher(V v){}
}

//类型实参: Student、Teacher(不是teacher对象哦)
School<Student> school = new School();
Teacher teacher = new Teacher();
school.hasTeacher(teacher);
为什么要引入泛型
  • 突破类型限制,适用于“非特定类型”的代码

  • 类型安全
    编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常,提高 Java 程序的类型安全

  • 消除强制类型转换
    使用时直接得到目标类型,消除强制类型转换

泛型命名

任意字母,通常情景下提高可读性代表含义:

  • E — Element,常用在java Collection里,如:List,Iterator,Set
  • K,V — Key,Value,代表Map的键值对
  • N — Number,数字
  • T — Type,类型,如String,Integer等等
    ...
  • 任何泛型变量都派生自Object,而不能使用原生类型如int、float、true

泛型作用位置

接口、类、方法中,分别被称为泛型接口、泛型类、泛型方法

: 类型声明,T: 类型形参,t:类型实参

泛型接口

类型声明:类型形参列表 写在接口名后

interface Info <T>{
    T get();
    void set(T t);
}

泛型类

类型声明:类型形参列表 写在类名后

class StuInfo<T>{
    private T t;
    public T get(){ return t;}
    public void set(T t){}
}

//泛型类实现接口:未填充接口的泛型,是泛型类
class StuInfo<T> implement Info<T>{
    public T get(){}
    public void set(T t){}
}


//非泛型类实现接口:填充接口的泛型,类已不是泛型类
class StuInfo implement Info<String>{
    public Stirng get(){}
    public void set(String str){}
}


//多泛型类实现接口,是泛型类
class StuInfo<T,V> implement Info<T>{
    public T get(){}
    public void set(T t){}
    
    public V get(){}
    public void set(V v){}
}

//多泛型类实现接口,是泛型类
class StuInfo<V> implement Info<String>{
    public Stirng get(){}
    public void set(String str){}
    
    public V get(){}
    public void set(V v){}
}

泛型方法

类型声明:类型形参列表 写在返回值前

泛型方法与所在的类是否是泛型类无关

泛型方法使得该方法能够独立于类而改变。作为准则,应“尽可能”使用泛型方法取代使整个类为泛型类的方式,因为将单个方法泛型化要比将整个类泛型化更清晰易懂。

static无法访问泛型类的类型参数,所以如果一个方法是static的,就必须使其成为泛型方法。

public class Test<T> {
    //不是泛型方法,只是使用了泛型类的类型参数
    public T setValue(T v){
        return v;
    }

    //泛型方法:跟类是否是泛型类无关
    public <V> V setValue2(V v){
        return v;
    }

    // 泛型方法:static方法不能访问该类的类型参数,只能在方法本身定义泛型类型
    public static <K> void setValue3(K k){}
}

//泛型类方法
Test<String> test = new Test();
test.setValue("Hello");

//泛型方法,推荐2写法
test.setValue2(1);
test.<Integer>setValue2(2);

//static泛型方法,推荐2写法
Test.setValue3(true);
Test.<Boolean>setValue3(true);

类型参数推断

使用泛型类时,必须在创建对象的时候指定类型参数。而使用泛型方法时,可以不必指明参数类,因为编译器会找出这些类型,这称为类型参数推断

类型擦除

c++泛型

template<class T> class Manipulator {
    T obj;
public:
    Manipulator(T x) { obj = x; }
    /**
    * 此处可以看到,c++中可以直接使用泛型类型的方法
    */
    void manipulate() { obj.f(); }
};

class HasF {
public:
    void f() { cout << "HasF::f()" << endl; }
};

int main() {
    HasF hf;
    Manipulator<HasF> manipulator(hf);
    manipulator.manipulate();
}

/* Output:
HasF::f()
*/

Manipulator 类存储了一个 T 类型的对象。manipulate() 方法会调用 obj 上的 f() 方法。它是如何知道类型参数 T 中存在 f() 方法的呢?C++ 编译器会在你实例化模版时进行检查,所以在 Manipulator<HasF> 实例化的那一刻,它看到 HasF 中含有一个方法 f()。如果情况并非如此,你就会得到一个编译期错误,保持类型安全。

因为擦除,Java 编译器无法将 manipulate() 方法必须能调用 objf() 方法这一需求映射到 HasF 具有 f() 方法这个事实上。

class Manipulator<T> {
    private T obj;

    Manipulator(T x) {
        obj = x;
    }

    // Error: cannot find symbol: method f():
    public void manipulate() {
        obj.f();
    }
}

Java 泛型

类型擦除:type erasure,编译器对带有泛型的java代码进行**编译时,执行类型检查类型推断**,生成普通的不带泛型的字节码 。

通俗的讲:泛型类和普通类在Java虚拟机内运行时是没有区别的

  • Java的泛型是“伪泛型”,在编译期有效,在运行期被删除,所有泛型参数类型在编译后都会被清除

    (编译期自动完成了从 Generic Java 到普通 Java 的翻译,也就是说Java 虚拟机运行时对泛型一无所知)

  • Java泛型是使用擦除来实现的,任何具体的类型信息都会被擦除为原生类型,唯一知道的就是在使用一个Object对象

    • 在泛型代码内部,无法获得任何有关泛型参数类型的信息
    • 泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof、new
  • 泛型类型参数将擦除到它的第一个边界(可能有多个边界)

/**
* 泛型的 class 对象是相同的
* 每个类都有一个 class 属性,泛型不会改变 class 属性的返回值
* List<String> 和 List<Integer> 擦除后的类型都是 原始类型的List
**/
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() // true

边界:extends

因为擦除,Java 编译器无法将 manipulate() 方法必须能调用 objf() 方法这一需求映射到 HasF 具有 f() 方法这个事实上。为了调用 f(),我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。在Java中重用了extends关键字来实现。

注意:这里的extends是边界,不同于通配符中extends的含义

public interface IWater{
    void drink();
}
public interface ISmell{
    void smell();
}

//边界:只能一个类,可以多个接口
public class Manipulator<T extends HasF & IWater & ISmell> {
    private T t;
    public void manipulate() {
        t.f();
    }
    public void test(T t){
        t.taste();
        t.smell();
    }
}

边界

泛型擦除到第一个边界

//只能一个类,可以多个接口
public class FWSAll<T extends Fruit & IWater & ISmell>{
    public void test(T t){}
}

public class FWSObject extends Fruit implements IWater, ISmell{...}


FWSAll<FWSObject> fwsAll = new FWSAll<>();
Class<? extends FWSAll> fwsClazz = fwsAll.getClass();
for (Method method : fwsClazz.getDeclaredMethods()) {
    System.out.println("GenericMain.run method: \n"+ method.toString());
}

===>
public void com.varmin.project.java.generic.GenericMain$FWSAll.test(com.varmin.project.java.generic.GenericMain$Fruit)

如果是反射使用test()方法时,应该调用getDeclaredMethod("test", Fruit.class)方法,因为类型被擦除时T被替换成了Fruit类型了。

擦除带来的问题

资料:

Java 泛型,你了解类型擦除吗?
Java中泛型 类型擦除
类型擦除以及类型擦除带来的问题
Java泛型擦除的缺陷及补救措施
面试官问我:“泛型擦除是什么,会带来什么问题?”

无法获取泛型信息

在泛型代码内部,无法获得任何有关泛型参数类型的信息

解决:传入泛型类对象

无法显示引用运行时类型信息

泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof、new

解决:传入泛型类对象

运行时绕过泛型限制
List<Integer> ls = new ArrayList<>();
ls.add(23);
ls.add("text"); //error

Method method = ls.getClass().getDeclaredMethod("add",Object.class);
method.invoke(ls,"test");//运行时:擦除泛型类型,可以存入

for ( Object o: ls){
	System.out.println(o);
}

===>
23
test
    
//编译期不报错,运行时报错
int i = ls[1];//RuntimeException

解决:

创建、操作泛型类对象时使用工厂类或封装好的方法:尽量避免在程序逻辑中直接操作,应使用验证过的封装程序来操作。

逆变、协变

资料:

逆变与协变-wiki

Java中的逆变与协变

概述

里氏替换原则

LSP由Barbara Liskov于1987年提出,其定义如下:

所有引用基类(父类)的地方必须能透明地使用其子类的对象

LSP包含以下四层含义:

  • 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法。
  • 子类中可以增加自己的方法。
  • 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更为宽松。
  • 当子类覆盖或实现父类的方法时,方法的返回值要比父类更严格。
逆变与协变

用来描述类型转换(type transformation)后的继承关系

其定义:如果A、B表示类型,f ( ⋅ ) 表示类型规则或者类型构造器,≤ 表示继承关系(比如,B≤A 表示B是由A派生出来的子类)

当B ≦ A时,如果有 f(B) ≦ f(A),f ( ⋅ )是协变(covariant)(保持子类型关系); 当B ≦ A时,如果有 f(A) ≦ f(B),f ( ⋅ )是逆变(contravariant)(逆转子类型关系); 如果上面两种关系都不成立,,则f ( ⋅ )是不可变(invariant)。

类型规则或类型构造器

有如下继承结构,便于下文示例所用:

/*
Food
    |--Fruit
       |--Banana
       |--Apple
           |--RedApple
    |--Meat
       |--Pork
       |--Beef
           |--RedBeef
*/
public class Food {}

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

public class Banana extends Fruit {}
public class Apple extends Fruit {}
public class RedApple extends Apple {}

public class Pork extends Meat {}
public class Beef extends Meat {}
public class RedBeef extends Beef {}
数组类型构造器

首先考虑数组类型构造器: 从Fruit类型,可以得到Fruit[], 是否可以把它当作:

  • 协变? 一个Apple[]也是一个Fruit[]
  • 逆变? 一个Fruit[]也是一个Apple[]
  • 以上二者均不是(不变)?

如果数组支持对其元素进行读写操作,那么只有不变是类型安全的:

  • 如果协变,不安全,因为也可以把一个Banana放到Fruit[]中
  • 如果逆变,不安全,因为期望得到一个Apple时可能从Fruit[]中拿到的是一个Banana

Java数组协变:

  • Java支持数组协变
  • 存在不安全因素,运行时检查写入元素类型

早期的Java不包含泛型,如果使数组“不变”将导致多态程序不可复用,所以Java把数组类型处理为协变

但是如上文所述,协变数组在写入操作中会存在不安全因素。Java与C#为此把每个数组对象在创建时附标一个类型。 每当向数组存入一个值,编译器插入一段代码来检查该值的运行时类型是否等于数组的运行时类型。如果不匹配,会抛出一个ArrayStoreException。

Fruit[] fruits = new Apple[10];
fruits[0] = new Apple();
fruits[1] = new RedApple();
// 编译期:Banana、Fruit放入Fruit[]没有问题
// 运行时:知道是Apple类型的数组,报错运行时异常:java.lang.ArrayStoreException
fruits[2] = new Banana();//RuntimeException
fruits[3] = new Fruit();//RuntimeException
函数类型

根据里氏替换原则,返回值的类型可以更具体,也就是协变;接受更宽泛的参数类型,也就是逆变。

返回值协变:

  • Java支持方法返回值协变

  • 重写父类的方法

  • 里氏原则:返回值比父类方法更严格

public class Fruit extends Food {
    public Fruit get(){ return new Fruit(); }
}

public class Apple extends Fruit {
    //重写
    @Override
    public Apple get() { return new Apple(); }
}


Fruit fruit = new Apple();
// Apple < Fruit,协变
Fruit fa = fruit.get(); 

参数逆变:

  • Java不支持方法参数逆变
  • 重载父类方法
  • 里氏原则:参数比父类方法更宽松
public class Fruit extends Food {
    public void set(Apple fruit){}
}

public class Apple extends Fruit {
    //重载
    public void set(Fruit fruit) {}
}


Apple apple = new Apple();
apple.set(new Apple());//调用的是Fruit的方法
// Fruit < Apple, 逆变
apple.set(new Fruit());//调用的是Apple的方法
泛型
  • Java的泛型是不变
  • 使用通配符实现“逆变、协变”
class SetGetter<T>{
    private T mT;
    public void set(T t){this.mT = t;}
    public T get(){ return mT;}
}

//Food < ? super Fruit 逆变
SetGetter<? super Fruit> setter = new SetGetter<Food>();
setter.set(new Fruit());
setter.set(new Apple());
Object sg = setter.get();
//Apple < ? extends Fruit 协变
SetGetter<? extends Fruit> getter = new SetGetter<Apple>();
//getter.set(new Object()); Compile Error: incompatible types(编译错误:不兼容类型)
Fruit g = getter.get();

通配符

  • 希望传入的类型不局限于某个单一类型,而是有一个指定的范围

  • Java泛型是“不变”的,使用通配符实现“协变/逆变”

  • 注意:通配符作用位置是,类型实参

  • :无限制通配符
  • <? extends E> :声明类型的上界 ,表示参数化的类型是:指定类型,或指定类型的子类
  • <? super E> :声明类型的下界 ,表示参数化的类型是:指定类型,或指定类型的父类

定义以下继承关系:

Food
|--Fruit
   |--Apple
       |--RedApple
       |--GreenApple
   |--Banana
|--Meat   
   |--Pork
   |--Beef


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

public class Banana extends Fruit {}
public class Apple extends Fruit {}
public class RedApple extends Apple {}
public class GreenApple extends Apple {}

public class Pork extends Meat {}
public class Beef extends Meat {}

public class Plate<T>{
    private T mT;
    public void add(T t){
        this.mT = t;
    }
    public T get(){
        return mT;
    }
}

3.1 泛型类型

/**
 * 虽然Fruit和Apple有继承关系,但Plate<Fruit>不是Plate<Apple>的父类
 * 它们的泛型类没有继承关系, 是完全不同的两种类型
 * Java中的泛型是"不变"的(协变逆变)
 */
//编译期不相同,运行时相同
Plate<Fruit> plateApple = new Plate<Apple>();// Compile Error: incompatible types
System.out.println("run: "+ new Plate<Fruit>()+", "+ new Plate<Apple>());
//GenericMain.run:  GenericMain$Plate, GenericMain$Plate


 /**
 * 使用通配符,使泛型逆变/协变
 */
 Plate<?> plate1 = new Plate<Apple>();
 Plate<? extends Fruit> plate2 = new Plate<Apple>();// 协变
 Plate<? super Fruit> plate3 = new Plate<Food>();// 逆变

3.2 无限制通配符 < ?>

对于不确定或者不关心实际要操作的类型,可以使用无限制通配符,表示可以持有任何某一具体类型

Plate<?> plate1 = new Plate<Apple>();
plate1.add(new Apple());//error
plate1.get().toString();

3.3 上界通配符 < ? extends E>

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

  • ? 是通配符,表示泛型类型是个未知类型
  • extends 限制了这个未知类型的上界
    • 它的范围不仅包括子类,还包括自己
    • 它还有implement的意思,即上界限也可以是interface
Plate<? extends Fruit> plate2 = new Plate<Apple>();//协变

plate2.add(new Food());//error
plate2.add(new Fruit());//error
plate2.add(new Apple());//error
Fruit g2 = plate2.get();//边界
  • ? extends Fruit 代表的是一个范围内的某一个具体类型,而不是代表某个范围
    • ? extends Fruit代表下图蓝框中的某一个具体类型,而不是指Plate<? extends Fruit>中可以存放篮框中的所有类型
  • ? extends Fruit的最大边界是Fruit,没有最小边界
    • 可能是Plate,也可能是Plate...
    • plate无法知道自己的确切类型,也不知道最小边界,所以不能add。因为有可能Plate.add( Fruit.class),这明显是不可以的
    • plate知道自己的最大边界是Fruit,存入的类型可以向上转型,所以可以get具体类型
    • 根据PECS原则,该类为生产者,能取不能存

image

3.4 下界通配符 < ? super E>

与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。

  • ? 是通配符,表示泛型类型是个未知类型
  • super限制了这个未知类型的下界
    • 它的范围不仅包括父类,还包括自己
    • 它还有implement的意思,即下界限也可以是interface
Plate<? super Fruit> plate3 = new Plate<Food>();

plate3.add(new Food());//error
plate3.add(new Fruit());
plate3.add(new Apple());
plate3.add(new RedApple());
Object g3 = plate3.get();
  • ? superFruit 代表的是一个范围内的某一个具体类型,而不是代表某个范围
    • ? super Fruit代表下图红框中的某一个具体类型,而不是指Plate<? super Fruit>中可以存放红框中的所有类型
  • ? superFruit的最小边界是Fruit,没有最大边界
    • 可能是Plate,也可能是Plate,Plate...
    • plate无法知道自己的确切类型,但知道最小边界为Fruit,只要是Fruit及其子类都可以存入。
    • plate没有最大边界,get无法向上转型,所以无法get具体类型
    • 根据PECS原则,该类为消费者,能存不能取
    • image

      3.5 PECS原则

      什么时候用extends,什么时候用super呢?

      为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:

      生产者有上限、消费者有下限 ,即要生产又要消费就不要使用通配符
      PECS: producer-extends, costumer-super

      /**
       * PECS
       * @param fruits:  producer-extends
       * @param plate:  consumer-super
       */
      public void fillPlate(List<? extends Fruit> fruits, List<? super Fruit> plate){
          for (int i = 0; i < fruits.size(); i++) {
              Fruit fruit = fruits.get(i);//生产者
              plate.add(fruit);//消费者
          }
      }
      

      泛型数组

      • 不能创建具体的泛型类型数组

      • 不能创建,但可以使用泛型数组

      为什么不能在Java中创建泛型数组?

      java 泛型详解(第4.7节)

      java为什么不支持泛型数组?

      不能创建泛型数组

      早期的Java由于没有泛型,为了使数组能够复用,所以把数组设计为“协变”。由于数组协变会产生不安全操作的隐患,所以在运行期插入元素时会进行类型检查

      但由于泛型擦除,会破坏泛型数组在运行时的类型检查机制,即数组需要进行类型检查,但泛型数组的泛型类型会被擦除为原始类型。所以在Java中不允许创建泛型数组。

      Plate<Fruit>[] p3 = new Plate<Fruit>[10];//error Java不支持创建具体类型的泛型数组
      Plate[] p1 = new Plate[10];
      Plate<?>[] p2 = new Plate<?>[10];
      Plate<Fruit>[] p4 = new Plate[10];
      
      List<String>[] ls = new ArrayList<String>[10];  //error,不能创建泛型数组 
      List<?>[] ls = new ArrayList<?>[10]; //不影响类型检查,可以创建
      List<String>[] ls = new ArrayList[10];//可以使用泛型数组
      

      区别:

      • Java数组,产生的不安全因素是:协变,插入不同类型的元素
        • 编译期不报错,运行时报报错(ArrayStoreException)
        • 存入时:无法通过类型检查机制
      • Java泛型数组,产生的不安全因素是:擦除,破坏数组的类型检查机制
        • 编译期不报错,运行时报错(ClassCastException)
        • 存入时:破坏类型检查机制
      //Java数组:由于协变,会使数组在编译期可以插入不同类型的元素,但在运行时会报错
      //存入时:无法通过类型检查机制
      Fruit[] fruits = new Apple[10];
      fruits[0] = new Apple();
      fruits[1] = new RedApple();
      // 编译期:Banana、Fruit放入Fruit[]没有问题
      // 运行时:知道是Apple类型的数组,报错运行时异常:java.lang.ArrayStoreException
      fruits[2] = new Banana();//RuntimeException
      fruits[3] = new Fruit();//RuntimeException
      
      
      //具体类型的泛型数组:由于擦除,会使数组可以插入不同泛型类型,在取出时转为所声明的泛型类型
      //存入时:破坏类型检查机制
      List<String>[] lsa = new List<String>[10]; // 实际上是不能这样创建的
      Object o = lsa;
      Object[] oa = (Object[]) o;
      List<Integer> li = new ArrayList<Integer>();
      li.add(new Integer(3));
      oa[1] = li; // 不健全,但通过运行时存储检查:Unsound, but passes run time store check
      List<String> s = lsa[1];// 把List<Integer>类型的元素,当作List<String>取出
      String s = lsa[1].get(0); // Run-time error: ClassCastException.
      

      可以使用通配符创建数组

      List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
      Object o = lsa;
      Object[] oa = (Object[]) o;
      List<Integer> li = new ArrayList<Integer>();
      li.add(new Integer(3));
      oa[1] = li; // Correct.
      Integer i = (Integer) lsa[1].get(0); // OK