Java 泛型

200 阅读12分钟

Java 泛型

1、概述

1.1、什么是泛型?

参数化类型

类似于在定义方法中的形参和实参,将类型由原来的具体类型转变为一个参数(类型形参),在使用调用的过程中传入具体的类型(类型实参)

1.2、为什么要使用泛型?

为了参数化类型,即在不创建新的类型的情况下,通过泛型指定不同的类型来控制形参具体限制的类型,从而达到复用的目的,也是多态的一种体现

2、泛型例子

List arrayList = new ArrayList();
arrayList.add("aa");
arrayList.add(100);

for(int i = 0; i < arrayList.size(); i++){
    String item = (String)arrayList.get(i);
    System.out.println(item)
}

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃,泛型就是为了解决这样的问题才出现的

List<String> arrayList = new ArrayList<>();

// 这样在程序的编译阶段,就会检查到错误

3、泛型的特性

泛型只在编译阶段有效。看下面的代码:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("泛型测试","类型相同");
}

编译之后程序会采取去泛型化的措施,也就是说Java中的泛型只在编译阶段有效,在编译的过程中,正确检验出泛型结果后,会将泛型的信息擦除, 并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

4、泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

4.1、泛型类

主要用在类的定义过程中,被称为泛型类,通过泛型可以完成对一组类的操作,其中最典型的就是JDK中各种容器类,如ListSetMap

public class Test<T> {
    private T info;
    
    public Test(T info){//泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.info = info;
    }
    
    public void setInfo(T t){
        this.info = t;
    }
    
    public T getInfo(){	//泛型方法getKey的返回值类型为T,T的类型由外部指定
        return info;
    }
}

泛型的类型参数只可以是类类型,不可以是基本类型

Generic<Integer> genericInteger = new Generic<>(123);

当然,定义的泛型类型也可以不传入类型参数,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

Generic generic1 = new Generic("111111");
Generic generic2 = new Generic(4444);
Generic generic3 = new Generic(55.55);

Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());

注意:不可以对泛型类型使用instanceOf操作符,会产生编译时错误

// 错误示范
if(a instanceOf Generic<Integer>){
    // 
}

4.2、泛型接口

和泛型类类似,常用在各种类的生产器中

public interface Generator<T> {
    T next();
}

当实现泛型接口的类,未传入泛型实参时:

public class FruitGeneric<T> implements Generator<T> {
    @Override
    public T next(){
        //xxx
    }
}

当实现泛型接口的类,传入了泛型实参时:

public class FruitGeneric implements Generator<String> {
    private String[] fruits = {"Apple", "Banana", "Pear"};
    
