Java泛型指南

1. 泛型概述

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。


本次讲解是从pre-Java 5 到利用泛型特性来讲解。所以不要看到下面的讲解觉得不是泛型。

1.1 使用Object表示泛型

Java中的基本思想就是可以通过使用Object这样适当的超类来实现泛型类。下面案例中所示的MemoryCell类就是这样一个例子。

//MemoryCell class
// Object read()    -->returns the stored value
// void write(Object x) --> x is stored

public class MemoryCell{
    
    private Object storedValue;
    
	public Object read(){ return storedValue;}
    
    public void write(Object x) { storedValue = x;}
}

我们如果使用这种策略时,必须考虑两个细节。第一个细节会在下面的例子中阐释,它描述一个main方法,该方法把字符串"37"写到MemoryCell对象中,然后又从MemoryCell对象读出。为了访问这中对象的一个特定的方法,必须要强制转换成正确的类型。(当然,这个例子中,可以不必进行强制转换,因为在程序的第6行可以调用toString()方法,这种调用对任意对象都能做到)。

public class TestMemoryCell{
	public static void main(String [] args){
    	MemoryCell m = new MemoryCell();
        m.write("37");
        String val = (String) m.read();
        System.out.println("字符串是:" + val);
    }
}

第二个重要的细节就是不能使用基本类型。只有引用类型能够与Object相融。这里我们其实可以采用基本类型的包装类,至于包装类,就不在这里多讨论了。

1.2 使用接口类表示泛型

只有在使用Object类中已有的那些方法能够表示锁执行的的操作的时候,才能使用Object作为泛型类来工作。
例如,考虑在由一些项组成的数组中找出最大项的问题。基本的代码是类型无关的,但是它确实需要一种能力来比较任意两个对象,并确定哪个是大的,哪个是小的。因此我们不能直接找出Object的数组中的最大元素——我们需要更多的信息。最简单的想法就是找出Comparable的数组中的最大元素。要确定顺序,可以使用compareTo方法,我们知道,它对所有的Comparable都必须是现成可用的。下面的例子中的代码做的就是这个,它提供了一种main方法,该方法能够找出String或Shape数组中的最大元素。

class FindMaxDemo{
	public static Comparable findMax(Comparable[] arr){
    	int maxIndex = 0;
        for(int i = 1; i < arr.length; i++ ){
        	if(arr[i].compareTo(arr[maxIndex])>0){
            	maxIndex = i;
            }
        }
        return arr[ maxIndex ];
    }
    
    public static void main(String [] args){
    	Shape[] sh1 = {new Circle(2.0)),
        			   new Square(2.0)),
        			   new Rectangle(2.0)};
        
        String [] st1 = {"Joe","Bob","Bill","Zeke"};
        System.out.println(findMax(sh1));
        System.out.println(findMax(st1));
        
    }
}
// 泛型findMax 案例,使用Shape和String 演示pre-Java 5

这里,提出几个很重要的忠告:

  1. 只有实现了Comparable接口的那些对象才能够作为Comparable数组的元素被传递进去。只有CompareTo方法,但是没有实现COmparable接口,他是不具有必须的IS-A关系。因为我们也许会比较Shape的面积,因此假设Shape实现了Comparable接口。Circle、Square和Rectangle都是Shape的子类。
  2. 如果数组中有两个不兼容的对象,一个是Shape一个是String,那么就会报错 ClassCastException。
  3. 上面所说的,基本类型不能作为Comparable传递,但是包装类可以

**提示:这个方案不是所有情况都适用的 **。

1.3 数组类型的兼容性

开发中我们遇到的困难可能是集合类型的继承问题。设Employee IS-A Person。那么,这是不是就代表数组Employee [] IS-A Person [] 呢?简单说就是,一个方法接收Person [] 作为参数,那么我们能不能把Employee [] 作为参数传递进去呢?
乍一看,可能我们第一感觉那肯定能传递进去呀,然而,这个问题却不是想的那样。下面我用一个例子来表述。

//假设Student IS-A Persion ,Employee IS-A Persion

//1.Person数组接收 5 个 空的Employee数组
Persion[] arr = new Employee[5];
arr[0] = new Student();

两句话都编译通过,而arr[0] 实际上是引用一个Employee,可是Student IS-Not-A Employee。 这样就产生了错误,运行的时候,会抛出一个ClassCastException的异常,因为类型转换不过去,这里在开发过程中需要特别注意。

2 利用Java 5 泛型特性实现泛型构件

Java 5 支持泛型类,这些类很容易使用。然而,编写泛型类却需要多做一些工作。

2.1 简单的泛型类和接口

public class MemoryCell<T>{
	public T read(){ return storedValue;}
    public void write(T x){storedValue = x;}
    private T storedValue;
}

当指定一个泛型类时,类的声明中则包含一个或多个类型参数,这些参数被放在累名后面的尖括号中。上面的例子则是对类型参数没有限制。
用户创建对象可以使用MemoryCell<Intger> memoryCell = new memoryCell<>();但是不能使用MemoryCell<int> memoryCell = new MemoryCell<>();在泛型类声明内部,我们可以使用泛型的域和泛型的类型作为参数和返回值。例子的第5行。
下面在看一个例子:

public interface Comparable<T>{
	public int compareTo(T ather)
}

接口也可以声明泛型的,在Java 5之前,Comparable是不是泛型的,接收的参数是Object,就会出现类型转换的错误,但是在Java 5 之后,Comparable接口是泛型接口类。上面的例子中所示。

2.2 自动装箱/拆箱


因为java1.5版本之前,是用包装类需要在使用write之前创建Integer对象,然后才能使用intValue方法从Integer中取出int值。如果不这么做,就会发生编译错误的提示。
Java 5矫正类这种情形,如果一个int型变量被传递到需要一个Integer对象的地方,那么,编译器将在幕后插入一个对Integer构造方法的调用。这个就叫做自动装箱。而如果一个Intger对象被放到需要int型变量的地方,则编译器会在幕后插入一个对intValue方法对调用,这个叫做自动拆箱。下面我们看一下实例:

class BoxingDemo{
	public static void main(String [] args) {
    	GenericMemoryCell<Integer> m = new GenericMemoryCell<Integer>();
        m.write(37);//1.5之前需要手动实例话Integer对象。
        
        int val = m.read();
        System.out.println("Contents are:" + val);
    }
}

2.3 菱形运算符

在上面实例中,第3行后面第写法,感觉有些烦人,因为既然m是GenericMemoryCell 类型第,显然创建了对象也必须是GenericMemoryCell类型的,任何其他类型的参数都会产生编译错误。Java 7中增加了一种新的语言特性,叫菱形运算符,使第三行改写为GenericMemoryCell m = GenericMemoryCell<>();
实例:

class BoxingDemo{
	public static void main(String [] args) {
    	GenericMemoryCell<Integer> m = new GenericMemoryCell<>();
        m.write(37);//1.5之前需要手动实例话Integer对象。
        
        int val = m.read();
        System.out.println("Contents are:" + val);
    }
}

2.4 带有限制的通配符


下面第一个实例显示一个static方法,该方法计算一个Shape数组的总面积(假设Shape是含有area方法的类;而Circle和Square则是继承Shape的类)。假设我们想要重写这个计算总面积的方法,使得该方法能够使用Collection这样的参数。当前唯一重要的是,它能够存储一些项,而且这些项可以用一个增强的for循环来处理。由于是增强的for循环,因此代码是相同的,最后的结果是第二个实例。如果传递一个Collection,那么,程序会正常运行,可是,要是传递一个Collection会发生什么情况呢?答案依赖于是否Collection IS-A Collection

