【Java】泛型(Generics)

Posted by 西维蜀黍 on 2019-04-02, Last Modified on 2021-09-21

什么是泛型(generics)?

泛型(generics)是指参数化类型的能力。可以定义带泛型类型的类或方法,随后编译器会用具体的类型来代替它。

s

举个栗子

问题引入

public static void main(String[] args) {
    List list = new ArrayList();
    list.add("1");
    list.add(2);

    int value1 = (int)list.get(0);
    int value2 = (int)list.get(1);
}

上述代码在编译期没有问题,但在运行期,将会报错。

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

因为对于List的add方法,传入参数的类型为Object,所以如果程序员在使用这个List对象实例时,不知道List对象实例中存储的元素的类型时,直接通过类型强行转换,可能就会报错。

解决

使用泛型,将会解决这个问题。

如上的截图所示,当声明List所能装载的类型后,List的add方法就只能装载指定的类型,不然在编译期便会报错。

而且在读取值时,不再需要强制转换。

特性

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

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

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

System.out.print(classStringArrayList.equals(classIntegerArrayList));  //true

分析

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。

也就是说Java中的泛型,只在编译阶段有效。

在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

泛型的使用

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

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

例子1

一个普通的泛型类:

//此处T可以随便写为任意标识,我们常用的表示泛型的标示包括T、E、K、V
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
调用
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
System.out.println("key is " + genericInteger.getKey()); //key is 123456
System.out.println("key is " + genericString.getKey()); // key is key_vlaue

例子2

使用泛型类的时候,一定要传入泛型类型实参么?

并不是这样。

事实上,在使用泛型的时候,如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到保证类型约束的作用。

如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

比如:

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

System.out.println("key is " + generic1.getKey()); // key is 111111
System.out.println("key is " + generic2.getKey()); // key is 4444
System.out.println("key is " + generic3.getKey()); // key is 55.55
System.out.println("key is " + generic4.getKey()); // key is false
注意

泛型的类型参数只能是类类型,不能是简单类型。

多泛型变量定义

在上面的例子中,我们只定义了一个泛型变量T,那如果我们需要传进去多个泛型要怎么办呢?

只需要在类似下面这样就可以了:

class MorePoint<T,U> {  
    private T x;  
    private T y;         
  
    private U name;  
  
    public void setX(T x) {  
        this.x = x;  
    }  
    public T getX() {  
        return this.x;  
    }  
    …………  
    public void setName(U name){  
        this.name = name;  
    }  
  
    public U getName() {  
        return this.name;  
    }  
}  

//使用  
MorePoint<Integer,String> morePoint = new MorePoint<Integer, String>();  
morePoint.setName("harvic");  
System.out.println("morPont.getName:" + morePoint.getName());  

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

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

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

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

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

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。

public class StaticFans {  
    //静态函数  
    public static <T> void StaticMethod(T a){  
        System.out.println("StaticMethod: "+a.toString());  
    }  
    //普通函数  
    public <T> void OtherMethod(T a){  
        System.out.println("OtherMethod: "+a.toString());  
    }  
}  

上面分别是静态泛型函数和常规泛型函数的定义方法,与以往方法的唯一不同点就是在返回值前加上来表示泛型变量。其它没什么区别。

使用方法如下:

//静态方法  
StaticFans.StaticMethod("adfdsa");//使用方法一  
StaticFans.<String>StaticMethod("adfdsa");//使用方法二  
  
//常规方法  
StaticFans staticFans = new StaticFans();  
staticFans.OtherMethod(new Integer(123));//使用方法一  
staticFans.<Integer>OtherMethod(new Integer(123));//使用方法二  

泛型边界符

现在我们要实现这样一个功能,查找一个泛型数组中大于某个特定元素的个数,我们可以这样实现:

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

但是这样很明显是错误的,因为除了short, int, double, long, float, byte, char等原始类型,其他的类并不一定能使用操作符>,所以编译器报错,那怎么解决这个问题呢?答案是使用边界符。

public interface Comparable<T> {    
	public int compareTo(T o);
}

做一个类似于下面这样的声明,这样就等于告诉编译器类型参数T代表的都是实现了Comparable接口的类,这样等于告诉编译器它们都至少实现了compareTo方法。

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray) if (e.compareTo(elem) > 0) ++count;
    return count;
}

泛型通配符

使用泛型的优点

Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

具体来说:

  1. 类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
  2. **消除强制类型转换。**泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
  3. 潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。

使用泛型的一些规则与限制

  1. 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
  2. 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
  3. 泛型的类型参数可以有多个。
  4. 泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上称为“有界类型”。
  5. 泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName(Java.lang.String);

Reference