1. 引言
jdk5.0 中引入了 Java 泛型,目标是缩小谬误,并在类型上增加额定的形象层。
本文将简要介绍 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,因而须要显式转换来确保类型是平安的。
这种转换可能会令人感到聒噪,咱们明明晓得这个列表中的数据类型是 整数 。转换的话,也把咱们的代码搞得乌七八糟。如果程序员在显式转换中出错,则可能会导致抛出与 类型相干的运行时谬误。
如果程序员可能表白他们应用特定类型的用意,并且编译器可能确保这种类型的正确性,那么这将更加容易。
这就是泛型背地的核心思想。
咱们将后面代码段的第一行批改为:
List<Integer> list = new LinkedList<>();
通过增加蕴含类型的菱形运算符 <>,咱们将此列表的特化范畴放大到 Integer 类型,即指定将保留在列表中的类型。编译器能够在编译时强制执行该类型。
在较小的程序中,这看起来像是一个微不足道的增加。然而在较大的程序中,这能够减少显著的健壮性并使程序更易于浏览。
3. 泛型办法
泛型办法是用单个办法申明编写的办法,能够用不同类型的参数调用。编译器将确保所用类型的正确性。以下是泛型办法的一些属性:
- 泛型办法在办法申明的返回类型之前有一个类型参数(包裹类型的菱形运算符)
- 类型参数能够有界(边界将在本文前面解释)
- 泛型办法能够具备不同的类型参数,这些参数在办法签名中用逗号分隔
- 泛型办法的办法体与一般办法一样
定义将数组转换为列表的泛型办法的示例:
public <T> List<T> fromArrayToList(T[] a) {return Arrays.stream(a).collect(Collectors.toList());
}
在后面的示例中,办法申明中的 <T>
示意该办法将解决泛型类型 T。即便办法返回的是 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 givenArrayOfIntegers_thanListOfStringReturnedOK() {Integer[] intArray = {1, 2, 3, 4, 5};
List<String> stringList
= Generics.fromArrayToList(intArray, Object::toString);
assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}
Oracle 倡议应用大写字母示意泛型类型,并抉择更具描述性的字母来示意模式类型,例如在 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>
类型的变量将导致编译器谬误。
这是为了避免在将异构类型增加到同一汇合时可能产生的抵触。
雷同的规定实用于类型及其子类型的任何汇合。看看这个例子:
public static void paintAllBuildings(List<Building> buildings) {buildings.forEach(Building::paint);
}
如果咱们构想一个子类型 Building,实例House,那么咱们不能将此办法与House 列表一起应用,即便 House 是Building的子类型。如果须要将此办法与类型构建及其所有子类型一起应用,则有界通配符能够实现以下性能:
public static void paintAllBuildings(List<? extends Building> buildings) {...}
当初,这个办法能够解决 Building 类型及其所有子类型。这称为上界通配符,其中类型 Building 是上界。
通配符也能够应用上限指定,其中未知类型必须是指定类型的超类型。能够应用 super 关键字后跟特定类型来指定上限,例如,<? super T>
示意未知类型,它是 T(= T 及其所有父类)的超类。
5. 类型擦除
泛型被增加到 Java 中以确保类型平安,并确保泛型不会在运行时造成开销,编译器在编译时对泛型利用一个名为 type erasure 的过程。
类型擦除删除所有类型参数,并将其替换为它们的边界,如果类型参数是无界的,则替换为 Object。因而,编译后的字节码只蕴含一般的类、接口和办法,从而确保不会生成新的类型。在编译时对Object 类型也利用了正确的强制转换。
以下是类型擦除的一个示例:
public <T> List<T> genericMethod(List<T> list) {return list.stream().collect(Collectors.toList());
}
应用类型擦除,无界类型 T 将替换为Object,如下所示:
// for illustration
public List<Object> withErasure(List<Object> list) {return list.stream().collect(Collectors.toList());
}
// which in practice results in
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 类。
举一个例子,让咱们看看列表的 add 办法:
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 的将来版本可能容许泛型应用原始数据类型。Valhalla 工程旨在改良解决泛型的形式。其思维是实现 JEP 218 中形容的泛型专门化.
7. 总结
Java 泛型是对 Java 语言的一个弱小的补充,因为它使程序员的工作更容易,也更不容易出错。泛型在编译时强制执行类型正确性,并且,最重要的是,可能实现泛型算法,而不会给咱们的应用程序带来任何额定的开销。
如果你感觉文章还不错,记得关注公众号:锅外的大佬
刘一手的博客