    @Override
    public String next() {
    	Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

注意:如果未传入泛型实参,又没有在实现类中声明就会报错

// 错误示范
public class FruitGenerator implements Generator<T> {
    @Override
    public T next() {
        //xxx
    }
}

4.3、泛型通配符

IntegerNumber的一个子类,同时我们在前面的特性中也了解到

Generic<Number>Generic<Integer> 实际上是一种基本类型

所以,在使用Generic<Number>作为形参的方法中,能否使用Generic<Integer>,作为实参传入呢?

在逻辑上类似于Generic<Number>Generic<Integer>是否可以看成具有父子关系的泛型类型呢?

public void showKeyValue(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
    
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gInteger);

// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);

通过提示信息我们可以看到Generic<Integer> 不能被看作为Generic<Number>的子类 :

同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的

那么如何解决上述问题?或者说,如何在逻辑上表示同时是Generic<Integer>Generic<Number>父类的引用类型呢?

类型通配符应运而生:

public void showKeyValue(Generic<?> obj) {
	Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是用?来替代具体的类型实参

请注意: 此处?是类型实参,而不是类型形参

换句话说,此处的?NumberStringInteger一样都是一种实际的类型,可以把?看成所有类型的父类

4.4、泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;

泛型方法,是在调用方法的时候指明泛型的具体类型

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}
Object obj = genericMethod(Class.forName("com.test.test"));
4.4.1、泛型方法的基本使用
public class Generic<T>{     
    private T key;

    public Generic(T key) {
        this.key = key;
    }
    //虽然在方法中使用了泛型,但是这并不是一个泛型方法。
    //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
    //所以在这个方法中才可以继续使用T这个泛型。
    public T getKey(){
        return key;
    }


   /* 这个方法显然是有问题的,在编译器会给我们错误信息"cannot reslove symbol E"
    * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
    * public E setKey(E key){
    *    this.key = keu
    * }
    */     
}
/** 
 * 这才是一个真正的泛型方法。
 * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
 * 这个T可以出现在这个泛型方法的任意位置.
 * 泛型的数量也可以为任意多个 
 *    如:public <T,K> K showKeyName(Generic<T> container){
 *        ...
 *        }
 */
public class GenericTest {
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        T test = container.getKey();
        return test;
    }
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public class GenericTest {
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public class GenericTest {
	public void showKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }
}
/**
 * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
 * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
 * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
 */
public class GenericTest {
    // 错误示范
    public <T> T showKeyName(Generic<E> container){
		// xxx
    }  
}
/**
 * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
 * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
 * 所以这也不是一个正确的泛型方法声明。
 */
public class GenericTest {
	// 错误示范
    public void showkey(T genericObj){
		// xxx
    }
}
4.4.2、类中的泛型方法

以上并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时

public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}
4.4.3、泛型方法和可变参数

一个泛型方法和可变参数的例子:

public <T> void printMsg(T... args){
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}
printMsg("111",222,"aaaa","2323.4",55.55); 
4.4.4、泛型静态方法

在类中的静态方法使用泛型需要注意:

静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

/*
 * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
 * 即使静态方法要使用泛型类中已经声明过的泛型也不可以
 * 如:public static void show(T t){..},此时编译器会提示错误信息:
 *  "StaticGenerator cannot be refrenced from static context"
 */ 
public class GenericTest<T> {
    
    public static <T> void show (T t){
        // xxx
    }
}
4.4.5、总结

泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:

无论何时,如果你能做到,你就该尽量使用泛型方法。

也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。

另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

4.5、泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制

如:类型实参只准传入某种类型的父类或某种类型的子类。

4.5.1、上边界

使用<? extends Number>的泛型定义称之为上界通配符, 泛型类型T的上界限定在Number了。

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型:

public void showKeyValue(Generic<? extends Number> obj) {
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

如果我们把泛型类的定义也改一下:

public class Generic<T extends Number>{
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
//这一行代码也会报错,因为String不是Number的子类
Generic<String> generic1 = new Generic<String>("11111");

再来一个泛型方法的例子:

//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();
    return test;
}
4.5.2、下边界

和上边界类似,如下这种情况能接收A类或A类的父类

public static void insertElement(List<? super A> list) {
    list.add(new A());
    list.add(new B());
    list.add(new C());
}

注意:

泛型通配符 <? extends T>来接收返回的数据,此方法的泛型集合不能使用add方法

<? super T> 不能使用get方法, 因为都是T类或父类,那么就可以安全的插入A类。

即:

带有子类限定的可以从泛型读取【也就是--->(? extend T)】-------->Producer Extends 带有超类限定的可以从泛型写入【也就是--->(? super T)】-------->Consumer Super

自己的理解:PECS原则 Producer Extends, Consumer Super

Apple<? extends T> 即<>里面只能是T或者T的一个子类,这个应该是确定的

所以我们无法对其中进行写操作,因为我们无法确定是哪个子类?但是我们可以进行读操作,因为肯定是T或者T的子类,都可以用T来接收

Apple<? super T> 即<>里面只能是T或者T的一个父类,这个类在运行时是唯一确定的

所以我们可以进行写操作,但是只能写进T的子类,因为我们可以确定T的子类一定是<>里面类的一个子类,但是我们无法进行读操作,因为我们不能够确定是哪个父类,无法引用,只能用Object来接收

4.5、泛型数组

在java中是**"不能创建一个确切的泛型类型的数组"**

也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];  

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10];  

这样也是可以的:

List<String>[] ls = new ArrayList[10];

使用Sun的一篇文档的一个例子来说明这个问题:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
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    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现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