Java泛型的原理

1-Java泛型的介绍

泛型是Java 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法,Java泛型被引入的好处是安全简单。

在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

泛型在使用中还有一些规则和限制:

  1. 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
  2. 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
  3. 泛型的类型参数可以有多个。
  4. 泛型的参数类型可以使用extends语句,例如。习惯上成为“有界类型”。
  5. 泛型的参数类型还可以是通配符类型。

泛型其实对于Jvm来说都是Object类型的,那咱们直接将类型定义成Object不就是的了,这种做法是可以,但是在拿到Object类型值之后,自己还得强转,因此泛型减少了代码的强转工作,而将这些工作交给了虚拟机。

2-Java泛型的使用

2.1-为什么需要泛型?

在泛型出现以前,类和方法只能接受具体的类型。假设我们自己实现一个简单的ArrayList,用来持有类A的实例,它可能是这样子的:

class A {}

class ArrayListA {
private int size = 0;

private A[] array = new A[100];

public void add(A a) {
array[size++] = a;
}

public A get(int index) {
return array[index];
}
}

现在如果需要一个ArrayList来持有类B的实例,由于没有泛型,那只能把同样的代码再写一遍,并将其中的A全部换成B。难道我们要为每一个类都写一个ArrayList吗,显然是不可能的。在泛型出现以前,jdk的ArrayList使用的方法是用Object作为类型参数,这样使用者需要自己做转型,就像下面这样:

public class GenericLearn {

public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
arrayList.add("aaa");
String str = (String) arrayList.get(0);
}

}

这样的向下转型,既不方便,也不安全。有没有一种办法,能让类型作为一种可选参数,使得一套代码能复用于多个类,且不需要自己做转型等动作,这便是泛型要解决的问题。

2.2-泛型的使用

1. 泛型类

class Holder<T> {

private T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}

public void print() {
System.out.println(t);
}
}

上面是一个简单的泛型类,用一个<>来指明参数化类型。现在我们在使用Holder类时就可以指明类型,一旦类型被确定,它就不能用于其他类:

Holder<String> holder = new Holder<>();
// 编译错误
holder.setT(1);

比如实际项目中,我们经常会遇到服务端返回的接口中都有errMsgstatus等公共返回信息,而变动的数据结构是data信息,因此我们可以抽取公共的BaseBean

public class BaseBean<T> {
public String errMsg;
public T data;
public int status;
}

2. 接口和抽象类上的泛型

interface Handler<T> {

void handle(T t);
}

class StringHandler implements Handler<String> {

@Override
public void handle(String s) {
// doNothing
}
}

泛型用于接口和用于类的方式类似。

//抽象类泛型
public abstract class BaseAdapter<T> {
List<T> DATAS;}//接口泛型public interface Factory<T> {
T create();
}
//方法泛型
public static <T> T getData() {
return null;
}

3. 泛型方法

class BatchUtil {

public static <T> void batchExec(List<T> list, int batchSize, Consumer<List<T>> action) {
for (int i = 0; i < list.size(); i += batchSize) {
int endIndex = i + batchSize > list.size() ? list.size() : i + batchSize;
List<T> tempList = list.subList(i, endIndex);
action.accept(tempList);
}
}
}

以上方法定义了一个批量操作的工具类,你可以像这样使用它:

public class GenericLearn {

public static void main(String[] args) {
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
BatchUtil.batchExec(list, 3, System.out::println);
}
}
[1, 2, 3]
[4, 5, 6]
[7]

4. 多元泛型

public interface Base<K, V> {
void setKey(K k);

V getValue();
}

5. 泛型边界

利用extends关键字,可以为泛型参数限定上边界。

public class GenericLearn {

public static void main(String[] args) {
Holder<A> holder1 = new Holder<>();
Holder<B> holder2 = new Holder<>();
// 编译错误
Holder<String> holder3 = new Holder<>();
}
}

class Holder<T extends A> {

private T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}
}

class A {}

class B extends A{}

限定了边界后,泛型的类型就只能是指定类型及其子类。

6. 通配符

<?>通配符<T>区别是在你不知道泛型类型的时候,可以用通配符来定义。

上面介绍的泛型类、接口、方法等都是如何定义泛型,至于使用泛型,最通常的就是指定泛型参数,如List,指定了泛型参数为String。但是有时候我们会碰到这样的情况:

public class GenericLearn {

public static void main(String[] args) {
List<A> listA = new ArrayList<>();
List<B> listB = new ArrayList<>();
print(listA);
// 编译错误
print(listB);
}

public static void print(List<A> list) {
for (A a : list) {
System.out.println(a);
}
}
}

class A {}

class B extends A{}

方法接受参数List,却无法将List作为入参,这是因为虽然B可以向上转型为A,List却无法向上转型为List,为了解决这一问题,引入了通配符。

public static void print(List<? extends A> list) {
for (A a : list) {
System.out.println(a);
}
}

将方法改写为这样后,可以编译运行。但是由此也会带来一个副作用:

public static void print(List<? extends A> list) {
for (A a : list) {
System.out.println(a);
}
// 编译错误
list.add(new A());
list.add(new B());
list.add(new Object());
}

用了通配符后,无法对List进行add操作,这是因为List的add方法,其参数是泛型类,而通配符仅仅指定了泛型类的上界,因此任何以泛型类为入参的方法都无法使用。这是可以理解的,因为假如我们传入的是List,那就不能往其中加入A的实例。

这是用通配符指定上界的情况,通配符也可以指定下界,还是以List为例:

public static void main(String[] args) {
List<? super B> list = new ArrayList<>();
list.add(new B());
Object obj = list.get(0);
// 编译错误
list.add(new A());
list.add(new Object());
B b = list.get(0);
A a = list.get(0);
}

用通配符指定下界后,可以执行add操作,但是只可以add类B的实例,因为我们不知道List持有的具体类型是什么,只知道它是B或其超类,在这样的条件下,只有往其中加入类B的实例是安全的,并且从其中拿到的对象只能当作Object来使用。

通配符也可以不指定边界,称为无界通配符,以List为例,对List<?>,不能执行add操作,从中取出的对象只能当作Object来使用。

对于通配符的使用有一个PECS原则(Producer Extends, Consumer Super),即如果将泛型类作为生产者使用,例如使用List的get方法,则用上界通配符;如果将泛型类作为消费者使用,例如使用List的add方法,则用下界通配符。

3-泛型的实现原理

很多人把Java的泛型称为伪泛型,因为Java的泛型只是编译期的泛型,一旦编译成字节码,泛型就被擦除了,即在Java中使用泛型,我们无法在运行期知道泛型的类型。

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

所有的泛型在jvm中执行的时候,都是以Object对象存在的,加泛型只是为了一种代码的规范,避免了开发过程中再次强转。

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。