Java泛型详解

什么是泛型程序设计

泛型程序设计是指编写的代码可以被很多不同类型的对象重用。比如,不需要给存储String的集合和存储File的集合分别设计不同的类。

类型参数的好处

在没有泛型类型之前,Java泛型程序设计是利用继承实现的。比如一个ArrayList,它内部维护一个Object数组,get出来的也是Object类型的对象,add进去的也是Object类型的对象,这样会导致get值出来的时候要手动做下类型转换,而且add东西的时候没有类型检查,可能会导致add不同类型的东西进去而导致业务出错。

通过类型参数,可以指定ArrayList里的元素的类型,比如一个存储String的ArrayList,其创建就是这样:

ArrayList<String> stringArrayList = new ArrayList<>();

可读性更好了,而且编译器据此能知道,get出来的东西是一个String,add的时候是add的String而不是其他类型的对象,省去了类型转换,增加了类型校验。

因此类型参数带来的好处是:可读性和安全性。

泛型类

泛型类就是具有一个或多个类型变量的类。

例如:

public class Pair<T>{  // 一个类型变量
    private T first;
    private T second;
    public Pair() { first = null; second = null; }
    public Pair(T first, T second) { this.first = first; this.second = second; }
    public T getFirst() { return this.first; }
    public T getSecond() { return this.second; }
    public void setFirst(T first) { this.first = first; };
    public void setSecond(T second) { this.second = second; }
}

public class Pair<T, U>{  //多个类型变量
    // …
}

泛型方法

泛型方法就是带有类型参数的方法。

例如:

class ArrayAlg{

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

}

注意:类型变量放在修饰符(public和static)的后面,返回类型的前面。

调用泛型方法:

String middle = ArrayAlg.<String>getMiddle(“john”, “lucy”, “tom”);

根据类型推断,也可以简化为:

String middle = ArrayAlg.getMiddle(“john”, “lucy”, “tom”);

类型变量的限定

可以限定类型变量是指定类/接口的子类/子接口。具体的,格式为:

<T extends BoundingType>

可以有多个限定,如:

<T extends Comparable & Serializable>

注意,如果用类作为限定,那它必须是限定列表的第一个。

示例代码,计算泛型数组的最大值和最小值:

class ArrayAlg{

    pubic static <T extends Comparable> Pair<T> minmax(T[] a){
		if (a == null || a.length == 0) return null;
		T min = a[0];
		T max = a[0];
		for(int i = 1; i <= a.length -1; i++){
			if(min.compareTo(a[i]) > 0) min = a[i];
			if (max.compareTo(a[i]) < 0) max = a[i];
		}
		return new Pair<>(min, max);
	}

}

在这个例子中,通过对类型变量T设置限定,确保了T一定具有compareTo方法。如果传入的类型没有实现Comparable,那么在编译期就会报错。

类型擦除

泛型机制其实是一种语法糖。

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

在字节码层面上,并没有泛型类型、泛型方法,所有的类和方法都还是原始类型和方法。Java编译器通过类型擦除,将源码里的泛型类型、泛型方法编译为原始类型和方法。

类型擦除,就是擦除类型变量,替换为限定类型。具体的,通过第一个限定的类型变量来替换,如果没有限定就用Object来替换。

例如,几个泛型类代码如下:

// 泛型类型变量无限定
public class Pair<T> {
    private T first;
    private T second;

