前言

自身是打算接着写JMM、JCStress,然后这两个是在公司空闲的时分随手写的,没有推到Github上,但写点什么能够让我取得安静的感觉,所性就从待办中拎了一篇文章,也便是这篇泛型。这篇文章来自于我朋友提出的一个问题,比方我在一个类里边声明晰两个办法,两个办法只要回来类型是int,一个是Integer,像下面这样,能否通过编译:

public class DataTypeTest {
   public int sayHello(){
        return 0;
    }
    public Integer sayHello(){
        return 1;
    }
}

我当时答复的时分是将Integer和int作为不同的类型来考虑的,我答复的是能够,可是我的朋友说,这是不行的。后边我想到了泛型擦除,但其实这跟泛型擦除却是不要紧,问题出在自动装箱和拆箱上,Java的编译器将原始类型转为包装类,包装类转为基本类型。但关于泛型,我用起来的时分,发现有些概念混乱,可是不影响开发速度,再加上平常觉得对我用途不大,所性就一向放在那里,不去考虑。最近也在一些工具类库,用到了泛型,发现自己对泛型的了解仍是有所欠缺,所以今日就重新学习泛型,顺带梳理一下自己对泛型的了解,后边发现都揉在一篇文章里边,篇幅仍是有些过大,这儿就分拆两篇。

  • 泛型的基本的运用
  • 泛型擦除、完成、向前兼容、与其他言语的比照。

泛型的含义

我在学习Java的时分,看的是Oracle出的《Java Tutorials》,地址如下:

  • docs.oracle.com/javase/tuto…

在开篇教程如是说:

In any nontrivial software project, bugs are simply a fact of life. Careful planning, programming, and testing can help reduce their pervasiveness, but somehow, somewhere, they’ll always find a way to creep into your code. This becomes especially apparent as new features are introduced and your code base grows in size and complexity.

在任何不平凡的软件工程,bug都是不可避免的现实。细心的规划、变成、测试能够帮助减少它们的普遍性,但不知何时,不知何地,它们总会找到一种办法渗入你的代码。随着新功能的引进和代码量的增加,这一点变得尤为显着。

Fortunately, some bugs are easier to detect than others. Compile-time bugs, for example, can be detected early on; you can use the compiler’s error messages to figure out what the problem is and fix it, right then and there. Runtime bugs, however, can be much more problematic; they don’t always surface immediately, and when they do, it may be at a point in the program that is far removed from the actual cause of the problem.

幸运的是,一些bug更简略发现相对其他类型的bug,例如,编译时的bug能够在前期发现; 你能够运用编译器给出的错误信息来找出问题地点,然后在当时就解决它。然而运转时的bug就要费事的多,它们并不总是当即复现出来,而且当它们复现出来的时分,或许是在程序的某个点上,与问题的实践原因相去甚远。

Generics add stability to your code by making more of your bugs detectable at compile time.

泛型能够增加你的代码的稳定性,让更多错误能够在编译时被发现。

总结一下,泛型能够增强咱们代码的稳定性,让更多错误能够在编译时就被发现。我一开始用的是JDK 8,在运用这个版别的时分,泛型已经进入Java十年了,泛型关于我来说是很理所当然的,就像鱼习惯了水相同。那Java为什么要引进泛型呢?

In a nutshell, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs. The difference is that the inputs to formal parameters are values, while the inputs to type parameters are types. Code that uses generics has many benefits over non-generic code:

