美文网首页Java学习笔记
Think in Java 回顾之泛型

Think in Java 回顾之泛型

作者: aJIEw | 来源:发表于2018-04-19 22:25 被阅读96次
想去海边玩沙子

什么是泛型?

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 类的任何基类" 来确定
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;

    // 无法完成调用set,原因同上,捕获的 Fruit 类无法应用到 Apple
    //nHolder.set(new Apple());

    // 获取 Fruit,但是实际类型为 Apple
    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<>();
    boundedNums.add(1);
    writeTo(boundedNums);
    System.out.println(boundedNums);


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

无界通配符

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

public class DemoCaptureConversion {

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

    // 调用该方法时,发生了类型参数的捕获
    static void f2(Holder<?> holder) {
        f1(holder);
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder(1);
        f1(raw);
        f2(raw);
        Holder<?> wildcarded = new Holder<>(1.2f);
        f1(wildcarded);
        f2(wildcarded);
    }
}

以上代码中,f1() 中的参数是确切的已知的,而 f2() 中使用了无界通配符,参数是未知的。在调用 f2() 的时候,首先会捕获参数类型,然后转换为确切的类型以供 f1() 调用。

问题

  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版

相关文章

  • Think in Java 回顾之泛型

    什么是泛型? Java SE 5 开始引入了泛型的概念,泛型即参数化类型,利用泛型我们可以编写出更通用的代码(先不...

  • Java泛型:类型擦除

    前情回顾 Java泛型:泛型类、泛型接口和泛型方法 类型擦除 代码片段一 显然在平时使用中,ArrayList (...

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • Java中的泛型/范型

    维基百科中关于Java泛型的描述 Java 泛型的参数只可以代表类,不能代表个别对象。由于Java泛型的类型参数之...

  • Java高级语言特性之泛型

    Java高级语言特性之泛型 Java泛型(generics)是JDK 5中引入的一个新特性,泛型提供了编译时类型安...

  • Java:泛型的理解

    本文源自参考《Think in Java》,多篇博文以及阅读源码的总结 前言 Java中的泛型每各人都在使用,但是...

  • Java基础之泛型

    Java基础之泛型 泛型基本介绍 简单介绍在jdk1.6之前,还没有使用到泛型,Java类型分为原始类型、复杂类型...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

  • java泛型中类型擦除的一些思考

    java泛型 java泛型介绍 java泛型的参数只可以代表类,不能代表个别对象。由于java泛型的类型参数之实际...

网友评论

    本文标题:Think in Java 回顾之泛型

    本文链接:https://www.haomeiwen.com/subject/gsigkftx.html