Java8的filter

前言

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
/** 
* 水果类,包含水果名称与水果价格
* 单独放在Fruit.java文件中
* P.S.:需要在pom中的dependency加上lombok依赖
* */
@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。所以,刚刚取不取消看心情,就是因为这个。

这段筛选代码是什么意思呢?

  1. fruits.stream():将fruits转换为Stream
  2. .filter(fruit -> fruit.getPrice() <= 10.0):筛选出价格小于等于10.0的水果。
  3. .sorted(Comparator.comparing(Fruit::getPrice)):对筛选出的水果按照价格排序。
  4. .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

这三个放在一起是因为他们往往是配套使用的。findFirstfindAny分别表示寻找到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好!