关于java:重学Java之泛型的基本使用

4次阅读

共计 17161 个字符,预计需要花费 43 分钟才能阅读完成。

前言

自身是打算接着写 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》,地址如下:

  • https://docs.oracle.com/javase/tutorial/java/generics/index.html

在开篇教程如是说:

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>());

当咱们应用泛型编程的时候,这是一个常见的误会,但这是一个须要学习的重要概念:

给两个具体类型 A 和 B,比方 Number 和 Integer,MyClass\<A> 和 MyClass\<B> 之间是没关系的,但不论 A 和 B 是否有关系,MyClass\<A> 和 MyClass\<B> 都有一个独特父类叫 Object。

泛型类和子类型

咱们能够实现或继承一个泛型类和接口,两个泛型类、接口之间的关系由继承和实现的语句决定。用汇合框架的例子来讲就是 ArrayList\<E> implements List\<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<T>)’; 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> 之间的分割如下:

只管 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 类之间的关系:

该怎么了解这幅关系图呢?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》在介绍多态的时候,指出多态首先是一个生物学上的概念,那对于这种父子关系,我想到了生物的谱系:

咱们将 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 的实现也泛型,具体的能够参看上面这个链接:

https://www.zhihu.com/question/28665443/answer/1873474818

写本篇的时候原本是想将认真探讨下泛型的,比方泛型的实现,Java 中泛型的将来,比照其余语言,然而前面发现越写越多,索性就拆成两篇了。本篇基本上能够了解为《The Java™ Tutorials》中泛型这一章节的翻译,也退出了本人的了解。

参考资料

  • Java 不能实现真正泛型的起因是什么?https://www.zhihu.com/question/28665443/answer/1873474818
  • 软件的「向前兼容」和「向后兼容」如何辨别?https://www.zhihu.com/question/47239021
  • What Binary Compatibility Is and Is Not https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html…
  • The Java™ Tutorials》https://docs.oracle.com/javase/tutorial/java/generics/methods…
正文完
 0