Javaベース-汎用

10365 ワード

一.汎用概念の提出(なぜ汎用が必要なのか)?
まず、次の短いコードを見てみましょう.
public class GenericTest {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // 1
            System.out.println("name:" + name);
        }
    }
}

リストタイプのセットを定義し、2つの文字列タイプの値を先に追加し、次にIntegerタイプの値を追加します.これはlistのデフォルトのタイプがObjectタイプであるため、完全に許可されます.以降のサイクルでは、リストにIntegerタイプの値や他の符号化が入っていたことを忘れたため、//1のようなエラーが発生しやすい.コンパイルフェーズが正常であるため、実行時に「java.lang.ClassCastException」異常が発生します.そのため、このようなエラー符号化中に発見されにくい.
上記のような符号化の過程で,主に2つの問題があることが分かった.
1.オブジェクトをコレクションに入れると、コレクションはオブジェクトのタイプを記憶しません.コレクションからオブジェクトを再び取り出すと、変更されたオブジェクトのコンパイルタイプはObjectタイプになりますが、実行時のタイプは任意です.
2.したがって、//1で集合要素を取り出す際に人為的な強制タイプを特定のターゲットタイプに変換する必要があり、「java.lang.ClassCastException」異常が発生しやすい.
では、集合が集合内の要素の各タイプを記憶し、コンパイル時に問題が発生しない限り、実行時に「java.lang.ClassCastException」異常が発生しないようにする方法はありませんか?答えは汎用型を使うことです.二.汎用とは?
二.汎用とは?
汎用、すなわち「パラメトリックタイプ」です.パラメータといえば、メソッドを定義するときにパラメータがあり、このメソッドを呼び出すときに実パラメータが渡されることが最もよく知られています.では、パラメトリックタイプはどのように理解されますか?名前の通り、タイプを元の特定のタイプからパラメータ化し、メソッド内の変数パラメータと同様にします.この場合、タイプもパラメータ形式(タイプパラメータと呼ぶことができます)として定義され、使用/呼び出し時に特定のタイプ(タイプ実パラメータ)が入力されます.
ちょっと複雑に見えますが、まず上の例を見てみましょう.汎用的な書き方を採用しています.
public class GenericTest {

    public static void main(String[] args) {
        /*
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);
        */

        List<String> list = new ArrayList<String>();
        list.add("qqyumidi");
        list.add("corn");
        //list.add(100);   // 1   

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i); // 2
            System.out.println("name:" + name);
        }
    }
}

汎用的な書き方を採用すると、//1にIntegerタイプのオブジェクトを1つ追加しようとするとコンパイルエラーが発生し、ListでリストセットにStringタイプの要素しか含まれないことを直接限定し、//2で強制的なタイプ変換を行う必要がない.この場合、集合は要素のタイプ情報を十分に記憶でき、コンパイラはStringタイプであることを確認できるからである.
上記の汎用定義と併せて,StringはListにおいてタイプ実パラメータであり,すなわち対応するListインタフェースには必ずタイプ形パラメータが含まれていることが分かった.get()メソッドの戻り結果も、直接このパラメータタイプ(すなわち、対応する受信タイプの実パラメータ)である.リストインタフェースの具体的な定義を見てみましょう.
public interface List<E> extends Collection<E> {

    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean addAll(int index, Collection<? extends E> c);

    boolean removeAll(Collection<?> c);

    boolean retainAll(Collection<?> c);

    void clear();

    boolean equals(Object o);

    int hashCode();

    E get(int index);

    E set(int index, E element);

    void add(int index, E element);

    E remove(int index);

    int indexOf(Object o);

    int lastIndexOf(Object o);

    ListIterator<E> listIterator();

    ListIterator<E> listIterator(int index);

    List<E> subList(int fromIndex, int toIndex);
}

リストインタフェースで汎用化定義を用いた後,のEはタイプパラメータを表し,具体的なタイプ実パラメータを受信することができ,このインタフェース定義では,Eが現れる場所はすべて同じ外部から受け入れられるタイプ実パラメータを表すことが分かる.
当然、ArrayListはListインタフェースの実装クラスとして、以下のように定義される.
public class ArrayList<E> extends AbstractList<E> 
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }
    
    //... 

}

これにより,ソースコードの観点から,//1にIntegerタイプオブジェクトコンパイルエラーが加えられ,//2にget()が与えられるタイプがStringタイプである理由が分かった.
 
三.カスタム汎用インタフェース、汎用クラス、および汎用メソッド
以上の内容から,汎用的な具体的な動作過程が分かった.インタフェース,クラス,メソッドも汎用的に定義し,対応する使用が可能であることも分かった.はい、具体的に使用する場合は、汎用インタフェース、汎用クラス、汎用メソッドに分けることができます.
カスタム汎用インタフェース、汎用クラス、および汎用メソッドは、上述したJavaソースコードのList、ArrayListと同様である.次に、最も簡単な汎用クラスとメソッド定義を見てみましょう.
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        System.out.println("name:" + name.getData());
    }

}

class Box<T> {

    private T data;

    public Box() {

    }

    public Box(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

}

汎用インタフェース、汎用クラス、および汎用メソッドの定義の過程で、T、E、K、Vなどの一般的なパラメータは、外部使用時に入力されたタイプの実パラメータを受信するため、汎用パラメータを表すためによく使用される.では、異なる受信タイプの実パラメータに対して、生成された対応するオブジェクトインスタンスのタイプは同じではないでしょうか.
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);

        System.out.println("name class:" + name.getClass());      // com.qqyumidi.Box
        System.out.println("age class:" + age.getClass());        // com.qqyumidi.Box
        System.out.println(name.getClass() == age.getClass());    // true

    }

}

