首頁  >  文章  >  Java  >  Java程式設計思想學習課程(三)第15章-泛型

Java程式設計思想學習課程(三)第15章-泛型

php是最好的语言
php是最好的语言原創
2018-08-09 14:39:022267瀏覽

泛型(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