Java核心技术 泛型程序设计

1.为什么要使用泛型程序设计

==泛型设计程序(Generic programming)==意味着编写的代码可以被不同类型的对象所重用。

类型参数的好处

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

// 引入泛型之前的ArrayList
public class ArrayList {
	private Object[] elementData;
	...
	public Object get(int i) {...}
	public void add(Object o) {...}
}

这样有两个问题,当获取一个值时必须进行强制类型转换:

ArrayList files = new ArrayList();
String filename = (String)files.get(0);

此外没有错误检查,可以向数组添加任何类的对象。编译和运行不会出错,如果get的结果强制类型转换成其他类型,就会产生一个错误。

泛型提供了更好的解决方案:类型参数(type parameters)

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

在Java SE 7之后的版本构造函数中可以省略泛型类型new ArrayList<>()。
编译器知道ArrayList< String >add方法有一个类型为String的参数,避免插入错误的对象(无法通过编译)。
类型参数的魅力在于:使得程序具有更好的可读性和安全性。

谁想成为泛型程序员

一个泛型程序员的任务就是预测出所有类的未来可能有的所有用途。
可以将ArrayList< Manager >中的所有元素添加到ArrayList< Employee >中去,但是反过来就不行。Java语言有一个独创性的新概念通配符类型(wildcard type),解决了这个问题。

2.定义简单泛型类

==泛型类(generic class)==就是具有一个或多个类型变量的类。

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 void setFirst(T first) {
        this.first = first;
    }
    public T getSecond() {
        return second;
    }
    public void setSecond(T second) {
        this.second = second;
    }
}

Pair类引入了一个类型变量T,用==尖括号(< >)==括起来,放在类名后面。泛型类可以引入多个类型变量:

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

类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。
类型变量使用大写形式,且比较短。Java库中,使用变量E表示集合的元素类型,K和V表示关键字和值的类型。T(有时是临近的U和S)便是任意类型。

用具体的类型可以替换掉类型变量就可以实例化泛型类型(带构造器和方法的普通类):
Pair< String >
Pair(String, String)
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)
泛型类可以看做普通类的工厂。

之前使用内部类实现的同时比较最大最小值的方法,可以使用泛型:

public class PairTest1 {
    public static void main(String[] args) {
        String[] words = { "Mary", "had", "a", "little", "lamb" };
        Pair<String> mm = ArrayAlg.minmax(words);
        System.out.println("最小值:" + mm.getFirst());
        System.out.println("最大值:" + mm.getSecond());
    }
}
class ArrayAlg {
    public static Pair<String> minmax(String[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        String min = a[0];
        String max = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) {
                min = a[i];
            }
            if (max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }
        return new Pair<>(min, max);
    }
}

3.泛型方法

可以定义一个带有类型参数的简单方法:

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

类型变量放在修饰符的后面,返回类型的前面。泛型方法可以定义在普通类中,也可以定义在泛型类中。
调用时在方法名前面的尖括号中放入具体类型:

String middle = ArrayAlg..<String>getMiddle("John", "Q", "public");

大多数情况下可省略< String >类型参数。编译器有足够的信息能够推断出所调用的方法。用Strintg[]与泛型类型T[]进行匹配并推断出T一定是String。
大多数情况下没有问题,偶尔编译器也会有提示错误:

double m = ArrayAlg.getMiddle(3.14, 1729, 0);

错误信息:解释这句代码有两种方法,而且两种方法都是合法的。1个Double和2个Integer对象,而后寻找共同的超类型,找到了两个超类型:Number和Comparable接口,其本身也是一个泛型类型。这时候我们需要补救措施将所有参数写为double值。

4.类型变量的限定

有时,类或方法需要对类型遍历加以约束。

public static <T extends Comparable> 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;
}

为了确信变量T具有conpareTo方法,将T限制为实现了Comparable接口(只含一个方法compareTo方法)的类。实际上Comparable接口本身就是一个泛型类型。现在,泛型的min方法只能被实现了Comparable接口的类的数组调用,否则会产生编译错误。
==< T extends BoundingType >==表示T应该是绑定类型的子类型。T和绑定类型可以是类,也可以是接口。选择extends关键字是更接近子类的概念,而非implements,并且Java设计者并不打算添加新的关键字。
一个类型变量或通配符可以有多个限定,限定类型用“&”分隔,而逗号用来分隔类型变量:

T extends Comparable & Serializable

