1. 程式人生 > >Java泛型 理解

Java泛型 理解

泛型是Java 1.5 以後新增的功能,可以在類或方法上指定其需要的引數或返回值型別。Java原本不支援泛型,因此使用了擦除機制作為折中。

類的型別

Java將類的型別封裝為介面Type, 包含ParameterizedType,GenericArrayType,TypeVariable和WildcardType四種類型的介面和Class這個直接子類。

其中,只有Class和ParameterizedType是明確型別的。TypeVariable和WildcardType是泛型型別, GenericArrayType兩者皆有可能。

  • Class 普通的類,如String
  • ParameterizedType
    類名帶引數型別的類, 如 ArrayList<String>
  • TypeVariable 可變型別, 如 List<T> 中的 T,在實際使用中傳遞給方法時,獲取的就是TypeVariable
  • WildcardType 萬用字元型別,包含上限或下限兩種,如List<? extend AbstractList>List<? super ArrayList>,不設定上限或者下限的話可以寫成List<?>,是List<? extend Object>的簡寫

泛型的作用

1. 增加資料結構的安全性

由於Java是強型別語言,而類似List的資料容器可以儲存任何型別的物件,在實際呼叫時容易出現型別轉換異常,泛型的加入規範了容器中應該儲存的資料型別,減少的錯誤發生。

//泛型限制僅對物件的引用有效,即等式的左邊部分
ArrayList<String> stringList1 = new ArrayList<String>();
//等式右邊只是在堆空間中申請記憶體,因此可以簡寫為如下
ArrayList<String> stringList2 = new ArrayList<>();
//反過來引用不使用泛型限定的情況下, 等式右邊的泛型不會起到任何作用
//容器依然能存入任何物件
ArrayList stringList3 = new ArrayList<String>();

2. 獲取更精確的返回值和引數

有時呼叫類的方法中需要使用一些變數的父類或者介面中的通用方法,但根據傳入物件的不同,也可能用到他們的子類特性,使用泛型就避免了將物件強轉。如下例子:

public static <T extends Comparable> T get(T t1,T t2) { //新增型別限定  
        if(t1.compareTo(t2)>=0);  
        return t1;  
    }

3. 更利於抽象

當我們抽取一些行為到介面或者抽取一些特性到基類的時候,類中基本都會包含或者返回物件的引用,而這些物件可能有一些可配置狀態或者自有實現,雖然與本類無關,但呼叫者希望明確這些物件是什麼型別,這時就需要泛型。比如Retrofit中的這個介面,就是對型別的靈活使用

package retrofit2;

import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * Adapts a {@link Call} into the type of {@code T}. Instances are created by {@linkplain Factory a
 * factory} which is {@linkplain Retrofit.Builder#addCallAdapterFactory(Factory) installed} into
 * the {@link Retrofit} instance.
 */
public interface CallAdapter<T> {
  /**
   * Returns the value type that this adapter uses when converting the HTTP response body to a Java
   * object. For example, the response type for {@code Call<Repo>} is {@code Repo}. This type
   * is used to prepare the {@code call} passed to {@code #adapt}.
   * <p>
   * Note: This is typically not the same type as the {@code returnType} provided to this call
   * adapter's factory.
   */
  Type responseType();

  /**
   * Returns an instance of {@code T} which delegates to {@code call}.
   * <p>
   * For example, given an instance for a hypothetical utility, {@code Async}, this instance would
   * return a new {@code Async<R>} which invoked {@code call} when run.
   * <pre>{@code
   * @Override
   * public <R> Async<R> adapt(final Call<R> call) {
   *   return Async.create(new Callable<Response<R>>() {
   *     @Override
   *     public Response<R> call() throws Exception {
   *       return call.execute();
   *     }
   *   });
   * }
   * }</pre>
   */
  <R> T adapt(Call<R> call);

  /**
   * Creates {@link CallAdapter} instances based on the return type of {@linkplain
   * Retrofit#create(Class) the service interface} methods.
   */
  abstract class Factory {
    /**
     * Returns a call adapter for interface methods that return {@code returnType}, or null if it
     * cannot be handled by this factory.
     */
    public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations,
        Retrofit retrofit);

    /**
     * Extract the upper bound of the generic parameter at {@code index} from {@code type}. For
     * example, index 1 of {@code Map<String, ? extends Runnable>} returns {@code Runnable}.
     */
    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
      return Utils.getParameterUpperBound(index, type);
    }

    /**
     * Extract the raw class type from {@code type}. For example, the type representing
     * {@code List<? extends Runnable>} returns {@code List.class}.
     */
    protected static Class<?> getRawType(Type type) {
      return Types.getRawType(type);
    }
  }
}

泛型的指向型別獲取

要獲取泛型中指向的型別只有在被被明確賦值的地方(類或者方法都可以)才能成功,一旦經過傳遞,那麼就會失敗。舉個例子