简而言之,泛型能够使得在界说类、接口和办法时能够将类型作为参数。就像在办法中声明形式参数相同,类型参数供给了一种办法,让你能够在不同的输入运用相同的代码。不同之处在于,形式参数输入的是值,而类型参数的输入是类型。运用泛型的代码相关于非泛型的代码有许多长处:

  • Stronger type checks at compile time. A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.

    编译时进行更强的类型查看,编译器会对运用了泛型代码进行强类型查看,假如类型不安全,就会报错。编译时的错误会比运转时的错误,简略修正和查找。

  • Elimination of casts. The following code snippet without generics requires casting:

    消除转化,下面代码片段是没有泛型所需的转化

     List list = new ArrayList();
     list.add("hello world");
     String s = (String) list.get(0);
    
  • When re-written to use generics, the code does not require casting:

    当咱们用泛型重写, 代码就不需求类型转化

    List<String> list = new ArrayList();
    list.add("hello world");
    String s =  list.get(0);
    
  • Enabling programmers to implement generic algorithms.

    使得程序员能够通用(泛型)算法。

    By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type

    safe and easier to read.

    用泛型,程序员能够能够在不同类型的调集上工作,能够被被定制,并且类型是安全的,更简略阅览。

简略总结一下,引进泛型的好处,将类型作为参数,能够让开发者能够在不同的输入运用相同的代码,我的了解是,提升代码的可复用性,在编译时履行更强的类型查看,消除类型转化,用泛型完成通用的算法。那该怎样运用呢?

泛型怎样运用

Hello World

上面咱们说到泛型是类型参数,那咱们怎样传递给一个类,类型呢,类似于办法,咱们首先要声明形式参数,它跟在类名后边,放在<>里边,在里边咱们能够声明接纳几个类型参数,如下所示:

class name<T1, T2, ..., Tn> {}

下面是一个简略的泛型运用示例:

public class Car<T>{
    private T data;
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
    public static void main(String[] args) {
        Car<Integer> car = new Car<>();
        car.setData(1);
        Integer result = car.getData();
    }
}

在没有泛型之前,咱们的代码假如想完成这样的作用就只能用Object,在运用的时分进行强制类型转化像下面这样:

public class Car{
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static void main(String[] args) {
        Car car = new Car();
        car.setData(1);
        Integer result = (Integer) car.getData();
    }
}

但类型转化的错误一般在运转时才能被发现,假如能在编译时发现,不是更好嘛。类型参数能够是指定的任何非原始类型: 类类型、接口类型、数组类型、乃至是另一个类型变量。相同的规则也能够被应用于泛型接口。

类型命名惯例

依照惯例,类型参数明示是单个大写字母的,常见的类型参数名称如下:

  • E- 元素 广泛被Java调集结构所运用
  • K – key
  • N – 数字
  • Y – 类型
  • V – 值
  • S,U,V etc – 2nd, 3rd, 4th types

原始类型(Raw Type)

泛型类和泛型接口没有接纳类型参数的姓名,拿上面的Car类举例, 为了给传递参数类型,咱们在创立car目标的时分就会给一个正常的类型:

Car<Integer>  car = new Car<>();

假如未供给类型参数,你将创立一个Car的原始类型:

Car car = new Car();

因而,Car是泛型类Car的原始类型,然而非泛型类、接口就不是原始类型。现在咱们有一个类叫Dog, 这个Dog类不接纳类型参数, 如下代码参数:

class Dog{
    private String name;
   // get/set 构造省掉
}

Dog就不是一个原始类型,原因在于Dog没有接纳泛型参数。这儿来讲下我的了解,一般办法需求的参数,调用方没有供给,编译不通过。为什么泛型没有引进此规划呢,不传递类型参数,那不通过编译不是更好嘛。那让咱们回想一下,泛型是从JDK的哪个版别开始引进的?没错,JDK 5引进的,也便是说假如咱们引进泛型,可是又强制要求泛型类的代码,比方调集结构,在运用的时分有必要传递类型参数,那么意味着JDK 5之前的项目在晋级JDK 之后就会跑不起来,向前兼容可是Java的特色,于是Java将原来的结构进行泛型化,为了向前兼容,创造了原始类型这个概念,那有泛型的类,不传递类型参数,里边的类型是什么类型呢?当然是Object。C#引进泛型的时分,也面临了这个问题,不同于Java的兼容早年规划,加入了一套平行于一套泛型化版别的新类型。咱们完全没有或许在一篇文章里边将泛型规划评论清楚,咱们将在后续的文章评论泛型的演进。本篇咱们着重于了解Java泛型的运用。

