java 泛型总结 - Go语言中文社区

java 泛型总结


前言

泛型机制是Java SE 5.0开始引入的,没有泛型之前,不同类型的对象重用相同的代码时,普遍使用Object变量,然后再进行强制类型转换。java中的ArrayList就是一个泛型类。假设自己实现一个ArrayList类CustomArrayList,里面用Object[]存储元素:

public class CustomArrayList {
    private Object[] elementData = new Object[10];
    private int index = 0;

    public void add(Object o){
        if(index < elementData.length)
            elementData[index++] = o;
    }

    public Object get(int i){
        if(i>=0 && i < elementData.length)
            return elementData[i];

        return null;
    }

    public int getLength(){
        return index;
    }
}

在调用get方法获取元素时就需要类型转换了。类型转换须与add方法添加的类型一致,否则就会导致异常。

在编译阶段并不检查强制类型转换的语法, 而转换究竟成功还是导致类型转换异常,只能在运行阶段才知晓。一个异常导致程序崩溃是非常不好的体验。如果能在编译就发现这类明显的错误显然要更好,这就是今天的主角—— 泛型
我们定义一个方法时,参数类型也同时定义,它们为形参。实际调用时,通过传入实参调用该方法。在这里,参数类型是确定的,只有符合该类型的实参才能成功调用该方法。在此基础上将参数类型抽象化,比如参数类型用T表示,它可以是Object及其任意子类。一旦T的具体类型确定,相关的方法类型也就确定了,编译器可以据此确定的类型来检查相关的错误。编写包含参数类型T的类和方法就是泛型编程(Generic programming)。泛型的本质就是参数类型化,又称类型参数(type parameters)。

//如果向该集合添加了非String对象,编译器很容易发现
ArrayList<String> strList = new ArrayList<>();

泛型类和泛型方法

泛型类

java中一般用T(必要时还可用临近的U、S)表示“任意类型”。用尖括号"<>"括起来,跟在类名的后面。类型变量T指定了方法的参数类型、返回类型、域的类型。来看一个简单的泛型类:

public class Pair<T> {
    private T min, max;

    public Pair(){
        min = null;
        max = null;
    }

