死磕Java泛型(一篇就够)

Java泛型,算是一个比较容易产生误解的知识点,由于Java的泛型基于擦除实现,在使用Java泛型时,每每会受到泛型实现机制的限制,若是不能深刻全面的掌握泛型知识,就不能较好的驾驭使用泛型,同时在阅读开源项目时也会到处碰壁,这一篇就带你们全面深刻的死磕Java泛型。java

泛型擦除初探

相信泛型你们都使用过,因此一些基础的知识点就不废话了,以避免显得啰嗦。 先看下面的一小段代码面试

public class FruitKata {
    class Fruit {}
    class Apple extends generic.Fruit {}
    
    public void eat(List fruitList) {}

    public void eat(List<Fruit> fruitList) { }   // error, both methods has the same erasure
}
复制代码

咱们在FruitKata类中定义了二个eat的方法,参数分别是List和List<> 类型,这时候编译器报错了,而且很智能的给出了“ both methods has the same erasure” 这个错误提示。显然,编译器在抱怨,这二个方法具备一样的签名,嗯~~,这就是泛型擦除存在的一个证据,要进一步验证也很简单。咱们经过ByteCode Outline这个插件,能够很方便的查看类被编译后的字节码,这里咱们只贴出eat方法的字节码。json

// access flags 0x1
  // signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V
  // declaration: void eat(java.util.List<generic.FruitKata$Fruit>)
  public eat(Ljava/util/List;)V
复制代码

能够看到参数确实已经被擦除为List类型,这里要明确一点是,这里擦除的只是方法内部的泛型信息,而泛型的元信息仍是保存在类的class字节码文件中,相信细心的同窗已经发现了上面我特地将方法的注释一并贴了出来api

// signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V
复制代码

这个signature字段大有玄机,后面会详细说明。 这里只是以泛型方法来作个说明,其实泛型类,泛型返回值都是相似的,兄弟们能够本身动手试试看。安全

为何用擦除来实现泛型

要回答这个问题,须要知道泛型的历史,Java的泛型是在Jdk 1.5 引入的,在此以前Jdk中的容器类等都是用Object来保证框架的灵活性,而后在读取时强转。可是这样作有个很大的问题,那就是类型不安全,编译器不能帮咱们提早发现类型转换错误,会将这个风险带到运行时。 引入泛型,也就是为解决类型不安全的问题,可是因为当时java已经被普遍使用,保证版本的向前兼容是必须的,因此为了兼容老版本jdk,泛型的设计者选择了基于擦除的实现。 因为Java的泛型擦除,在运行时,只有一个List类,那么相对于C#的基于膨胀的泛型实现,Java类的数量相对较少,方法区占用的内存就会小一点,也算是一个额外的小优势吧。bash

泛型擦除带来的问题

因为泛型擦除,下面这些代码都不能编译经过app

T t = new T();
T[] arr = new T[10];
List<T> list = new ArrayList<T>();
T instanceof Object
复制代码

通配符

做为泛型擦除的补偿,Java引入了通配符框架

List<? extends Fruit> fruitList;
List<? super Apple> appleList;
复制代码

这二个通配符不少同窗都存在误解。dom

? extends

?extends Fruit 表示Fruit是这个传入的泛型的基类(Fruit是泛型的上界),仍是以上面的Fruit和Apple为例,看下面这段代码ide

List<? extends Fruit> fruitList = new ArrayList<>();
fruitList.add(new Fruit());  //error
复制代码

按照咱们上面对? extends的理解,fruitList应该是能够添加一个Fruit的,可是编译器却给咱们报错了。我第一次看到这里时也感受不太好理解,咱们来看个例子就能理解了。

List<? extends Fruit>  fruitList = new ArrayList<>();
List<Apple> appleList = new ArrayList<>();
fruitList = appleList;
fruitList.add(new Fruit());   //error
复制代码

若是fruitList容许添加Fruit,咱们就将Fruit添加到了AppleList中了,这确定是不能接受的。

? 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,

  1. 若是须要一个只读的泛型集合,使用?extends T
  2. 若是须要一个只写的泛型集合,使用?super T

我本身是这样来理解通配符的

  1. 由于? extends T给外界的承诺语义是,这个集合内的元素都是T的子类型,可是究竟是哪一个子类型不知道,因此添加哪一个子类型,编译器都认为是危险的,因此直接禁止添加。
  2. 由于? super T 给外界的承诺语义是,这个集合内的元素的下界是T,因此向集合中添加T以及T的子类型是安全的,不会破坏这个承诺语义。
  3. List, List 都是List<? super Apple>的子类型。 List 是List<? extends Apple>的子类型。

关于泛型的使用,Jdk中有不少经典的应用范例,好比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());
            }
        }
    }
复制代码

泛型擦除了,咱们还能拿到泛型信息吗

前面咱们提到过class字节码中会有个signature字段来保存泛型信息。咱们新建一个泛型方法

public <T extends Apple> T plant(T fruit) {
        return fruit;
    }
复制代码

查看class文件的二进制信息,发现里面确实有Signature字段信息。

Signature%<T:Lgeneric/FruitKata$Apple;>(TT;)TT;
复制代码

既然泛型信息仍是在class文件中,那咱们有没有办法在运行时拿到呢? 办法确定是有的。 来看一个例子

Class clazz = HashMap<String, Apple>(){}.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> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
    Object object = fromJson(json, (Type) classOfT);
    return Primitives.wrap(classOfT).cast(object);
  }
复制代码

最终会执行到

TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
  TypeAdapter<T> typeAdapter = getAdapter(typeToken);
  T object = typeAdapter.read(reader);
复制代码

经过咱们传入的Class类型构造TypeToken,而后经过TypeAdapter将json字符串转化为对象T,中间的细节这里就不继续深刻了。

泛型在retrofit中的使用

咱们在使用retrofit时,通常都会定义一个或多个ApiService接口类

@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
复制代码

接口方法的返回值都使用了泛型,因此注定在编译期是要被擦除的,那retrofit是如何获得原始泛型信息的呢。其实有上面的泛型知识以及Gson的使用说明,相信你们以及有答案了。 retrofit框架自己设计的很优雅,细节这里咱们不深刻展开,这里咱们只关心泛型数据转换为返回值的过程。 咱们须要定义以下几个类

// ApiService.class
public interface ApiService {
    Observable<List<Apple>> 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<Apple> 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<List<Apple>> 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<V extends IView, P extends IPresenter<V>> extends AppCompatActivity {
   protected P mPresenter;
  //....
}
public class MainActivity extends BaseActivity<MainView, MainPresenter> 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泛型时都可以云淡风轻,在使用泛型编写代码时也可以信手拈来。

相关文章
相关标签/搜索