Java 泛型简介

982 阅读7分钟
原文链接: www.ripjava.com

1.简介

Java 泛型是在JDK 5.0中引入的,旨在减少错误并在类型上添加额外的抽象层。

本文是Java中泛型的快速介绍。来帮助大家是使用泛型提高代码质量。

2. 为什么需要泛型

让我们想象一下,在Java中创建一个存储Integer的列表的场景:

List list = new LinkedList();
list.add(new Integer(1)); 
Integer i = list.iterator().next();

上面的代码,编译器提示我们,它不知道返回什么数据类型。需要手动的显式转换:

Integer i = (Integer) list.iterator.next();

但是,我们不能保证从列表中取出的元素都是整数。我们刚才定义的列表可以包含任何对象。

我们需要通过检查上下文来检索列表。在查看类型时,它只能保证它是一个Object,因此需要显式转换以确保类型是安全的。

我们知道这个列表中的数据类型是Integer。但是也必须要显式转换。显示转换可能会让我们的代码变得混乱。

如果显式转换时出错,还可能导致与类型相关的运行时错误。

如果我们可以表达使用的类型并且编译器可以确保这种类型的正确性,这样就不要显式转换了。

这就是泛型背后的核心理念。

让我们将前一个代码段的第一行修改为:

List<Integer> list = new LinkedList<>();

通过添加包含类型的菱形运算符<>,我们将此列表能够保持的类型仅限于Integer类型,即我们指定将保留在列表中的类型。编译器可以在编译时强制执行该类型。

在小程序中,这似乎是一个微不足道的补充,但是,在较大的程序中,这可以增加显着的稳健性并使代码更容易阅读。

3. 泛型方法

泛型方法是使用单个方法声明编写的方法,但可以使用不同类型的参数调用。

编译器将确保使用哪种类型的正确性。

下面是泛型方法的一些属性:

  • 泛型方法在方法声明的返回类型之前有一个类型参数(包含该类型的菱形运算符)
  • 类型参数可以是有界的(边界将在本文后面解释)
  • 通用方法可以在方法签名中使用逗号分隔不同的类型参数
  • 泛型方法的方法体就像普通方法一样

定义将数组转换为列表的通用方法:

public <T> List<T> fromArrayToList(T[] a) {   
    return Arrays.stream(a).collect(Collectors.toList());
}

在方法签名中意味着该方法将被处理的通用类型为Ť。即使这个方法返回void,我们也需要这么做。

泛型方法可以处理多个泛型类型,在这种情况下,必须将所有泛型类型添加到方法签名中。

例如,如果我们要修改上面的方法来处理类型T和类型G

public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

我们传递的函数是将元素为类型T的数组转换为元素为类型G的列表*。*

例如,将Integer转换为其String

    @Test
    public void Test_fromArrayToList() {
        Integer[] intArray = {1, 2, 3, 4, 5};
        List<String> stringList
                = Generics.fromArrayToList(intArray, Object::toString);

        assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
    }

建议使用大写字母来表示泛型类型,并选择更具描述性的字母来表示形式类型,

例如在Java集合中,T用于类型,K表示键,V表示值。

3.1 有界泛型

类型参数可以是有界的。有界意味着“ 受限制 ”,我们可以限制方法可以接受的类型。

例如,我们可以指定一个方法接受一个类型及其所有子类(上限)或一个类型的所有超类(下限)。

要声明一个上限类型,我们在类型之后使用关键字extends,然后使用我们想要使用的上限。

比如:

public <T extends Number> List<T> fromArrayToList(T[] a) {
    ...
}

关键字extends在此用于表示类型T的上限。

  • 在类的情况下扩展上限
  • 在接口的情况下实现上限。

3.2 多个边界

类型也可以有多个上限,如下所示:

<T extends Number & Comparable>

如果由T扩展的类型之一是类(Number),则必须将其放在边界列表中的第一个。

否则,将导致编译时错误。

4. 通配符和泛型一起使用

通配符由Java中的问号代表 。它们用于表示未知类型。

通配符在使用泛型时特别有用,可以用作参数类型,但首先需要考虑一个重要的注意事项。

Object是所有Java类的超类型,但是,Object的集合不是任何集合的超类型。

例如,一个List<Object>不是 List<String>的超类型,讲一个类型为List<Object>的变量赋值到类型为List<String>的变量将会导致编译错误。

比如

    @Test
    public void Test_Generic() {
        List<Object> b = new ArrayList<>();
        List<String> c = new ArrayList<>();
        c = b;
    }

编译错误

Error:(26, 13) java: 不兼容的类型: java.util.List<java.lang.Object>无法转换为java.util.List<java.lang.String>

相同规则适用于任何类型及其子类型的集合。

想一下这个例子:

    public static void paintAllBuildings(List<Building> buildings) {
        buildings.forEach(Building::paint);
    }

如果我们想要一个Building的子类型,例如House,我们就不能将这个方法与House列表一起使用,即使HouseBuilding的子类型。

如果我们需要将此方法与类型Building及其所有子类型一起使用,那么有界通配符可以实现:

    public static void paintAllBuildings(List<? extends Building> buildings) {
        buildings.forEach(Building::paint);
    }

现在,此方法将适用于类型Building及其所有子类型。这是上限带通配符,其中类型Building是上限。

通配符也可以使用下限指定,其中未知类型必须是指定类型的超类型。

可以使用super关键字后跟特定类型指定下限,例如<?super T>表示未知类型,它是T的超类(= T及其所有父类)。

5. 类型擦除

将泛型添加到Java以确保类型安全并确保泛型不会在运行时引起开销,编译器在编译时对泛型的处理称为类型擦除

如果类型参数是无界的,则类型擦除将删除所有类型参数并将其替换为其边界或替换为Object

因此,编译后的字节码只包含普通的类,接口和方法,从而确保不会产生新的类型。

在编译时也将适当的强制转换应用于Object类型。

下面是一个类型擦除的例子:

    public <T> List<T> genericMethod(List<T> list) {
        return list.stream().collect(Collectors.toList());
    }

对于类型擦除,无界类型T会被Object替换:

// 类似是这样
public List<Object> withErasure(List<Object> list) {
    return list.stream().collect(Collectors.toList());
}
// 实际是这样
public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}

如果类型有界,则类型将在编译时被替换:

public <T extends Building> void genericMethod(T t) {
    ...
}

编译后会:

public void genericMethod(Building t) {
    ...
}

6. 泛型和基本类型

Java中泛型的限制是类型参数不能是基本类型

例如,以下内容无法编译:

List<int> list = new ArrayList<>();
list.add(17);

要理解为什么原始数据类型不起作用,让我们记住泛型是一个编译时特性,类型参数会被擦除,所有泛型类型都被实现为Object类型。

比如

List<Integer> list = new ArrayList<>();
list.add(17);

而add方法的签名是:

boolean add(E e);

将会编译为:

boolean add(Object e);

所以,类型参数必须可转换为Object由于原始类型不是扩展Object,因此我们不能将它们用作类型参数。

但是,Java提供了基本类型的包装类型,以及自动装箱和拆箱来使用它们:

Integer a = 17;
int b = a;

所以,如果我们想创建一个可以保存整数的列表,可以这样:

List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);

将会编译为:

List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();

不过未来版本的Java可能允许基本数据类型用于泛型。相关的信息在JEP218中查看。

7.结论

泛型是Java语言的强大补充,因为它使我们的工作更容易,更不容易出错。

泛型在编译时强制检查类型的正确性,最重要的是,支持实现通用算法,而不会给我们的应用程序带来任何额外开销。

最后,往常一样,代码可以在Github上找到。