java 泛型

泛型类

  java泛型类的定义中,在尖括号中把类型变量依次列举出来,按照惯例,在JDK中,使用E表示集合的元素类型,KV分别表示键与值。T用来表示“任意类型”。一个简单的泛型类如下:

public class Generic<T,U>{
	private T a;
	private U b;
	public Generic(T a,U b){
		this.a=a;
		this.b=b;
	}
}
泛型方法

  泛型方法可以定义在普通类中,与泛型类的定义比较相似的一点是,在定义中需要添加类型变量。需要注意的一点是:类型变量所放的位置在修饰符与返回类型之间。

public class Original{
	public <T> void hello(T message){
		System.out.println(message);
	}
}
类型变量的限定

  对于类型变量有时候需要对其做一些约束,可以通过如下的一个场景引出:

public static <T> T findMin(T[] vals){
	T minVal=vals[0];
	for(int i=1;i<vals.length;i++){
		if(minVal.compareTo(vals[i])>0)//默认了T类型有该方法
			minVal=vals[i];
	}
	return minVal;
}	

为了保证元素比较中一定可以使用compareTo方法,则必须对类型变量T加以约束:

public static <T extends Comparable> T findMin(T[] vals){
	...
}

  在泛型中,通过extends关键字,使用语法<T extends BoundingType>来约束变量类型。一个类型变量或通配符可以有多个绑定类型,如:<T extends Comparable & Serializable>

类型擦除

  对于JVM而言,其中所有的对象都是普通对象,并没有泛型类型对象。这里面一个衔接源代码中的泛型代码与JVM中普通字节码文件的一个重要机制就是类型擦除
  对于任意一个泛型类型,都对应一个相应的普通类型(raw type)。普通类型即泛型类型去掉类型变量。擦除是指将泛型类型中的类型变量替换为限定类型/绑定类型(DoundingType),默认为Object

泛型表达式的擦除

  对于刚才说的Generic类,使用我之前java 反射博客中提到的ReflectionTest测试,在控制台输入Generic,会得到如下结果:

public class Generic
{
	public Generic(java.lang.Object, java.lang.Object);

	private java.lang.Object a;
	private java.lang.Object b;

}

如果将之前的Generic修改为:

public class Generic<T extends Comparable,U extends Serializable>{
    private T a;
    private U b;
    public Generic(T a,U b){
        this.a=a;
        this.b=b;
    }
}

那么对应的结果变成了:

public class Generic
{
	public Generic(java.lang.Comparable, java.io.Serializable);

	private java.lang.Comparable a;
	private java.io.Serializable b;

}

这样就验证了擦除机制。这里面还有一个细节问题就是,如果类型变量的限定类型不止一个,那么在擦除之后,泛型类型变成了第一个限定类型。再次修改Generic类为:

public class Generic<T extends Comparable & Serializable,U>{
    private T a;
    private U b;
    public Generic(T a,U b){
        this.a=a;
        this.b=b;
    }
}

ReflectionTest程序执行结果:

public class Generic
{
	public Generic(java.lang.Comparable, java.lang.Object);

	private java.lang.Comparable a;
	private java.lang.Object b;

}

调整Comparable,Serializable的位置之后,结果为:

public class Generic
{
	public Generic(java.io.Serializable, java.lang.Object);

	private java.io.Serializable a;
	private java.lang.Object b;

}

为了提高效率,应该将标记接口放在列表的末尾(如这里面的标记接口为Serializable)。

  由于泛型擦除,那么编译器对泛型源代码编译的过程中,会通过插入一些强制类型转化的代码保持原来的语义。对于之前的Generic,稍作修改,如下:

public class Generic<T,U>{
    private T a;
    private U b;
    public Generic(T a,U b){
        this.a=a;
        this.b=b;
    }
    public T getA() {
        return a;
    }
	public static void main(String[] args){
		Generic<Integer,String> obj=new Generic(1,"hello");
		Integer a=obj.getA();//插入强制类型转化字节码
	}
}

使用jdk自带的javap反汇编程序分析main函数,控制台执行javap -v Generic有如下结果:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=1
         0: new           #4                  // class Generic
         3: dup
         4: iconst_1
         5: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         8: ldc           #6                  // String hello
        10: invokespecial #7                  // Method "<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
        13: astore_1
        14: aload_1
        15: invokevirtual #8                  // Method getA:()Ljava/lang/Object;
        18: checkcast     #9                  // class java/lang/Integer
        21: astore_2
        22: return
      LineNumberTable:
        line 12: 0
        line 13: 14
        line 14: 22

第18行的字节码checkcast就验证了之前的结论。

泛型方法的擦除

  由于java中的实例方法会涉及到动态绑定,而动态绑定需要知道运行时对象的实际类型,这样就和擦除机制相互矛盾了。下面举例说明:

class Pair<T extends Comparable> {
    private T start;
    private T end;
    public Pair(){}
    public Pair(T start,T end){
    	this.start=start;
    	this.end=end;
    }
    
    public T getStart() {
        return start;
    }
    public void setStart(T start) {
        this.start = start;
    }
    public T getEnd() {
        return end;
    }
    public void setEnd(T end) {
        this.end = end;
    }
}

public class TimePair extends Pair<Long>{
    public void setEnd(Long end){
        if(this.getStart().compareTo(end)<=0)
            setEnd(end);
    }
	public static void main(String[] args){
        TimePair timePair=new TimePair();
        Pair<Long> pair=timePair;//此处发生了类型擦除
        pair.setEnd(1L);//此处又需要记住实际的元素类型为Long
    }
}

通过执行javap TimePair,输出如下:

