検索
ホームページJava&#&チュートリアルJava ジェネリックスの詳細な説明

はじめに

ジェネリックは Java の非常に重要な知識ポイントであり、Java コレクション クラス フレームワークで広く使用されています。この記事では、Java ジェネリックの設計をゼロから見ていきます。これには、ワイルドカード処理と煩わしい型の消去が含まれます。

一般的な基本

汎用クラス

最初に単純な Box クラスを定義します:

public class Box {
    private String object;
    public void set(String object) { this.object = object; }
    public String get() { return object; }
}

これは最も一般的なアプローチです。この方法の欠点は、今後 Integer などの他のタイプの要素をロードする必要がある場合、コードを書き直す必要があることです。再利用に関しては、ジェネリックを使用するとこの問題をうまく解決できます。

りー

このようにして、Box クラスを再利用でき、T を任意の型に置き換えることができます:

public class Box<T> {
    // T stands for "Type"
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

一般的なメソッド

ジェネリック クラスについて読んだ後、ジェネリック メソッドを見てみましょう。ジェネリック メソッドの宣言は非常に簡単で、戻り値の型の前に のような形式を追加するだけです。 次のような汎用メソッドを呼び出すことができます:

Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();

または、Java1.7/1.8 で型推論を使用して、Java が対応する型パラメータを自動的に導出できるようにします:

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}
public class Pair<K, V> {
    private K key;
    private V value;
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

境界記号

ここで、汎用配列内の特定の要素より大きい要素の数を見つける関数を実装したいと思います。これは次のように実装できます。 しかし、これは明らかに間違っています。short、int、double、long、float、byte、char などのプリミティブ型を除いて、他のクラスは演算子 > を使用できない可能性があるため、コンパイラはエラーを報告します。この問題 毛織物?答えは、境界文字を使用することです。

りー

次のようなステートメントを作成することは、型パラメーター T が Comparable インターフェイスを実装するクラスを表すことをコンパイラーに伝えることと同じであり、すべてのクラスが少なくとも CompareTo メソッドを実装することをコンパイラーに伝えることと同じです。

りー

ワイルドカード

ワイルドカードを理解する前に、まず概念を明確にする必要があります。上で定義した Box クラスを次のようなメソッドに追加するとします。 それでは、Box n は現在どのような種類のパラメータを受け入れることができるのでしょうか? Box または Box を渡すことはできますか?答えは「ノー」です。Integer と Double は Number のサブクラスですが、ジェネリックスでは Box と Box の間には関係がありません。これは非常に重要です。完全な例を通して理解を深めましょう。

まず、以下で使用するいくつかの単純なクラスを定義します。 次の例では、ジェネリック クラス Reader を作成し、f1() で Fruit f = FruitReader.readExact(apples); を実行すると、List と Listりー

しかし、私たちの通常の思考習慣によれば、Apple と Fruit の間には関連性があるはずですが、コンパイラーはそれを認識できません。では、この問題を汎用コードで解決するにはどうすればよいでしょうか。この問題はワイルドカードを使用することで解決できます:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

これは、fruitReader の readCovariant メソッドによって受け入れられるパラメーターは Fruit のサブクラス (Fruit 自体を含む) のみである必要があるため、サブクラスと親クラスの間の関係も関連付けられることをコンパイラーに伝えるのと同じです。

PECS原則

上では extends T> と同様の使用法を示し、これを使用してリストから要素を取得できるので、リストに要素を追加できますか?試してみましょう:

rreee

答えは「いいえ」です。Java コンパイラではこれを行うことができません。なぜでしょうか?この問題をコンパイラの観点から検討することもできます。 List は Fruit> を拡張したものなので、それ自体が複数の意味を持つ可能性があります: Apple を追加しようとすると、flist が新しい ArrayList();

を指す場合があります。 Orange を追加しようとすると、flist が新しい ArrayList();

  • を指す可能性があります。 フルーツを追加しようとすると、フルーツはどのタイプのフルーツでも構いませんが、flist は特定のタイプのフルーツのみを必要とする可能性があり、コンパイラはそれを認識できず、エラーを報告します。
  • したがって、 を実装するコレクション クラスは
  • 要素を追加したい場合はどうすればよいでしょうか? スーパー T> を使用できます:

    public class GenericWriting {
        static List<Apple> apples = new ArrayList<Apple>();
        static List<Fruit> fruit = new ArrayList<Fruit>();
        static <T> void writeExact(List<T> list, T item) {
            list.add(item);
        }
        static void f1() {
            writeExact(apples, new Apple());
            writeExact(fruit, new Apple());
        }
        static <T> void writeWithWildcard(List<? super T> list, T item) {
            list.add(item)
        }
        static void f2() {
            writeWithWildcard(apples, new Apple());
            writeWithWildcard(fruit, new Apple());
        }
        public static void main(String[] args) {
            f1(); f2();
        }
    }

      这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List super Apple> list,它可以有下面几种含义:

    List<? super Apple> list = new ArrayList<Apple>();
    List<? super Apple> list = new ArrayList<Fruit>();
    List<? super Apple> list = new ArrayList<Object>();

      当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。

      根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:

    • “Producer Extends” – 如果你需要一个只读List,用它来produce T,那么使用? extends T。


    • “Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用? super T。


    • 如果需要同时读取以及写入,那么我们就不能使用通配符了。

      如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:

    public class Collections {
        public static <T> void copy(List<? super T> dest, List<? extends T> src) {
            for (int i=0; i<src.size(); i++)
                dest.set(i, src.get(i));
        }
    }

     类型擦除

      Java泛型中最令人苦恼的地方或许就是类型擦除了,特别是对于有C++经验的程序员。类型擦除就是说Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。对于这一点,如果阅读Java集合框架的源码,可以发现有些类其实并不支持泛型。

      说了这么多,那么泛型擦除到底是什么意思呢?我们先来看一下下面这个简单的例子:

