深入浅出 Java 泛型,一文搞定

对于 java 泛型一直没太深入了解,心中的疑团也不断增多,比如 , 是什么意思,都TM什么玩意?

最近抽出时间系统学习了一下 java 的泛型知识,稍稍缓解了这种“焦虑感”。

这篇内容灵感来自于 javase 官方的文档,自己学习下来,觉得还是挺系统的,随即把原文英文做了个翻译汉化版本说上来这篇知识也算的上独家、全网首发了,翻译过程中也插入自己的小提示,认真阅读的你可能会发现。

我想了解泛型知识其实是带着疑惑去找的,网络有挺多文章,找到自己需要的却还是挺难的。

有时候网络上资源太多,鱼龙混杂,可信度还是有待考究的,学来学去,有时还是觉得官方更靠谱些。

官方原文链接:https://docs.oracle.com/javase/tutorial/java/generics/index.html

此文你能学到什么?

你可以系统的学到 java 泛型的常用知识。

tips: 建议利用好这个目录,整体把握,局部深入。

继承相关的知识点,可能会冲击你的认知哦,嘿嘿,看下去吧。

文章目录

    • 为什么要有泛型?
    • 泛型类型简介
      • 普通的 Box 类
      • 泛型版的 Box 类
      • 类型参数命名约定
      • 调用和实例化泛型类型
      • 菱形写法
      • 多类型参数
      • 参数化类型
    • 泛型方法
    • 有界类型参数
      • 有界类型参数在泛型方法中的应用
    • 泛型与继承和子类型概念(可能颠覆你的认知哦)
          • 泛型类和子类型
    • 通配符(Wildcards)
      • 无界通配符
      • 上界通配符
      • 下界通配符
      • 通配符和子类继承关系
      • 通配符使用指南

为什么要有泛型?

概括地说,泛型在定义类、接口和方法时使类型(类和接口)成为参数。与我们更为熟悉的方法声明中使用的形式参数类似,类型参数(type params)为你提供了一种可以使用不同的输入重用相同的代码的方式。不同之处在于,形式参数的输入是值,而类型参数的输入是类型。

使用泛型的代码比非泛型的代码有许多优点:

  • 在编译时进行更强的类型检查

    java 编译器对泛型代码应用强类型检查,如果代码违反了类型安全,则发出错误。修复编译时错误比修复运行时错误更容易,后者可能很难找到。

  • 消除显示的类型转换

    下面没有泛型的代码片段需要强制转换(类型转换是不是很烦人?):

    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    

    代码使用泛型重构后,可以看到,不需要强制转换了,这会给程序员带来很大的便利:

    List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0);   // no cast
    
  • 使程序员能够实现泛型算法

    通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以进行定制,并且类型安全且易于阅读。

泛型类型简介

泛型类型( generic type)是类型参数化的类或接口。下面用一个 Box 类的改进演示这个概念。

普通的 Box 类

首先示例一个对任意类型的对象(Object)进行操作的非泛型 Box 类。只有简单的set、get 方法。

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

因为它的方法接受或返回一个 Object,所以你可以自由地传入任何你想要的内容,只要它不是基本类型。所以,没有办法在编译时验证类的真正使用方式。代码的一部分可能会在Box中放置一个 Integer 并期望从中获得 Integers,而另一部分代码可能会错误地传入 String,从而导致运行时错误。

泛型版的 Box 类

A generic class is defined with the following format:

`class name { /* ... */ }`

类名之后是类型参数部分,由尖括号(< >)分隔。它指定类型参数(也称为类型变量) T1、 T2、 … 和 Tn。

要更新 Box 类以使用泛型,可以把将代码“ public class Box”更改为“ public class Box < t >”来创建泛型类型声明。这将引入类型变量 T,它可以在类中的任何地方使用。

改进后的Box 类是这样:

