Java汎用型を死滅させる(1編で十分)

10135 ワード

Java汎用型は、誤解を生みやすい知識点である.Javaの汎用型は消去に基づいて実現されるため、Java汎用型を使用する場合、汎用型の実現メカニズムの制限を受けることが多い.汎用型の知識を深く全面的に身につけることができなければ、汎用型の使用をよりよく制御できない.同時に、オープンソースプロジェクトを読むときも壁にぶつかる.この記事では、Javaの汎用性を全面的に深く理解してください.
汎用消去プローブ
汎用型はみんな使ったことがあると信じているので、いくつかの基礎的な知識点はくだらないことを言わないで、くどくどしないようにします.まず次のコードを見てみましょう
public class FruitKata {
    class Fruit {}
    class Apple extends generic.Fruit {}
    
    public void eat(List fruitList) {}

    public void eat(List fruitList) { }   // error, both methods has the same erasure
}

我々はFruitKataクラスで2つのeatメソッドを定義し,パラメータはそれぞれListとListタイプであり,このときコンパイラはエラーを報告し,知能的に「both methods has the same erasure」というエラープロンプトを与えた.明らかに、コンパイラは、この2つの方法は同じ署名を持っていると愚痴をこぼしています.うん~~、これは汎用消去の存在の証拠であり、さらに検証するのも簡単です.ByteCode Outlineというプラグインを使用すると、クラスがコンパイルされたバイトコードを簡単に表示できます.ここではeatメソッドのバイトコードだけを貼り付けます.
  // access flags 0x1
  // signature (Ljava/util/List;)V
  // declaration: void eat(java.util.List)
  public eat(Ljava/util/List;)V

パラメータは確かにリストタイプに消去されていることがわかりますが、ここで明らかにしたいのは、ここで消去されているのはメソッド内部の汎用情報だけで、汎用のメタ情報はクラスのclassバイトコードファイルに保存されていることです.
 // signature (Ljava/util/List;)V

このsignatureフィールドには大きな玄機があり、後で詳しく説明します.ここでは汎用的な方法で説明するだけですが、実際には汎用クラス、汎用的な戻り値は似ています.兄弟たちは自分でやってみてください.
なぜ消去で汎用化するのか
この質問に答えるには、汎用の歴史を知る必要があります.Javaの汎用はJdk 1.5で導入されています.その前にJdkのコンテナクラスなどはObjectでフレームワークの柔軟性を保証し、読み取り時に強く回転します.しかし、このようにするには、タイプが安全ではなく、コンパイラがタイプ変換エラーを事前に発見することができず、このリスクを実行時に持ち込むという大きな問題があります.汎用型の導入、すなわちタイプが安全でないという問題を解決するために、javaが広く使用されていたため、バージョンの前方互換性を保証することが必要であったため、古いバージョンjdkと互換性を持つために、汎用型の設計者は消去ベースの実装を選択した.Javaの汎用消去により、実行時にリストクラスが1つしかないため、C#の膨張ベースの汎用実装に比べてJavaクラスの数が相対的に少なく、メソッド領域の消費メモリが小さくなるという利点もあるでしょう.
汎用消去による問題
汎用消去のため、次のコードはコンパイルできません.
T t = new T();
T[] arr = new T[10];
List list = new ArrayList();
T instanceof Object

ワイルドカード
汎用消去の補償としてJavaはワイルドカードを導入した
List extends Fruit> fruitList;
List super Apple> appleList;

この2つのワイルドカードには多くの学生が誤解している.
? extends
?extends FruitはFruitがこの伝達された汎用ベースクラス(Fruitは汎用の上界)であるか,それとも上のFruitとAppleを例にとると,次のコードを見る.
List extends Fruit> fruitList = new ArrayList<>();
fruitList.add(new Fruit());  //error