    public Pair(T min, T max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(T max) {
        this.max = max;
    }

    public void setMin(T min) {
        this.min = min;
    }

    public T getMax() {
        return max;
    }

    public T getMin() {
        return min;
    }

泛型类可以有多个类型变量,如 class P<T, U>{…}。
泛型类可看作普通类的工厂。实际使用时,只需用实际类型替换T即可。

泛型方法

泛型方法的类型变量须放在方法的修饰符后面,返回类型前面。泛型方法即可以定义在泛型类中,也可以定义在普通类中。

public class ArrayAlg<T> {
	 public T getMid(T... array) {
        return array[array.length/2];
    }

    public <T> T getMid2(T... array) {
        return array[array.length/2];
    }
}

ArrayAlg是一个泛型类,里面有两个成员方法。可知getMid2是一个泛型方法,getMid则不是,它只是泛型类的一个普通方法。需要明确的是,这里泛型方法getMid2中的T 与泛型类ArrayAlg的T分别代表两种类型,并没有什么关系。

静态泛型方法

在上面的类中添加一个静态方法:

public class ArrayAlg<T> {

    public static T getMiddle(T... array) {
        return array[array.length/2];
    }
 }   

编译报错:

com.milanac007.genericdemo.ArrayAlg.this cannot by referenced from a static context

泛型类中的带类型变量的静态方法,必须声明为泛型方法。 正确的写法为:

public static <T> T getMiddle(T... array) {
        return array[array.length/2];
    }

假设如下调用上面的静态方法:

double middle = ArrayAlg.getMiddle(3.14, 2 ,3);

编译报错:
在这里插入图片描述
编译器自动打包参数为一个Double和两个Integer对象,然后寻找它们共同的超类型。实际上找到了两个超类型:Nubmer和Comparable。
补救:让参数类型或它的超类型有且只有一个。

方法一 将所有实参改为double:

double middle = ArrayAlg.getMiddle(3.14, 2.0 ,3.0);

方式二 接收数据类型改为NumberComparable:

Number middle = ArrayAlg.getMiddle(3.14, 2 ,3);

类型变量的限定

<T extends BoundingType>

限定T为绑定类型BoundingType的子类型。BoundingType可以为类或接口。这里选择关键字extends是因为更接近子类的概念,注意:即使BoundingType为接口,也不能用implements。

一个类型变量可以有多个限定,用&分割:

<T extends BoundingType1 & BoundingType2 & BoundingType3>

又因为java是单继承的,可以实现多个接口,所以限定中至多有一个类,且必须在限定列表中的第一个。

<T extends bClass1 & bInterface1 & bInterface2 ...>

类型擦除

类型擦除是java泛型的一个特性,也是一个理解上的难点。
java代码经过编译后生成的class字节码是在虚拟机中运行的,而虚拟机并没有泛型的概念,里面都是普通类的对象和方法。编译器在编译阶段会将源码中的泛型变量擦除并用相关的类型变量替换,最终在虚拟机上运行。这样的好处就是几乎不需要修改,调用泛型的新代码就可以和旧代码兼容,因为新代码在虚拟机中并没有泛型了,与旧代码一样。

擦除后的类型称为原始类型(raw type), 任何时候定义一个泛型类型,都会自动提供一个相应的原始类型。
原始类型的生成步骤如下:

  1. 删除类型参数
  2. 擦除类型变量,并替换为限定类型,无限定的变量用Ojbect

前面提到的泛型类Pair< T>的原始类型为:

//raw type原始类型
public class Pair {
    private Object min, max;

    public Pair(){
        min = null;
        max = null;
    }

    public Pair(Object min, Object max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(Object max) {
        this.max = max;
    }

    public void setMin(Object min) {
        this.min = min;
    }

    public Object getMax() {
        return max;
    }

    public Object getMin() {
        return min;
    }
}

我们看到,类型参数"< T>“被删除, 原来的T类型的域和方法都被替换为了Object类型的,因为这里的T并没有跟限定符"extends”。由此可知,Pair< String>、Pair< Number>、Pair< Date>的基本类型均为Pair。

如果T为限定类型,比如<T extends bClass1 & bClass2…>,用第一个限定的类型变量来替换。

class Pair<T extends Comparable & Serializable> implements Serializable{
	private T min, max;
    public Pair(T min, T max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(T max) {
        this.max = max;
    }
    
    public T getMax() {
        return max;
    }
}

擦除后的原始类型为:

class Pair<Comparable> implements Serializable{
	private Comparable min, max;
    public Pair(Comparable min, Comparable max) {
        this.min = min;
        this.max = max;
    }

    public void setMax(Comparable max) {
        this.max = max;
    }
    
    public Comparable getMax() {
        return max;
    }
	...
}

假设将两个限定的位置交换一下:

class Pair<T extends Serializable & Comparable >

原始类型将用Serializable替换T,当需要比较两个数的大小时,需要先强制类型转换成Comparable对象。所以为了提高效率,应该将标签接口(即没有方法的接口)放在边界列表的末尾。


虚拟机解析泛型

前面提到,虚拟机中的都是普通类对象,没有泛型。当实际执行泛型语句时,由于类型擦除,编译器会将它们翻译成相关的虚拟机指令。比如:

Pair<String> stringPair = new Pair<>();
...
String maxStr = stringPair.getFirst();

第二条语句被翻译成如下语句在虚拟机中执行:

Object obj = stringPair.getFirst();
String maxStr = (String)obj;

多态与类型擦除

public class DateInterval extends Pair<Date> {
    public DateInterval(Date v1, Date v2){
        super(v1, v2);
    }

    @Override 
    public void setMax(Date value) {
        if(value.compareTo(getMin()) > 0){
            super.setMax(value);
        }
    }
}

DateInterval继承自泛型类Pair< Date>,并覆盖实现了自己的setMax方法,来保证getMax()大于getMin(),而原始的Pair< T> 并没有保证这点。但是这里的覆盖生效了吗?
子类方法覆盖的前提是必须与父类方法具有相同的方法签名(方法名➕参数列表),同时子类方法的访问权限应>=父类方法的访问权限。
当类型擦除后,Pair< Date>变为Pair, 其中的setMax为
setMax(Object value)。同时DateInterval变为:

public class DateInterval extends Pair {
    public DateInterval(Date v1, Date v2){
        super(v1, v2);
    }

    @Override 
    public void setMax(Date value) {
        if(value.compareTo(getMin()) > 0){
            super.setMax(value);
        }
    }
}

DateInterval中的setMax与Pair中的setMax的参数类型不一致,显然子类方法覆盖失败了。这样DateInterval有两个setMax方法:

public void setMax(Date value);//DateInterval类定义
public void setMax(Object value);//继承自Pair

多态,父类引用调用子类方法。我们本意想调用子类中的setMax(Date), 但并没有覆盖成功。这是否意味着泛型和多态调用冲突呢?我们实验一下:

DateInterval interval = new DateInterval(new Date(118, 11, 10),new Date(119, 4, 1));
        Pair<Date> pair = interval;
        Date newDate = new Date(118, 3, 9);
        pair.setMax(newDate);
        System.out.println(String.format("after setMax(newDate), pair: (%s, %s)", pair.getMin(), pair.getMax()));

父类引用Pair< Date> pair引用了子类对象DateInterval,并试图调用子类的setMax方法重设max。

	public void setMax(Date value) {
        if(value.compareTo(getMin()) > 0){
            super.setMax(value);
        }
    }

如果父类的方法被调用,newDate将被设置为max。
如果子类的方法被调用,显然newDate要小于getMin(),所以setMax并没有设置成功,getMax()依然应返回new Date(119, 4, 1)。
看下日志:

11-12 16:13:42.244 2498-2498/com.milanac007.genericdemo I/System.out: after setMax(newDate), pair: (Mon Dec 10 00:00:00 GMT+08:00 2018, Wed May 01 00:00:00 GMT+08:00 2019)

显然多态是正常的。父类引用成功的调用了子类的方法。这是怎么回事呢?这是编译器的功劳。它在DateInterval中生成了一个桥方法(bridge method), 内部调用子类的多态方法。

public void setMax(Object value){
	setMax((Date)value);
}

具体的调用过程如下:
pair 被声明为Pair< Date> 类型,Pair< Date>类有且只有一个setMax:
public void setMax(Object value);
虚拟机用pair实际引用的对象调用上述方法,故实际调用DateInterval的
public void setMax(Object value); 这就是编译器为我们合成的桥方法。

最后我们用反射来验证一下:

try {
            Class<?> cls = Class.forName("com.milanac007.genericdemo.DateInterval");
            Method[] methods = cls.getDeclaredMethods();
            for(Method m:methods) {
                String name = m.getName();
                String returnType = m.getReturnType().getName();
                String modifiers = Modifier.toString(m.getModifiers());
                System.out.print(" ");
                if(modifiers.length() >0) System.out.print(modifiers + " ");
                System.out.print(returnType+ " " + name + "(");

                //print parameter types
                Class[] paramTypes = m.getParameterTypes();
                for(int i=0; i< paramTypes.length; i++) {
                    if(i>0) System.out.print(", ");
                    System.out.print(paramTypes[i].getName());
                }
                System.out.print(");n");

            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

日志:

...
11-12 16:13:42.244 2498-2498/com.milanac007.genericdemo I/System.out:  public volatile void setMax(java.lang.Object);
11-12 16:13:42.244 2498-2498/com.milanac007.genericdemo I/System.out:  public void setMax(java.util.Date);

小结

  • 虚拟机中没有泛型,只有普通的类和方法;
  • 所有的类型参数都用它们的限定类型替换;
  • 桥方法被合成来保持多态;
  • 为保持类型安全性,必要时插入强制类型转换;

java泛型的限制

  • 不能用基本类型实例化类型参数
  • 不能抛出或捕获泛型类的实例
    既不能抛出也不能捕获泛型类对象,甚至扩展Throwable也不合法。
public class Problem<T> extends Exception{//ERROR can't extend throwable

public <T extends Throwable> void doWork(Class<T> t){
	try{
	
	}catch(T e){//ERROR can't catch type variable

	}
} 
  • 不能实例化类型变量
    比如 new T(…),new T[…], T.class 这样的表达式都是非法的。
    可以调用Class.newInstance方法来构造泛型对象:
Class<T> cl;
cl.newInstance();

Class类本身就是泛型。比如String.class是Class< String>的唯一的实例。

String str = String.class.newInstance();
  • 泛型类的静态上下文中类型变量无效
public class ArrayAlg<T> {
	private static T instance; //ERROR
   	public static T getMiddle(T... array) {//ERROR
        return array[array.length/2];
    }
    //com.milanac007.genericdemo.ArrayAlg.this cannot by referenced from a static context
}

注意,静态方法可以构造成泛型方法:

 	//这里泛型方法中的T与该泛型类的T没关系 
 	public class ArrayAlg<T> {
		public static <T> T getMiddle(T... array) {
	        return array[array.length/2];
	    }
	}  
  • 运行时类型检查只适用于原始类型
if(a instanceof Pair<String>) //ERROR
运行时仅仅能测试a是否为任意类型的一个Pair:
if(a instanceof Pair)//CORRECT

Pair<String> p = (Pair<String>)a;//warning-- can only test a is a Pair

再看getClass的例子:

ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println("list1.getClass(): " + list1.getClass());//getClass方法总返回原始类型
System.out.println("list2.getClass(): " + list2.getClass());
if(list1.getClass() == list2.getClass())
  System.out.println("list1.getClass() == list2.getClass()");

运行时,list1和list2对应的原始类型都是ArrayList, 故结果应该为相等。
日志:

11-12 16:35:26.857 4824-4824/com.milanac007.genericdemo I/System.out: list1.getClass(): class java.util.ArrayList
11-12 16:35:26.857 4824-4824/com.milanac007.genericdemo I/System.out: list2.getClass(): class java.util.ArrayList
11-12 16:35:26.857 4824-4824/com.milanac007.genericdemo I/System.out: list1.getClass() == list2.getClass()
  • 不能创建参数化类型的数组
 Pair<String>[] pairs = new Pair<String>[10];//声明Pair<String>[]这个变量是合法的,但不能用new Pair<String>[10]初始化这个变量```

因为类型擦除后,table的类型为Pair[],可以转换为Object[]。
Object[] objarray = table;
objarray会记住它的元素类型,只接收Pair类型的数据,如果试图存储器态类型的元素,就会抛出一个ArrayStoreException:

objarray[0] = "ssss"; //ERROR component type is Pair

下面的代码可以运行,创建了一个Pair[],并赋值给了Object[] array。所以该数组array可以存放Pair型变量,但不能存放Object;注意:pairs[]

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/milanac007/article/details/103014651
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