在一些老旧的项目中(这儿的老旧指的是JDK 5.0之前的Java项目),你会看见原始类型, 由于在JDK 5.0之前,Java的许多API都没有泛型化(或通用化), 如调集结构。当运用原始类型的时分,原始类型将取得泛型之前的行为,像上面的Car目标,在调用getData()办法的时分,会回来Object类型,这么做是为了向后兼容,这儿是为了确保新代码能够和旧代码彼此操作,Java编译器答应在新的代码中运用旧版别的代码和类库,Java言语的规划者考虑到了向后兼容性。 这儿却是取得了一些新的概念,曾经我的脑际里边就没有向后兼容这个概念,只要向前兼容,那什么是向前兼容呢? 我也如同只要含糊的概念,我在写的时分,考虑了一下向前兼容这个词,向前面兼容,这个是前是指曾经,仍是前方呢? 上面说到的向后兼容指的是,后边的代码能够用之前的代码,向前兼容指的是,JDK 5之前的代码能够运转在JDK 5之后的版别上,这也便是二进制兼容性,Java所着重的兼容性,是”二进制向后兼容性”。例如说,一个在Java 1.2,1.4版别上能够正常运转的Class文件,放在Java 5、6、7、8的JRE(包括JVM与规范库)上仍然要能够正常运转。”Class文件”这儿便是Java程序的“二进制表现”。 需求特别着重的是, “二进制兼容性”并不等于”源码兼容性”(source compatibility)。既然谈到了,向前兼容、向后兼容,咱们无妨评论的再细心一点,软件是一个很大的词,某种程度上来说,操作系统也是一个软件,关于系统的兼容性来说,向后兼容能够了解为Windows 10系统能够兼容运转Windows 3.1开发的程序上,Windows 10具有向后兼容性,这个向后中的后能够了解为过去,而不是以后指未来,backward。咱们上面评论的向后兼容也便是这个语义。向前兼容呢,Forward Compatibility, Windows 3.1能兼容运转Windows 10开发的程序,这就能够说明Windows 3.1 具有向前兼容性,一般操作系统都向后兼容。所以JDK 引进泛型的时分,将曾经没有泛型的代码视为原始类型,是一种向后兼容的规划,为了Java的承诺,二进制兼容性。所以上面的用词仍是有些问题,评论问题的时分没有确认主体。

咱们在来看下软件兼容,以安卓软件为例,每年都在发大版别,可是安卓手机现在的版别便是什么样的都有,2023年最新的安卓版别是13,但我手机的安卓版别是安卓11,那我去应用商场下载软件的时分,丝毫不考虑下载的软件是否能正常运转,原因就在于基本上软件也保留了一定的向前兼容。举一个比方来说,Android11的存储权限变更导致APP无法访问根目录文件,可是为了让为安卓11开发的软件能够跑在低版别的安卓上,这就要求开发者向前兼容。

泛型办法 Generic Method

Generic methods are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter’s scope is limited to the method where it is declared. Static and non-static generic methods are allowed, as well as generic class constructors.

所谓泛型办法指的便是办法上引进参数类型的办法,这与声明泛型类似。可是类型参数的规模仅于声明的规模。答应静态和非静态办法,也答应泛型构造函数。

下面是一个泛型静态办法:

// 比方来自于: The Java™ Tutorials
public class Pair<K,V> {
    private K key;
    private V value;
    // 泛型构造函数
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
	// getters, setters etc.
}
public class CompareUtil {
    // 静态泛型办法
    public static <K,V> boolean compare(Pair<K,V> p1,Pair<K,V> p2){
        return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
    }
    // 泛型办法
  	// 回来值左边声明接纳几个类型参数
    public <T1,T2> void compare(T1 t1,T2 t2){
    }
}

运用示例如下:

 Pair<Integer, String> p1 = new Pair<>(1, "apple");
 Pair<Integer, String> p2 = new Pair<>(1, "apple");
 //  可是往往没人这么写,compare左边的实践类型参数能够通过p1,p2揣度出来。
 boolean isSame = CompareUtil.<Integer, String>compare(p1, p2);

