Think in Java 回顾之泛型

想去海边玩沙子

什么是泛型?

Java SE 5 开始引入了泛型的概念,泛型即参数化类型,利用泛型我们可以编写出更通用的代码(先不指定类型,使用时再指定类型)。泛型出现的最大的目的之一就是用来指定容器要持有的对象的类型,而且这种指定是由编译器来保证其正确性的。来看个例子:

class Holder<T> {
    private T value;

    public Holder() {
    }

    public Holder(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }

    public void set(T value) {
        this.value = value;
    }
    
    public static void main(String[] args) {
        // 可以不指定类型,类型参数 T 就是 Object,因此会被初始化为 null
        Holder holder = new Holder();
        System.out.println(holder.get());

        Holder<String> strHolder = new Holder<>("Aaron");
        System.out.println(strHolder.get());
        
        strHolder.set(1); // Error
    }
}

以上代码中定义了一个 Holder 类,使用类型参数 T 作为持有的对象的类型。T 可以表示任何对象,所以Holder 类也就具有了持有任何对象的能力。当我们使用它的时候,可以使用 <> 来确定 Holder 所持有的对象的类型,这样我们就可以保证持有对象的类型的正确性。

泛型方法

如果普通方法中定义了泛型参数,那么这就是一个泛型方法。泛型方法和与该类是否是泛型类无关,但是如果静态方法想要使用泛型参数,那么它就必须定义为泛型方法,因为静态方法无法访问泛型类中的泛型参数。

public class DemoGenericMethod<E> {

    private E e;

    public DemoGenericMethod() {
        e = (E) new Object();
    }

    public DemoGenericMethod(E e) {
        this.e = e;
    }

    /**
     * 根据泛型类的类型变量返回相应类型的 List
     */
    public List<E> getAsList(E e) {
        System.out.println(e.getClass().getName());
        return new ArrayList<>();
    }

    /**
     * 普通泛型方法
     */
    public <T> T printClassName(T t) {
        System.out.println(t.getClass().getName());
        return t;
    }

    public <T> void printSelfAndThis(T t) {
        System.out.println("this = " + e.getClass().getName()
                + ", that = " + t.getClass().getName());
    }

    /**
     * 静态方法一旦使用了泛型参数就必须定义为泛型方法。
     * 因为静态方法是独立于类之外的,无法访问类中的泛型参数
     */
    public static <T> void getName(T t) {
        System.out.println(t.getClass().getName());
    }

    /**
     * 可变参数列表与泛型方法
     */
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<>();
        Collections.addAll(result, args);
        return result;
    }
}

擦除

在使用泛型时,你是无法通过代码获得任何有关泛型参数类型的信息的,这其实是因为擦除的存在。在泛型类或泛型方法中,关于泛型参数的任何具体的类型信息都被擦除了(wiped),所以我们只能把类型参数当作一个 Object 使用。

// ArrayList<String> 被擦除为 ArrayList
Class c1 = new ArrayList<String>().getClass();
// ArrayList<Integer> 被擦除为 ArrayList
Class c2 = new ArrayList<Integer>().getClass();
// 这两个 Class 对象都被擦除为 ArrayList了,所以是相等的
System.out.println("c1 == c2? " + (c1 == c2)); // true

擦除意味着无法使用 instanceofnew 或者转型等需要在运行时才能知道确切类型信息的操作。

边界

边界允许我们在参数类型上设置限制条件,这样就能部分抵消擦除带来的负面影响。来看代码:

interface HasColor {
    Color getColor();
}

/**
 * 用 extend 关键字指定类型参数的边界
 */
class Colored<T extends HasColor> {
    T element;

    Colored(T element) {
        this.element = element;
    }

    T getElement() {
        return element;
    }

    Color color() {
        // 因为设置了边界,所以调用是安全的
        return element.getColor();
    }
}

可以看到,我们用 extend 关键字指定泛型边界。当参数类型继承多个边界时,定义的规则与类的继承相同,类在前,接口在后,类与多个接口的连接符用 &,比如:

class Solid<T extends Dimension & HasColor & Weight> {...}

通配符

通配符允许我们更加自由地使用泛型类。

  1. 首先是通配符结合 extends 关键字,用于确定泛型类的上边界。
// 表示"具有任何从 Number 类继承的类型的 List"
List<? extends Number> numbers = new ArrayList<Integer>();

但是此时往 numbers 中添加任何元素都是不被允许的,因为只声明了上边界,编译器是无法确定捕获(capture)的类型到底是什么,所以无论添加任何对象都是类型不安全的。

  1. 通配符 + super 关键字,即超类型通配符,用于确定泛型类的下边界。
// 表示边界范围是 "Number 类的任何父类",也就是说至少 Number 类及其子类是可以安全添加的
List<? super Number> numbers = new ArrayList<>();

使用超类型通配符后,由于下边界确定,所以当我们向 list 中添加 Number 类或者其子类的时候,才可能保证是类型安全的。不过,如果添加的是 Number 类的超类,也是不被允许的,因为编译器无法确定捕获的超类型到底是哪个超类。

可能文字比较难以理解,来看几个例子,更为直观:

public static void main(String[] args) {

    // 通配符必须是单一边界的,无法使用继承
    //List<? extends Fruit & Something > wildcard = new ArrayList<>();

    // Incompatible types,泛型不支持协变返回类型
    //List<Number> numberList = new ArrayList<Integer>();

    System.out.println("----------确定泛型的上边界----------");
    // 但是利用通配符和 extends 可以做到这一点
    // 这样的 List 可以看作是“具有任何从 Number 类继承的 List”,即为通配符确定了上界
    List<? extends Number> numbers = new ArrayList<Integer>();

    /*
    * 但是这样的List是非常有局限性的,无法添加任何有意义的元素
    * 因为List的参数类型为 ? extends Fruit,也就是任何继承自 Fruit 的对象
    * 编译器无法确定 List 所持有的类型,这样就无法保证类型安全性,所以不允许添加任何有意义的对象
    * */
    //numbers.add(1);

    //numbers.add(new Object()); // 甚至连 Object 都无法添加

    // 可以添加 null 进去,因为 null 可以表示任何对象,这也说明了此时添加某个对象是不安全的
    numbers.add(null);

    // 可以从中取出元素,因为至少可以确定这是一个 Number 类的对象
    Number number = numbers.get(0);
    System.out.println(number);


    System.out.println("----------泛型的转型----------");
    Holder<Number> numberHolder = new Holder<>(1);
    // Incompatible types,无法向下转型
    //Holder<Integer> intHolder = numberHolder;

    // 利用通配符设定上边界后才可以完成转型
    Holder<? extends Number> nHolder = numberHolder;
    Number num = nHolder.get();
    Integer iNum = (Integer) nHolder.get();
    System.out.println("num: " + num + ", iNum: " + iNum);
    System.out.println("num.equals(iNum) " + num.equals(iNum)); // true


    System.out.println("----------确定泛型的下边界----------");
    // 用 super 关键字指定下边界后才可以添加内容
    List<? super Number> boundedNums = new ArrayList<>();
    // 可以向其中添加 Number 及其子类型
    boundedNums.add(1);
    boundedNums.add(1.0f);
    boundedNums.add(1.33d);
    // 但是无法添加父类对象,因为无法确定捕获到的是哪个父类
    // boundedNums.add(new Object());
    System.out.println(boundedNums);


    System.out.println("----------无界通配符----------");
    List<?> list = new ArrayList<String>();
    // 捕获的参数类型是 capture<?>,无法应用到 String
    //list.add("1");
}

无界通配符

上面的例子中有关于无界通配符的使用,第一次看到会觉得似乎难以理解,其实它表示的意思是”我可以持有任何类型“,它是更为泛化的参数化类型,但也因此无法像有界的泛型参数那样做更多的事。它最主要的用途就是捕获转换,即捕获未指定的通配符类型,然后将之转换为确切的某种类型。

public class DemoCaptureConversion {

    static <T> void captureWithType(Holder<T> holder) {
        System.out.println(holder.get().getClass().getSimpleName());
    }

    /**
     * 未指定的通配符类型会被捕获并转换为类型参数来使用
     */
    static void captureWithWildcard(Holder<?> holder) {
        // 通配符可以捕获到类型参数
        captureWithType(holder);
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder(1);
        captureWithType(raw);
        captureWithWildcard(raw);
      
        // 同样发生了类型参数的捕获
        Holder<?> wildcarded = new Holder<>(1.2f);
        captureWithType(wildcarded);
        captureWithWildcard(wildcarded);
    }
}

以上代码中,第一个方法中的参数是确切的已知的,而第二个方法中使用了无界通配符,参数是未知的。在调用第二个方法的时候,可以捕获到类型参数并进行调用。

泛型存在的问题

  1. 基本类型无法作为类型参数,必须使用其包装类。
  2. 不能同时实现同一个泛型接口的两种变体,原因是接口的参数类型会被擦除,也就相当于同一个接口被实现了两次。
interface Pay<T> {}

class Emp implements Pay<Emp> {}

// cannot be inherited with different type arguments
class Hour extends Emp implements Pay<Hour>{}
  1. 对泛型转型有时会产生”unchecked cast“的警告,因为编译期无法确定转型是否安全。
  2. 无法使用泛型参数作为区分两个方法,也就是无法用类型参数作为重载的依据。
  3. 基类劫持接口,最常见的例子就是实现 Comparable 接口。
// Pet 类把自己作为参数类型传到 Comparable 接口中
class Pet implements Comparable<Pet> {

    private String name;
    private int age;

    public Pet(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(@NotNull Pet pet) {
        return Integer.compare(this.age, pet.age);
    }
}

结语

在 Think in Java 中,除了以上问题外,还详细讲解了自限定的类型、动态类型安全、泛型在异常中的使用、混型、潜在类型机制的缺失及补偿、将函数对象用作策略等,想要深入了解泛型的,不妨仔细阅读下这部分内容,如果有新的感受记得留言交流哦~


参考资料:

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

推荐阅读更多精彩内容

  • 在之前的文章中分析过了多态,可以知道多态本身是一种泛化机制,它通过基类或者接口来设计,使程序拥有一定的灵活性,但是...
    _小二_阅读 676评论 0 0
  • 参数类型的好处 在 Java 引入泛型之前,泛型程序设计是用继承实现的。ArrayList 类只维护一个 Obje...
    杰哥长得帅阅读 871评论 0 3
  •   在Effective中讲到泛型之处提到了一个概念,类型擦除器,这是什么呢?接下来我们跟随这篇文章探索类型擦除的...
    凌云_00阅读 2,130评论 0 8
  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 1,040评论 0 3
  • 一、为什么要使用泛型 1.类型参数的好处 类型安全:泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛...
    SeanMa阅读 7,036评论 1 18