public class FamilyMember<T> {
    public FamilyMember() {
        printRole();
    }
    public FamilyMember(T member) {
        printRole();
        this.member = member;
        leader = new FamilyLeader(member);
    }
    private T member;
    private FamilyLeader leader;
    public void printRole(){
        Type genericSuperclass = getClass().getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType){
            Type[] types = ((ParameterizedType) genericSuperclass).getActualTypeArguments();
            Type type = types[0];
            Log.i("printType_" + getClass().getSimpleName(), type.getClass().getSimpleName());
        }
    }
    private class FamilyLeader extends FamilyMember<T>{
        private T leader;
        public FamilyLeader(T member) {
            super();
            leader = member;
        }
    }
}

public class Son {
}

public class Daughter {
}

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Children children = new Children(new Son());
    }
    private class Children extends FamilyMember<Son>{
        public Children(Son member) {
            super(member);
        }
    }
}

列印結果

06-20 23:30:51.270 28074-28074/com.yomii.mactest I/printType_Children: Class
06-20 23:30:51.270 28074-28074/com.yomii.mactest I/printType_FamilyLeader: TypeVariableImpl

在FamilyMember類的結構中列印時,範型是被明確的,型別為class,傳遞到FamilyLeader類中,只傳遞了型別變數,無法獲取到明確的型別。

泛型擦除機制

Java為了向前相容不支援泛型的版本,會在編譯檢查過後將泛型擦除,因此,在編譯完成後,引數型別轉化為了原始型別,如果型別變數有限定,那麼原始型別就用第一個邊界的型別變數來替換。

如果Pair這樣宣告public class Pair<T extends Serializable&Comparable> ,那麼原始型別就用Serializable替換,而編譯器在必要的時要向Comparable插入強制型別轉換。為了提高效率,應該將標籤(tagging)介面(即沒有方法的介面)放在邊界限定列表的末尾。

然後在呼叫的地方,位元組碼中檢查了申明型別並自動轉換

拿一個例子來說明

public class Test {  
public static void main(String[] args) {  
ArrayList<Date> list=new ArrayList<Date>();  
list.add(new Date());  
Date myDate=list.get(0);  
}
public static void main(java.lang.String[]);  
Code:  
0: new #16 // class java/util/ArrayList  
3: dup  
4: invokespecial #18 // Method java/util/ArrayList."<init  
:()V  
7: astore_1  
8: aload_1  
9: new #19 // class java/util/Date  
12: dup  
13: invokespecial #21 // Method java/util/Date."<init>":()  

16: invokevirtual #22 // Method java/util/ArrayList.add:(L  
va/lang/Object;)Z  
19: pop  
20: aload_1  
21: iconst_0  
22: invokevirtual #26 // Method java/util/ArrayList.get:(I  
java/lang/Object;  
25: checkcast #19 // class java/util/Date  
28: astore_2  
29: return

看第22 ,它呼叫的是ArrayList.get()方法,方法返回值是Object,說明型別擦除了。然後第25,它做了一個checkcast操作,即檢查型別#19, 在在上面找#19引用的型別, 9: new #19 他是一個Date型別,即做Date型別的強轉。所以不是在get方法裡強轉的,是在你呼叫的地方強轉的。

泛型序列化的問題

在封裝網路請求庫時,將json資料格式反序列化為物件,我們的json轉換工具比如Gson、fastjson通常需要該物件的Class物件,但如果將TypeVariable傳入則會變為它擦除後的原始型別或者限定型別,轉化後的結果會丟失資訊。

針對這種情況,有以下3種解決方案。

  1. 將具體型別傳入並儲存為變數,比如Gson中的TypeToken和fastjson中的TypeReference就是儲存轉化的詳細型別資訊的包裝類。

    缺點: 需要手動傳入,json轉化的行為無法進行抽取

  2. 通過內部類+getGenericSuperclass方法獲取父類ParameterizedType物件,然後呼叫getActualTypeArguments獲取明確引數型別,規避了泛型擦除問題。 注意:必須以子類或者內部類形式呼叫getGenericSuperclass方法,因為java沒有提供獲取自身ParameterizedType的方式,因此只能這樣寫。

    缺點: 型別無法向內傳遞,只能從最外層賦值泛型的地方獲取,因為向內傳遞的是TypeVariable

  3. 參考Retrofit的Service定義機制,從方法的Method物件中獲取方法返回值型別物件,並儲存為變數。需要json轉換時將它包裝並傳入。

    算不上缺點的缺點:獲取資料的方法需要單獨定義。這種方式可以抽取Json轉換行為,單獨定義Service,只提供引數並獲取結果,也符合 view-dispatch-loader的分層原則。

以上三種解決方法可根據實際需求靈活使用,用起來最方便就好。