咱们更习惯的写法如下:

boolean isSame = CompareUtil.compare(p1,p2);

上面的特性, 咱们称之为类型揣度(type,inference) ,答应开发者将一个泛型办法作为一般办法来调用,而不需求在角括号中指定一个类型。更详细的评论见下方的类型揣度。

有鸿沟的类型参数(Bounded type Parmeters)

有的时分咱们期望对泛型进行约束,比方我写了一个比较办法,可是这个比较办法想约束传递进来的实践类型参数,只能为数字类型,这就需求对传入的类型参数加以约束,像下面这样:

public <U extends Number> boolean compare(U u){
        return false;
}

U extends Number,compare接纳的参数只能是Number或Number子类的实例,extends后边跟上界。咱们传入String类型,编译就会不通过:

// IDE 中会直接报错
CompareUtil.compare("2");

说到这儿想起了《数学分析》上的确界原理: 任一有上界的非空实数集必有上确界(最小上界);相同任一有下界的非空实数集必有下确界(最大下界)

当咱们约束了泛型的上界,那咱们就能够在泛型办法里边调用上界类的办法, 像下面这样:

public static  <U extends Number> boolean compare(U u){
   u.intValue();
   return false;
}

但有的时分一个上界或许还不够,咱们期望有多个上界:

<T extends B1 & B2 & B3>

Java中虽然不支撑多承继,可是能够完成多个接口,可是假如多个上界中某个上界是类,那么这个类一定要呈现在第一个方位,如下所示:

class A {}
interface B {}
interface C {}
class D <T extends A & B & C>

假如A不在第一个方位,就会编译报错。

有界类型参数和泛型办法

有界类型参数是完成通用算法的要害,考虑下面一个办法,该办法核算数组中大于指定元素elem的元素数量, 咱们或许这么写:

public static <T> int countGreaterThen(T[] anArray,T elem){
     int count = 0;
     for (T t : anArray) {
         if (t > elem ){
            count++;
         }
     }
    return count;
}

但由于你没有约束泛型参数的规模,上面的办法报错原因也很简略,原因在于操作符号(>)只能用于基本数据类型,比方short,int,double,long,float,byte,char。目标之间不能运用(>),但这些数据类型都有包装类,包装类都完成了Comparable接口,咱们就能够这么写:

public static <T extends Comparable> int countGreaterThen(T[] anArray,T elem){
  int count = 0;
  for (T t : anArray) {
      if (t.compareTo(elem) > 0){
           count++;
       }
   }
   return count;
}

泛型,承继,子类型

我想你也知道,假如类型兼容,你能够将一个类型的目标引证指向另一个类型的目标,例如你能够将Object的引证指向Integer目标,原因在于Integer是Object类的子类:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;

在面向目标的术语中,这种被称为“is a” 联系,由于Integer是一种Object,所以答应赋值,可是Integer 也是Number的一种,所以下面的代码也是有用的:

public void someMethod(Number n){};
someMethod(new Integer(10));
someMethod(new Double(10.1)); // ok

现在咱们来看下面这个办法:

public void boxTest(Car<Number> n);

这个办法接纳哪些类型参数? 单纯看办法签名, 咱们能够看到,它接纳的是Box<Number>类型的参数,那它能接纳Box<Integer>、Box<Double>之类的参数嘛,当然是不答应的:

// 编译不会通过
CompareUtil.boxTest(new Car<Integer>());

当咱们运用泛型编程的时分,这是一个常见的误解,但这是一个需求学习的重要概念:

重学Java之泛型的基本使用

给两个详细类型A和B,比方Number和Integer,MyClass<A>和MyClass<B>之间是不要紧的,但不管A和B是否有联系,MyClass<A>和MyClass<B>都有一个共同父类叫Object。

泛型类和子类型

