Java 泛型

《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<>();

参考资料

泛型介绍及泛型类、泛型方法、泛型接口
疯狂Java讲义-泛型

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容