    public class Node<T> {
        private T data;
        private Node<T> next;
        public Node(T data, Node<T> next) }
            this.data = data;
            this.next = next;
        }
        public T getData() { return data; }
        // ...
    }

      编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:

    public class Node {
        private Object data;
        private Node next;
        public Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
        public Object getData() { return data; }
        // ...
    }

      这意味着不管我们声明Node还是Node,到了运行期间,JVM统统视为Node。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:

    public class Node<T extends Comparable<T>> {
        private T data;
        private Node<T> next;
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
        public T getData() { return data; }
        // ...
    }

      这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:

    public class Node {
        private Comparable data;
        private Node next;
        public Node(Comparable data, Node next) {
            this.data = data;
            this.next = next;
        }
        public Comparable getData() { return data; }
        // ...
    }

      上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题,有些问题在C++的泛型中可能不会遇见,但是在Java中却需要格外小心。

      问题一

      在Java中不允许创建泛型数组,类似下面这样的做法编译器会报错:

    List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

      为什么编译器不支持上面这样的做法呢?继续使用逆向思维,我们站在编译器的角度来考虑这个问题。

      我们先来看一下下面这个例子:

    Object[] strings = new String[2];
    strings[0] = "hi";   // OK
    strings[1] = 100;    // An ArrayStoreException is thrown.

      对于上面这段代码还是很好理解,字符串数组不能存放整型元素,而且这样的错误往往要等到代码运行的时候才能发现,编译器是无法识别的。接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:

    Object[] stringLists = new List<String>[];  // compiler error, but pretend it&#39;s allowed
    stringLists[0] = new ArrayList<String>();   // OK
    // An ArrayStoreException should be thrown, but the runtime can&#39;t detect it.
    stringLists[1] = new ArrayList<Integer>();

      假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM实际上根本就不知道new ArrayList()和new ArrayList()的区别。类似这样的错误假如出现才实际的应用场景中,将非常难以察觉。

      如果你对上面这一点还抱有怀疑的话,可以尝试运行下面这段代码:

    public class ErasedTypeEquivalence {
        public static void main(String[] args) {
            Class c1 = new ArrayList<String>().getClass();
            Class c2 = new ArrayList<Integer>().getClass();
            System.out.println(c1 == c2); // true
        }
    }

      问题二

      继续复用我们上面的Node的类,对于泛型代码,Java编译器实际上还会偷偷帮我们实现一个Bridge method。

    public class Node<T> {
        public T data;
        public Node(T data) { this.data = data; }
        public void setData(T data) {
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    public class MyNode extends Node<Integer> {
        public MyNode(Integer data) { super(data); }
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }

      看完上面的分析之后,你可能会认为在类型擦除后,编译器会将Node和MyNode变成下面这样:

    public class Node {
        public Object data;
        public Node(Object data) { this.data = data; }
        public void setData(Object data) {
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    public class MyNode extends Node {
        public MyNode(Integer data) { super(data); }
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }

      实际上不是这样的,我们先来看一下下面这段代码,这段代码运行的时候会抛出ClassCastException异常,提示String无法转换成Integer:

    MyNode mn = new MyNode(5);
    Node n = mn; // A raw type - compiler throws an unchecked warning
    n.setData("Hello"); // Causes a ClassCastException to be thrown.
    // Integer x = mn.data;

      如果按照我们上面生成的代码,运行到第3行的时候不应该报错(注意我注释掉了第4行),因为MyNode中不存在setData(String data)方法,所以只能调用父类Node的setData(Object data)方法,既然这样上面的第3行代码不应该报错,因为String当然可以转换成Object了,那ClassCastException到底是怎么抛出的?

      实际上Java编译器对上面代码自动还做了一个处理:

    class MyNode extends Node {
        // Bridge method generated by the compiler
        public void setData(Object data) {
            setData((Integer) data);
        }
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
        // ...
    }

      这也就是为什么上面会报错的原因了,setData((Integer) data);的时候String无法转换成Integer。所以上面第2行编译器提示unchecked warning的时候,我们不能选择忽略,不然要等到运行期间才能发现异常。如果我们一开始加上Node n = mn就好了,这样编译器就可以提前帮我们发现错误。

      问题三

      正如我们上面提到的,Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过:

    public static <E> void append(List<E> list) {
        E elem = new E();  // compile-time error
        list.add(elem);
    }

      但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:

    public static <E> void append(List<E> list, Class<E> cls) throws Exception {
        E elem = cls.newInstance();   // OK
        list.add(elem);
    }

      我们可以像下面这样调用:

    List<String> ls = new ArrayList<>();
    append(ls, String.class);

      实际上对于上面这个问题,还可以采用Factory和Template两种设计模式解决,感兴趣的朋友不妨去看一下Thinking in Java中第15章中关于Creating instance of types(英文版第664页)的讲解,这里我们就不深入了。

      问题四

      我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息,正如我们上面验证过的JVM在运行时期无法识别出ArrayList和ArrayList的之间的区别:

    public static <E> void rtti(List<E> list) {
        if (list instanceof ArrayList<Integer>) {  // compile-time error
            // ...
        }
    }
    => { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }

      和上面一样,我们可以使用通配符重新设置bounds来解决这个问题:

    public static void rtti(List<?> list) {
        if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
            // ...
        }
    }

                   

以上がJava ジェネリックスの詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
Javaがクロスプラットフォームデスクトップアプリケーションを開発するための人気のある選択肢なのはなぜですか?Javaがクロスプラットフォームデスクトップアプリケーションを開発するための人気のある選択肢なのはなぜですか?Apr 25, 2025 am 12:23 AM

javaispopularforsoss-platformdesktopapplicationsduetoits "writeonce、runaynay" philosophy.1)itusesbytecodatiTatrunnanyjvm-adipplatform.2)ライブラリリケンディンガンドジャヴァフククレアティック - ルルクリス

Javaでプラットフォーム固有のコードを作成する必要がある場合がある状況について話し合います。Javaでプラットフォーム固有のコードを作成する必要がある場合がある状況について話し合います。Apr 25, 2025 am 12:22 AM

Javaでプラットフォーム固有のコードを作成する理由には、特定のオペレーティングシステム機能へのアクセス、特定のハードウェアとの対話、パフォーマンスの最適化が含まれます。 1)JNAまたはJNIを使​​用して、Windowsレジストリにアクセスします。 2)JNIを介してLinux固有のハードウェアドライバーと対話します。 3)金属を使用して、JNIを介してMacOSのゲームパフォーマンスを最適化します。それにもかかわらず、プラットフォーム固有のコードを書くことは、コードの移植性に影響を与え、複雑さを高め、パフォーマンスのオーバーヘッドとセキュリティのリスクをもたらす可能性があります。

プラットフォームの独立性に関連するJava開発の将来の傾向は何ですか?プラットフォームの独立性に関連するJava開発の将来の傾向は何ですか?Apr 25, 2025 am 12:12 AM