咱们能够完成或承继一个泛型类和接口,两个泛型类、接口之间的联系由承继和完成的语句决定。用调集结构的比方来讲便是ArrayList<E>implementsList<E>, and List<E> extends Collection<E>。所以ArrayList<String>是List<String>的一个子类型,而List<String>是Collection<String>的一个子类型。假如咱们想界说自己的List接口,它将一个泛型P的可选值和List的每个元素都关联起来。它的声明或许像下面这样:

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
}

下面参数类型是List<String>的子类型:

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>

通配符

In generic code, the question mark (?), called the wildcard, represents an unknown type. The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type (though it is better programming practice to be more specific). The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.

在泛型代码中 ,?被称为通配符,代表不知道类型。通配符能够在各种情况下运用: 作为参数、字段或局部变量的类型;有时作为回来类型(虽然更好的编程实践是更详细的)。通配符从不用作泛型办法的调用,泛型类示例创立或父类型的类型参数。 《Java Tutorial》

其实看到这块的时分,我对这个通配符是有点不了解的,我将这个符号了解为和T、V相同的泛型参数名,可是我用?去替代T的时分,发现IDEA里边呈现了错误提示。那代表?号是特殊的一类泛型符号,有专门的含义。 假如咱们想制作一个处理List<Number>的办法,咱们期望约束调集中的元素只能是Number的子类,咱们看了上面的有界类型参数就或许会很天然的写出下面的代码:

public static <T extends Number> int processNumberList(List<T> anArray) {
     // 省掉处理逻辑
     return 0;
}

但有了通配符之后,现实上咱们能够这么声明:

public static int processNumberList(List<? extends  Number> numberList ) {
    return 0;
}

现实上编译器会以为这两个办法是相同的,IDEA上会给出提示是:

‘processNumberList(List<? extends Number>)’ clashes with ‘processNumberList(List)’; both methods have same erasure

两个办法拥有相同的泛型擦除

咱们将在下文专门评论泛型擦除 , 咱们这儿仍是熟悉泛型的基本运用。

? extends Number

这种语法咱们称之为上界类型通配符(Upper Bounded Wildcards),表示的是传入的List中的元素只能是Number实例、或Number子类型的实例。在遍历中能够调用上界的办法。

下界通配符

有上界通配符对应的就有下界通配符,上界通配符约束的是传入的类型有必要是约束类型或约束类型的子类型,而下界类型则约束传入类型是约束类型或约束类型的父类型。举个比方,你只想传入的类型是List<Integer>,List<Number>, List<Object>,或任何容纳Integer类型的List 。咱们就能够如下写:

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

但值得注意的是,上界下界不能一起呈现。

无界通配符

在《Java Tutorial》中给出了两个通配符的经典运用场景:

  • If you are writing a method that can be implemented using functionality provided in the Object class.

假如你正在编写的办法能够用Object类供给的办法进行完成。

  • When the code is using methods in the generic class that don’t depend on the type parameter. For example, List.size or List.clear. In fact, Class<?> is so often used because most of the methods in Class<T> do not depend on T.

类中的代码不依赖类型参数,例如List.size、List.clear。现实上,Class<?> 经常被运用,原因在于,Class<T>的大部分办法都不依赖于类型参数T。

考虑下面的办法:

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

这个办法的意图是打印恣意List元素,可是这么写的话,你再调用的时分只能传递List<Object>类型的参数,不能传递List<Integer>类型的参数,原因也是在咱们评论过的,List<Integer> 并不是List<Object>的子类型。 这个时分咱们就能够用到 ? 通配符。

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

由于恣意类型A,List<A>都是List<?>的子类型。值得注意的是List<Object>和List<?> 并不相同,在List<Object>里边你能够刺进全部实例,可是在List<?>你就只能添加null值。

通配符和子类型化

现在咱们有两个类A和B,联系如下:

class A {}
class B extends A{}

B是A的子类,所以咱们能够写出这样的代码:

B b = new B();
A a = b;

这种写法咱们一般称之为向上转型,可是下面的代码就不会编译通过:

