代码编织梦想

Doker官网:Doker 多克

一、介绍

JDK5.0为Java编程语言引入了几个新的扩展。其中之一就是泛型的引入。

这条线索是对泛型的介绍。您可能熟悉其他语言中的类似结构,尤其是C++模板。如果是这样的话,你会发现两者既有相似之处,也有重要的区别。如果你不熟悉其他地方的相似结构,那就更好了;你可以重新开始,而不必忘记任何误解。

泛型允许对类型进行抽象。最常见的例子是容器类型,例如“集合”层次结构中的容器类型。

二、为什么要使用泛型?

简而言之,泛型使类型(类和接口)在定义类、接口和方法时成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种使用不同输入重用相同代码的方法。不同之处在于,形式参数的输入是值,而类型参数的输入则是类型。

使用泛型的代码比非泛型代码有很多好处:

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

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
  • 使程序员能够实现通用算法。
    通过使用泛型,程序员可以实现泛型算法,这些算法适用于不同类型的集合,可以自定义,并且类型安全且更易于阅读。

三、泛型类型

泛型类型是在类型上参数化的泛型类或接口。将修改以下 Box 类以演示该概念。

1、一个简单的盒子类

首先检查一个对任何类型的对象进行操作的非泛型Box类。它只需要提供两种方法:set和get,前者向框中添加对象,后者检索对象:

public class Box {
    private Object object;

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

由于它的方法接受或返回Object,所以只要它不是基元类型之一,就可以自由地传入任何您想要的内容。在编译时,无法验证类是如何使用的。代码的一部分可能会将Integer放在框中,并期望从中取出Integers,而代码的另一部分可能错误地传入String,从而导致运行时错误。

2、盒子类的泛型版本

泛型类使用以下格式定义:

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

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

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

通过此更改,Box类变为:

/**
 * Generic version of the Box class.
 * @param <T> 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替换。类型变量可以是您指定的任何非基元类型:任何类类型、任何接口类型、任何数组类型,甚至其他类型变量。

这种相同的技术可以应用于创建通用接口。

3、类型参数命名约定

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

最常用的类型参数名称包括:

  • E - Element(被Java集合框架广泛使用)

  • K - 键

  • N - 数字

  • T - 类型

  • V - 值

  • S,U,V等- 第2、3、4类

您将看到这些名称在整个 Java SE API 和本课程的其余部分使用。

4、调用和实例化泛型类型

要从代码中引用泛型Box类,必须执行泛型类型调用,该调用将T替换为一些具体值,例如Integer:

Box<Integer> integerBox;

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

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

泛型类型的调用通常被称为参数化类型。

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

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

5、Diamond

在Java SE 7及更高版本中,只要编译器能够从上下文中确定或推断类型参数,就可以将调用泛型类的构造函数所需的类型参数替换为一组空的类型参数(<>)。这对尖括号,<>,被非正式地称为钻石。例如,您可以使用以下语句创建Box<Integer>的实例:

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

6、多种类型参数

如前所述,泛型类可以有多个类型参数。例如,泛型 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 的构造函数的参数类型分别为字符串和整数。由于自动装箱,将字符串和 int 传递给类是有效的。

如 The Diamond 中所述,由于 Java 编译器可以从声明 OrderedPair<String, Integer> 推断出 K 和 V 类型,因此可以使用菱形表示法缩短这些语句:

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

若要创建泛型接口,请遵循与创建泛型类相同的约定。

7、参数化类型

还可以将类型参数(即 K 或 V)替换为参数化类型(即 List<String>)。例如,使用 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);

类型已显式提供,如粗体所示。通常,这可以省略,编译器将推断所需的类型:

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

此功能称为类型推断,允许您将泛型方法作为普通方法调用,而无需在尖括号之间指定类型

五、有界类型参数

有时可能需要限制可以用作参数化类型中的类型参数的类型。例如,一个对数字进行操作的方法可能只想接受Number或其子类的实例。这就是有界类型参数的作用。

要声明有界类型参数,请列出类型参数的名称,后跟extends关键字,然后是其上界,在本例中为Number。请注意,在本文中,extends在一般意义上用于表示“扩展”(如在类中)或“实现”(如接口中)。

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!
    }
}

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

public class NaturalNumber<T extends Integer> {

    private T n;

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

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

    // ...
}

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

多个边界

前面的示例说明了使用具有单个边界的类型参数,但类型参数可以具有多个边界:

<T extends B1 & B2 & B3>

具有多个边界的类型变量是边界中列出的所有类型的子类型。如果其中一个边界是类,则必须首先指定它。例如:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

如果未首先指定绑定A,则会出现编译时错误:

class D <T extends B & A & C> { /* ... */ }  // compile-time error

六、泛型、继承和子类型

正如您已经知道的,只要类型兼容,就可以将一种类型的对象分配给另一种类型。例如,您可以将Integer指定给Object,因为Object是Integer的超类型之一:

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

在面向对象的术语中,这被称为“是一种”关系。由于Integer是Object的一种,因此可以进行赋值。但Integer也是一种数字,因此以下代码也是有效的:

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>的子类型。

当涉及到使用泛型编程时,这是一个常见的误解,但这是需要学习的一个重要概念。

4714e81aa71e7787babb3c9487bcc525.gif

泛型类和子类型

您可以通过扩展或实现泛型类或接口来对其进行子类型划分。一个类或接口的类型参数与另一个类的类型参数之间的关系由extends和implements子句决定。

以Collections类为例,ArrayList<E>实现了List<E〉,List<E<扩展了Collection<E>。所以ArrayList<String>是List<String<的一个子类型,它是Collection<String>的子类型。只要不改变类型参数,类型之间的子类型关系就会保留下来。

64220815961e71bc8dfffdd01b29e47a.gif

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

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

PayloadList的以下参数化是List<String>的子类型:

  • PayloadList<String,String>

  • PayloadList<String,Integer>

  • PayloadList<String,Exception>

e6758ca2664a2c73aa8d36a9acdf732f.gif

七、类型推断

类型推断是Java编译器查看每个方法调用和相应声明的能力,以确定使调用适用的类型参数。推理算法确定参数的类型,如果可用,还确定分配或返回结果的类型。最后,推理算法试图找到适用于所有参数的最具体的类型。

为了说明最后一点,在以下示例中,推断确定传递给pick方法的第二个参数的类型为Serializable:

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

类型推断和泛型方法

泛型方法向您介绍了类型推理,它使您能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。考虑以下示例BoxDemo,它需要Box类:

public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

以下是此示例的输出:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

泛型方法addBox定义了一个名为U的类型参数。通常,Java编译器可以推断泛型方法调用的类型参数。因此,在大多数情况下,您不必指定它们。例如,要调用泛型方法addBox,可以指定具有类型见证的类型参数,如下所示:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

或者,如果省略类型见证,Java编译器会自动推断(从方法的参数)类型参数是Integer:

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

泛型类的类型推断和实例化

只要编译器可以从上下文中推断类型参数,就可以用一组空的类型parameters (<>)替换调用泛型类的构造函数所需的类型参数。这对尖括号被非正式地称为diamond。

例如,考虑以下变量声明:

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

您可以将构造函数的参数化类型替换为一组空的类型parameters(<>):

Map<String, List<String>> myMap = new HashMap<>();

请注意,要在泛型类实例化期间利用类型推断,必须使用菱形。在以下示例中,编译器生成未检查的转换警告,因为HashMap()构造函数引用的是HashMap原始类型,而不是Map<String,List<String>>类型:

Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

类型推理与泛型类和非泛型类的泛型构造函数

请注意,构造函数在泛型类和非泛型类中都可以是泛型的(换句话说,声明它们自己的形式类型参数)。考虑以下示例:

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

考虑MyClass类的以下实例化:

new MyClass<Integer>("")

此语句创建参数化类型MyClass<Integer>的实例;该语句为泛型类MyClass<X>的形式类型参数X显式指定Integer类型。请注意,该泛型类的构造函数包含一个形式类型参数T。编译器为该泛型类构造函数的形式类型参数T推断类型String(因为该构造函数的实际参数是String对象)。

Java SE 7之前版本的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。然而,如果使用菱形(<>),Java SE 7及更高版本中的编译器可以推断出正在实例化的泛型类的实际类型参数。考虑以下示例:

MyClass<Integer> myObject = new MyClass<>("");

在本例中,编译器推断泛型类MyClass<X>的形式类型参数X的类型Integer。它为这个泛型类的构造函数的形式类型参数T推断类型String。

目标类型

Java编译器利用目标类型来推断泛型方法调用的类型参数。表达式的目标类型是Java编译器所期望的数据类型,具体取决于表达式的出现位置。考虑Collections.emptyList方法,该方法声明如下:

static <T> List<T> emptyList();

请考虑以下赋值语句:

List<String> listOne = Collections.emptyList();

此语句需要List<String>的一个实例;此数据类型是目标类型。由于方法emptyList返回List<T>类型的值,因此编译器推断类型参数T必须是值String。这在Java SE 7和8中都有效。或者,您可以使用类型见证并指定T的值,如下所示:

List<String> listOne = Collections.<String>emptyList();

然而,在这种情况下,这是不必要的。不过,在其他情况下,这是必要的。考虑以下方法:

void processStringList(List<String> stringList) {
    // process stringList
}

假设您想用一个空列表调用方法processStringList。在Java SE 7中,以下语句不会编译:

processStringList(Collections.emptyList());

Java SE 7编译器生成类似于以下内容的错误消息:

List<Object> cannot be converted to List<String>

编译器需要类型参数T的值,因此它以值Object开头。因此,Collections.emptyList的调用返回类型为List<Object>的值,该值与方法processStringList不兼容。因此,在Java SE 7中,必须指定类型参数的值,如下所示:

processStringList(Collections.<String>emptyList());

在Java SE 8中不再需要这样做。目标类型的概念已经扩展到包括方法参数,例如方法processStringList的参数。在这种情况下,processStringList需要List<String>类型的参数。方法Collections.emptyList返回值List<T>,因此使用目标类型List<String>,编译器推断类型参数T的值为String。因此,在Java SE 8中,编译以下语句:

processStringList(Collections.emptyList());

八、通配符

在泛型代码中,被称为通配符的问号(?)表示未知类型。通配符可以用于各种情况:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体一些是更好的编程实践)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。

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

1、上限通配符

可以使用上限通配符来放宽对变量的限制。例如,假设您想编写一个在List<Integer>、List<Double>和List<Number>上工作的方法;您可以通过使用上限通配符来实现这一点。

要声明一个上界通配符,请使用character (“?”),后跟extends关键字,再后跟其上界。请注意,在本文中,extends在一般意义上用于表示“扩展”(如在类中)或“实现”(如接口中)。

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

请考虑以下处理方法:

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

上限通配符<?extends Foo>,其中Foo是任何类型,与Foo和Foo的任何子类型匹配。进程方法可以访问列表元素,类型为 Foo:

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

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

sumOfList 方法返回列表中数字的总和:

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

以下代码使用 Integer 对象列表打印 sum = 6.0:

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

双精度值列表可以使用相同的 sumOfList 方法。以下代码打印 sum = 7.0:

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

九、类型擦除

泛型被引入Java语言,以在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:

将泛型类型中的所有类型参数替换为其边界,如果类型参数是无边界的,则替换为Object。因此,生成的字节码只包含普通的类、接口和方法。

如有必要,请插入类型强制转换以保持类型安全。

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

类型擦除确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。

官网:泛型

大家好,我是Doker品牌的Sinbad,欢迎点赞和评论,您的鼓励是我们持续更新的动力!欢迎加微信进入技术群聊!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/leesinbad/article/details/129670099