首页  >  文章  >  Java  >  Java编程思想学习课时(三)第15章-泛型

Java编程思想学习课时(三)第15章-泛型

php是最好的语言
php是最好的语言原创
2018-08-09 14:39:022312浏览

泛型(generics)的概念是Java SE5的重大变化之一。泛型实现了参数化类型(parameterized types)的概念,使代码可以应用于多种类型。“泛型”这个术语的意思是:“适用于许多许多的类型”。

1 泛型方法

  泛型方法与其所在的类是否是泛型没在关系,即泛型方法所在的类以是泛型类也可以不是泛型类。

  • 泛型方法使得该方法能够独立于类而产生变化 。

  • 一个基本指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。

  • 对于一个static方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。

  • 要定义泛型方法,只需将泛型参数列表置于返回值之前。

1.1 类型参数推断

  使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。

  • 类型推断只对赋值操作有效

  • 如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断

1.1.1 显式的类型说明

  在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内,即显式的类型说明

2 擦除的神秘之处

根据JDK文档的描述,Class.getTypeParameters()将“返回一个TypeVariable对象数组,表示有泛型声明的类型参数…..”,这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中看到,你能够发现的只是用作参数占位符标识符,这并非有用的信息。

因此,残酷的现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息

因此,你可以知道诸如泛型参数标识符泛型类型边界这类信息——你却无法知道创建某个特定实例的实际的类型参数。……,在使用Java泛型工作时它是必须处理的最基本的问题

Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此 Listf7e83be87db5cd2d9a8a0b8117b38cd4Listc0f559cc8d56b43654fcbe4aa9df7b4a 在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生类型,即 List

2.1 C++的方式

  2.1.1 以下C++模板示例:

它怎么知道f()方法是为类型参数T而存在的呢?当你实例化这个模板时,C++编译器将进行检查,因此在Manipulatorda98a08537ace02ebe5efb4a0b237f4a实例化的这一刻,它看到HasF拥有一个方法f()。如果情况并非如此,就会得到一个编译期错误,这样类型安全就得到了保障

// Templates.cpp#include <iostream>using namespace std;template<class T> class Manipulator{
    T obj;public:
    Manipulator(T x) { obj = x; }    void manipulate() { obj.f(); }
};class HasF{public:    void f() { cout << "HasF::f()" << endl; }
};int main(){
    HasF hf;
    Manipulator<HasF> manipulator(hf);
    manipulator.manipulate();
}
  2.1.2 翻译成Java,将不能编译。

由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。

2.2 泛型边界

为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。