Javaは、クラウドネイティブアプリケーション、マルチプラットフォームの展開、および言語間の相互運用性を通じて、プラットフォームの独立性をさらに強化します。 1)クラウドネイティブアプリケーションは、GraalvmとQuarkusを使用してスタートアップ速度を向上させます。 2)Javaは、埋め込みデバイス、モバイルデバイス、量子コンピューターに拡張されます。 3)Graalvmを通じて、JavaはPythonやJavaScriptなどの言語とシームレスに統合して、言語間の相互運用性を高めます。

Javaの強力なタイピングは、プラットフォームの独立性にどのように貢献しますか?Javaの強力なタイピングは、プラットフォームの独立性にどのように貢献しますか?Apr 25, 2025 am 12:11 AM

Javaの強力なタイプ化されたシステムは、タイプの安全性、統一タイプの変換、多型を通じてプラットフォームの独立性を保証します。 1)タイプの安全性は、コンパイル時間でタイプチェックを実行して、ランタイムエラーを回避します。 2)統一された型変換ルールは、すべてのプラットフォームで一貫しています。 3)多型とインターフェイスメカニズムにより、コードはさまざまなプラットフォームで一貫して動作します。

Javaネイティブインターフェイス(JNI)がプラットフォームの独立性をどのように妥協できるかを説明します。Javaネイティブインターフェイス(JNI)がプラットフォームの独立性をどのように妥協できるかを説明します。Apr 25, 2025 am 12:07 AM

JNIはJavaのプラットフォームの独立を破壊します。 1)JNIは特定のプラットフォームにローカルライブラリを必要とします。2)ローカルコードをターゲットプラットフォームにコンパイルおよびリンクする必要があります。3)異なるバージョンのオペレーティングシステムまたはJVMは、異なるローカルライブラリバージョンを必要とする場合があります。

Javaのプラットフォームの独立性を脅かしたり強化したりする新しいテクノロジーはありますか?Javaのプラットフォームの独立性を脅かしたり強化したりする新しいテクノロジーはありますか?Apr 24, 2025 am 12:11 AM

新しいテクノロジーは、両方の脅威をもたらし、Javaのプラットフォームの独立性を高めます。 1)Dockerなどのクラウドコンピューティングとコンテナ化テクノロジーは、Javaのプラットフォームの独立性を強化しますが、さまざまなクラウド環境に適応するために最適化する必要があります。 2)WebAssemblyは、Graalvmを介してJavaコードをコンパイルし、プラットフォームの独立性を拡張しますが、パフォーマンスのために他の言語と競合する必要があります。

JVMのさまざまな実装は何ですか、そしてそれらはすべて同じレベルのプラットフォームの独立性を提供しますか?JVMのさまざまな実装は何ですか、そしてそれらはすべて同じレベルのプラットフォームの独立性を提供しますか?Apr 24, 2025 am 12:10 AM

JVMの実装が異なると、プラットフォームの独立性が得られますが、パフォーマンスはわずかに異なります。 1。OracleHotspotとOpenJDKJVMは、プラットフォームの独立性で同様に機能しますが、OpenJDKは追加の構成が必要になる場合があります。 2。IBMJ9JVMは、特定のオペレーティングシステムで最適化を実行します。 3. Graalvmは複数の言語をサポートし、追加の構成が必要です。 4。AzulzingJVMには、特定のプラットフォーム調整が必要です。

プラットフォームの独立性は、開発コストと時間をどのように削減しますか?プラットフォームの独立性は、開発コストと時間をどのように削減しますか?Apr 24, 2025 am 12:08 AM

プラットフォームの独立性により、開発コストが削減され、複数のオペレーティングシステムで同じコードセットを実行することで開発時間を短縮します。具体的には、次のように表示されます。1。開発時間を短縮すると、1セットのコードのみが必要です。 2。メンテナンスコストを削減し、テストプロセスを統合します。 3.展開プロセスを簡素化するための迅速な反復とチームコラボレーション。

See all articles

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

Dreamweaver Mac版

Dreamweaver Mac版

ビジュアル Web 開発ツール

VSCode Windows 64 ビットのダウンロード

VSCode Windows 64 ビットのダウンロード

Microsoft によって発売された無料で強力な IDE エディター

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser は、オンライン試験を安全に受験するための安全なブラウザ環境です。このソフトウェアは、あらゆるコンピュータを安全なワークステーションに変えます。あらゆるユーティリティへのアクセスを制御し、学生が無許可のリソースを使用するのを防ぎます。

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール