Java プログラミング思考の学習クラス (3) 第 15 章 - ジェネリックス

php是最好的语言
リリース: 2018-08-09 14:39:02
オリジナル
2303 人が閲覧しました

ジェネリック (ジェネリック) の概念は、Java SE5 の大きな変更点の 1 つです。ジェネリックはパラメータ化された型 (パラメータ化された型) の概念を実装し、コードを複数の型に適用できるようにします。 「ジェネリック」という用語は、「非常に多くの種類に適用できる」ことを意味します。

1 ジェネリックメソッド

ジェネリックメソッドが配置されているクラスがジェネリックかどうか、つまりジェネリックメソッドが配置されているクラスがジェネリッククラスであるかどうかとは関係がありません。

  • ジェネリック メソッドを使用すると、クラスとは独立してメソッドを変更できます。

  • 基本的な指針: できる限り、一般的な方法を使用するように努めるべきです。つまり、クラス全体のジェネリック化をジェネリック メソッドの使用で置き換えることができる場合は、物事がより明確になるため、ジェネリック メソッドのみを使用する必要があります。

  • static メソッドの場合、ジェネリック クラスの型パラメーターにアクセスできないため、static メソッドでジェネリック機能を使用する必要がある場合は、ジェネリックである必要があります。方法。 static方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。

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

1.1 类型参数推断

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

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

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

1.1.1 显式的类型说明

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

2 擦除的神秘之处

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

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

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

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

2.1 C++的方式

  2.1.1 以下C++模板示例:

它怎么知道f()方法是为类型参数T而存在的呢?当你实例化这个模板时,C++编译器将进行检查,因此在Manipulator<HasF>实例化的这一刻,它看到HasF拥有一个方法f()ジェネリックメソッドを定義するには、戻り値の前にジェネリックパラメータリストを置くだけです。 1.1 型パラメータの推論

ジェネリックメソッドを使用する場合、コンパイラが特定の型を見つけてくれるため、通常はパラメータの型を指定する必要はありません。これを型引数の推論と呼びます。

型推論は代入演算
に対してのみ有効です。

ジェネリックメソッド呼び出しの結果がパラメータとして別のメソッドに渡される場合、コンパイラは型推論を実行しません。

1.1.1 明示的な型の指定

ドット演算子とメソッド名の間に山括弧を挿入し、山括弧内に型を配置します。つまり、明示的な型の説明

2 消去の謎🎜🎜🎜 JDK ドキュメントによると、🎜Class.getTypeParameters()🎜 は「オブジェクトの 🎜TypeVariable🎜 配列を返します。これは、これは、パラメーターの型に関する情報が見つかる可能性があることを暗示しているようですが、出力からわかるように、見つけられるものはすべて🎜🎜パラメーターのプレースホルダーとして使用されています🎜🎜識別子 🎜🎜 は有用な情報ではありません。 🎜🎜したがって、厳しい現実は次のとおりです: 🎜🎜ジェネリック コード内では、 ジェネリック パラメータの型に関する情報を取得する方法はありません🎜🎜。 🎜🎜つまり、🎜ジェネリック パラメーター識別子🎜や🎜ジェネリック型境界🎜などはわかりますが、🎜固有のインスタンスを作成する実際の型パラメーター🎜を知ることはできません。 …、これは Java ジェネリックスを扱うときに 🎜対処しなければならない🎜最も基本的な 🎜問題 🎜 です。 🎜🎜Java ジェネリックスは 🎜🎜erasure🎜🎜 を使用して実装されます。つまり、ジェネリックスを使用すると、特定の型情報はすべて消去されます。🎜知っていることは、オブジェクトを使用していることだけです 🎜。したがって、 🎜List<String>🎜 と 🎜List<Integer>🎜 は、実行時には実際には 🎜🎜同じ型🎜🎜 になります。どちらのフォームも、🎜🎜ネイティブ タイプ🎜🎜、つまり 🎜List🎜 に消去されます。 🎜🎜🎜2.1 C++ の方法 🎜🎜 2.1.1 次の C++ テンプレートの例: 🎜🎜 🎜その方法f() メソッドが型パラメータ T に存在することを知っていますか? このテンプレートを 🎜インスタンス化すると、C++ コンパイラがそれをチェックします。そのため、Manipulator<HasF> ; code>🎜🎜がインスタンス化される瞬間🎜🎜は、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コンパイラは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 编译器有多聪明

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

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

5.2 逆变(Contravariance)

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

  • 参数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)

  原生泛型HolderHolder<?>

原生Holder将持有任何类型的组合,而Holder<?>将持有具有某种具体类型同构集合,因此不能只是向其中传递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 プログラミング思考の学習クラス (3) 第 15 章 - ジェネリックスの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!