5.泛型代码和虚拟机

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

类型擦除

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

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

因为T是一个无限定的变量,所以直接用Object替换。结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。
在程序中可以包含不同类型的Pair。而擦除类型后就变成原始的Pair类型了。原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。

public class Interval <T extends Comparable & Serializable> implements Serializable {
    private T lower;
    private T upper;
    
    public Interval(T first, T second) {
        if (first.compareTo(second) <= 0) {
            lower = first;
            upper = second;
        } else {
            lower = second;
            upper = first;
        }
    }
}

原始类型Interval如下:

public class Interval implements Serializable {
    private Comparable  lower;
    private Comparable  upper;
    
    public Interval(Comparable first, Comparable second) {...}
}

如果class Interval <T extends Serializable & Comparable>,原始类型用Serializable替换T,而编译器在必要时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。

翻译泛型表达式

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

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

擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。编译器将这个方法调用翻译为两条虚拟机指令:
1.对原始方法Pair.getFirst的调用
2.将返回的Object类型强制转换为Employee类型

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

翻译泛型方法

泛型方法看似是一个完整的方法族,而擦除方法之后,只剩下一个方法:

public static Comparable min(Comparable[] a)

方法擦除带来了两个复杂问题:

class DateInterval extends Pair<LocalDate> {
	public void setSecond(LocalDate second) {
		if (second.compareTo(getFirst()) >= 0) {
			super.setSecond(second);
		}
	}
}

擦除后变成:

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

存在另一个Pair继承来的setSecond方法:
public void setSecond(Object second)
这显然是一个不同的方法,因为它有一个不同类型的参数——Object,而不是LocalDate。考虑下面语句:

DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);

这里希望对setSecond的调用具有多态性,并调用最合适的那个方法。由于pair引用DateInterval对象,所以应该调用dateInterval.setSecond。问题在于类型擦除与多态发生了冲突。编译器在DateInterval类中生成了一个桥方法(bridge method)

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

变量pair已经声明为类型Pair< LocalDate >,并且这个类型只有一个简单方法叫setSecond(Object)。虚拟机用pair引用对象调用方法。这个对象是DateInterval类型,因而将会调用DateInterval.setSecond(Object)方法,即合成的桥方法。

假设:

class DateInterval extends Pair<LocalDate> {
	public LocalDate getSecond() {
		return (Date)super.getSecond().clone();
	}
}

DateInterval类中有两个getSecond方法:
LocalDate getSecond()
Object getSecond()
在虚拟机中用参数类型和返回类型确定一个方法。因此编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确处理。但不能这么编写Java代码(具有相同参数类型的两个方法是不合法的),它们都没有参数。

桥方法不仅用于泛型类型。在一个方法覆盖另一个方法时可以指定一个更严格的返回类型:

public class Employee implements Cloneable{
	public Employee clone() {...}
}

Object.clone和Employee.clone方法被说成具有协变的返回类型(covariant return types),实际上Employee类有两个克隆方法:
Employee clone()
Object Clone()
合成的桥方法调用了新定义的方法。

总之,需要记住有关Java泛型转换的事实:
1.虚拟机中没有泛型,只有普通类和方法
2.所有的类型参数都用他们的限定类型替换
3.桥方法被合成来保持多态
4.为保持类型安全性,必要时插入强制类型转换

调用遗留代码

设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。

Dictionary<Integer, Component> labelTable = new Hashtable<>()
labelTable.put(0, new JLabel(new ImageIcon("nine.gif")));
labelTable.put(1, new JLabel(new ImageIcon("ten.gif")));
JSlider jSlider = new JSlider();
jSlider.setLabelTable(labelTable);
@SuppressWarnings("unchecked")
Dictionary<Integer, Component> lt = jSlider.getLabelTable();

调用setLabelTable时,比编译器会发出一个警告。这个警告不会产生什么影响,因此可以忽略。
相反的情形,由一个遗留的类得到一个原始类型的对象Dictionary<Integer, Component> lt = jSlider.getLabelTable(),或看到一个警告,确保标签表已经包含了Integer和Component对象,当然从来也不会有绝对的承诺,最差的情况将抛出一个异常。
在查看警告之后,可以利用==注解(annotation)==使之消失,@SuppressWarnings(“unchecked”)。
注解必须放在生成这个警告的代码所在方法之前,或者标注整个方法(关闭对方法所有代码的检查)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值