List<B> lb = new ArrayList<>();
List<A> la = lb;   // compile-time error

Integer是Number的子类型,List<Integer>、List<Number> 之间的联络如下:

重学Java之泛型的基本使用

虽然Integer是Number的子类型,可是List<Integer>却不是List<Number>的子类型,现实上,这两种类型并没有联系。它们的共同父类是List<?>, 为了让List<Integer>和List<Number>之间产生联系,咱们能够借助上界通配符:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

下面这张图声明晰用上界和下界通配符声明的几个List类之间的联系:

重学Java之泛型的基本使用

该怎样了解这幅联系图呢? Integer是Number的子类,所以List<? extends Integer> 是 List<? extends Number>的子类,有没有更严格的了解呢,我在了解这个联系的时分,尝试将这种父子联系笼统为区间,所以 ? extends Number <=> [oo,Number] , ? extends Integer <=> [oo,Integer], 那用到了数学的区间,咱们无妨将Number和Integer兑换为数字,越是笼统的数字越大,由于表现能力更丰富,所以咱们权且将Number了解为5,Integer了解为4。 这样的话, 如同也能了解的动:

? extends   Number <=> [oo,5]
? extends  Integer <=> [oo,4] 
? super  Integer  <=> [4,oo] 
? extends  Number <=>  [5,oo]   

这是一种了解办法,《The Java™ Tutorials》在介绍多态的时分,指出多态首先是一个生物学上的概念,那关于这种父子联系,我想到了生物的谱系:

重学Java之泛型的基本使用

咱们将Number了解为牛亚科,Integer了解为羚羊亚科,那一切羚羊亚科的下级科都是牛亚科的下级科,一切牛亚科的上机科目都是羚羊亚科的上级科目。这样了解似乎更天然。

通配符捕获和辅佐办法

在某些情况下,编译器会尝试揣度通配符的类型。例如一个List被定为List<?>,编译器履行表达式的时分,编译器会从代码中揣度出一个详细的类型。这种情况被称为通配符捕获。大部分情况下,你都不需求担心通配符捕获的问题,除非你看到包括”捕获” 这一短语的错误信息。通配符错误一般产生在编译器:

public class WildcardError {
    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

这段代码就无法通过编译。那咱们在运用泛型的时分,何时运用上界通配符,何时运用下界通配符。下面是一些通用的一些规划原则。

通配符运用指南

首先咱们将变量分为两种功能:

  • 输入变量

输入变量向代码供给数据。想象一个有两个参数的仿制办法: copy(src,desc), src参数供给了要仿制的数据,所以他是输入参数.

  • 输出变量

输出变量保存数据以便在其他当地运用,在仿制的比方中,copy(src,dest),dest接纳要仿制的数据,所以他是输出参数。

你能够运用”输入”和”输出” 原则来决定是否运用通配符以及什么类型的通配符适宜,下面的列表供给了遵循的原则:

  • An “in” variable is defined with an upper bounded wildcard, using the extends keyword.

入参用上界通配符,运用extends要害字。

  • An “out” variable is defined with a lower bounded wildcard, using the super keyword.

输出变量用下界通配符, 运用super要害字

  • In the case where the “in” variable can be accessed using methods defined in the Object class, use an unbounded wildcard.

假如需求运用入参能够运用界说在Object类中的办法时,运用无界通配符。

  • In the case where the code needs to access the variable as both an “in” and an “out” variable, do not use a wildcard.

当代码需求将变量一起用作输入和输出时,不要运用无界通配符。

泛型擦除

Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming.

泛型被引进Java, 在编译时供给了强类型查看,支撑了通用泛型编程。

To implement generics, the Java compiler applies type erasure to:

Java挑选用泛型擦除完成泛型

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.

假如泛型的类型参数是有鸿沟的,则用鸿沟来替换,假如是无界的,就用Object来替换。所以最后的字节码,仍是一般的类、办法、接口。

  • Insert type casts if necessary to preserve type safety.

必要时刺进类型转化确保类型安全

