当前位置: 首页 > 科技观察

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

时间:2023-03-21 14:25:36 科技观察

1。简介在jdk5.0中引入了Java泛型以减少错误并在类型上添加额外的抽象层。本文将简要介绍Java中的泛型、泛型背后的目标以及如何使用泛型来提高代码质量。2.为什么要使用泛型?想象一个场景,我们想在Java中创建一个列表来存储Integer;代码可以这样写:Listlist=newLinkedList();list.add(newInteger(1));Integeri=list.iterator().next();令人惊讶的是,编译器提示输入最后一行。它不知道返回的数据类型是什么。因此,编译器提示需要显式转换:Integeri=(Integer)list.iterator.next();没有合同保证列表的返回类型是整数。定义的列表可以包含任何对象。我们只知道我们正在通过检查上下文来检索列表。在查看类型时,它只能保证它是一个对象,因此需要显式转换以确保类型是安全的。这种转换可能有噪音,我们知道这个列表中的数据类型是整数。如果它被转换,它也会弄乱我们的代码。如果程序员在显式转换中出错,可能会引发类型相关的运行时错误。如果程序员可以表达他们使用特定类型的意图,并且编译器可以确保该类型的正确性,那就更容易了。这是泛型背后的核心思想。我们将前面代码片段的第一行修改为:Listlist=newLinkedList<>();通过添加包含类型的菱形运算符<>,我们将此列表的特化范围缩小到Integer类型,指定将保存在列表中的类型。编译器可以在编译时强制执行类型。在较小的程序中,这似乎是一个微不足道的添加。但在较大的程序中,这可以显着增加健壮性并使程序更易于阅读。3.泛型方法泛型方法是用单个方法声明编写的方法,可以用不同类型的参数调用。编译器将确保所使用类型的正确性。以下是泛型方法的一些属性:泛型方法在方法声明的返回类型之前有一个类型参数(包装类型的菱形运算符)类型参数可以有界(边界在本文后面解释)泛型方法可以有不同的方法的类型参数在方法签名中以逗号分隔。泛型方法的方法体定义为普通方法。将数组转换为列表的通用方法示例:publicListfromArrayToList(T[]a){returnArrays.stream(a).collect(Collectors.toList());}在前面的示例中,方法声明中的表示该方法将处理泛型类型T。即使该方法返回void,这也是必需的。上面说了,一个方法可以处理多个泛型类型,这种情况下所有的泛型类型都必须添加到方法声明中,比如我们要修改上面的方法来处理类型T和类型G,应该是这样的写入:publicstaticListfromArrayToList(T[]a,FunctionmapperFunction){returnArrays.stream(a).map(mapperFunction).collect(Collectors.toList());}我们正在传递一个函数,该函数将具有类型T元素的数组转换为包含类型G元素的列表。例如,将Integer转换为其String表示形式:@TestpublicvoidgivenArrayOfIntegers_thanListOfStringReturnedOK(){Integer[]intArray={1,2,3,4,5};ListstringList=Generics.fromArrayToList(intArray,Object::toString);assertThat(stringList,hasItems("1","2","3","4","5"));}Oracle建议对泛型类型使用大写字母,选择更具描述性的字母来表示Formal类型,如在Java集合中,T代表类型,K代表键,V代表值。3.1.GenericBounds如前所述,类型参数可以有界。Bounded是“受限”的意思,我们可以限制一个方法可以接受的类型。例如,您可以指定一个方法接受一个类型及其所有子类(上限)或一个类型及其所有超类(下限)。要声明上限类型,我们在类型后使用关键字extends,然后是要使用的上限。例如:publicListfromArrayToList(T[]a){...}这里使用关键字extends表示类型T扩展类的上限,或者实现接口的上限.3.2.多个边界类型也可以有多个上限,如下所示:如果T扩展的类型之一是类(即Number),则它必须放在边界列表的第一位。否则,将导致编译时错误。4.使用通配符通配符用问号“?”表示在Java中,它们用于指代未知类型。通配符在使用泛型时特别有用,可以用作参数类型,但首先要考虑一个重要的注意事项。众所周知,Object是所有Java类的超类型,但是Object的集合并不是任何集合的超类型。(可能有点绕,仔细看)比如List不是List的超类型,将List类型的变量赋值给List类型的变量会导致编译错误。这是为了防止在将异构类型添加到同一集合时可能发生的冲突。相同的规则适用于任何类型及其子类型的集合。看一下这个例子:即使House是Building的子类型。如果您需要将此方法用于Building类型及其所有子类型,则可以按如下方式实现有界通配符:publicstaticvoidpaintAllBuildings(Listbuildings){...}现在,此方法可以处理Building类型和所有子类型。这称为上限通配符,其中类型Building是上限。也可以使用下限指定通配符,其中未知类型必须是指定类型的超类型。可以使用super关键字后跟特定类型来指定下限,例如对于未知类型,它是T的超类型(=T及其所有父类)。5.类型擦除泛型被添加到Java中以确保类型安全并确保泛型不会在运行时造成开销,编译器在编译时对泛型应用称为类型擦除的过程。类型擦除删除所有类型参数并用它们的边界替换它们,如果类型参数是无界的,则用Object替换它们。因此,编译后的字节码只包含普通的类、接口和方法,保证不会产生新的类型。正确的类型转换也适用于编译时的Object类型。下面是一个类型擦除的例子:publicListgenericMethod(Listlist){returnlist.stream().collect(Collectors.toList());}使用类型擦除,无界类型T将是替换为Object如下://forillustrationpublicListwithErasure(Listlist){returnlist.stream().collect(Collectors.toList());}//whichinpracticeresultsinpublicListwithErasure(Listlist){returnlist.stream().collect(Collectors.toList());}如果类型是有界的,在编译时类型将被绑定替换:publicvoidgenericMethod(Tt){...}将在编译后改变:publicvoidgenericMethod(Buildingt){...}6.泛型和原始数据类型Java中泛型的一个限制是类型参数不能是原始类型例如,以下将不会编译:Listlist=newArrayList<>();list.添加(17);要理解为什么原始数据类型不起作用,只要记住泛型是一个编译时特性,这意味着类型擦除,所有泛型类型都作为Object类实现。例如,让我们看一下列表的添加方法:Listlist=newArrayList<>();添加列表(17);add方法的声明如下:booleanadd(Ee);并将被编译为:booleanadd(Objecte);因此,类型参数必须可以转换为Object。由于原始类型不继承自Object,因此它们不能用作类型参数。然而,Java为它们提供了装箱类型,以及自动装箱和自动拆箱:Integera=17;intb=a;所以,如果我们想创建一个可以容纳整数的列表,我们可以使用一个包装器:Listlist=newArrayList<>();list.add(17);intfirst=list.get(0);编译后的代码等效于:Listlist=newArrayList<>();list.add(Integer.valueOf(17));intfirst=((Integer)list.get(0)).intValue();Java的未来版本可能允许泛型使用原始数据类型。Valhalla项目旨在改进处理泛型的方式。这个想法是为了实现JEP218中描述的泛型专业化。7.总结Java泛型是对Java语言的强大补充,因为它们使程序员的工作更轻松,更不容易出错。泛型在编译时强制执行类型正确性,最重要的是,它可以在不给我们的应用程序带来任何额外开销的情况下实现泛型算法。本文转载自微信公众号《锅外大哥》,可通过以下二维码关注。转载本文请联系锅外老板公众号。