关于Java泛型的随笔

对于Java泛型的相关使用,我就不再说明了,这里主要是对于《Java核心技术 卷Ⅰ》里关于Java泛型介绍得表述不清的地方做个记录。

Java泛型的类型擦除和多态的冲突及解决方法

Java的泛型是伪泛型,即在Java代码经过编译器编译之后转换成字节码文件时,代码里的泛型都会转换成原生类型,这叫做Java的类型擦除。

看一个例子

//一个自定义泛型类Pair
class Pair<T> {  

	private T value;
	
	public T getValue() {  
		return value;
	}  

	public void setValue(T value) {  
		this.value = value;  
	}  
}

//定义一个DateInter类来继承Pair<Date>
class DateInter extends Pair<Date> {  

	@Override  
	public void setValue(Date value) {  
		super.setValue(value);  
	}  

	@Override  
	public Date getValue() {  
		return super.getValue();  
	}  
}

这段代码是完全正确的代码,不会报错,但是有没有感觉有点怪怪的?
不是说泛型在编译时会经过类型擦除吗,那 Pair 类在经过编译之后不应该是这样吗?

class Pair {  

	private Object value;
	
	public Object getValue() {  
		return value;
	}  

	public void setValue(Object value) {  
		this.value = value;  
	}  
}

那这样 Pair 类中的setValue(Object value)方法不是和 DateInter 类中的setValue(Date value)是重载方法吗?那这样不是违背了多态的规则了吗?为什么编译器的注释@Override没有报错?

是的没错,这两个方法确实是重载的,这就是Java泛型的类型擦除和多态的矛盾所在。但是编译器确实是没有报错,那编译器是怎么解决这个问题的?

因为Java的泛型是个伪泛型,这个语法完全是为了方便开发人员能在编码的阶段发现代码里的类型错误而设立的,JVM里是没有泛型这一概念的。所以编译器在把以上代码编译成字节码文件的时候,会在其中为我们自动添加桥方法。

我们可以把文件经过编译之后生成的字节码拿出来看

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
  com.tao.test.DateInter();
    Code:
       0: aload_0
       1: invokespecial #8
       4: return
 
  public void setValue(java.util.Date);  //我们重写的setValue方法  
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #16
       5: return
 
  public java.util.Date getValue();    //我们重写的getValue方法  
    Code:
       0: aload_0
       1: invokespecial #23
       4: checkcast     #26
       7: areturn
 
  public java.lang.Object getValue();     //编译时由编译器生成的桥方法  
    Code:
       0: aload_0
       1: invokevirtual #28               // Method getValue:()Ljava/util/Date
                                          // 去调用我们重写的getValue方法
       4: areturn
 
  public void setValue(java.lang.Object);   //编译时由编译器生成的桥方法  
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #26
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date 
           									// 去调用我们重写的setValue方法
       8: return
}

我们可以很清晰地看到,在字节码中有四个方法,对于我们重写的setValue方法,编译器自动为我们生成了一个setValue(java.lang.Object)方法,并在该方法中调用了我们重写的方法。

所以当我们在调用我们重写的方法时,实际上是在调用编译器为我们生成的桥方法,然后在桥方法中再调用我们的重写方法。这样一看就符合多态的规则了,这就是编译器为解决类型擦除和多态的冲突而采用的方式。

Java为什么不能创建参数化类型数组?

在《Java核心技术 卷Ⅰ》中介绍了Java里是不能创建参数化类型数组的,但是在解释原因的时候说得让人云里雾里的。

看个例子

//一个自定义泛型类Pair
public class Pair<T> {
	public void info( )
	{
		System.out.println("I am Pair");
	}
}

//该句代码在编程环境中会提示错误,连编译都不能通过,即不能创建泛型数组
//Pair<String>[] p = new Pair<String>[10];

//声明参数化类型数组是不会报错的
Pair<String>[] p;
//该句代码运行时会抛出一个ArrayStoreException
//p[0] = new Pair<Interger>;

假设我们可以创建参数化类型的数组,p[0] = new Pair<Interger>;这一句运行时就不会抛出 ArrayStoreException(因为Pair<Interger>Pair<String>经过编译器的类型擦除后,类型都为Pair),这也就导致类型擦除让数组能够记住它的元素类型的机制失效了。这会让我们在之后遍历数组元素进行操作的时候产生更大的错误。所以Java的创造者就把创建参数化类型的数组这一行为给禁止了。

Java泛型通配符

直接上代码

class A {
    public A() { }
}

class B extends A {
    public B() { }
}

class Pair<T>{

    private T data;

    public Pair() {
    }

    public Pair(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        Pair<? extends A> pair = new Pair<>();
        
        //pair.setData(new A());// 报错
        //pair.setData(new B());// 报错
        pair.getData();// 正确
        A a = pair.getData();// 正确
        //B b = pair.getData();// 报错
    }
}

为什么注释掉的那些代码会报错呢?我们来解释一下。

首先我们知道Java中向上转型是一定安全的。

通配符?表示的是一个类型集合,普通泛型T表示的是某一个类型。

当声明<? extends A>的时候,编译器认为无法判断setData方法需要传入的是什么类型(因为<? extends A>表示一个规定了上限的类型集合,这个集合里的所有类型都可以向上转型成A,所以编译器就认为参数的实际类型是A。但是如果我想要传入的是一个B,A向下转型成B是不安全的,所以编译器认为不能传递任何参数给setData方法)

getData方法是可以使用的,只不过是需要注意返回的参数类型只能是A(同理,<? extends A>表示的集合里所有类型都是可以安全向上转型成A的,所以编译器认为返回一个A是一定安全的)

我们修改一下以上代码

class A {
    public A() { }
}

class B extends A {
    public B() { }
}

class Pair<T>{

    private T data;

    public Pair() {
    }

    public Pair(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        Pair<? super B> pair = new Pair<>();

        //pair.setData(new A());// 报错
        pair.setData(new B());// 正确
        pair.getData();// 正确
        //A a = pair.getData();// 报错
        //B b = pair.getData();// 报错
        Object obj = pair.getData();// 正确
    }
}

当声明<? super B>的时候,编译器认为setData方法传入一个B或B的子类是一定可以的(因为<? super B>表示一个规定了下限的类型集合,当传入一个B或B的子类时,可以向上转型为这个集合里的任何类型,所以编译器认为传入一个B或B的子类是一定安全的)

getData方法是可以使用的,只不过是需要注意返回的参数类型只能是Object(同理,<? super B>表示的集合里所有类型都是可以安全向上转型成Object的,所以编译器认为返回一个Object是一定安全的。然而当指定返回值为Object的子类时,会让返回的Object向下转型,然而向下转型是不安全的,所以编译器认为不能指定返回值为Object的子类)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

48k纯贱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值