  • Generate bridge methods to preserve polymorphism in extended generic types.

生成桥接办法以保留扩展泛型类型中的多态性。

Erasure of Generic Types

首先咱们声明一个泛型类:

public class Node<T>{
  private T data;
  private Node<T> next;
  public Node(T data , Node<T> next) {
      this.data = data;
      this.next = next;
  }
  public T getData(){return data};
}

类型参数没有限界,编译器会将T替换为Object:

public class Node{
  private Object data;
  private Node next;
  public Node(Object data , Node next) {
      this.data = data;
      this.next = next;
  }
  public Object getData(){return data};
}

假如咱们对类型参数进行了约束:

public class Node<T extends Comparable<T>> {
    private T data;
    private Node<T> next;
    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }
	public T getData() { return data; }
}    

Java编译器会用类型参数的第一个限界来替换,实践擦除之后,变成了下面这样:

public class Node {
    private Comparable data;
    private Node next;
    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Comparable getData() { return data; }
    // ...
}

泛型办法擦除

现在咱们声明一个泛型办法,如下所示:

public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

泛型参数未被约束,通过Java编译器的处理,T会被替换为Object。

public static  int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

对泛型参数进行约束:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
public static <T extends Shape> void draw(T shape) { /* ... */ }

Java的编译器会用shape替换T:

public static void draw(Shape shape) { /* ... */ }

类型擦除的影响和桥接办法

有时,类型擦除会导致意料之外的事情产生,下面的比方显现了这种情况是怎样产生的:

public class Node<T> {
    public T data;
    public Node(T data) { this.data = data; }
    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}
MyNode mn = new MyNode(5);
Node n = mn; // 原始类型会给一个正告
n.setData("Hello"); // 这儿会抛出一个类型转化异常
Integer x = mn.data;

编译器在编译泛型类或泛型接口的时分,编译器或许会创立一种办法,咱们称之为桥办法。一般不需求担心桥办法,但假如它呈现在仓库中,或许你会感到困惑。类型擦除之后,Node和MyNode会变成下面这样:

public class Node {
    public Object data;
    public Node(Object data) { this.data = data; }
    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

在类型擦除之后,父类和子类的签名不一致,Node.setData(T )办法变成`Node.setData(Object) 。因而,MyNode.setData(T)办法并没有掩盖Node.setData(Object)办法, 为了保护泛型的多态,Java编译器产生了桥接办法,以便让子类型也能持续工作。依照咱们对泛型的了解,Node中的setData办法入参也应当是Integer, 假如没有桥接办法,那么MyNode中就会承继一个setData(Object data)办法。

总结一下

Java为什么要引进泛型呢,原因大致有这么几个: 增强代码复用性、让错误在编译的时分就显现出来。Java的泛型机制现实上将泛型分为两类:

  • 类型参数 type Parameter
  • 通配符 Wildcard

类型参数作用在类和接口上,通配符作用于办法参数上。为了坚持向后兼容,Java挑选了泛型擦除来完成泛型,这一完成机制在前期的我来看,这种完成并不好,我以为这种完成影响了Java的性能,我乃至以为这不能称之为真实的泛型, 比不上C#,可是在重学泛型的过程中, 现实上Java的完成也泛型,详细的能够参看下面这个链接:

www.zhihu.com/question/28…

写本篇的时分本来是想将细心评论下泛型的,比方泛型的完成,Java中泛型的未来,比照其他言语,可是后边发现越写越多,干脆就拆成两篇了。本篇基本上能够了解为《The Java™ Tutorials》中泛型这一章节的翻译,也加入了自己的了解。

参考资料

  • Java 不能完成真实泛型的原因是什么? www.zhihu.com/question/28…
  • 软件的「向前兼容」和「向后兼容」怎样区别? www.zhihu.com/question/47…
  • What Binary Compatibility Is and Is Not docs.oracle.com/javase/spec…
  • The Java™ Tutorials》 docs.oracle.com/javase/tuto…