これにより、汎用クラスを使用する場合、異なる汎用実パラメータが転送されるが、実際には異なるタイプが生成されるわけではないことが分かった.異なる汎用実パラメータが転送される汎用クラスはメモリ上で1つしかない.すなわち、元の最も基本的なタイプ(本例ではBox)であり、もちろん、論理的には複数の異なる汎用タイプと理解できる.
その原因を究明すると、Javaにおける汎用という概念が提案した目的は、コードコンパイル段階にのみ作用し、コンパイル過程において、汎用結果を正確に検証した後、汎用的な関連情報を消去することにある.すなわち、コンパイルに成功したclassファイルには汎用的な情報は含まれていない.汎用情報はランタイムフェーズには入りません.
これをまとめると、汎用タイプは論理的に複数の異なるタイプと見なされ、実際には同じ基本タイプである.
 
四.タイプワイルドカード
次に、上記の結論から、BoxとBoxは実際にはBoxタイプであることがわかり、現在は引き続き問題を検討する必要があるが、論理的には、BoxとBoxのように親子関係を持つ汎用タイプと見なすことができるだろうか.
この問題を明らかにするために、次の例を見てみましょう.
public class GenericTest {

    public static void main(String[] args) {

        Box<Number> name = new Box<Number>(99);
        Box<Integer> age = new Box<Integer>(712);

        getData(name);
        
        //The method getData(Box<Number>) in the type GenericTest is 
        //not applicable for the arguments (Box<Integer>)
        getData(age);   // 1

    }
    
    public static void getData(Box<Number> data){
        System.out.println("data :" + data.getData());
    }

}

コード//1にエラーメッセージが表示されることが分かった:The method getData(Box)in the t ype GenericTest is not applicable for the arguments(Box).明らかに,ヒント情報により,Boxは論理的にBoxの親と見なすことができないことが分かった.では、原因は何でしょうか.
public class GenericTest {

    public static void main(String[] args) {

        Box<Integer> a = new Box<Integer>(712);
        Box<Number> b = a;  // 1
        Box<Float> f = new Box<Float>(3.14f);
        b.setData(f);        // 2

    }

    public static void getData(Box<Number> data) {
        System.out.println("data :" + data.getData());
    }

}

class Box<T> {

    private T data;

    public Box() {

    }

    public Box(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

}

この例では、//1および//2でエラーメッセージが表示されることは明らかです.ここでは反証法を用いて説明することができる.
Boxが論理的にBoxの親と見なすことができると仮定すると、//1と//2でエラーメッセージはなく、問題が出てきますが、getData()メソッドでデータを取り出すときはいったいどんなタイプなのでしょうか.Integer? Float? それともNumber?また、プログラミング中の順序が制御不能であるため、必要に応じてタイプ判断を行い、強制タイプ変換を行う必要がある.明らかに、これは汎用的な理念と矛盾しているため、論理的にはBoxはBoxの親と見なすことができない.
では、振り返って「タイプワイルドカード」の最初の例を見てみましょう.具体的なエラーヒントの深い原因を知っています.では、どのように解決すればいいのでしょうか.新しい関数を定義することはできないでしょう.これはJavaのマルチステートコンセプトとは明らかに逆であるため,BoxとBoxの両方の親を論理的に表すための参照タイプが必要であり,これによりタイプワイルドカードが誕生する.
タイプワイルドカードは一般的に使用されますか?特定のタイプの実パラメータの代わりに.ここでは、タイプパラメータではなく、タイプインスタンスパラメータであることに注意してください.そしてBox論理的にはBox、Box...などのすべてのBox<特定のタイプの実パラメータ>の親クラスを参照してください.これにより、このようなニーズを達成するために、汎用的な方法を定義することができます.
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }

}

タイプワイルドカードの上限とタイプワイルドカードの下限も聞こえる場合があります.具体的にはどんなものがありますか?
上記の例では、getData()と同様の機能を定義する必要がある場合がありますが、タイプの実パラメータにはさらに制限があります.Numberクラスとそのサブクラスのみです.この場合、タイプワイルドカードの上限を使用する必要があります.
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);
        
        //getUpperNumberData(name); // 1
        getUpperNumberData(age);    // 2
        getUpperNumberData(number); // 3
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }
    
    public static void getUpperNumberData(Box<? extends Number> data){
        System.out.println("data :" + data.getData());
    }

}

この場合、コード//1で呼び出されるとエラーメッセージが表示され、//2//3で呼び出されるのは正常であることは明らかです.
タイプワイルドカードの上限通過形はBox形式定義、対応するタイプワイルドカード下限はBox形式であり,その意味はタイプワイルドカードの上限とは正反対であり,ここではあまり述べない.
 
五.口外編
本文の例は主に汎用型のいくつかの思想を述べるために簡単に挙げられ,必ずしも実際の可用性があるとは限らない.また、汎用型といえば、集合の中で最も多く使われていると信じていますが、実際のプログラミングの過程で、自分で汎用型を使って開発を簡素化し、コードの品質をよく保証することができます.さらに注意すべき点は,Javaにはいわゆる汎用配列という説はないことである.