私たちの上で正しいですか?extendsの理解では、fruitListはFruitを追加できるはずですが、コンパイラは私たちに間違って報告してくれました.私は初めてここを見たときもよく理解できなかったので、例を見て理解できました.
List extends Fruit>  fruitList = new ArrayList<>();
List appleList = new ArrayList<>();
fruitList = appleList;
fruitList.add(new Fruit());   //error

fruitListでFruitの追加が許可されている場合は、AppleListにFruitを追加します.これは受け入れられないに違いありません.
? super
もう一つ見てみましょうか.superの例
List super Apple> superAppleList = new ArrayList<>();
superAppleList.add(new Apple());
superAppleList.add(new Fruit());  // error

superAppleListにAppleを追加してもいいですが、Fruitを追加するか間違って報告します.はい、上記がPECSの原則です.
PECS
英語フルネーム、Producer Extends Consumer Super、
  • 読み取り専用の汎用集合が必要な場合、使用しますか?extends T
  • 書くだけの汎用集合が必要な場合、使用しますか?super T

  • 私自身はワイルドカードをこのように理解しています.
  • だって?extends Tが外部に与える承諾の意味は,この集合内の要素はすべてTのサブタイプであるが,いったいどのサブタイプなのか分からないため,どのサブタイプを追加するか,コンパイラは危険であると判断し,直接追加を禁止することである.
  • だって?super Tが外部に与える承諾の意味は,この集合内の要素の下限がTであるため,集合にTおよびTを追加するサブタイプは安全であり,この承諾の意味を破壊しない.
  • List,ListはいずれもList super Apple>のサブタイプである.Listは、List extends Apple>のサブタイプです.

  • 汎用的な使用については、Collectionsのcopyメソッドなど、Jdkには古典的な応用例がたくさんあります.
        public static  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 di=dest.listIterator();
                ListIterator extends T> si=src.listIterator();
                for (int i=0; i

    汎用消去して、私たちはまだ汎用情報を手に入れることができますか?
    前述したclassバイトコードにはsignatureフィールドがあり、汎用情報を保存します.汎用メソッドを新規作成します
        public  T plant(T fruit) {
            return fruit;
        }
    

    classファイルのバイナリ情報を見ると、確かにSignatureフィールド情報が入っています.
    Signature�%(TT;)TT;
    

    汎用情報がclassファイルにある以上、実行時に入手できませんか?方法はあるに違いない.例を見てみましょう
      Class clazz = HashMap(){}.getClass();
      Type superType = clazz.getGenericSuperclass();
      if (superType instanceof ParameterizedType) {
      ParameterizedType parameterizedType = (ParameterizedType) superType;
      Type[] actualTypes = parameterizedType.getActualTypeArguments();
       for (Type type : actualTypes) {
                System.out.println(type);
           }
       }
    
    //     
    class java.lang.String
    class generic.FruitKata$Apple
    

    汎用型の元のタイプ情報を入手して印刷したことがわかります.汎用的な使用に対する理解を深めるために、次にいくつかの小さな例を見ます.
    汎用型Gson解析での使用
    String jsonString = ".....";  //     json   
    Apple apple = new Gson().fromJson(jsonString, Apple.class);
    

    これはとても簡単なGsonの解析がコードを使うので、私達は更にそのfromJsonの方法の実現を見に行きます
      public  T fromJson(String json, Class classOfT) throws JsonSyntaxException {
        Object object = fromJson(json, (Type) classOfT);
        return Primitives.wrap(classOfT).cast(object);
      }
    

    最終的には
      TypeToken typeToken = (TypeToken) TypeToken.get(typeOfT);
      TypeAdapter typeAdapter = getAdapter(typeToken);
      T object = typeAdapter.read(reader);
    

    我々が入力したClassタイプによってTypeTokenを構築し,TypeAdapterによってjson文字列をオブジェクトTに変換することで,中間の詳細はここではこれ以上深くは進まない.
    retrofitでの汎用的な使用
    我々はretrofitを使用する場合、一般的に1つ以上のApiServiceインタフェースクラスを定義します.
    @GET("users/{user}/repos")
    Call> listRepos(@Path("user") String user);
    

    インタフェースメソッドの戻り値はすべて汎用型を使用しているので、コンパイル期間中に消去されることに決まっていますが、retrofitはどのようにして元の汎用型情報を得るのでしょうか.実は上の一般的な知識とGsonの使用説明があって、みんなと答えがあると信じています.retrofitフレームワーク自体は優雅に設計されており、詳細はここでは深く展開されていません.ここでは、汎用データが戻り値に変換される過程だけに関心を持っています.次のクラスを定義する必要があります
    // ApiService.class
    public interface ApiService {
        Observable> getAppleList();
    }
    
    // Apple.class
    class Apple extends Fruit {
        private int color;
        private String name;
        public Apple() {}
    
        public Apple(int color, String name) {
            this.color = color;
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "color:" + this.color + "; name:" + name;
        }
    }
    
    

    次に、動的エージェントを定義します.
    InvocationHandler handler = new InvocationHandler() {
           @Override
           public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Type returnType = method.getGenericReturnType();
                if (returnType instanceof ParameterizedType) {
                   ParameterizedType parameterizedType = (ParameterizedType) returnType;
                   Type[] types = parameterizedType.getActualTypeArguments();
                   if (types.length > 0) {
                       Type type = types[0];
                       Object object = new Gson().fromJson(mockAppleJsonString(), type);
                       return Observable.just(object);
                 }
               }
              return null;
         }
      };
    
    // mock json  
    public static String mockAppleJsonString() {
       List apples = new ArrayList<>();
       apples.add(new Apple(1, "   "));
       apples.add(new Apple(2, "   "));
       return new Gson().toJson(apples);
    }
    

    次に、retrofitデータ変換のプロセスをシミュレートする通常の呼び出しです.
    ApiService apiService = (ApiService) Proxy.newProxyInstance(ProxyKata.class.getClassLoader(),
                    new Class[] {ApiService.class}, handler);
    
    Observable> call = apiService.getAppleList();
    if (call != null) {
          call.subscribe(apples -> {
               if (apples != null) {
                  for (Apple apple : apples) {
                     System.out.println(apple);
                  }
             }
         });
    }
    
    //     
    color:1; name:   
    color:2; name:   
    

    MVPにおける汎用型の応用
    MVPモデルはAndroid開発をしていると信じていますが、私たちにはいくつかの種類があるとします.
    public class BaseActivity> extends AppCompatActivity {
       protected P mPresenter;
      //....
    }
    public class MainActivity extends BaseActivity implements MainView {
      //....
    }
    

    汎用消去の関係では,BaseActivityに直接Presenterを新規作成してmPresenterを初期化することはできないので,一般的にはcreatePresenter法を暴露してサブクラスを書き換えるのが一般的である.しかし、今日は別の方法を紹介します.コードを直接見てみましょう.
    // BaseActivity.class
            Type superType = getClass().getGenericSuperclass();
            if (superType instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) superType;
                Type[] types = parameterizedType.getActualTypeArguments();
                for (Type type : types) {
                    if (type instanceof Class) {
                        Class clazz = (Class) type;
                        try {
                            mPresenter = (P) clazz.newInstance();
                            mPresenter.bindView((V) this);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InstantiationException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
    

    我々はBaseActivityで汎用的な元の情報を得ることができ,反射によってmPresenterを初期化しbindViewを呼び出して我々のビューインタフェースをバインドする.このようにして、私たちは汎用的な能力を利用して、ベースクラスはすべての初期化任務をパッケージして、論理が簡単であるだけではなくて、その上高い集約を体現して、実際のプロジェクトの中で試して使用することができます.
    まとめ
    Java汎用型はエンジニアの進級に必要なスキルであることを深く理解し、この文章を読んでほしい.今後、面接でも他の時でも、Java汎用型について話すときは軽く、汎用型を使ってコードを書くときも手当たり次第に手に入れることができる.