package net.mrliuli.generics.erase;/**
 * Created by li.liu on 2017/12/7.
 *//**
 * 由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
 * @param <T>
 */class Manipulator<T>{    private T obj;    public Manipulator(T x){ obj = x; }    // Error: Cannot resolve method &#39;f()&#39;
    //public void manipulate(){ obj.f(); }}/**
 * 为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。
 * @param <T>
 */class Manipulator2<T extends HasF>{    private T obj;    public Manipulator2(T x){ obj = x; }    public void manipulate(){ obj.f(); }
}public class Manipulation {
    public static void main(String[] args){
        HasF hf = new HasF();
        Manipulator<HasF> manipulator = new Manipulator<>(hf);        //manipulator.manipulate();
        Manipulator2<HasF> manipulator2 = new Manipulator2<>(hf);
        manipulator2.manipulate();
    }
}

2.3 擦除

我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。T 擦除到HasF,就好像在类的声明中用 HasF 替换T 一样。

2.4 擦除的问题

擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为迁移兼容性

因此,擦除主要的正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。

擦除的代码是显著的。

  如果编写了下面这样的代码:

class Foo<T>{ T var; }

  那么看起来当你在创建Foo的实例时:

Foo<Cat> f = new Foo<Cat>();
  • class Foo中的代码应该知道现在工作于Cat之上,而泛型语法也强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实上并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”

  • 擦除和迁移兼容性意味着,使用泛型并不是强制的。

class GenericBase<T>{}class Derived1<T> extends GenericBase<T>{}class Derived2 extends GenericBase{} // No warning

2.5 边界处的动作

  • 即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性

  • 因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查插入转型代码的地点。

  • 在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”

3 擦除的补偿(Compensating for erasure)

有时必须通过引入类型标签(type tag)来对擦除进行补偿(compensating)。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。

  • 创建类型实例

  • 泛型数组

4 边界(bound)

  • 边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法

  • 因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。

  • 但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。

  • 通配符被限制为单一边界

5 通配符(wildcards)

  • 数组的一种特殊行为
      可以将子类型的数组赋给基类型的数组引用。然后编译期数组元素可以放置基类型及其子类型的元素,即编译时不报错,但运行时的数组机制知道实际的数组类型是子类,因此会在运行时检查放置的类型是否是实际类型及其再导出的子类型,不是则抛出java.lang.ArrayStoreException异常。

  • 容器的类型与容器持有的类型

// Compile Error: incompatible types:List<Fruit> list = new ArrayList<Apple>();

  与数组不同,泛型没有内建的协变类型。即*协变性对泛型不起作用

package net.mrliuli.generics.wildcards;import java.util.*;/**
 * Created by leon on 2017/12/8.
 */public class GenericsAndCovariance {
    public static void main(String[] args){        // Compile Error: incompatible types:
        //List<Fruit> list = new ArrayList<Apple>();

        // Wildcards allow covariance:
        List<? extends Fruit> flists = new ArrayList<Apple>();        // But, 编译器并不知道flists持有什么类型对象。实际上上面语句使得向上转型,丢失掉了向List中传递任何对象的能力,甚至是传递Object也不行。
        //flists.add(new Apple());
        //flists.add(new Fruit());
        //flists.add(new Object());

        flists.add(null);   // legal but uninteresting
        // We know that it returns at least Fruit:
        Fruit f = flists.get(0);
    }
}

5.1 编译器有多聪明

  • 对于 List57019040ccef885c8e3bd8f9deb31922set() 方法不能工作于 AppleFruit,因为 set() 的参数也是 ? extends Furit,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。

  • 但是,equals() 方法工作良好,因为它将接受Object类型而并非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。

5.2 逆变(Contravariance)

  • 使用超类型通配符。声明通配符是由某个特定类的任何基类界定的,方法是指定bbee182344df36d3890f89a83f7ca198,甚至或者使用类型参数:117c5a0bdb71ea9a9d0c2b99b03abe3e。这使得你可以安全地传递一个类型对象到泛型类型中。

  • 参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。

package net.mrliuli.generics.wildcards;import java.util.*;public class SuperTypeWildcards {
    /**
     * 超类型通配符使得可以向泛型容器写入。超类型边界放松了在可以向方法传递的参数上所作的限制。
     * @param apples    参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。
     */
    static void writeTo(List<? super Apple> apples){
        apples.add(new Apple());
        apples.add(new Jonathan());        //apples.add(new Fruit());    // Error
    }
}
  • GenericWriting.java 中 writeExact(fruitList, new Apple()); 在JDK1.7中没有报错,说明进入泛型方法 writeExact()T 被识别为 Fruit,书中说报错,可能JDK1.5将 T 识别为 Apple

package net.mrliuli.generics.wildcards;import java.util.*;/**
 * Created by li.liu on 2017/12/8.
 */public class GenericWriting {
    static <T> void writeExact(List<T> list, T item){
        list.add(item);
    }    static List<Apple> appleList = new ArrayList<Apple>();    static List<Fruit> fruitList = new ArrayList<Fruit>();    static void f1(){
        writeExact(appleList, new Apple());
        writeExact(fruitList, new Apple());
    }    static <T> void writeWithWildcard(List<? super T> list, T item){
        list.add(item);
    }    static void f2(){
        writeWithWildcard(appleList, new Apple());
        writeWithWildcard(fruitList, new Apple());
    }    public static void main(String[] args){
        f1();
        f2();
    }
}

5.3 无界通配符(Unbounded wildcards)

  原生泛型HolderHolder6b3d0130bba23ae47fe2b8e8cddf0195

原生Holder将持有任何类型的组合,而Holder6b3d0130bba23ae47fe2b8e8cddf0195将持有具有某种具体类型同构集合,因此不能只是向其中传递Object。

5.4 捕获转换

以下示例,被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。

package net.mrliuli.generics.wildcards;/**
 * Created by leon on 2017/12/9.
 */public class CaptureConversion {
    static <T> void f1(Holder<T> holder){
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }    static void f2(Holder<?> holder){
        f1(holder);     // Call with captured type
    }    public static void main(String[] args){
        Holder raw = new Holder<Integer>(1);
        f1(raw);
        f2(raw);
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        f2(rawBasic);
        Holder<?> wildcarded = new Holder<Double>(1.0);
        f2(wildcarded);
    }
}

6 问题

  • 基本类型不能作为类型参数

  • 由于探险,一个类不能实现同一个泛型接口的两种变体

  • 由于擦除,通过泛型来重载方法将产生相同的签名,编译出错,不能实现重载

  • 基类劫持了接口

7 总结

我相信被称为泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。

泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型有更少的限制,因此单个的代码段能够应用到更多的类型上

相关文章:

Java编程思想学习课时(一):第1~13、16章

Java编程思想学习课时(二)第14章-类型信息

以上是Java编程思想学习课时(三)第15章-泛型的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn