《Thinking in Java》 第15章 泛型
泛型理解
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
“泛型”术语的意思:适用于许多许多的类型。
泛型实现了参数化类型的概念,使代码可以应用于多种类型。
泛型出现的原因: 最引人注目的原因就是为了创建容器类。
泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。
所有的类都默认继承Object类。
有些情况下,我们确实希望容器能够同时持有多种类型的对象,这个可以用Object类作为参数的类型。但是,通常而言,我们只会使用容器来存储一种类型的对象。
Java 泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
Java中泛型在经过编译过,就不存在了。
泛型使用:
- 泛型类
- 泛型接口
- 泛型方法
泛型的局限性:
- 基本类型无法作为类型参数
泛型接口
泛型可以应用于接口。例如:生成器(Generator),这是一种专门负责创建对象的类。实际上,这是工厂方法设计模式的一种应用。不过,当使用生成器创建对象时,它不需要任何参数,而工厂方法一般需要参数。也就是说,生成器无需额外的参数就知道如何创建新对象。
一般而言,一个生成器只定义一个方法,该方法用以生成新的对象。
public interface Generator<T> {
T next();
}
编写一个类,实现上面的Generator<T> 接口。
public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
private Class[] types = {Latte.class, Mocha.class};
private static Random rand = new Random(47);
public CoffeeGenerator() { }
private int size = 0;
public CoffeeGenerator(int size) {
this.size = size;
}
@Override
public Coffee next() {
try{
return (Coffee) types[rand.nextInt(types.length)].newInstance();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
class CoffeeIterator implements Iterator<Coffee>{
int count = size;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for(int i = 0; i< 5; i++){
System.out.println(gen.next());
}
for(Coffee c : new CoffeeGenerator(5)){
System.out.println(c);
}
}
}
参数化的Generator接口确保next()的返回值是参数的类型。
CoffeeGenerator 同时还实现了Iterable接口,所以它可以在循环语句中使用。不过,它还需要一个“末端哨兵”来判断何时停止,这正是第二个构造器的功能。
使用Generator<T>接口的另一个实现,负责生产Fibonacci数列:
public class Fibonacci implements Generator<Integer>{
private int count = 0;
@Override
public Integer next() {
return fib(count++);
}
private int fib(int n) {
if(n<2) return 1;
return fib(n-2) + fib(n-1);
}
public static void main(String[] args) {
Fibonacci gen = new Fibonacci();
for(int i =0; i < 18; i++){
System.out.println(gen.next() + " ");
}
}
}
Fibonacci类的里里外外都使用的是int类型,但是其类型参数确实Integer。所以Java泛型的局限性:基本类型无法作为参数类型。
不过,Java SE5 具备了自动打包和自动拆包的功能,可以很方便在基本类型和其相应的包装器类型之间进行转换。所以Fibonacci类对int的使用可以证明这点。
改进:
编写一个实现了Iterable接口的Fibonacci生成器。
方法1:重写这个Fibonacci类,让它实现Iterable接口。不好之处:我们不可能永远能够拥有已经写好的代码,并且改变它。除非必须重写这个类,否则,一般来说,不愿意重写一个类。
方法2:创建一个适配器来实现所需要的接口。
有多种方法可以实现适配器,如下是使用继承来创建适配器类:
public class IterableFibonacci extends Fibonacci implements Iterable<Integer> {
private int n;
public IterableFibonacci(int count) {
this.n = count;
}
@Override
public Iterator<Integer> iterator() {
return new IteratorFibonacci();
}
private class IteratorFibonacci implements Iterator<Integer>{
@Override
public boolean hasNext() {
return n>0;
}
@Override
public Integer next() {
n--;
return IterableFibonacci.this.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public static void main(String[] args) {
for(int i : new IterableFibonacci(18)){
System.out.print(i + " ");
}
}
}
如果要在循环语句中使用 IterableFibonacci, 必须项 IterableFibonacci 的构造器提供一个边界值,然后 hasNext() 方法才能知道何时应该返回false。
泛型方法
泛型同样可以适用于方法,这个方法所在的类可以是泛型类,也可以不是泛型类。
泛型方法使得该方法能够独立于类而产生变化。
基本的指导原则:
无论何时,只要你能做到,你就应该尽量使用泛型方法,因为它可以使事情更清楚命。
对于一个static的方法而言,无法访问泛型类的类型参数,所以,如何static方法需要使用泛型能力,就必须使其成为泛型方法。
定义泛型方法,只需将泛型参数列表置于返回值之前。
public <T> void f(T x){
System.out.print("Hello.");
}
类型通配符 和 边界
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。
边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。
为了执行这种限制,Java泛型重用了extends 和 supper
关键字。
public class GenericDemo {
public static void main(String[] args) {
// 泛型如果明确的写的时候,前后必须一致 Collection<Object> c1 = new ArrayList<Object>();
// Collection<Object> c2 = new ArrayList<Animal>();//报错
// Collection<Object> c3 = new ArrayList<Dog>();//报错
// Collection<Object> c4 = new ArrayList<Cat>();//报错
// ?表示任意的类型都是可以的
Collection<?> c5 = new ArrayList<Object>();
Collection<?> c6 = new ArrayList<Animal>();
Collection<?> c7 = new ArrayList<Dog>();
Collection<?> c8 = new ArrayList<Cat>();
// ? extends E:向下限定,E及其子类
// Collection<? extends Animal> c9 = new ArrayList<Object>();//报错
Collection<? extends Animal> c10 = new ArrayList<Animal>();
Collection<? extends Animal> c11 = new ArrayList<Dog>();
Collection<? extends Animal> c12 = new ArrayList<Cat>();
// ? super E:向上限定,E极其父类
Collection<? super Animal> c13 = new ArrayList<Object>();
Collection<? super Animal> c14 = new ArrayList<Animal>();
// Collection<? super Animal> c15 = new ArrayList<Dog>();//报错
// Collection<? super Animal> c16 = new ArrayList<Cat>();//报错
}
}
class Animal {
}
class Dog extends Animal {
}
class Cat extends Animal {
}
杠杆利用类型参数推断
在Java7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。
上面两条语句中的构造器后面的尖括号部分完全是多余的,在Java7以前这是必需的,不能省略。从Java7开始,Java允许在构造器后不需带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。
Map<Coffee, List<Coffee>> cup = new HashMap<Coffee, List<Coffee>>();
可以简写成
Map<Coffee, List<Coffee>> cup = new HashMap<>();
网友评论