public static double totalArea(Shape[]arr){
	double total = 0 ;
    for(Shape s:arr){
    	if(s != null){
        	total += s.area();
        }
    }
    return total;
}
public static double totalArea(Collection<Shape> arr){
	double total = 0;
    for(Shape s: arr){
    	if(s != null){
        	total += s.area();
        }
    }
    return total;
}


上面我们讲到过,Square[] IS-A Shape[] 。一方面,这种一致性意味着,如果数组是协变的,那么集合也将是协变的。另一面,我们在上面讲的,数组的协变性导致代码得以编译,但此后会产生一个运行时一场(ArrayStoreException)。因为使用泛型的远不原因在于产生编译器错误而不是类型不匹配的运行时异常,,所以,反省集合不是协变的。因此,我们不能把Collection作为参数传递进去。
现在的问题是,泛型(以及泛型集合)不是协变的(但有意义),而数组是协变的。若无附加的愈发,则用户就会避免使用集合(collection),因为失去协变性使得代码缺少了灵活性。
Java 5 用通配符来弥补这个不足。通过通配符用来表示参数类型的子类(或超类)。
下面实例中将Collection作为参数的方法totalArea,其中T IS-A Shape。因此,Collection和Collection都是可以接受的参数。通配符可以不带限制使用(此时假设为 extends Object),或不用extends而super(来表示超类而不是子类);此外还存在一些其他的语法,这里就不多讨论了。

public static double totalArea(Collection<? extents Shape> arr){
	double total = 0;
    for(Shape s: arr){
    	if(s != null){
        	total += s.area();
        }
    }
    return total;
}

2.5 泛型static方法

从某种意义上说,上面的实例中的totalArea方法是泛型方法,因为他能够接受不同类型的参数。但是,这里没有特定的参数表,正如在GenericMemoryCell类的生命中所做的那样。有时候特定的类型很重要,这或许因为下列原因:

  1. 该特定的类型用作返回类型;
  2. 该类型用在多于一个的参数类型中;
  3. 该类型用于生命一个局部变量;

如果是这样,那么,必须要生命一个带有若干个类型参数的显示泛型方法。
示例:

public static <T> boolean contains(T [] arr,T x){
	for(T val : arr){
        if(x.equals(val)){
            return true;
        }
    }
    return false;
}

显示一种泛型static方法,该方法对值x 在数组arr中进行一系列查找。通过使用一种泛型方法,替代使用Object作为参数的非泛型方法,当在Shape对象的数组中查找Apple对象时,我们能得到编译错误的提示。
泛型方法特别像是泛型类,因为类型参数使用相同的语法。在泛型方法中的类型参数位于返回值之前。
提示:泛型方法于泛型类无关,普通的类也可以使用定义泛型方法,这里不要混淆。

2.6 类型限界

如果我们想要编写一个findMax历程,可能会有下面的代码:

public static <T> T findMax(T[] arr){
    int maxIndex = 0;
    
    for(int i = 0; i < arr.length; i++){
        if(arr[i].compareTo(arr[maxIndex]) > 0 ){
        	maxIndex = i
        }
    }
    return arr[maxIndex];
}

由于编译器不能证明证明compareTo的调用是合法的,因此,程序不能正常运行;只有在T是Comparable的情况下,才能保证compareTo存在。我们可以使用类型限界解决这个问题。类型限界在尖括号内指定,它指定参数类型必须具有的性质。一种自然的想法是把性质改写成:public static T findMax(T[] arr) …
我们知道,因为Comparable接口如今是泛型的,所以这种做法很自然。虽然这个程序能够被编译,但是更好的做法是
public static <T extends Comparable> T findMax(T[] arr) …
然而,这个做法还是不能令人满意。为了看清楚问题,假设Shape实现了Comparable,设Square继承了Shape。此时,我们所知道的只是Square实现了Comparable。于是,Square IS-A Comparable,但它 IS-NOT-A Comparable
应该说,T IS-A Comparable,其中,SUPT是T的父类。由于我们不需要知道准确的类型T,因此可以通过通配符。最后的结果变成:
public static <T extends Comparable<? super T>> T findMax(T[] arr) …

