Java-Lambda篇-第二章:流方法
第二章开始我们开始介绍这些Lambda语法糖,可以说这一章满满的都是一股甜味~
Java程序员在使用集合类的时候,一个通用模式是进行迭代操作,然后返回每个元素,这需要for循环,但是这样并行改造会变得非常麻烦。若是单一的for循环问题不大,但是面对一整个满是循环的庞大代码就显得负担很重。
我们的方法就是用内部迭代的方式,接下来我们必须理解Stream流 。接下来我们掌握一些基本的Stream操作:
一、生成列表
collect(toList()) 方法可以由Stream里的值生成一个列表(List)。
Stream中的of方法可以使用一组初始值生成Stream组。*实际上collect用法不止于此。
List<String> coo = Stream.of("a","b","c").collect(Collectors.toList());
这个例子展示了示例代码的通用格式,首先由列表生成一个Stream,然后在Stream中进行一定的操作,最终由collect操作,由Stream生成列表。想象下,Stream是一个汉堡包,第一份步骤和最后一步都是给汉堡包加上第一片和最后一篇面包。
二、中间方法
中间方法相当于我们在汽车工厂中的各种机械臂,给汽车上发动机、喷漆、拧螺丝、拼接等等操作:
方法 |
简介 |
传入类型(接口) |
返回值形式 |
JDK |
map |
接收一个转换函数作为参数,利用该函数将流中的参数转换成另一种形式. |
Function<T,R> |
Stream |
8 |
filter |
接收一个条件函数作为参数,返回满足函数条件(结果为true)的值或数组。 |
Predicate<T> |
Stream |
8 |
flatMap |
接收一个转换函数作为参数,最终返回并平铺流中的所有结果(二,三,多维流转一维流)。 |
Function<T,R> |
Stream |
8 |
sorted |
接收一个比较器和get函数作为参数(数组、List类可不接收)对数组进行排序。 |
Comparator |
Stream |
8 |
distinct |
去除流中的重复值。 |
- |
Stream |
|
Limit |
接收一个参数,指定流最终返回的最大数量 |
- |
Stream |
|
skip |
接收一个参数,指定流截断前n个数值,可与limit相互配合 |
- |
Stream |
|
takeWhile |
接收一个条件函数,对数组中的值进行顺序条件判断,直至有参数返回false终止,并返回true数组 |
Predicate |
Stream |
9 |
dropWhile |
接收一个条件函数,对数组中的值进行顺序条件判断,直至有参数返回false终止,并返回剩余数组 |
Predicate |
Stream |
9 |
iterate |
生成一个无限流,使用迭代的方式,传入一个起始值和一个递增函数 |
|
|
|
generate |
生成一个无限流,接受一个 Supplier类型的 Lambda 提供新的值 |
|
|
三、终端方法
终端方法是我们最终给汽车出厂的各种方法,是合格生产,还是定制化操作,它只能被消费一次,成功出产后这个流就不能再次被中间方法所操作了
方法 |
简介 |
传入类型(接口) |
返回值形式 |
partitioningBy |
接受一个条件函数,并返回Map<Boolen,List<T>>的集合,分为true和false两组。 |
Collector |
Map<Boolean, List<T>> |
groupingBy |
接受一个条件函数,并返回 Map<K, List<T>>的集合,根据不同的返回值分为不同的Map键。 |
Collector |
Map<K, List<T>> |
joining |
接受一个函数,并返回String,其中可以传入三个参数,分隔符、起始符、结尾符。 |
Collector |
String |
count |
统计流总数,和list.size()作用相同 |
- |
Long |
reduce |
Steam类归约,设定一个初始值(或不设定),传入一个函数作为积累器,返回最终函数积累结果。 |
BinaryOperator |
<T>,Optional |
reducing |
Collectors类归约,设定一个初始值(或不设定),传入一个函数作为积累器,返回最终函数积累结果。 |
- |
<T> |
forEach |
接收一个函数,对其数组中所有的值进行迭代器操作,无返回值. |
Consumer |
无返回值 |
maxBy&minBy |
接收一个比较器,返回Optional对象。 |
Comparator |
Optional |
allMatch |
接收一个函数,如果全部元素符合函数条件,则返回true,否则flase。 |
Function<T,R> |
Boolean |
anyMatch |
接收一个函数,如果任意元素符合函数条件,则返回true,否则flase。 |
Function<T,R> |
Boolean |
noneMatch |
接收一个函数,如果没有元素符合函数条件,则返回true,否则flase。 |
Function<T,R> |
Boolean |
FindAny |
返回数组中随机的值(需要在并行流下,且数据量较大的时候,才有效果) |
- |
Optional |
FindFirst |
返回数组中首个的值 |
- |
Optional |
max & min |
传入一个比较器并接受一个get函数,将根据比较器和get函数的值返回Optional对象。 |
Comparator |
Optional |
summingInt |
传入累计函数,返回数值类型,可以是返回Int,Long,Double类型 |
- |
Int/Long/Double |
summarizingInt |
传入累计函数,返回一个IntSummaryStatistics,它包含了数值数量,平均数,最大,最小等等数据 |
- |
IntSummaryStatistics |
接下来我们可以测试这些方法的使用模式了:
中间方法介绍:
1.筛选方法 filter
提示:遍历数据并检查其中元素的时候可以使用filter,它接受(传入)一个函数作为参数,返回满足函数条件(结果为true)的值或数组。
实例1:
Stream<Integer> test1 = Stream.of(10,5,20);
//生成流
Predicate<Integer> asLset10 = x -> x>5;
//书写Lambda表达式
test1 = test1.filter(asLset10);
//传入函数给流
system.out.println(test1.collect(Collectors.toList()));
//流最终操作转化为List并打印List
实例2:
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian) //检查是否是素食者
.collect(toList()); //最终操作,转化为List
2.筛选重复项 distinct
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
3.切片 takeWhile,dropWhile
Java 9 引入的新方法,它可以高效的选择和丢弃元素中的元素,例如忽略前几个元素或者按照设定大小对流实施截断。
List<Dish> specialMenu = Arrays.asList(
new Dish("seasonal fruit", true, 120, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER));
//添加菜肴(已经排好序了)
//takeWhile:筛选直到卡路里大于或等于320的菜肴为止,触碰到结果为Fales时返回已经检查过的结果集。
List<Dish> slicedMenu1 = specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
//dropWhile:返回筛选卡路里大于或者等于320的菜肴剩余的数组,触碰到结果为Fales时返回剩余未检查的结果集。
List<Dish> slicedMenu1 = specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
这两个方法并非不能一同使用的,同时使用的话,我们可以取得卡路里在320和420之间的菜肴,你可以自己尝试一范。
4.截断流 limit(n),skip(n)
如果你用过SQL limit关键字恐怕已经颤动了你的DNA,没错,它可以限制输出多少结果,保证最终只有多少结果存在于List中,而skip相反,它可以设定跳过多少结果,前几个对象并不会被List所收录。
//limit,限制数据返回数量
List<Dish> dishes = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3) //返回三个数据
.collect(toList());
//skip,跳过前置数据
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2) //跳过前两个数据
.collect(toList());
//二者合二为一,可导致数据无返回(操作猛如虎,智商二百五)
List<Dish> specialMenu = Arrays.asList(
new Dish("seasonal fruit", true, 120, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH));
List<Dish> dishes = menu.stream()
.skip(2) //跳过前两个数据
.filter(d -> d.getCalories() > 300)
.limit(2) //返回两个数据
.collect(toList());
//总共就两个数据,你还都跳过了,数据都没啦!
5.映射 map
map可以将数组中的每个值都进行函数操作,并进行转化为一个全新的流,例如对List<String> 进行map操作后,你可以得到List<Integer>!
//实例:
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName) //菜谱List<Dish> 转化为 List<String>
.map(String::length) //名称List<String> 转化为 List<Interger>
.collect(toList());//最终操作
6.平铺 flatMap
哦,这个比较难以理解,在你深入学习后可能会遇到这样的情况:Optional<Optional>,包装对象中还包装着包装对象,这会不会很难懂?简单来说flatMap遇到二维数组时会将它平铺为一维数组,来应对流中还嵌套着流的情况。
static int student = 1;//学生编号
static int group = 1;//小组编号
List<String[]> eggs = new ArrayList<>();
eggs.add(new String[]{"鸡蛋1","鸡蛋1","鸡蛋1","鸡蛋1","鸡蛋1"});
eggs.add(new String[]{"鸡蛋2","鸡蛋2","鸡蛋2","鸡蛋2","鸡蛋2"});
//map方法:
eggs.stream()
.map(x-> Arrays.stream(x).map(y -> y.replace("鸡","荷包")))
.forEach(x -> System.out.println("组"+ group++ + ":" + Arrays.toString(x.toArray())));
//组1:[荷包蛋1, 荷包蛋1, 荷包蛋1, 荷包蛋1, 荷包蛋1]
//组2:[荷包蛋2, 荷包蛋2, 荷包蛋2, 荷包蛋2, 荷包蛋2]
//flatmap方法:
eggs.stream()
.flatMap(x-> Arrays.stream(x).map(y -> y.replace("鸡","荷包")))
.forEach(x -> System.out.println("学生"+ student++ + ":" + x));
//学生1:荷包蛋1
//学生2:荷包蛋1
//学生3:荷包蛋1
//学生4:荷包蛋1
//学生5:荷包蛋1
//学生6:荷包蛋2
//学生7:荷包蛋2
//学生8:荷包蛋2
//学生9:荷包蛋2
//学生10:荷包蛋2
7. 随机返回 findAny
顾名思义
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
.ifPresent(dish -> System.out.println(dish.getName());如果包含值则打印,否则什么都不做
8. 首个值 findFirst
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional firstSquareDivisibleByThree = someNumbers.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst(); // 9
9. 元素求和 reduce
可以设定一个初始值,接收一个函数作为积累器,返回最终的结果。如果不设定初始值,那么会返回一个Optional对象
Stream<Integer> myTest2 = Stream.of(4,2,4,2,4);
int cont = myTest2.reduce(0,Integer::sum);
System.out.println(cont);
//reduce操作中第一参数的 0 ,表示以 0 为起点,目前展示的是求和,也可以求差,求乘积等等操作,
//要注意的是,如果是乘积就不能以0为开头了,否则0乘以任何数都是0!
10. 最大最小值 max&min
顾名思义,返回列表中的最大值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
最终方法介绍:
1.最大值和最小值
Collectors.maxBy 和 Collectors.minBy 来计算流中的最大值或最小值。
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
//建立一个比较器对Int值作比较
Optional<Dish> mostCalorieDish = menu.stream()
.collect(Collectors.maxBy(dishCaloriesComparator));
//传入比较器,得到最大的Dish
2.汇总 summingInt
Collectors.summingInt 它接受一个把对象映射所求和所需int的函数,并返回一个收集器。
int totalCalories = menu.stream()
.collect(summingInt(Dish::getCalories));
Collectors.summingLong 和 Collectors.summingDouble 方法的作用完全一样,可以用于求和字段为 long 或 double 的情况。
但汇总不仅仅是求和;还有 Collectors.averagingInt,连同对应的 averagingLong 和 averagingDouble 可以计算数值的平均数:
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
但如果你需要获得更多这样的结果你可以使用summarizingInt 工厂方法返回的收集器,它可以一次性获得以上所有的结果:
IntSummaryStatistics menuStatistics = menu.stream()
.collect(summarizingInt(Dish::getCalories));
//你可以观察到一个非常特殊的结果:
//IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}
同理,还有的summarizingLong和summarizingDouble工厂方法有相关的LongSummaryStatistics 和 DoubleSummaryStatistics 类型,适用于收集的属性是原始类型 long 或 double 的情况。
3.字符串串联 joining
joining 工厂方法返回的收集器会把对流中每一个对象应用 toString 方法得到的所有字符串连接成一个字符串。
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
//porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
//中间没有标点符号,所以这些菜谱名字都结到一块去了
这样做的可读性并不好,所以joing有个重载版本可以接受元素之间的分界符:
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
//pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
同时,如果还可以添加前缀符和后缀符
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", ","[","]"));
//[pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
4. 归约汇总 reducing
Collectors.reducing 工厂方法也可以直接返回一个归约汇总:
int totalCalories = menu.stream()
.collect(reducing( 0,Dish::getCalories,(i, j) -> i + j));
它需要三个参数。
- 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值 和而言 0 是一个合适的值。
- 第二个参数就是将菜肴转换成一个表示其所含热量的 int。
- 第三个参数是一个 BinaryOperator 函数表达式,描述了将两个项目如何进行累积操作。这里它就是 对两个 int 求和。
同理也能通过这种方式来找到热量最高的菜:
Optional mostCalorieDish = menu.stream()
.collect(reducing((d1, d2)->d1.getCalories() > d2.getCalories() ? d1 : d2));
而这种单参数的reducing是一种特殊情况,它把流中的第一个项目作为起点,把恒等函数作为一个转换函数.这意味着它没有起点,最终会返回一个Optional<Dish>对象.
reduce方法和Stream接口的collect方法有何不同?
reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设计就是要改变容器,从而累积要输出的结果。
在多线程并发修改同一个数据结构很可能会导致List本身遭到破坏!所以你如果需要保证线程安全的情况下collect方法特别适合表达可变容器上的归约
进一步理解:
int totalCalories = menu.stream()
.collect(reducing(0, //初始值
Dish::getCalories, //转换函数
Integer::sum)); //积累函数
但这里有个要点,必须要注意:
List<Dish> menu = dishService.getDishList();
//实例1:
String shortMenu = menu.stream()
.map(Dish::getName)
.collect( reducing ( (s1, s2) -> s1 + s2 ) ).get();
//这里获取的字符串,而没有初始值的reducing返回的也是字符串
//实例2:
String shortMenu = menu.stream()
.collect(reducing( "",Dish::getName, (s1, s2) -> s1 + s2 ) );
//这里初始值是字符串类型,而不断叠加的对象也是字符串类型
//实例3
String shortMenu = menu.stream()
.collect(reducing((d1, d2) -> d1.getName() + d2.getName())).get();
//这里就错误了,传入的时Dish对象流,而最终收集者要求返回String对象流,前后不一致导致无法编译。
5.分组 groupingBy
用 Collectors.groupingBy工厂方法返回 的收集器就可以轻松地完成类似于数据库分组的任务,如下所示:
Map<Dish.Type,List<Dish> dishesByType = menu.stream()
.collect(groupingBy(Dish::getType));
//返回结果:
//{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}
分组有个重载方法,允许我们传入一个Collector类型的参数,通过这种方式,我们可以传入过滤的谓词(filtering),现在我们想要筛选出卡路里大于500的菜肴
Map<Dish.Type,List<Dish> caloricDishesByType = menu.stream()
.collect(groupingBy(Dish::getType,
filtering(dish -> dish.getCalories() > 500, toList())));
//{OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
如果你需要转换类型,那么你可以使用mapping
Map<Dish.Type,List<Dish>> dishNamesByType = menu.stream()
.collect(groupingBy(Dish::getType,
mapping(Dish::getName, toList())));
//{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}
6.partitioningBy 分区
分区是分组的特殊情况由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组 Map 的键类型是 Boolean,于是它最多可 以分为两组——true 是一组,false 是一组。
Map<Boolen,List<Dish>> partitionedMenu = menu.stream()
.collect(partitioningBy(Dish::isVegetarian));
//分区函数,判断条件就是蔬菜,返回Boolean
//结果:
//{false=[pork, beef, chicken, prawns, salmon],
// true=[french fries, rice, season fruit, pizza]}
相比于groupingBy,partitioningBy的效果更加清晰,紧凑,高效.
实战之质数分区
1.首先我们需要将前n个自然数分为指数和非质数,但在这之前,找出测试某个待测数字是否是质数的谓词会很有帮助:
public boolean isPrime(int candidate){
return IntStream.rangeClosed(2, candidate) //range出从2到传入整数的所有数字
.noneMatch(i -> candidate % i == 0); //所有数字去除去传入的整数,如果等于0代表它不是质数
//如果该数字不能被任何数字整除,那么则返回true
}
2.优化操作,如果使用平方根,那么我们的操作会少不少
public boolean isPrime(int candidate){
int candidateRoot = (int) Math.sqrt((double) candidate); //对传入的整数进行平方根操作;
return IntStream.rangeClosed(2, candidateRoot )
.noneMatch(i -> candidate % i == 0); //列出从2到平方根得所有整数去整除;
}
3.分流操作:
public Map<Boolean,List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2, n).boxed()
//这个是个Int特化流,所以并没有进行装箱,所以需要boxed()进行装箱
.collect(
partitioningBy(candidate -> isPrime(candidate)));
}
非常简单~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~