Javaジェネリックの深い理解-PECSルール

ジェネリックスと言えば、誰もがそれに精通している必要があります。最も一般的に使用されるのはList <T>リストです。メソッドのカプセル化、リフレクションの使用、およびジェネリッククラスの直接記述がない場合、実際には、特別な注意を払う必要はありません。ジェネリックがないため、コンパイラーは自然に思い出させます。しましょう。

さて、最初に簡単なジェネリックを紹介しましょう。

     /**
         * 这个Integer可以写成int么?为什么?
         */
        List<Integer> list = new ArrayList<>();

コレクションを作成するには、ジェネリック型を定義する必要があることはわかっています。もちろん、List <T> listなどのように漠然と定義することもできます。この整数はintとして書くことができますか?もちろん、これは不可能です。次の図に示すように、ジェネリックは一般にObjectから継承されていることがわかります。つまり、ラッパークラスです。Intは単なる基本型であり、ジェネリックとして使用することはできません。

さて、<T>と<?>を使用する多くのパートナーがこれらの2つのタイプのジェネリックに遭遇したはずだと言いましょう。彼らは一定期間途方に暮れるかもしれません。これらの2つの仲間はタイプの代表です。違いは何ですか?

これは、実際には非常に簡単に言うことができます。<T>(もちろん、<E>は同じで、すべて文字に置き換えられます)ロゴ、文字<T>が固定され、文字がAに置き換えられていることがわかります。ラッパークラス、つまりジェネリックはクラスです。たとえば、Tは整数を表し、List <T>またはTを含む他の用途を使用している限り、整数を表します。

<?> Genericは疑問符です。実際、それも非常に明確です。つまり、どのタイプが渡されるかわかりません。どのタイプでも表すことができます。

ジェネリックスの使用は、一般的にメソッドのカプセル化であり、リフレクションやその他の手法を使用してコードの共通部分を抽出することを知っています。現時点では、<T>、<?>が一般的に使用されています。一部のパートナーはまだそれを理解していない可能性があります。 。展開しましょう。。

Javaジェネリックスは、J2 SE1.5で導入された新機能です。その本質はパラメーター化された型です。つまり、操作されるデータ型はパラメーター(型パラメーター)として指定されます。このパラメーター型は、クラスインターフェイス、およびで使用できます。メソッドの作成では、それらはジェネリッククラス、ジェネリックインターフェイス、およびジェネリックメソッドと呼ばれます。

コンセプトを説明するのはあまり得意ではありません。コピーを作成して、すぐ下で使用する方法について説明しましょう。

最初にいくつかのクラスを作成します(スキップできます)

/**
 * 动物(祖父类级别)
 */
@Data
public class Animal {

    /**
     * 判断条件,默认为false
     */
    private Boolean flag = false ;

    /**
     * 名字
     */
    private String name;
    /**
     * 重量
     */
    private String weight;

}

/**
 * 鸟类
 */
@Data
public class Bird extends Animal{
    /**
     * 飞多高
     */
    private String flyHeight;

}

/**
 * 鹰
 */
@Data
public class Eagle extends Bird{

    /**
     * 住在山上
     */
    private String mountain;
    /**
     * 吃肉
     */
    private String meat;

}

/**
 * 鹦鹉
 */
@Data
public class Parrot extends Bird {

    /**
     * 住的笼子
     */
    private String cage;

    /**
     * 吃米饭
     */
    private String rice;
}

それを段階的に見ていきましょう。オブジェクトAnimalを渡し、ルールに従って判断し、名前を割り当てるという単純なものから始めましょう。ジェネリックなしでは、抽出の一般的な部分です。メソッドは、鳥がそれを処理することです。オウムも着信を処理します。次のロジックを実装するための簡単なメソッドsetAnimalName(Animal animal)を記述します。

  /**
     * 设置动物名称
     * 无泛型,入参直接写父类名称,子类也可以传进来处理
     * 不过要处理的类型最好是父类的,不然要特殊处理(用反射)
     * @param animal
     */
    private static void setAnimalName(Animal animal){
        if(animal.getFlag()){
            animal.setName("animal");
        }
    }

    public static void main(String[] args) {
        Eagle eagle = new Eagle();
        eagle.setMeat("beef");
        eagle.setName("jake");

        Parrot parrot = new Parrot();
        parrot.setFlag(true);
        parrot.setCage("red-cage");

        setAnimalName(eagle);
        setAnimalName(parrot);
        System.out.println(" eagle:"+ JSON.toJSONString(eagle)+
                 "\n parrot:"+JSON.toJSONString(parrot));

    }