    public Pair() {
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

// 泛型类型变量,一个限定
public class Pair2<T extends Comparable> {
    private T first;
    private T second;

    public Pair2() {
    }

    public Pair2(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

// 泛型类型变量,多个限定
import java.io.Serializable;

public class Pair3<T extends Serializable & Comparable> {
    private T first;
    private T second;

    public Pair3() {
    }

    public Pair3(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

编译生成class文件,然后反编译查看其内容:

public class Pair
{

    private Object first;
    private Object second;

    public Pair()
    {
    }

    public Pair(Object first, Object second)
    {
        this.first = first;
        this.second = second;
    }

    public Object getFirst()
    {
        return first;
    }

    public void setFirst(Object first)
    {
        this.first = first;
    }

    public Object getSecond()
    {
        return second;
    }

    public void setSecond(Object second)
    {
        this.second = second;
    }
}

public class Pair2
{

    private Comparable first;
    private Comparable second;

    public Pair2()
    {
    }

    public Pair2(Comparable first, Comparable second)
    {
        this.first = first;
        this.second = second;
    }

    public Comparable getFirst()
    {
        return first;
    }

    public void setFirst(Comparable first)
    {
        this.first = first;
    }

    public Comparable getSecond()
    {
        return second;
    }

    public void setSecond(Comparable second)
    {
        this.second = second;
    }
}

import java.io.Serializable;

public class Pair3
{

    private Serializable first;
    private Serializable second;

    public Pair3()
    {
    }

    public Pair3(Serializable first, Serializable second)
    {
        this.first = first;
        this.second = second;
    }

    public Serializable getFirst()
    {
        return first;
    }

    public void setFirst(Serializable first)
    {
        this.first = first;
    }

    public Serializable getSecond()
    {
        return second;
    }

    public void setSecond(Serializable second)
    {
        this.second = second;
    }
}

可以看到,类型变量不存在限定的时候,通过Object替换了类型变量;存在类型限定,通过第一个限定类型替换了类型变量。

使用泛型类的地方也是同样,经过类型擦除后,只有原始类型。

例如对上述三个泛型类型的调用如下:

 Pair<String> pair = new Pair<>();
 pair.setFirst("first value");
 String tmp = pair.getFirst();

 Pair2<Integer> pair2 = new Pair2<>();
 pair2.setFirst(new Integer(20));
 Integer tmp2 = pair2.getFirst();

 Pair3<Long> pair3 = new Pair3<>();
 pair3.setFirst(new Long(20));
 Long tmp3 = pair3.getFirst();

反编译class,其内容如下:

 Pair pair = new Pair();
 pair.setFirst("first value");
 String tmp = (String)pair.getFirst();
 Pair2 pair2 = new Pair2();
 pair2.setFirst(new Integer(20));
 Integer tmp2 = (Integer)pair2.getFirst();
 Pair3 pair3 = new Pair3();
 pair3.setFirst(new Long(20L));
 Long tmp3 = (Long)pair3.getFirst();

可以看到,class中只有原始类型。在返回类型变量型值的时候,编译器自动帮我们生成了强制类型转换代码。

桥方法

当我们覆盖具体的泛型类型的方法的时候,情况会变得有些特殊。

以下面这段代码为例:

public class PairSon extends Pair<String> {
    @Override
    public void setSecond(String second){
        System.out.println("PairSon's setSecond(String)");
        super.setSecond(second);
    }
}

这里PairSon类覆盖了Pair<String>的setSecond(String)方法。由于类型擦除,它会继承到来自父类Pair的setSecond(Object)方法。那么,当我们将PairSon的引用赋给Pair<String>引用变量,再通过Pair<String>引用调用setSecond(String)方法时,由于类型擦除,Pair类只有setSecond(Object)方法,所以最终会调用PairSon的setSecond(Object)方法,与传统多态的预期调用子类PairSon的setSecond(String)不相符合,因此这里类型擦除和多态发生了冲突。

Java是如何解决这个问题的呢?

我们编译PairSon,反编译其class文件:

public class PairSon extends Pair
{

    public PairSon()
    {
    }

    public void setSecond(String second)
    {
        System.out.println("PairSon's setSecond(String)");
        super.setSecond(second);
    }

    public volatile void setSecond(Object obj)
    {
        setSecond((String)obj);
    }
}

我们看到PairSon的class文件里多了一个setSecond(Object)方法,覆盖了父类的setSecond(Object),并且该方法将输入强转为String,然后直接调用setSecond(String)。这样就使得虽然调用了PairSon的setSecond(Object),但是起到的效果和调用PairSon的setSecond(String)是一样的。

这里的PairSon里编译器自动合成的setSecond(Object)就是桥方法。

泛型类型的继承规则

以上面的Pair<T>类为例,即使S是F的子类,Pair<S>也不是Pair<F>的子类。

泛型类可以扩展或实现其他泛型类。例如,ArrayList<E>继承自AbstractList<E>,同时实现了List<E>。这一点和普通的类没有什么两样。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	// ....
}

通配符类型

通配符类型表示允许类型参数变化,具体的,又分为:上界通配符;下界通配符;无限定通配符。

上界通配符

上界通配符形如:

<? extends className>

例如:Pair<? extends Comparable>。任何其他的Pair<T>,T实现了Comparable,都是Pair<? extends Comparable>的子类,对应的实例引用都可以赋值给Pair<? extends Comparable>引用变量。

Pair<? extends Comparable> pair_upper_bound = new Pair<String>(); // OK

不能对上界通配符型参数传除了null以外的任何值,否则都将引起类型错误,但是上界通配符可以作为方法的返回类型。

pair_upper_bound.setFirst(""); // 编译期异常
Comparable tmp_upper_bound = pair_upper_bound.getFirst(); // OK,返回值可以赋给Comparable及其基类

上述代码的class文件反编译后结果如下:

Pair pair_upper_bound = new Pair();
Comparable tmp_upper_bound = (Comparable)pair_upper_bound.getFirst();

下界通配符

下界通配符形如:

<? super className>

例如:Pair<? super ArrayList>。任何其他的Pair<T>,T是ArrayList的超类或者是其实现的接口,都是Pair<? super ArrayList>的子类,对应的实例引用都可以赋值给Pair<? super ArrayList>引用变量。

Pair<? super ArrayList> pair_lowner_bound = new Pair<List>(); // OK

与上界通配符相反,下界通配符可以做为方法参数,但是若做为返回类型,则只能赋给Object类型引用。

pair_lower_bound.setFirst(new ArrayList()); // OK
Object tmp_lower_bound = pair_lower_bound.getFirst(); // OK

反编译上述代码的class文件,结果如下:

Pair pair_lower_bound = new Pair();
pair_lower_bound.setFirst(new ArrayList());
Object tmp_lower_bound = pair_lower_bound.getFirst();

PECS原则

PECS原则,意为Producer Extends Consumer Super,是泛型程序设计中的一个好的实践规则。即如果泛型类型表示一个生产者,就用<? extends className>;如果泛型类型表示一个消费者,就用<? super className>。结合上面所讲,其原因很好理解。

无限定通配符

无限定通配符就是一个直接的<?>,例如Pair<?>。其他的具体的Pair类型,都可以赋给Pair<?>引用,即其他的Pair<String>、Pair<LocalDate>...都是Pair<?>的子类型。

无限定通配符类型做为方法返回值时,只能用Object类型引用来承载;做为方法参数时,只能给它传null,其他任何类型都会导致编译错误。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值