重温Java泛型,带你更深入地理解它,更好的使用它!

 1. 引言

创新互联公司服务项目包括仙居网站建设、仙居网站制作、仙居网页制作以及仙居网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,仙居网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到仙居省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!

jdk5.0中引入了Java泛型,目的是减少错误,并在类型上添加额外的抽象层。本文将简要介绍Java中的泛型、泛型背后的目标以及如何使用泛型来提高代码的质量。

2. 为什么要用泛型?

设想一个场景,我们希望用Java创建一个列表来存储Integer;代码可能会写成这样:

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

令人惊讶的是,编译器会提示最后一行。它不知道返回的数据类型是什么。因此,编译器提示需要进行显式转换:

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

没有任何约定可以保证列表的返回类型是整数。定义的列表可以包含任何对象。我们只知道我们是通过检查上下文来检索列表的。在查看类型时,它只能保证它是一个Object,因此需要显式转换来确保类型是安全的。

这种转换可能会令人感到聒噪,我们明明知道这个列表中的数据类型是整数。转换的话,也把我们的代码搞得乱七八糟。如果程序员在显式转换中出错,则可能会导致抛出与 类型相关的运行时错误 。

如果程序员能够表达他们使用特定类型的意图,并且编译器能够确保这种类型的正确性,那么这将更加容易。

这就是泛型背后的核心思想。

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

 
 
 
 
  1. List list = new LinkedList<>(); 

通过添加包含类型的菱形运算符<>,我们将此列表的特化范围缩小到 Integer 类型,即指定将保存在列表中的类型。编译器可以在编译时强制执行该类型。

在较小的程序中,这看起来像是一个微不足道的添加。但是在较大的程序中,这可以增加显著的健壮性并使程序更易于阅读。

3. 泛型方法

泛型方法是用单个方法声明编写的方法,可以用不同类型的参数调用。编译器将确保所用类型的正确性。以下是泛型方法的一些属性:

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