プリントアウトを見ることができます。ジェネリックは使用されていませんが、継承関係は実現できます。

 eagle:{"flag":false,"meat":"beef","name":"jake"}
 parrot:{"cage":"red-cage","flag":true,"name":"animal"}

一部の友人は、これはジェネリックを使用する必要があると常に感じていますよね?なぜ役に立たないのですか?もちろん使用可能ですので、見てみましょう。

   /**
     * 使用泛型
     * @param t
     */
    private static <T extends Animal> void setAnimalName(T t){
        if(t.getFlag()){
            t.setName("animal");
        }
    }

少しおもしろいと思いますか?結果は同じなので、投稿しません。

拡張してみましょう。継承関係がない場合は、渡されたオブジェクトにフィールド名とフィールドフラグがあることしかわかりません。現時点では、もちろん、ジェネリックのみを使用できます。つまり、単純なものを使用します。反射と再帰。見てみましょう。

    /**
     * 使用泛型
     * @param t
     */
    private static <T> void setAnimalName(T t){
        Class<?> temp = t.getClass();
        try {
            //flag 可能在父类中,使用clzz = clzz.getSuperclass() 递归
            Method method = getMethod(temp,"getFlag");
            Object invoke = method.invoke(t);
            if(invoke instanceof Boolean && (Boolean) invoke){
                Method setNameMethod = getMethod(temp,"setName",String.class);
                //设置名称
                setNameMethod.invoke(t, "animal");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 递归获取方法
     * @param temp
     * @param methodName
     * @return
     */
    private static Method getMethod(Class<?> temp,String methodName,Class<?>... parameterTypes){
        Method method = null ;
        while (method == null && !temp.getName().equalsIgnoreCase("java.lang.Object")){
            try {
                method = temp.getDeclaredMethod(methodName,parameterTypes);
            } catch (NoSuchMethodException e) {
                //源码中抛出了异常,所以还要捕获处理掉
            }
            temp = temp.getSuperclass();
        }
        return method;
    }
}

一般的に使用される一般的な処理については以上です。PECSを見てみましょう。簡単に言えば、プロデューサー(プロデューサー)はextendsを使用し、コンシューマー(Consumer)はスーパーを使用します。コードが少し厄介な場合は、最初に次のことを確認できます。図。。

 /**
     * PECS理解
     */
    static public void test(){

        //继承Bird 的类是有多个的,这个类不是固定的,所以使用?,使用T会报错
        List<? extends Bird> pList = new ArrayList<>();

        pList = Arrays.asList(new Bird(),new Bird());
//        pList.add(new Bird());//报错
//        pList.add(new Eagle());//报错
//        pList.add(new Parrot());//报错
//        pList.add(new Animal()); //报错
        Bird bird = pList.get(0);
        System.out.println("sList的大小:"+pList.size() +"\n bird"+bird);

        List<? super Bird> sList = new ArrayList<>();
        sList.add(new Bird());
        sList.add(new Parrot());
        sList.add(new Eagle());
//        Object o = sList.get(0);  //返回的是Object,没啥意义

        System.out.println("sList的大小:"+sList.size());
    }
    

上記はPECSのオペコードです。直接見ると少しめまいがします。下はみんなが見られる写真です。間違っている場合は指摘してください。

 

PECSの原則の要約:

  • コレクションからタイプTのデータを読み取りたいが、書き込みができない場合は、?Extendsワイルドカードを使用できます;(Producer Extends)
  • コレクションからタイプTのデータを書き込みたいが、それを読み取る必要がない場合は、?スーパーワイルドカードを使用できます;(コンシューマースーパー)
  • 保存して取得する場合は、ワイルドカードを使用しないでください。

 次に、古典的なアプリケーションJDK 8 Collections.copy()を見てください。

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

ご覧のとおり、srcはdestに割り当てられたデータソースであり、srcが(プロデューサー)データを生成するのと同等であり、destは(顧客)データを消費します。destはデータを再割り当てし、データ型はTまたはTのサブクラスであり、もちろんTの親クラスのサブクラスでもあります。srcがデータの削除を実行している間、返されるデータのタイプはすべてTです(Tのサブクラスが存在する場合がありますが、アップコンバートされるとTにもなります)。このソースコードは難しくありませんが、その中のアイデアはまだ非常に古典的です。どなたでもご覧いただけます。このソースコードはそれほど面倒ではなく、誰もが理解できるはずなので、コメントは書きません。

犠牲も勝利もありません〜

おすすめ

転載: blog.csdn.net/zsah2011/article/details/109130371