4.2 基本类型
以上部分还没有用到基本类型。在Java中,有一些相伴的类型,比如int和Integer——前者是基本类型,后者是装箱类型。基本类型是建在语言和运行环境中,是基本的程序构建模块;而装箱类型属于普通的Java类,只不过是对基本类型的一种封装。
Java的泛型是基于对泛型参数类型的擦除——换句话说,假设它是Object对象的实例——因此只有装箱类型才能作为泛型参数。这就解释了为什么在Java中想要一个包含整型值的列表List,实际上得到的却是一个包含整型对象的列表List。
麻烦的是,由于装箱类型是对象,因此在内存中存在额外开销。比如,整型在内存中占用4字节,整型对象却要占用16字节。这一情况在数组上更加严重,整型数组中的每个元素只占用基本类型的内存,而整型对象数组中,每个元素都是内存中的一个指针,指向Java堆中的某个对象。在最坏的情况下,同样大小的数组,Integer[]要比int[]多占用6倍内存。
将基本类型转换为装箱类型,称为装箱,反之则称为拆箱,两者都需要额外的计算开销。对于需要大量数值运算的算法来说,装箱和拆箱的计算开销,以及装箱类型占用的额外内存,会明显减缓程序的运行速度。
为了减小这些性能开销,Stream类的某些方法对基本类型和装箱类型做了区分。图4-1所示的高阶函数mapToLong和其他类似函数即为该方面的一个尝试。在Java 8中,仅对整型、长整型和双浮点型做了特殊处理,因为它们在数值计算中用得最多,特殊处理后的系统性能提升效果最明显。
图4-1:ToLongFunction
对基本类型做特殊处理的方法在命名上有明确的规范。如果方法返回类型为基本类型,则在基本类型前加To,如图4-1中的ToLongFunction。如果参数是基本类型,则不加前缀只需类型名即可,如图4-2中的LongFunction。如果高阶函数使用基本类型,则在操作后加后缀To再加基本类型,如mapToLong。
图4-2:LongFunction
这些基本类型都有与之对应的Stream,以基本类型名为前缀,如LongStream。事实上,mapToLong方法返回的不是一个一般的Stream,而是一个特殊处理的Stream。在这个特殊的Stream中,map方法的实现方式也不同,它接受一个LongUnaryOperator函数,将一个长整型值映射成另一个长整型值,如图4-3所示。通过一些高阶函数装箱方法,如mapToObj,也可以从一个基本类型的Stream得到一个装箱后的Stream,如Stream。
图4-3:LongUnaryOperator
如有可能,应尽可能多地使用对基本类型做过特殊处理的方法,进而改善性能。这些特殊的Stream还提供额外的方法,避免重复实现一些通用的方法,让代码更能体现出数值计算的意图。例4-4展示了如何使用这些方法:
例4-4 使用summaryStatistics方法统计曲目长度
public static void printTrackLengthStatistics(Album album) {IntSummaryStatistics trackLengthStats= album.getTracks().mapToInt(track -> track.getLength()).summaryStatistics();System.out.printf("Max: %d, Min: %d, Ave: %f, Sum: %d",trackLengthStats.getMax(),trackLengthStats.getMin(),trackLengthStats.getAverage(),trackLengthStats.getSum());}
例4-4向控制台输出曲目长度的一系列统计信息。无需手动计算这些信息,这里使用对基本类型进行特殊处理的方法mapToInt,将每首曲目映射为曲目长度。因为该方法返回一个IntStream对象,它包含一个summaryStatistics方法,这个方法能计算出各种各样的统计值,如IntStream对象内所有元素中的最小值、最大值、平均值以及数值总和。
这些统计值在所有特殊处理的Stream,如DoubleStream、LongStream中都可以得出。如无需全部的统计值,也可分别调用min、max、average或sum方法获得单个的统计值,同样,三种基本类型对应的特殊Stream也都包含这些方法。
