一、基本数据类型与基本数据的包装类
在Java编程语言中,int、long和double等基本数据类型都各有它们的包装类型Integer、Long和Double。
基本数据类型是Java程序语言内置的数据类型,可直接使用。
而包装类型则归属于普通的Java类,是对基本数据类型的封装,以便于把基本数据类型作为对象操作。
在集合框架中广泛使用泛型来定义集合,集合中只允许插入对象元素,例如列表List<E>,列表中只能保存Integer数据,不能保存int数据,当插入int数据时将自动装箱包装为Integer。集合框架中的所有集合都不支持直接处理基本数据类型的数据。
但是,基本数据类型的包装类型也有其缺点,包装类型所占存储空间比较大。例如,整型数在内存中占用4个字节存储空间,包装类型的整型对象却要占用16个字节。这种情况在数组等集合类数据结构中更严重,整型数组中的每个元素只占用基本数据类型的内存空间,而整型对象数组的每个元素都需要额外增加一个指针,这个指针指向保存在堆内存中的整型对象。在最极端的情况,同样大小的数组,Integer[]要比int[]占用的内存空间大6倍。
二、对象流(Stream<T>)和基本数据类型流
在 Java 8 中,流(Stream)可以分为两种类型:对象流(Stream<T>)和基本数据类型流。
前面介绍的都是对象流,例如,对象流Stream<Integer>必须将整型数包装成Integer对象。
在对象流中基本数据类型数据元素的装箱和拆箱,都需要额外的计算开销。对于大规模的数值运算情形,装箱和拆箱的计算开销,以及包装类型占用的额外内存空间,会明显影响程序的性能。
为了提高基本数据类型在大规模数值运算中的效率,Java 8 在Stream API中定义了IntStream、LongStream和DoubleStream三种专门用于直接存储基本类型数据的基本数据类型流。这三种基本数据类型流,虽然流的名称上用的是Int、Long、Double,好像是包装对象,实际上它们处理的是基本数据类型int、long和double的数据。由于不需要装箱和拆箱,在大规模的数值计算中,程序处理性能会有明显提升。
对于七种基本数据类型,Java 库中只定义了IntStream、LongStream和DoubleStream三种基本数据类型流,因为它们在数值计算中应用最广泛。大数据处理情形,如要处理存储short、char、byte和boolean,可使用IntStream。而对于float,可使用DoubleStream。
IntStream, LongStream 和 DoubleStream 是专门处理基本类型 int, long, 和 double 数据的流。这些流是为了提高性能而设计的,因为它们避免了装箱/拆箱操作,这对于大数据量的处理尤其重要。
三、基本数据类型流详解
基本数据类型流又细分为下面三种:IntStream, LongStream 和 DoubleStream
IntStream、LongStream和DoubleStream三种基本类型数据流具有类似的行为和方法,下面就以IntStream为例进行介绍,其他二种用法类似,以此类推。
整型数据流 IntStream
IntStream 是一种特殊的流,它用于处理 int 类型的数据。它提供了一系列方法来高效地操作整数序列。
(一)、IntStream类创建 IntStream流的各种方式:
1,使用 IntStream.range 或 IntStream.rangeClosed 方法来生成一个指定范围内的整数序列。
2,使用 IntStream.iterate 方法来迭代生成一个无限或有限的整数序列。
3,使用 IntStream.generate 方法使用产生器来生成一个无限或有限的整数序列。
4,使用IntStream.of方法 提供指定的一组值来创建stream。
5,使用IntStream.empty方法 可创建一个不包含任何元素的空流。
6,使用IntStream.builder方法 类似于构造器设计模式的stream创建方式,可以通过构造器添加若干个元素,然后通过build方法获取流对象。
创建 IntStream例程:
public static void crtIntStream() {// 生成从0到9的整数序列IntStream.range(0, 10).forEach(System.out::println);// 生成从1到10(包括10)的整数序列IntStream.rangeClosed(1, 10).filter(n -> n % 2 != 0).forEach(System.out::println);// 生成一个无限的偶数序列IntStream.iterate(0, n -> n + 2).limit(5).forEach(System.out::println);// 借助随机发生器利用产生器生成一个无限的随机整型数序列Random random = new Random();IntStream.generate(() -> random.nextInt(1000)).limit(10).forEach(n -> System.out.println(n));//Math::random返回double型的随机数DoubleStream.generate(Math::random).limit(10).sorted().forEach(System.out::println);IntStream.of(9, 90, 876, 12).forEach(v -> System.out.println(v));IntStream.builder().add(90).add(87).build().forEach(v -> System.out.println(v));}
(二)、其他创建 IntStream 流的方式:
还有很多其他创建 IntStream 流的方法,我们这里介绍两种:
- 用随机数类Random创建 IntStream 流
函数式编程的第一个例程中用随机数类Random的ints()方法生成的就是IntStream流,类似地类Random也有方法longs()、方法doubles()可生成LongStream和DoubleStream流。下面是一个示例:
//用随机数类Random的ints()方法创建IntStream流,并收集到int数组int[] intArray = new Random(61).ints(10).distinct().limit(10).sorted().toArray();
- 用字符串( String)创建 IntStream 流
字符串( String)创建 IntStream 流有两种方法,分别是 java.lang.CharSequence 接口定义的默认方法 chars 和 codePoints。详细请见后文。
(三)、 IntStream 流的基本用法:
Java语言为基本数据类型流设计了一套类似的操作,我们下面主要以IntStream为例进行演示,其他两种基本数据类型流也有类似操作,其用法也类似。
基本数据类型流也有和普通对象流类似的一些操作方法,例如map、filter、sorted等,但性能更优。请先看几个示例:
IntStream.of (1, 2, 3, 4, 9).filter(e -> e > 2).peek(e -> System.out.println("符合条件的值: " + e)).map(e -> e * e).peek(e -> System.out.println("映射为平方值: " + e)).sum();
下面是一个使用 IntStream 的完整示例,该示例展示了如何生成一个整数序列,并计算其中所有数字的平方和和极值。
public class IntStreamExample {public static void main(String[] args) {int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};// 打印所有的数字System.out.println("整数序列:");IntStream.of(numbers).forEach(System.out::println);// 计算所有数字的平方和int sumOfSquares = IntStream.of(numbers).map(n -> n * n).sum();System.out.println("整数序列的平方和: " + sumOfSquares);// 找到最大的数字OptionalInt maxNumber = IntStream.of(numbers).max();System.out.println("最大整型数: " + (maxNumber.isPresent() ? maxNumber.getAsInt() : "不存在"));// 找到最小的数字OptionalInt minNumber = IntStream.of(numbers).min();System.out.println("最小整型数: " + (minNumber.isPresent() ? minNumber.getAsInt() : "不存在"));}
}
(四)、基本数据类型流的reduce规约操作
按照特定规则处理每个元素,然后返回处理后的结果:
public static void testIntStreamReduce(){//单个参数的reduce操作//求1到100的和System.out.println("1-100的和为:"+IntStream.rangeClosed(1,100).reduce((x,y)->x+y).getAsInt());//orElse和orElseGet可以防止返回值为空,如果为空则默认返回0System.out.println(IntStream.of(1,5,19).reduce((x, y) -> x + y).orElse(0));System.out.println(IntStream.of(1,5,19).reduce((x, y) -> x + y).orElseGet(() -> 0));System.out.println(IntStream.of(1,5,19).reduce((x, y) -> x + y).orElseThrow(()->new RuntimeException("参数为空,返回值为空")));//两个参数的reduce操作IntStream intStream = IntStream.of (1, 2, 3, 4, 9);int sum = intStream.reduce(0, (a, b) -> a+b);int total = intStream.reduce(0, Integer::sum);}
(五)、基本数据类型流的数学计算与专用的SummaryStatistics 汇总统计
Java语言为基本数据类型流内置了一些实用的方法:例如,都有内置的规约操作,包括average、count、max、min、sum;还有最常用的汇总统计SummaryStatistics()方法,这个方法可得到一组统计数据:元素个数、汇总值、最大值、最小值和平均值。当然了,如果你不需要这组统计数值,只需要其中几个,也可直接调用内置的规约操作min、max、average或sum方法获得单个的统计值。
下面的例程演示基本数据类型流使用SummaryStatistics方式来进行统计数据。
public static void testMathOpt() {System.out.println("总和:"+IntStream.of(10, 20, 30, 40).sum());System.out.println("最大值:"+IntStream.of(10, 20, 30, 40).max().getAsInt());System.out.println("最小值:"+IntStream.of(10, 20, 30, 40).min().getAsInt());System.out.println("元素个数:"+IntStream.of(10, 20, 30, 40).count());System.out.println("平均值:"+IntStream.of(10, 20, 30, 40).average().getAsDouble());System.out.println("---使用summaryStatistics进行数学运算:---");IntSummaryStatistics summaryStatistics = IntStream.of(10, 20, 30, 40).summaryStatistics();System.out.println("总和:" + summaryStatistics.getSum());System.out.println("最大值:" + summaryStatistics.getMax());System.out.println("最小值:" + summaryStatistics.getMin());System.out.println("元素个数:" + summaryStatistics.getCount());System.out.println("平均值:" + summaryStatistics.getAverage());}
测试结果:
(六)、流的装箱与拆箱
流(Stream)通常是对象流(Stream<T>),本文则主要介绍的是基本数据类型流(IntStream, LongStream 和 DoubleStream)当把基本数据类型流转换为对象流(Stream<T>)我们常称这为流的装箱;反之,把对象流(Stream<T>)转换为基本数据类型流(IntStream, LongStream 和 DoubleStream)则称之为流的拆箱。
对象流(Stream<T>)和基本数据类型流(IntStream, LongStream 和 DoubleStream)各有优缺点:对象流(Stream)可使用的操作工具更丰富;而基本数据类型流在大规模数据运算场景时运算效率更高。
在处理对象流(Stream<T>)的时候,可以利用 Collectors 类的收集器收集结果到集合,例如,将字符串流收集到列表 List ,这种方式是没有问题的。但基本数据类型流默认只能收集到数组。
基本数据类型流的装箱方法
基本类型流的装箱操作,集合只能处理对象数据,对象流可以利用Collectors类的收集器来收集,但基本数据类型流就无法用收集器来处理。遇到这种情形若要用收集器来收集数据,可把基本数据类型流(如int类型)装箱转换为包装类型(Integer型)的对象流。
叁种装箱方法:mapToObj() 、 boxed()和三叁数的collect(supplier,accumulator,combiner)
(1)Stream<Integer> boxed() 用装箱方法boxed()把基本类型流转换为对象流。
(2)<U> Stream<U> mapToObj(IntFunction<? extends U> mapper) 用方法mapToObj映射为对象流。
(3)利用三参数的收集器方法collect(supplier,accumulator,combiner),这个方法第一个参数是一个供给器,相当于初始化(创建)一个容器;第二个参数是累加器,相当于给初始化的供给器添加一个元素;第三个参数是组合器,相当于将这些元素全部组合到一个容器。
流的装箱:使用 boxed 方法将 IntStream 转换为 Stream<Integer>,然后可用Collect()收集操作收集结果到列表中。
【例程10-34】基本数据类型流三种包装为对象流例程BoxedTest
例程BoxedTest.java源代码开始:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
/**** @author QiuGen* @description 基本数据类型流的三种装箱例程BoxedTest* @date 2024/8/20* ***/
public class BoxedTest {public static void main(String[] args) {
//IntStream.of(1, 2, 3, 4).collect(Collectors.toList()); //编译器通不过。IntStream.of(1, 2, 3, 4).boxed() //装箱为对象流
.collect(Collectors.toList()).forEach(System.out::println);IntStream.of(25,50,75,95).mapToObj(Integer::new) //映射为对象流.collect(Collectors.toList()).forEach(System.out::println);List<Double> list = DoubleStream.of(10.60,20.10,30.50) //使用三参数收集方法.collect(ArrayList<Double>::new, ArrayList::add, ArrayList::addAll); list.forEach(System.out::println);}
} //例程BoxedTest.java源码结束。
流的拆箱方法:
从对象流拆箱转换为基本数据类型流,可以用方法flatMapToxxx(其中的xxx表示基本数据类型,下同)和方法mapToxxx从普通的对象流转换为基本数据类型流。以下是这六种拆箱方法的方法签名:
IntStream mapToInt(ToIntFunction<? super T> mapper)LongStream mapToLong(ToLongFunction<? super T> mapper)DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper)DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream>
使用 mapToInt 方法将 Stream<T> 转换为 IntStream。其他两个拆箱方法也实现类似功能。
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);IntStream intStream = list.stream().mapToInt(Integer::intValue);
flatMapToInt是一个非常有用的流拆箱操作,它可以将嵌套的流结构"扁平化"为一维的IntStream。下面是一个演示例程FlatMapToIntExample :
package stream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
public class FlatMapToIntExample {public static void main(String[] args) {// 创建一个包含多个整数列表的列表List<List<Integer>> listOfLists = Arrays.asList(Arrays.asList(1, 2, 3),Arrays.asList(4, 5),Arrays.asList(6, 7, 8, 9));// 使用flatMapToInt将所有整数扁平化为一个IntStreamIntStream intStream = listOfLists.stream().flatMapToInt(list -> list.stream().mapToInt(Integer::intValue));// 输出结果intStream.forEach(System.out::println);}
} //演示例程FlatMapToIntExample
请再看一个完整的拆箱示例:
public static void unBoxedIntStream() { /***拆箱***/List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);List<Integer> list2 = Arrays.asList(6, 7, 8, 9, 10);// 将两个列表转换为 IntStreamIntStream stream1 = list1.stream().mapToInt(Integer::intValue);IntStream stream2 = list2.stream().mapToInt(Integer::intValue);// 合并两个 IntStreamIntStream combinedStream = IntStream.concat(stream1, stream2);// 计算总和int sum = combinedStream.sum();System.out.println("汇总: " + sum);List<Integer> list = Arrays.asList(8, 9, 10, 3, 4, 5);IntStream intStream = list.stream().mapToInt(Integer::intValue);// 过滤出大于 5 的数IntStream filteredStream = intStream.filter(x -> x > 5);list = filteredStream.boxed().collect(Collectors.toList());System.out.println("过滤数据: " + list);}
还可用IntStream的asLongStream()方法,把IntStream流转换为LongStream。
基本数据类型流之间转换的例程:
//把IntStream流转换为LongStream的两个版本的示例public static void chgIntStreamToLongStream() {LongStream longStream = IntStream.rangeClosed(1, 5).mapToLong(v -> v * 2);longStream.forEach(v1 -> System.out.println(v1 + " -> " + ((Object)v1).getClass()));LongStream lnStream = IntStream.rangeClosed(1, 10).asLongStream(); //转换为LongStream}
(七)、从字符串创建基本数据类型流
字符串( String)创建 IntStream 流有两种方法,分别是 java.lang.CharSequence 接口定义的默认方法 chars 和 codePoints。
这两个方法的不同这处:
- 方法 codePoints() 返回的是码点值,表示Unicode字符集中的一个字符。
- 方法 chars()返回的是一个码元值。在Java中,字符是以两个字节(16位)的UTF-16编码存储的。UTF-16编码的码元长度是双字节(16bit位)长。UTF-16编码,对于基本多语言平面(BMP)中的字符用一个码元就可以表示一个码点;但对于补充平面中的字符则需要使用两个码元来表示一个码点(字符)。
public static void strAndIntStream() {// 增加 笑脸字符😊String msg = "Welcome to Java8,欢迎光临!😊".codePoints()//转换成IntStream流.collect(StringBuffer::new,StringBuffer::appendCodePoint,StringBuffer::append)//将流转换为字符串.toString();System.out.println("msg: " + msg);IntStream strStream = "Welcome to Java8,中国崛起!😊".chars();//转换成IntStream流String message = strStream.collect(StringBuffer::new,StringBuffer::appendCodePoint,StringBuffer::append)//将流转换为字符串.toString();System.out.println("message: " + message);}
程序测试效果图:
码点(codePoint)整型流的一个演示例程:
public class MultiByteCodePointExample {public static void main(String[] args) {// 选择一个多字节字符String emoji = "😊"; // 这个字符串有一个多字节字符int codePoint = emoji.codePointAt(0);// 使用toChars将码点转换为字符char[] chars = Character.toChars(codePoint);System.out.printf("Emoji: %s, Codepoint: U+%04X, Characters: %s%n", emoji, codePoint, new String(chars));String symbols = "𝕆"; //数学符号的码点值 codePoint = 0x1D546; //数学符号的码点值 chars = Character.toChars(codePoint);System.out.printf("数学符号: %s, Codepoint: U+%04X, Characters: %s%n", symbols, codePoint, new String(chars)); }}
程序测试结果:
(八)、长整型数据流 LongStream
LongStream 与 IntStream 类似,但是它处理的是 long 类型的数据。
创建 LongStream
使用 LongStream.range 或 LongStream.rangeClosed 方法来生成一个指定范围内的长整数序列。
使用 LongStream.iterate 方法来生成一个无限或有限的长整数序列。
使用 LongStream.generate 方法来生成一个无限或有限的长整数序列。
// 生成从0L到9L的长整数序列LongStream.range(0L, 10L).forEach(System.out::println);// 生成从1L到10L(包括10L)的长整数序列LongStream.rangeClosed(1L, 10L).forEach(System.out::println);// 生成一个无限的偶数长整数序列LongStream.iterate(0L, n -> n + 2).limit(5).forEach(System.out::println);
【例程10-33】斐波那契数列函数式编程的算法版本FibonacciStream
例程FibonacciStream.java是斐波那契数列函数式编程的算法版本:
package fibonacci;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.LongStream;
import java.util.stream.Stream;
/**** @author QiuGen* @description 斐波那契数列的函数式编程的算法* @date 2024/8/20* ***/
public class FibonacciStream {/***迭代生成斐波那契数列的无限流***/public static Stream<long[]> fibStream( ) {Stream<long[]> fibStream = Stream.iterate(new long[]{0, 1},fib -> new long[]{fib[1], fib[0] + fib[1]});return fibStream;}/***用Collect收集结果***/public static void fibStreamA( ) {List<Long> lst = fibStream().limit(30) //前30项.map(fib -> fib[0]).collect(ArrayList::new, ArrayList::add,ArrayList::addAll);lst.forEach(System.out::println);}/**转换为LongStream用toArray()收集结果为long[],并打印结果**/public static void fibStreamB( ) {LongStream fibLongStream = fibStream().limit(30) //前30项
.map(fib -> fib[0]).mapToLong(k->k);long fib[] = fibLongStream.toArray();for (int i = 0; i < fib.length; i++) {System.out.println(fib[i]);}}public static void main(String[] args) {fibStreamA();fibStreamB();}
} //斐波那契数列的函数式编程的算法版本FibonacciStream.java。
请注意函数fibStreamB()的处理方式,只有基本数据类型的流才能用toArray()方法把结果收集为基本数据类型的数组。对象流只能收集为对象数组。
(九)、双精度型数据流 DoubleStream
DoubleStream 处理 double 类型的数据。
创建 DoubleStream
使用 DoubleStream.iterate 方法来生成一个无限或有限的双精度浮点数序列。
使用 DoubleStream.generate 方法来生成一个无限或有限的双精度浮点数序列。
// 生成一个无限的双精度浮点数序列DoubleStream.iterate(0.0, n -> n + 0.1).limit(5).forEach(System.out::println);//Math::random返回double型的随机数DoubleStream.generate(Math::random).limit(10).sorted().forEach(System.out::println);