/**
 * Generic version of the Box class.
 * @param  the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

可以发现,所有 Object 出现的地方都被 T 所替换了。类型变量可以是你指定的任何非基基本类型: 任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量。

使用这种模式可以创建出通用接口。

类型参数命名约定

按照约定,类型参数名称为单个大写字母。这与你已经知道的变量命名约定形成了鲜明的对比,并且有充分的理由: 没有这个约定,很难区分类型变量和普通类或接口名之间的区别。

最常用的类型参数名是:

  • E - Element 元素(java集合框架广泛使用)
  • K - Key
  • N - Number 数字类型
  • T - Type
  • V - Value
  • S,U,V etc. - 第二个,第三个,第四个类等

调用和实例化泛型类型

要在代码中引用泛型 Box 类,你必须执行泛型类型调用,它用一些具体的值替换 T,比如 Integer:

Box<Integer> integerBox;

你可以认为泛型类型调用类似于普通方法调用,但是你不是将参数传递给方法,而是将类型参数(在本例中为 Integer)传递给 Box 类本身。

类型参数和类型变量术语: 许多开发人员可以混淆地使用术语“类型参数”和“类型变量”,但这两个术语并不相同。编码时,提供类型参数以创建参数化类型。所以 T 在Foo 食物 < t > 是一个类型参数,而 String 字符串在Foo f 是一个类型参数。本课在使用这些术语时遵循这个定义。

与任何其他变量声明一样,此代码实际上并不创建新的 Box 对象。它只是声明 integerBox 将保存对“ Box of Integer”的引用,这就是 Box < Integer > 的读取方式。

泛型类型的调用通常称为参数化类型(相当于方法的实参)。

实例化这个类,像往常一样使用 new 关键字,但是在类名和括号之间放置 < integer > :

Box<Integer> integerBox = new Box<Integer>();

菱形写法

在 java SE 7和更高版本中,只要编译器能够从上下文中确定或推断类型参数,就可以用一组空的类型参数(< >)替换调用泛型类的构造函数所需的类型参数。这对尖括号 < > ,通常被称为菱形。例如,你可以用下面的语句创建 Box < integer > 的实例:

Box<Integer> integerBox = new Box<>();

多类型参数

如前所述,泛型类可以有多个类型参数。例如,泛型类 OrderedPair ,它实现泛型 Pair 接口:

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

下面的语句创建 OrderedPair 类的两个实例:

Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("hello", "world");

该代码名为 new OrderedPair < String,Integer > ,它将 K 实例化为 String,将 V 实例化为 Integer。因此,OrderedPair 构造函数的参数类型分别为 String 和 Integer。由于是自动装箱,所以传递 String 和 int 到类是有效的。

参数化类型

你还可以用参数化类型(即 List < string >)替换类型参数(即 k 或 v),这种也可称为泛型的嵌套。

例如,使用 OrderedPair < k,v > 示例:

OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

泛型方法

泛型方法是引入自己的类型参数的方法。这类似于声明泛型类型,但类型参数的作用域仅限于声明它的方法。静态和非静态泛型方法以及泛型类构造函数都允许使用。

泛型方法的语法包括类型参数列表(在尖括号内) ,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。

看个例子:Util 类包含一个通用方法 compare,它比较两个 Pair 对象:

public class Util {
    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 class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

调用这个泛型方法的完整语法是:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

显式地提供了类型,如Util.所示。不过一般来说,这可以省略,编译器会推断出需要的类型:

这个特性称为类型推断,允许你将泛型方法作为普通方法调用,而不需要在尖括号之间指定类型。

有界类型参数

有时可能希望限制可用作参数化类型(形参)(parameterized type)中的类型实参(type arguments)的类型。

例如,对数字进行操作的方法可能只希望接受 Number 或其子类的实例。这就是有界类型参数的作用。

若要声明有界类型参数,则列出类型参数的名称,然后是 extends 关键字,最后是它的上界,在本例中为 Number。注意,在这个上下文中,extends 一般用来表示“ extends”(如类)或“ implements”(如接口)。

public class Box<T> {

    private T t;          

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

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
}

通过修改我们的泛型方法来包含这个有界类型参数,因为我们调用 inspect 时仍然使用一个 String,所以编译将会失败。

Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  be applied to (java.lang.String)
                        integerBox.inspect("10");
                                  ^
1 error

除了限制可用于实例化泛型类型的类型之外,有界类型参数还允许你调用边界中定义的方法,如下:

public class NaturalNumber {

	private T n;

	public NaturalNumber(T n)  { this.n = n; }

	public boolean isEven() {
		return **n.intValue()** % 2 == 0;
	}

	// ...
}

isEven 方法调用 Integer 类中定义的 intValue 方法。

有界类型参数在泛型方法中的应用

有界类型参数是泛型算法实现的关键。看下下面的方法,该方法计算数组 t []中大于指定元素 elem 的元素数。

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

该方法的实现非常简单,但是它不能编译,因为大于号操作符(>)仅适用于基本类型,如 short、 int、 double、 long、 float、 byte 和 char。不能使用 > 操作符比较对象。要解决这个问题,可以使用 Comparable 接口所限定的类型参数:

public interface Comparable<T> {
    public int compareTo(T o);
}

如下:

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

泛型与继承和子类型概念(可能颠覆你的认知哦)

正如你已经知道的,可以将一种类型的对象分配给另一种类型的对象,前提是这些类型是兼容的。例如,你可以将一个 Integer 分配给一个 Object,因为 Object 是 Integer 的超类型之一:

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

在面向对象的术语中,这被称为“ is a”关系。因为 Integer 是 Object 的一种,所以允许赋值。但是整数也是一种数字,所以下面的代码也是有效的:

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

泛型也是如此。你可以执行一个通用类型调用,传递 Number 作为它的类型参数,如果参数与 Number 兼容,那么任何后续的 add 调用都是允许的:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

一切看起来,都很熟悉,尽在掌握,是吧。

但是,咱们看看下面的方法:

public void boxTest(Box<Number> n) { /* ... */ }

它接受什么样的类型呢?通过查看它的签名,你可以看到它接受一个类型为 Box < number > 的参数。但这意味着什么呢?你是否被允许传入 Box < integer > 或 Box < double > ,正如你所期望的?答案是“否”,因为 Box < integer > 和 Box < double > 不是 Box < number > 的子类型。

有没有被惊吓到?对,没错,没有如你所愿。

在使用泛型进行编程时,这是一个常见的误解,但这是一个需要学习的重要概念。

深入浅出 Java 泛型,一文搞定_第1张图片

Box 不是Box的一个子类型 ,即使 IntegerNumber的子类型。

给定两个具体类型A和B(例如,Number和Integer),MyClass与MyClass是没有关系的,无论A和B是否相关。MyClass和MyClass的共同父级是Object,学到了没?拿出小本本记下吧。

泛型类和子类型

你可以通过继承或实现来对泛型类或接口进行子类型化。一个类或接口的类型参数与另一个类型参数之间的关系由 extends 和 implements 关系决定。

以 Collections 类为例,ArrayList < e > 实现 List < e > ,List < e > 扩展 Collection < e > 。因此 ArrayList < string > 是 List < string > 的子类型,它是 Collection < string > 的子类型。只要不改变类型参数,类型之间就保留了子类型关系。

深入浅出 Java 泛型,一文搞定_第2张图片

现在假设我们想要定义自己的列表接口 PayloadList,它将泛型类型 p 的可选值与每个元素关联起来。它的声明可能看起来像是:

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

下面的 PayloadList 参数化是 List < string > 的子类型:

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

深入浅出 Java 泛型,一文搞定_第3张图片

通配符(Wildcards)

在泛型代码中,问号(?),称为通配符,表示未知类型

通配符可以在各种情况下使用: 作为参数、字段或局部变量的类型; 有时作为返回类型(尽管更具体的编程实践更好)。通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型实参(type argument)。

下面的部分将更详细地讨论通配符,包括上限通配符、下限通配符和通配符捕获。

无界通配符

无界通配符类型是使用通配符指定的,例如列表 < ? > .这被称为未知类型的列表。

在下面两种情况下,无界通配符是一种有用的方法:

  • 如果你正在编写一个可以使用 Object 类中提供的功能来实现的方法。
  • 当代码在泛型类中使用不依赖于类型参数的方法时。例如,List.size 或 List.clear。事实上,很多使用Class的方法不依赖于T,所以Class 经常被使用(`Classis so often used because most of the methods inClassdo not depend onT`.);

看下下面的方法,printList:

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

printList 的目标是打印任何类型的列表,但是它不能实现这个目标ー它只打印一个 Object 实例列表; 它不能打印 List < integer > 、 List < string > 、 List < double > 等,因为它们不是 List < Object > 的子类型。要编写通用的 printList 方法,请使用 List < ? > :

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

注意 List < object > 和 List < ? > 是不一样的。你可以将 Object 或 Object 的任何子类型插入 List < Object > 中。但是你只能在 List < ? > 中插入 null(换句话说,通配符不适合插入数据的场景)

上界通配符

你可以使用上限通配符来放松对变量的限制。例如,假设你想要编写一个方法来处理 List < integer > 、 List < double > 和 List < number > ,那么你可以通过使用上限通配符来实现这一点。

若要声明上界通配符,请使用通配符值(’?’),然后是 extends 关键字,最后是它的上界。注意,在这个上下文中,extends 在一般意义上被用来表示“ extends”(如类)或“ implements”(如接口)。

要编写处理 Number 列表和 Number 子类型(如 Integer、 Double 和 Float)的方法,你需要指定 List < ?extends Number > 。术语 List < number > 比 List 更加有限制性,因为前者只匹配 Number 类型的列表,而后者匹配 Number 类型的列表或其任何子类。

看下下面的process方法:

public static void process(List<? extends Foo> list) { /* ... */ }

上界通配符 ,其中 Foo 是任何类型,匹配 Foo 和任何 Foo 的子类型。方法可以以 Foo 类型访问列表元素:

public static void process(List<? extends Foo> list) {
    for (Foo elem : list) {
        // ...
    }
}

在 foreach 子句中,elem 变量迭代列表中的每个元素。Foo 类中定义的任何方法现在都可以在 elem 上使用。

更进一步,看看这个方法,返回列表中所有数字的和:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

下面的代码,使用一个 Integer 对象的List,打印 sum = 6.0:

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

Double 值的 List 也可以使用相同的 sumOfList 方法:

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

下界通配符

上限通配符部分显示,上限通配符将未知类型限制为特定类型或该类型的子类型,并使用 extends 关键字表示。类似地,下限通配符将未知类型限制为特定类型或该类型的超类型。

下界通配符使用通配符(’?’),表示,后面是 super 关键字,然后是它的下限: < ?super A > 。

注意:可以为通配符指定上限,也可以指定下限,但不能同时指定两者

假设你想要编写一个将 Integer 对象放入列表的方法。为了最大限度地提高灵活性,你希望该方法能处理 List < Integer > 、 List < number > 和 List < object > ——任何可以存储 Integer 值的东西。

要编写处理 Integer 列表和 Integer 超类型(如 Integer、 Number 和 Object)的方法,需要指定 List < ?super Integer> 。术语 List < integer > 比 List 更具有限制性,因为前者只匹配 Integer 类型的列表,而后者匹配任何作为 Integer 超类型的类型的列表。

下面的代码将数字1到10添加到列表的末尾:

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

通配符和子类继承关系

正如泛型、继承和子类型中所描述的那样,泛型类或接口之间没有关联,仅仅是它们的类型之间存在关系。但是,可以使用通配符在泛型类或接口之间创建关系。

给定以下两个常规(非泛型)类:

class A { /* ... */ }
class B extends A { /* ... */ }

编写以下代码是合理的:

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

这个例子表明,常规类的继承遵循这个子类型规则: 如果 b 扩展了 a,那么 b 类就是 a 类的子类型。但是此规则不适用于泛型类型:

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

既然 Integer 是 Number 的子类型,那么 List < Integer > 和 List < Number > 之间有什么关系?

深入浅出 Java 泛型,一文搞定_第4张图片

虽然 Integer 是 Number 的子类型,但 List < Integer > 不是 List < Number > 的子类型,而且事实上,这两种类型并不相关。

List < number > 和 List < integer > 的共同父类是 List < ? > 。学到没?

为了创建这些类之间的关系,以便代码可以通过 List < integer > 的元素访问 Number 的方法,使用上限通配符:

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

因为 Integer 是 Number 的子类型,numList 是 Number 对象的列表,所以 intList (Integer 对象的列表)和 numList 之间现在存在关系。

下图显示了使用上下界通配符声明的几个 List 类之间的关系。

深入浅出 Java 泛型,一文搞定_第5张图片

看到这个惊掉下巴没?嘿嘿嘿,学无止境。

通配符使用指南

在学习使用泛型编程时,最容易混淆的一个方面是确定何时使用上界通配符以及何时使用下界通配符。本页提供了一些设计代码时应遵循的指导原则。

为了便于讨论,将关注点转移到给函数提供的两种变量,可能是有帮助的:

一个“ In”变量

“ in”变量为代码提供数据。想象一个具有两个参数的 copy 方法: copy (src,dest)。Src 参数提供要复制的数据,因此它是“ in”参数。

一个"Out" 变量

“ out”变量保存的数据可用于其他地方。在复制示例中,copy (src,dest) ,dest 参数接受数据,因此它是“ out”参数。

当然,有些变量同时用于“In”和“Out”目的ー这种情况在指南中也有说明。

在决定是否使用通配符以及适合使用哪种类型的通配符时,可以使用**“ in”和“ out”原则**。

以下清单提供了应遵循的指导原则:

  • 使用上限通配符定义“ in”变量,使用 extends 关键字。
  • 使用下限通配符定义“ out”变量,使用 super 关键字。
  • 在可以使用 Object 类中定义的方法访问“ In”变量的情况下,使用无界通配符。
  • 如果代码需要同时作为“ In”和“ out”变量访问变量,则不要使用通配符。

这些准则不适用于方法的返回类型。应该避免使用通配符作为返回类型,因为它迫使程序员使用代码来处理通配符

列表 List 可以非正式地被认为是只读的,但是这不是一个严格的保证。假设你有以下两个类:

// 自然数
class NaturalNumber {

    private int i;

    public NaturalNumber(int i) { this.i = i; }
    // ...
}
// 偶数
class EvenNumber extends NaturalNumber {

    public EvenNumber(int i) { super(i); }
    // ...
}

看下下面的代码:

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35));  // compile-time error

因为 List 是 List 的子类,你可以将 le 分配给 ln。

但是你不能使用 ln 将一个自然数加到偶数列表中,加入了就不合适了对吧?

不过下面列的操作是允许的:

  • 你可以添加 null
  • 你可以执行 clear
  • 你可以获取迭代器 iterator 和 remove
  • 你可以捕获通配符并编写从列表中读取的元素。

可以看出,上界通配符也是不适合插入数据的场景。


end,其实并没有全部翻译,而是满足目前的需求:一个是系统化,一个是常用知识点。

祝你打的愉快!

你可能感兴趣的:(JAVASE,泛型,java)