class TimePair extends Pair<java.lang.Long> {
  TimePair();
  public void setEnd(java.lang.Long);
  public static void main(java.lang.String[]);
  public void setEnd(java.lang.Object);//该方法就是为了多态性而添加的【桥方法】
}

  这里面的逻辑是这样的:父类Pair类型擦除之后,setEnd()方法的参数类型为Object,而在子类TimePair中,并没有对该方法进行重写(override),而因为public void setEnd(java.lang.Long)发生了覆盖,这样就无法进行动态绑定了,通过添加桥方法即解决了这个问题。

  对之前的内容做个简单的总结如下:

  • 泛型对JVM透明,JVM中只有普通的类和方法;
  • 类型参数在编译过程中都会被它们的限定类型替换;
  • 桥方法用来保持多态性;
泛型擦除的局限性
类型检查与原始类型

  由于发生了类型擦除,无论是通过instanceof方式,该是通过getClass()方法来检查一个对象的实际类型,其实际类型只适合于原始类型。

Pair<Long> longPair=new Pair();
Pair<String> strPair=new Pair();
System.out.println(longPair.getClass()==strPair.getClass());//true

System.out.println(longPair instanceof Pair<Long>);//error
System.out.println(longPair instanceof Pair);//true
带类型参数的数组

  无法实例化带类型参数的数组,这是由于多态与类型擦除的矛盾造成的。

Pair<Integer>[] intPairs=new Pair<>[10];
Object[] objs=intPairs;//Pair[] 自动转化为 Object[],Object[] 能记住元素类型为Pair,但不是Pair<Integer>
Pair<String> strPair=new Pair();//类型擦除之后为Pair
objs[0]=strPair;//不会触发ArrayStoreException,但是存在类型不一致错误

为了防止上述情况的发生,不允许(人为)创建带类型参数的数组。在一些特殊的情况下,这种数组可由虚拟机构造,如下:

public class GenericArray{
	static <T> void addAll(Collection<T> c,T... vals){
        for(T val:vals)
            c.add(val);
    }
    public static void main(String[] args){
    	addAll(new ArrayList<Pair<Integer>>(),new Pair<Integer>());
    }
}

调用addAll()方法,JVM就必须对vals建立一个Pair[]数组,不过由于这不是显示编写的,编译器会警告,而不会报错。执行javac GenericArray,会看到如下结果:

Note: TimePair.java uses unchecked or unsafe operations.
类型变量的实例化

  不能使用类似new T(); new T[…],T.class的语句/表达式。由于类型擦除new T()转化为new Object(),这是和原意不符的,而T.class也会转化为Object.class,会导致相同的问题。
  解决该问题可以通过下面两种方法:

  使用lambda表达式,需要提供一个接受lambda表达式的方法。对于上面的Pair类,可以这么写:

public static <T> Pair<T> makePair(Supplier<T> constr){
	return new Pair<>(constr.get(),constr.get());
}

Supplier<T>是一种函数式接口,表示无参数且返回类型为T的函数。

  使用反射机制解决,实现方式如下:

public static <T> Pair<T> makePair(Class<T> cls){
	try{
		return new Pair<>(cls.newInstance(),cls.newInstance())
	}
	catch(Exception e)[return null;}
}

由于Class类本身就是泛型的,Integer.class,String.class分别是Class<Integer>,Class<String>的子类的唯一实例对象。makePair方法自然就能推断出pair的类型。

泛型数组

  由于类型擦除,无法创建泛型数组,举例如下:

public <T extends Comparable> void createGenericArray(){
	T[] genericArray=new T[2];//类型擦除导致出现 new Comparable[2];
}
泛型类型的继承规则

  即使类型变量之间具有继承关系,但是这和对应的泛型类型无关,泛型类型之间依然是相互独立的。

class A{}
class B extends A{}
Pair<B> bPair=new Pair<>();
Pair<A> aPair=bPair;//error
通配符类型

  使用通配符类型,可以使得变量类型更加灵活,如

Pair<? extends Number>

表示类型变量为Number子类型的Pair泛型类型。通过这种方法,就可以很好地解决上一节出现的问题:

class A{}
class B extends A{}
Pair<? extends A> aPair=new Pair<A>();
Pair<? extends A> bPair=new Pair<B>();
aPair=bPair;//right
bPair=aPair;//right

这里的逻辑关系是:Pair<? extends A>Pair<A>,Pair<B>的公共父类。不过由于Pair<? extends A>在类型擦除之后并不知到存储的对象start,end的实际类型,如果调用setXXX()赋值,就无法保证类型一致。而对于getXXX()方法,没有这个问题,因为类型擦除后,类型变量会替换为限定类型,此处即父类A,而父类引用可以引用子类对象。
  另外一个通配符使用super关键字,如<? super B>。这种情况下,无法调用getXXX()方法,因为编译器并不知道返回的究竟是那个父类。对于setXXX(),可以把限定类型对应的对象作为实参传入,也就是:

class A{}
class B extends A{}
Pair<? super B> pair=new Pair<>();
pair.setStart(new B());//right
pair.setEnd(new A());//error

  总结下来也就是,对于<? super BoundType>,可以向泛型对象写入,对于<? extends BoundType>,可以从泛型对象读取。
  最后一种是无限定通配符?。使用无限定通配符构成的泛型类型与原始类型是有区别的:

Pair<?> pair=new Pair<A>();
pair.setStart(new A());//error
Pair rawPair=new Pair();
rawPair.setStart(new A());//right

Pair<?>Pair本质区别在于:可以使用任意的Object对象调用原始PairsetXXX()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值