前言
在Python
中,如果需要对list
进行筛选,直接给出b = [i for i in a if i > 5]
就行。但Java
中,筛选又该怎么表述?
构造测试类
我们假设有这么一个数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@Data public class Fruit { private String name; private double price; @Override public String toString() { return "Fruit{name='" + name + '\'' + ", price=" + price + '}'; } }
|
生成测试集
然后我们在另一个Main.java
文件中加入初始化列表的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Main { public static List<Fruit> generateFruits() { List<Fruit> fruits = new ArrayList<>(); fruits.add(new Fruit("apple", 1.0)); fruits.add(new Fruit("banana", 2.0)); fruits.add(new Fruit("orange", 3.0)); fruits.add(new Fruit("pear", 4.0)); fruits.add(new Fruit("grape", 5.0)); fruits.add(new Fruit("watermelon", 6.0)); fruits.add(new Fruit("pineapple", 7.0)); fruits.add(new Fruit("mango", 8.0)); fruits.add(new Fruit("strawberry", 9.0)); fruits.add(new Fruit("blueberry", 10.0)); fruits.add(new Fruit("raspberry", 11.0)); fruits.add(new Fruit("blackberry", 12.0)); fruits.add(new Fruit("cranberry", 13.0)); fruits.add(new Fruit("apricot", 14.0)); fruits.add(new Fruit("cherry", 15.0)); fruits.add(new Fruit("plum", 16.0)); fruits.add(new Fruit("peach", 17.0)); fruits.add(new Fruit("pomegranate", 18.0)); fruits.add(new Fruit("kiwi", 19.0)); fruits.add(new Fruit("mangosteen", 20.0)); fruits.add(new Fruit("papaya", 21.0)); fruits.add(new Fruit("guava", 22.0)); fruits.add(new Fruit("lychee", 23.0)); fruits.add(new Fruit("passion fruit", 24.0)); fruits.add(new Fruit("coconut", 25.0)); fruits.add(new Fruit("dragon fruit", 26.0)); fruits.add(new Fruit("pomegranate", 27.0)); return fruits; } }
|
这个时候肯定会报错的。因为一般的,类中不会允许你有静态方法,有也只能是main
方法。IDEA
也一定会让你把static
取消的。现在取不取消先看心情吧。
筛选逻辑
然后我们加入主函数逻辑:
1 2 3 4 5 6 7 8
| public static void main(String[] args) { List<Fruit> fruits = generateFruits(); List<Fruit> filteredFruits = fruits.stream().filter(fruit -> fruit.getPrice() <= 10.0) .sorted(Comparator.comparing(Fruit::getPrice)) .collect(Collectors.toList()); for (Fruit f : filteredFruits) System.out.println(f.toString()); }
|
众所周知,Java
中静态方法中不允许调用非静态方法。如果之前你删除了generateFruits
方法的static
,你现在完善了main
方法后,IDEA
又会要求你加上static
。所以,刚刚取不取消看心情,就是因为这个。
这段筛选代码是什么意思呢?
fruits.stream()
:将fruits
转换为Stream
。
.filter(fruit -> fruit.getPrice() <= 10.0)
:筛选出价格小于等于10.0的水果。
.sorted(Comparator.comparing(Fruit::getPrice))
:对筛选出的水果按照价格排序。
.collect(Collectors.toList())
:将筛选出的水果收集到一个新的List
中。
完成后,我们使用for
循环遍历筛选出的水果,并使用System.out.println()
打印每个水果的信息,也就是调用toString()
方法获取了水果的名称和价格。
再复杂一点
除了筛选与排序,我们的米奇妙妙List
还有一些其他的米奇妙妙小工具。
map
我们可以将main
方法魔改成这个样子:
1 2 3 4 5 6 7 8 9
| public static void main(String[] args) { List<Fruit> fruits = generateFruits(); List<String> filteredFruitNames = fruits.stream() .filter(fruit -> fruit.getPrice() <= 10.0) .map(Fruit::getName) .collect(Collectors.toList()); for (String s : filteredFruitNames) System.out.println(s); }
|
发现了吗?我们如果并不关系其他的字段,而只需要筛选出name
字段的时候,就可以使用map
方法。使用map(${class}::${field})
,我们就可以针对原本filter
筛选出来的${class}
的实例化对象中,提取出其中的${field}
字段。
很方便对吧?
toMap
我们筛选出来的内容在之后可能会以JSON
格式给出来,这个也就在后续步骤中要求我们将List
修改为Map
对象或者VO
对象。而如果直接使用toMap
,虽然原理上也是类似的,但是过程将会被简化。
我们可以将main
方法魔改成这个样子:
1 2 3 4 5 6 7 8
| public static void main(String[] args) { List<Fruit> fruits = generateFruits(); Map<String, Fruit> fruitMap = fruits.stream() .filter(fruit -> fruit.getPrice() <= 10.0) .collect(Collectors.toMap(Fruit::getName, fruit -> fruit)); for (Map.Entry<String, Fruit> entry : fruitMap.entrySet()) System.out.println(entry.getKey() + ": " + entry.getValue().toString()); }
|
当然,这个只是一个示例。没事为啥要把好不容易查出来的Fruit
对象又包装一层呢。
不过这个示例确实说明了toMap
的功能,能够相当灵活的配置输出的键值对。
当然,这个并没有真正的HashMap
那么智能,没有办法避免重复的键。这就需要我们额外设置取舍逻辑。比如,我们可以保留遇到的第一个键:
1 2 3 4 5 6 7
| Map<String, Fruit> fruitMap = fruits.stream() .filter(fruit -> fruit.getPrice() <= 10.0) .collect(Collectors.toMap( Fruit::getName, fruit -> fruit, (f1, f2) -> f1 ));
|
发现了吗,我们在fruit -> fruit
后面增加了一个逻辑:(f1, f2) -> f1
。这个代表着如果遇到重复的键,保留第一个。同样的,如果设置(f1, f2) -> f2
,那么将会保留第二个。
比较少见的
除了以上用到的以外,还有一些比较少用的,用处基本上算得上是SQL
的补充。
distinct
在筛选结果之后,如果我们使用toMap
删掉了我们并不关心的字段,最终获得了若干列完全一样的map
,那么我们就需要进行去重,避免部分内容在特定的业务下被重复计算。
而我们只需要这么使用就好:
1 2 3
| Map<String, Fruit> fruitMap = fruits.stream() .filter(fruit -> fruit.getPrice() <= 10.0).distinct() .collect(Collectors.toMap(Fruit::getName, fruit -> fruit, (f1, f2) -> f1));
|
对,就是在filter
后面增加一个distinct()
。
findFirst、findAny以及orElse
这三个放在一起是因为他们往往是配套使用的。findFirst
与findAny
分别表示寻找到filter
中搜索到的第一个与任意一个,两者并不能同时使用;而orElse
则表示如果找不到,则返回一个默认值。
我们可以将main
方法魔改成这个样子:
1 2 3 4 5 6 7 8 9 10 11
| public static void main(String[] args) { List<Fruit> fruits = generateFruits(); Fruit f = fruits.stream() .filter(fruit -> fruit.getPrice() <= 10.0) .findFirst() .orElse(null); if (f != null) System.out.println(f.toString()); else System.out.println("not found"); }
|
这就是说,查找第一个价格小于等于10.0的水果,如果找不到,则返回null
。如果加上sort
,那这就意味着找到价格最便宜的水果。
当然,我们可以不用局限于这个索引,毕竟我们并不太希望关心查询结果列表中到底是怎么样排布的。所以可以继续魔改:
1 2 3 4 5 6 7 8 9 10 11
| public static void main(String[] args) { List<Fruit> fruits = generateFruits(); Fruit f = fruits.stream() .filter(fruit -> fruit.getPrice() <= 10.0) .min(Comparator.comparing(Fruit::getPrice)) .orElse(null); if (f != null) System.out.println(f.toString()); else System.out.println("not found"); }
|
加上了min
方法后,整段代码的意义就变成了:找到了最便宜的水果,如果找不到,则返回null
。
虽然这些内容都能够在SQL
中获取到,不过如何使用还是看整体团队的风格进行取舍。有些人喜欢用SQL
一把完成,因为这样高效。但是相应的,这样的方法也使得维护基本上是不可能的。我想,后台越来越大也是因为这样吧。
最后说说为什么要用stream
stream的优点在于,我们能够很方便的进行筛选与排序,并且能够很方便的进行map
操作。
实际上,stream
的效率往往是不如for
循环的。不管你数据量是大还是不大,效率就是不如人家。
但是呢,stream
写代码的过程中,能变得简洁(说白了就是能缩行),所以stream
好!