泛型初探

泛型

泛型程序设计意味着编写的代码可以被很多不同类型的的对象所重用。

演变

在 Java 增加泛型类之前,泛型程序设计使用继承实现的。ArrayList 类只维护一个 Object 引用的数组

public class ArrayList {
    private Object[] elementData;
	...
    public Object get(int i){ ... }
    public void add(Object o){ ... }

}

这个方法有两个问题。

  1. 当获取一个值时必须进行强制类型转换

    ArrayList files = new ArrayList();
    ...
    String filename = (String)files.get(0);
    
  2. 没有进行错误检查,可以向数组中添加任何类的方法

    files.add(new File("..."));
    

    对于这个调用,编译和运行都不会出错。但是在其他地方,如果将 get 的结果进行强制类型转换为 String,就会产生一个错误。

泛型提供了一个很好的解决方案 : 类型参数 (type parameters)。ArrayList 类有一个类型参数用来指示元素的类型:

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

同时也增加的程序的可读性。一看就是到这个数组列表包含的是 String 对象。

Tips: Java SE 7 之后,构造函数可以省略泛型类型。省略的类型可以从变量的类型推断出。

编译器可以很好地利用这个信息。当调用 get 的时候,不需要进行强制类型转换,编译器就知道返回类型是 String。

String filename = files.get(0);

编译器还知道 ArrayList<String> 中 add 方法有一个类型为 String 的参数。这将比使用 Object 类型的参数安全一些。

类型参数的魅力在于 使程序具有更好地可读性和安全性。

简单泛型类

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

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  first;}
    public T getSecond(){ return  second;}

    public void setFirst(T newValue){ first= newValue;}
    public void setSecond(T newValue){second = newValue;}

}

Pair 类引入了一个变量 T,用尖括号 (<>) 括起来。泛型类也可以引入多个变量。

public class Pair<T,U>{ ...}

类型中的类型变量制定方法的返回类型以及域和局部变量的类型。

Tips:类型变量使用大写形式,且比较短。

泛型方法

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

房型方法可以定义在普通类中,也可以定定义在泛型类中。

当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型。大部分情况下可以省略类型参数,编译器会通过信息推断出所调用的方法。

String middle = ArrayAlg.<String>getMiddle("Johh","Q","Public");

类型变量

class ArrayAlg{
	public static<T> T min(T[] a){
		if(a==null || a.length ==0) return null;
		T smallest = a[0];
		for(int i=0;i<a.length;i++){
			if(smallest.compareTo(a[i])>0) smallest = a[i];
		}
		return smallest;
	}
}

smallest 的类型为 T,这意味着它可能是任意一个类的对象。怎么确定 T 所属类有 compareTo 方法?

解决方法就是限定 T 为实现了 Comparable 接口(只包含一个方法 compareTo 的标准接口)的类。

public static<T extends Comparable> T min(T[]a){...}

为什么使用关键字 extends 而不是 implements? <T extends Comparable> 表示绑定类型是 Comparable 的子类型。使用 extnds 是因为更接近子类这个概念。

限定类型使用 “&” 分隔,分隔类型变量使用逗号。

在 Java 的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

泛型代码

虚拟机没有泛型类型对象 --- 所有对象都属于普通类。

类型擦除

无论何时定义一个泛型类型,都自动提供了一个对应的原始类型。原始类型就是删除类型参数后的泛型类型名。擦除类型各变量,并替换为限定类型(无限定的变量用 Object)。

eg:

public class Pair{
    private Object first;
    private Object second;

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

    public Object getFirst(){ return  first;}
    public Object getSecond(){ return  second;}

    public void setFirst(Object newValue){ first= newValue;}
    public void setSecond(Object newValue){second = newValue;}
}

因为 T 是一个无限定的变量,所以直接使用 Object 替换。

<T extends Comparable> T 中 T 就会使用 COmparable 替换。

泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

Pair<Employee> buddies = ....;
Employee buddy = buddies.getFirst();

擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插入 Employee 的强制类型转换。

也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  1. 对原始方法 Pair.getFirst 的调用
  2. 将返回的 Object 类型强制转换为 Employee 类型

当存取一个泛型域时也要插入强制类型转换。

泛型方法

类型擦除也出现在泛型方法中。

Class DateInterval extends Pair<LocalDate>{
	public void setSecond(LocalDate second){...}
	... 
}

类型擦除后

Class DateInterval extends Pair{
	public void setSecond(LocalDate second){...}
}

这样存在一个问题,存在一个从 Pair 继承来的方法

public vodi setSecond(Object second){..}

这显然不是同一个方法。这是怎么确定调用哪个方法?

解决这个问题,编译器在 DateInterval 中生成一个桥方法。

public void setSecond(Object second){ setSecond(Date second); }

Tips:

泛型转换的事实:

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

通配符

Pair<? extends Employee> 表示任何泛型 Pair 类型,它的参数类型是 Employee 的子类,可以作为返回值,但不能作为参数。编译器只知道是 Employee 的子类型,但不知道具体是什么。拒绝传递任何特定类型。
Pair<? super Manager> 表示任何泛型 Pair 类型,它的参数类型是 Manager 的父类类。可以为方法提供参数,但不能作为返回值。

总结: 带有超类型限定的通配符可以向反省对象写入,带有子类型限定的通配符可以从泛型对象读取。

还有无限定通配符 Pair<?>。Pair<?> 有以下方法

? getFirst();
void setFirst(?);

getFirst 的返回值只能赋给一个 Object。setFirst 方法不能调用,甚至不能使用 Object 调用。Pair<?> 和 Pair 本质不同在于:可以用任意 Object 对象调用原始 Pair 类的 setObject 方法。

通配符捕获

通配符不是类型变量,所以不能使用 " ? " 作为一种类型。这是可以使用一个辅助方法来解决。

public static <T> void swapHelper(Pair<T> p){
	T t = p.getFirst();
	p.setFirst(p.getSecond);
	p.setSecond(t);
}
public static void swap(Pair<?> p){ swapHelper(p);}

swapHelper 是一个泛型方法,而 swap 不是,它具有固定的 Pair<?> 类型的参数。这种情况下, swapHelper 方法的参数 T 捕捉通配符。它不知道是哪种类型的通配符,但是 这是一个明确的类型。并且 <T>swapHelper 的定义只有在 T 支出具体类型是才有明确意义。

posted @ 2020-11-15 18:00  风雨长安  阅读(123)  评论(0编辑  收藏  举报
博客