定义将数组转换为列表的泛型方法的示例:

 
 
 
 
  1. public  List fromArrayToList(T[] a) {    
  2.     return Arrays.stream(a).collect(Collectors.toList()); 

在前面的示例中,方法声明中的 表示该方法将处理泛型类型 T。即使方法返回的是void,也需要这样做。如上所述,方法可以处理多个泛型类型,在这种情况下,所有泛型类型都必须添加到方法声明中,例如,如果我们要修改上面的方法来处理类型 T 和类型 G ,应该这样写:

 
 
 
 
  1. public static  List fromArrayToList(T[] a, Function mapperFunction) { 
  2.     return Arrays.stream(a) 
  3.       .map(mapperFunction) 
  4.       .collect(Collectors.toList()); 

我们正在传递一个函数,该函数将具有T类型元素的数组转换为包含G类型元素的列表。例如,将 Integer 转换为其 String 表示形式:

 
 
 
 
  1. @Test 
  2. public void givenArrayOfIntegers_thanListOfStringReturnedOK() { 
  3.     Integer[] intArray = {1, 2, 3, 4, 5}; 
  4.     List stringList 
  5.       = Generics.fromArrayToList(intArray, Object::toString); 
  6.   
  7.     assertThat(stringList, hasItems("1", "2", "3", "4", "5")); 

Oracle建议使用大写字母表示泛型类型,并选择更具描述性的字母来表示形式类型,例如在Java集合中,T 用于类型,K 表示键,V 表示值。

3.1.泛型边界

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

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

要声明上界类型,我们在类型后面使用关键字extends,后跟要使用的上限。例如:

 
 
 
 
  1. public  List fromArrayToList(T[] a) { 
  2.     ... 

这里使用关键字extends表示类型 T 扩展类的上限,或者实现接口的上限。

3.2. 多个边界

类型还可以有多个上界,如下所示:

如果 T 扩展的类型之一是类(即Number),则必须将其放在边界列表的第一位。否则,将导致编译时错误。

4. 使用通配符

通配符在Java中用问号“?“ 表示,它们是用来指一种未知的类型。通配符在使用泛型时特别有用,可以用作参数类型,但首先要考虑的是一个重要的注释。

众所周知,Object是所有Java类的超类型,但是,Object的集合不是任何集合的超类型。(可能有点绕,大家好好细品一下)

例如,List 不是 List 的超类型,将List类型的变量赋值给List 类型的变量将导致编译器错误。

这是为了防止在将异构类型添加到同一集合时可能发生的冲突。

相同的规则适用于类型及其子类型的任何集合。看看这个例子:

 
 
 
 
  1. public static void paintAllBuildings(List buildings) { 
  2.     buildings.forEach(Building::paint); 

如果我们设想一个子类型Building,实例House,那么我们不能将此方法与House列表一起使用,即使House是Building的子类型。如果需要将此方法与类型构建及其所有子类型一起使用,则有界通配符可以实现以下功能:

 
 
 
 
  1. public static void paintAllBuildings(List buildings) { 
  2.     ... 

现在,这个方法可以处理Building类型及其所有子类型。这称为上界通配符,其中类型Building是上界。

通配符也可以使用下限指定,其中未知类型必须是指定类型的超类型。可以使用super关键字后跟特定类型来指定下限,例如, 表示未知类型,它是 T(=T及其所有父类)的超类。

5. 类型擦除

泛型被添加到Java中以确保类型安全,并确保泛型不会在运行时造成开销,编译器在编译时对泛型应用一个名为type erasure的进程。

类型擦除删除所有类型参数,并将其替换为它们的边界,如果类型参数是无界的,则替换为Object。因此,编译后的字节码只包含普通的类、接口和方法,从而确保不会生成新的类型。在编译时对Object类型也应用了正确的强制转换。以下是类型擦除的一个示例:

 
 
 
 
  1. public  List genericMethod(List list) { 
  2.     return list.stream().collect(Collectors.toList()); 

使用类型擦除,无界类型T将替换为Object,如下所示:

 
 
 
 
  1. // for illustration 
  2. public List withErasure(List list) { 
  3.     return list.stream().collect(Collectors.toList()); 
  4.   
  5. // which in practice results in 
  6. public List withErasure(List list) { 
  7.     return list.stream().collect(Collectors.toList()); 
  8. 如果类型是有界的,则在编译时该类型将替换为绑定:

     
     
     
     
    1. public  void genericMethod(T t) { 
    2.     ... 

    编译后会发生变化:

     
     
     
     
    1. public void genericMethod(Building t) { 
    2.     ... 

    6. 泛型和原始数据类型

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

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

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

    为了理解原始数据类型为什么不起作用,只需记住 泛型是编译时特性,这意味着类型将会被擦除,所有泛型类型都实现为 Object 类。举一个例子,让我们看看列表的 add 方法:

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

    add 方法的声明如下:

     
     
     
     
    1. boolean add(E e); 

    并将被编译为:

     
     
     
     
    1. boolean add(Object e); 

    因此,类型参数必须可转换为Object。由于基本类型不继承自 Object,所以不能将它们用作类型参数 但是,Java为它们提供了装箱类型,以及自动装箱和自动拆箱:

     
     
     
     
    1. Integer a = 17; 
    2. int b = a; 

    因此,如果我们想创建一个可以保存整数的列表,我们可以使用包装器:

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

    编译后的代码相当于:

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

    Java的未来版本可能允许泛型使用原始数据类型。Valhalla 工程旨在改进处理泛型的方式。其思想是实现JEP 218中描述的泛型专门化.

    7. 总结

    Java泛型是对Java语言的一个强大的补充,因为它使程序员的工作更容易,也更不容易出错。泛型在编译时强制执行类型正确性,并且,最重要的是,能够实现泛型算法,而不会给我们的应用程序带来任何额外的开销。

    本文转载自微信公众号「锅外的大佬」,可以通过以下二维码关注。转载本文请联系锅外的大佬公众号。 

    文章题目:重温Java泛型,带你更深入地理解它,更好的使用它!
    文章起源:http://www.csdahua.cn/qtweb/news18/141818.html

    网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网