public static <T extends Comparable<? super T>> T findMax(T[] arr){
	int maxIndex = 0;
    
    for(int i = 0; i < arr.length; i++){
        if(arr[i].compareTo(arr[maxIndex]) > 0 ){
        	maxIndex = i
        }
    }
    return arr[maxIndex];
}

2.7 类型擦除

泛型在很大程度上是Java语言中的成分而不是虚拟机中的结构。泛型类可以由编译器通过所谓的类型擦除过程而转变为非泛型类。这样,编译器就生成一种与泛型同类名的原始类,但是类型参数都被删去类。类型变量由他们的类型限界来代替,当一个具有擦出返回类型的泛型方法被调用的时候,一些特性被自动的插入。如果使用一个泛型类而不带类型参数,那么使用的是原始类。
类型擦出的一个重要推论是,所生成的代码与程序员在泛型之前所写的代码没有太多的差异,而且事实上运行的也并不快。其显著的有点于,程序员不必把一些类型转换放到代码中,编译器将进行重要的类型校验。

2.8 对泛型的限制

对于泛型类型由许多的限制。由于类型擦除的原因,这里列出的每一个限制都是必须要遵守的。

基本类型

基本类型不能用作泛型中的类型参数。因此,GenericMemoryCell是非法的。我们必须使用包装类,这也是包装类存在的意义之一。

instanceof检测

instanceof 检测和类型转换工作只对原始类型进行。在下列代码中:

GenericMemoryCell<Integer> cell1 = new GenericMemoryCell<>();
cell1.write(4);
Object cell = cell1;
GenericMemoryCell<String> cell2 = (GenericMemoryCell<String>) cell;
String s = cell2.read();

这里对类型转换时是成功的,因为所有的类型都是GenericMemoryCell。但在最后一行,由于对read对题哦用企图返回一个String对象从而产生一个运行时错误。结果类型转换产生一个警告,而对应的instanceof检测是非法对。

static的语境

在一个泛型类中,static方法和static域均不可引用类的类型变量,因为在类型擦除后类型变量就不存在了。另外,由于实际上只存在一个原始的类,因此static域在该类的诸泛型实例之间是共享的。

泛型类型的实例化

也不能创建一个泛型的数组。如果T是一个类型变量,则语句
T[] arr = new T[10];//右边是非法的
上面语句是非法的。T将由它的界限代替,这很可能是Object T,于是(由类型擦除产生的)对T[]的类型转换将无法进行,因为Object[] IS-Not-A T[] 。由于我们不能创建泛型对象的数组,因此一般说我们必须创建一个擦除类型的数组,然后是用类型转换。这种类型转换将产生一个关于为检测的类型转换的编译警告。

参数化类型的数组

参数化类型的数组的实例化是非法。考虑下列代码:

GenericMemoryCell<String> [] arr1 = new GenericMemoryCell<>[10];
GenericMemoryCell<Double> cell = new GenericMemoryCell<>();
Object[] arr2 = arr1;
arr2[0] = cell;
String s = arr[0].read();

正常情况下,我们认为第4行的复制会生成一个ArrayStoreException,因为赋值的类型有错误。可是,在类型擦除后,数组的类型为GenericMemoryCell[],而驾到数组中的对象也是GenericMemoryCell,因此不存在ArrayStoreException异常。于是,该段代码没有类型转换,它最终将在第5行产生一个ClassCastException异常,这正式泛型应该避免的情况 。

总结

这里将泛型的使用从Java 1.5之前 的使用以及Java1.5之后的转变与使用,都给大家交代了,使用了大量的示例来展示用法,当然还有某些思考问题的场景也用示例的方式给大家展示来。后面也给大家写出了一些泛型中我们应该注意的规范,希望能帮助大家,在开发中避免许多坑。














已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页