我的博客:zhenganwen.topjava
函数式编程给个人直观感觉:数据库
假设你如今是一个农场主,你采摘了一筐苹果以下:编程
List<Apple> apples = Arrays.asList(
new Apple("red", 100),
new Apple("red", 300),
new Apple("red", 500),
new Apple("green", 200),
new Apple("green", 400),
new Apple("green", 600),
new Apple("yellow", 300),
new Apple("yellow", 400),
new Apple("yellow", 500)
);
复制代码
Apple
数组
@Data
@AllArgsConstructor
public class Apple {
private String color;
private int weight;
}
复制代码
如今须要你编写一个方法,挑选出箩筐中颜色为绿色的苹果,因而你垂手可得地写了以下代码:缓存
@Test
public void pickGreenApples() {
List<Apple> list = new ArrayList<>();
for (Apple apple : apples) {
if (Objects.equals(apple.getColor(), "green")) {
list.add(apple);
}
}
System.out.println(list);
}
[Apple(color=green, weight=200), Apple(color=green, weight=400), Apple(color=green, weight=600)]
复制代码
若是须要你挑选出红色的呢?你发现能够将按照颜色挑选苹果抽取出来以供复用:bash
public void pickByColor(List<Apple> apples, String color) {
for (Apple apple : apples) {
if (Objects.equals(apple.getColor(), color)) {
System.out.println(apple);
}
}
}
@Test
public void testPickByColor() {
pickByColor(apples, "red");
}
Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)
复制代码
好了,如今我须要你挑选出重量在400g以上,颜色不是绿色的苹果呢?你会发现,根据不一样的颜色和重量挑选标准可以组合成若干的挑选策略,那是否是针对每一个策略咱们都须要编写一个方法呢?这固然不可能,咱们也没法事先预知顾客须要什么样的苹果。并发
如今咱们来理一下业务需求,其实也就是咱们给顾客一个苹果,由他来判断是否符合他的食用标准,这能够采用策略模式来实现:app
将判断某个苹果是否符合标准这一行为抽象为一个接口:框架
public interface AppleJudgementStrategy {
/** * 你给我一个apple,我判断他是否符合挑选出来的标准 * @param apple the apple in container * @return true if apple need be picked */
boolean judge(Apple apple);
}
复制代码
挑选苹果的方法根据策略挑选苹果(面向接口编程,灵活性更大)dom
public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
for (Apple apple : apples) {
if (strategy.judge(apple)) {
// apple 符合既定的挑选策略
System.out.println(apple);
}
}
}
复制代码
业务方法根据实际的业务需求建立具体的挑选策略传给挑选方法
@Test
public void testPickByStrategy() {
// 挑选400g以上且颜色不是绿色的
pickByStrategy(apples, new AppleJudgementStrategy() {
@Override
public boolean judge(Apple apple) {
return apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green");
}
});
}
Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
复制代码
那么以上的代码重构是无需基于Java 8的,但其中有一个弊端:策略模式的实现要么须要建立一个新类,要么使用匿名类的方式。在此过程当中,new/implements AppleJudgementStrategy
和public boolean judge
的大量重复是很冗余的,Java 8引入的Lambda
表达式就很好的改善了这一点,如上述代码可简化以下:
@Test
public void testPickByStrategyWithLambda() {
pickByStrategy(apples, apple -> apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green"));
}
Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
复制代码
本节经过挑选苹果的例子初步体验了一下Lambda
表达式的魅力,但其的做用不只于此,接下来让咱们打开函数式编程的大门。
正如上一节说表现的,面对同一业务(挑选苹果)的需求的频繁变动,业务方法仅能接受基本类型、引用类型(对象类型)的参数已经不能知足咱们的需求了,咱们指望可以经过参数接受某一特定的行为(如判断某个苹果是否应该被挑选出来)以使方法可以“以不变应万变”。
上例中,虽然咱们能经过策略模式达到此目的,但若是究其本质,策略接口其实就是对一个函数的封装,而咱们业务方法参数接收该对象也仅仅是为了调用该对象实现的接口方法。不管咱们是在调用业务方法以前建立一个该接口的实现类而后在调用业务方法传参时new
该实现类,仍是在调用业务方法传参时直接new
一个匿名类,这些建立类的操做都只是为了遵照“在Java中,类是第一公民的事实”(即先有类,后有方法,方法必须封装在类中)。所以,建立类和声明方法的过程显得有些多余,其实业务方法只是想要一个具体的策略而已,如return apple.getWeight() >= 400
(挑选出重量大于400g的苹果)。
所以你会看到Java 8引入Lambda
表达式以后,业务调用方能够变动以下:
// 业务方法
public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
for (Apple apple : apples) {
if (strategy.judge(apple)) {
// apple 符合既定的挑选策略
System.out.println(apple);
}
}
}
// 调用业务
@Test
public void testPickByStrategy() {
// 挑选400g以上的
pickByStrategy( apples, (apple) -> { return apple.getWeight() >= 400 } );
}
复制代码
咱们使用一个Lambda
表达式(apple) -> { return apple.getWeight() >= 400 }
代替了AppleJudgementStrategy
实例的建立
Lambda
表达式能够理解为函数表达式,在上例中指的就是(apple) -> { return apple.getWeight() >= 400 }
,表示对接口AppleJudgementStrategy
函数boolean judge(Apple apple);
的一个实现。其中(apple)
表示函数接受一个Apple
类型的对象;{ return apple.getWeight() >= 400 }
则是函数体,表示传入Apple
对象的重量大于400时返回true
。
Lambda
表达式和接口是密切相关的,并且能使用Lambda
表达式代替其实例的接口必须是只声明了一个抽象方法的接口
default
或static
方法@FunctionalInterface
以表示该接口是函数式接口,其实例能够经过Lambda
表达式建立,而且该注解能够约束该接口知足上述三点规则注意:Java 8 对接口进行了从新定义,为了得到更好的兼容性,接口中的方法能够有方法体,此时该方法需被标记为
default
,即该方法有一个默认的实现,子类能够选择性的重写。不像抽象方法,必须被具体子类重写。此外接口中还能够定义带有方法体的静态方法,能够经过
接口名.方法名
的形式访问,这一点与类静态方法无异。上述两点打破了Java 8 以前对接口的定义:接口中的方法必须都是抽象方法(
public abstract
)。
也就是说在AppleJudgementStrategy
添加若干default
或static
方法,都是不影响使用Lambda
表达式来代替其实例的(在IDE中,@FunctionalInterface
注解不会报红表示这是一个合法的函数式接口):
@FunctionalInterface
public interface AppleJudgementStrategy {
/** * 你给我一个apple,我判断他是否符合挑选出来的标准 * @param apple the apple in container * @return true if apple need be picked */
boolean judge(Apple apple);
default void fun1() {
System.out.println("这是带有默认实现的方法");
}
static void fun2() {
System.out.println("这是定义在接口中的静态方法");
}
}
复制代码
有了函数式接口以后咱们就能够在须要该接口实例的地方使用lambda
表达式了。
下面咱们经过实战来巩固lambda
表达式的运用。
编写函数式接口:
@FunctionalInterface
public interface AccumulatorFunction {
/** * 该函数聚合两个整数经过运算生成一个结果 * @param a 整数a * @param b 整数b * @return 运算结果 */
int accumulate(int a, int b);
}
复制代码
编写业务方法,参数接受函数式接口实例,让该参数具有行为能力
/** * 经过既定的计算规则对输入值a和b得出输出结果 * @param a * @param b * @param accumulatorFunction * @return */
public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
return accumulatorFunction.accumulate(a,b);
}
复制代码
编写业务调用方,经过lambda
表达式阐述行为
@Test
public void testAccumulatorFunction() {
int res1 = compute(1, 2, (a, b) -> { //阐述的行为是求和
return a + b;
});
System.out.println("1加2的结果是:" + res1);
int res2 = compute(1, 2, (a, b) -> { //阐述的行为是求乘积
return a * b;
});
System.out.println("1乘2的结果是:" + res2);
}
1加2的结果是:3
1乘2的结果是:2
复制代码
经过上一节咱们知道lambda
表达式是和函数式接口密切相关的,在须要传入函数式接口实例时咱们能够编写lambda
表达式,编写时咱们要特别关注该接口抽象方法定义的参数列表以及返回值。
假设函数式接口声明以下(其中R,T1,T2,T3
为基本类型或引用类型):
@FunctionalInterface
public interface MyFunction{
R fun(T1 t1, T2 t2, T3 t3);
}
复制代码
那么你的lambda
表达式就应该编写以下:
(t1, t2, t3) -> { // 参数名自定义,为a,b,c也能够,可是要知道在方法体中访问时a的类型是T1,b的类型是T2
// do you service with t1,t2,t3
return instance_of_R;
}
复制代码
总结以下,严格意义上的lambda
表达式需包含:
return
语句做为结尾可是为了书写简洁,上述几点有时不是必须的。
t1 -> {
// do your service with t1
return instance_of_R;
}
复制代码
其余状况下(包括无参时)都必须有小括号
当函数体只有一条语句时,如return t1+t2+t3
(假设t1,t2,t3,R
都是int
型),那么方法体能够省略大括号和return
:
(t1,t2,t3) -> t1+t2+t3
复制代码
只要函数体包含了语句(必须以分号结尾),那么函数体就须要加上大括号
在Java 8 中,为咱们新增了一个java.util.function
包,其中定义的就所有都是函数式接口,其中最为主要的接口以下:
Consumer
,接受一个参数,没有返回值。表明了消费型函数,函数调用消费你传入的参数,但不给你返回任何信息Supplier
,不接收参数,返回一个对象。表明了生产型函数,经过函数调用可以获取特定的对象Predicate
,接收一个对象,返回一个布尔值。表明了断言型函数,对接收的对象断言是否符合某种标注。(咱们上面定义的AppleJudgementFunction
就属于这种函数。Function
,接收一个参数,返回函数处理结果。表明了输入-输出型函数。该包下的其余全部接口都是基于以上接口在参数接受个数(如BiConsumer
消费两个参数)、和参数接收类型(如IntConsumer
仅用于消费int
型参数)上作了一个具体化。
对每一个苹果进行“消费”操做:
public void consumerApples(List<Apple> apples, Consumer<Apple> consumer) {
for (Apple apple : apples) {
consumer.accept(apple);
}
}
复制代码
如“消费”操做就是将传入的苹果打印一下:
@Test
public void testConsumer() {
consumerApples(apples, apple -> System.out.println(apple));
}
Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)
Apple(color=green, weight=200)
Apple(color=green, weight=400)
Apple(color=green, weight=600)
Apple(color=yellow, weight=300)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
复制代码
若是你使用的IDE是IDEA,那么它会提示你System.out.println(apple)
能够Lambda can be replaced with method inference
(即该lambda
表达式可使用方法推导),因而咱们使用快捷键alt + Enter
寻求代码优化提示,表明被替换成了apple -> System.out::println
。
这里引伸出了lambda
的一个新用法:方法推导。当咱们在编写lambda
时,若是方法体只是一个表达式,而且该表达式调用的方法行为与此处对应的函数接口的行为一致时,可使用方法推导(类名::方法名
或对象名::方法名
)。
由于这里咱们须要一个Consumer
,而println
的定义与Consumer.accept
的函数行为是一致的(接受一个对象,无返回值):
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
复制代码
所以IDEA提示咱们此处可使用方法推导apple -> System.out::println
代替方法调用apple -> System.out.println(apple)
。如此,前者看起来更具可读性:使用println
行为消费这个apple
。
Supplier
像是一个工厂方法,你能够经过它来获取对象实例。
如,经过Supplier
你给我一个苹果,我来打印它的信息
public void printApple(Supplier<Apple> appleSupplier) {
Apple apple = appleSupplier.get();
System.out.println(apple);
}
@Test
public void testSupplier() {
printApple(() -> {
return new Apple("red", 666);
});
}
Apple(color=red, weight=666)
复制代码
若是Apple
提供无参构造方法,那么这里可使用构造函数的方法推导(无参构造函数不接收参数,但返回一个对象,和Supplier.get
的函数类型一致):
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Apple {
private String color;
private int weight;
}
@Test
public void testSupplier() {
// printApple(() -> {
// return new Apple("red", 666);
// });
printApple(Apple::new);
}
Apple(color=null, weight=0)
复制代码
Predicate
则是用来断言对象,即按照某种策略断定入参对象是否符合标准的。
为此咱们能够将先前写的挑选苹果的业务方法中的AppleJudgementStrategy
换成Predicate
,做用是同样的,之后用到策略模式的地方直接使用Predicate<T>
就好了
// public void pickByStrategy(List<Apple> apples, AppleJudgementStrategy strategy) {
// for (Apple apple : apples) {
// if (strategy.judge(apple)) {
// // apple 符合既定的挑选策略
// System.out.println(apple);
// }
// }
// }
public void pickByStrategy(List<Apple> apples, Predicate<Apple> applePredicate) {
for (Apple apple : apples) {
if (applePredicate.test(apple)) {
// apple 符合既定的挑选策略
System.out.println(apple);
}
}
}
@Test
public void testPickByStrategyWithLambda() {
pickByStrategy(apples, apple ->
apple.getWeight() >= 400 && !Objects.equals(apple.getColor(),"green"));
}
Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
复制代码
Function
就不用说了,表明了最普通的函数描述:有输入,有输出。
咱们此前编写的AccumulatorFunction
就属于一个BiFunction
,让咱们来使用BiFucntion
对其进行改造:
// public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
// return accumulatorFunction.accumulate(a,b);
// }
public int compute(int a, int b, BiFunction<Integer,Integer,Integer> biFunction) {
return biFunction.apply(a,b);
}
@Test
public void testAccumulatorFunction() {
int res1 = compute(1, 2, (a, b) -> {
return a + b;
});
System.out.println("1加2的结果是:" + res1);
int res2 = compute(1, 2, (a, b) -> {
return a * b;
});
System.out.println("1乘2的结果是:" + res2);
}
1加2的结果是:3
1乘2的结果是:2
复制代码
如今有一个菜品类定义以下:
public class Dish {
public enum Type {MEAT, FISH, OTHER}
private final String name; //菜品名称
private final boolean vegetarian; //是不是素食
private final int calories; //提供的卡路里
private final Type type; //菜品类型
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return name;
}
}
复制代码
给你一份包含若干菜品的菜单:
List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH));
复制代码
如今要你以卡路里升序的方法打印卡路里在400如下的菜品名称清单,在Java 8 以前,你可能须要这样作:
@Test
public void beforeJava8() {
// 1. filter which calories is lower than 400 and collect them
List<Dish> filterMenu = new ArrayList<>();
for (Dish dish : menu) {
if (dish.getCalories() < 400) {
filterMenu.add(dish);
}
}
// 2. sort by calories ascending
Collections.sort(filterMenu, new Comparator<Dish>() {
@Override
public int compare(Dish o1, Dish o2) {
return o1.getCalories() - o2.getCalories();
}
});
// 3. map Dish to Dish.getName and collect them
List<String> nameList = new ArrayList<>();
for (Dish dish : filterMenu) {
nameList.add(dish.getName());
}
// print name list
System.out.println(nameList);
}
[season fruit, prawns, rice]
复制代码
在Java 8以后,经过Stream
只需简洁明了的一行代码就能搞定:
@Test
public void userJava8() {
List<String> nameList = menu.stream()
// 1. filter which calories is lower than 400
.filter(dish -> dish.getCalories() < 400)
// 2. sort by calories ascending
.sorted(Comparator.comparing(Dish::getCalories))
// 3. map Dish to Dish.getName
.map(Dish::getName)
// 4. collect
.collect(Collectors.toList());
System.out.println(nameList);
}
[season fruit, prawns, rice]
复制代码
Stream
的强大才刚刚开始……
《Java 8 In Action》给出的解释以下:
定义
Sequence of elements
——Stream
是一个元素序列,跟集合同样,管理着若干类型相同的对象元素Source
——Stream
没法凭空产生,它从一个数据提供源而来,如集合、数组、I/O资源。值得一提的是,Stream
中元素组织的顺序将听从这些元素在数据提供源中组织的顺序,而不会打乱这些元素的既有顺序。Data processing operations
——Stream
提供相似数据库访问同样的操做,而且只需传入lambda
表达式便可按照既定的行为操纵数据,如filter(过滤)
、map(映射)
、reduce(聚合)
、find(查找)
、match(是否有数据匹配)
、sort(排序)
等。Stream
操做还能够被指定为串行执行或并行执行以充分利用多CPU核心。特性
Pipelining
——大多数Stream
操做返回的是当前Stream
对象自己,所以能够链式调用,造成一个数据处理的管道,可是也有一些terminal
操做会终结该管道,如调用Stream
的collect
方法后表示该数据处理流的终结,返回最终的数据集合Internal iteration
——Stream
将元素序列的迭代都隐藏了,咱们只需提供数据处理流中的这一个阶段到下一个阶段的处理行为(经过调用Stream
方法传入lambda
表达式)。上一节菜品的例子中数据处理流可描述以下(其中的limit
表示取前n
个元素):
Stream
内部集成了Java 7 提供的ForkJoin
框架,当咱们经过调用它的parallel
开启并行执行开关时,Stream
会将数据序列的处理经过ForkJoinPool
进行并行化执行(不只仅是开启多个线程,底层还会根据你CPU核心数量将子任务分配到不一样的核心执行):
@Test
public void userJava8() {
List<String> nameList = menu.stream().parallel()
.filter(dish -> {
System.out.println(Thread.currentThread().getName());
return dish.getCalories() < 400;
})
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
System.out.println(nameList);
}
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-3
main
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-1
[season fruit, prawns, rice]
复制代码
不然在当前线程串行化执行:
@Test
public void userJava8() {
List<String> nameList = menu.stream()
.filter(dish -> {
System.out.println(Thread.currentThread().getName());
return dish.getCalories() < 400;
})
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
System.out.println(nameList);
}
main
main
main
main
main
main
main
main
main
[season fruit, prawns, rice]
复制代码
上文曾提到,Stream
是没法凭空而来的,须要一个数据提供源,而咱们最多见的就是数组和集合了。
做为一个接口,咱们是没法经过new Stream
获取其实例的,获取其实例的经常使用方法有如下几种
经过Collection
接口中的stream()
方法,从一个集合实例中建立一个Stream
,对同一个集合对象调用屡次stream()
会产生不一样的Stream
实例。
开篇挑选苹果的例子中,apples.stream
就是调用了此方法
经过Arrays.stream()
,由一个数组(引用类型或基本类型组)建立一个Stream
@Test
public void testCreateStream() {
Arrays.stream(new Object[]{"hello",21,99.9,true}).forEach(System.out::println);
}
hello
21
99.9
true
复制代码
Stream.of(T t)
能够建立一个仅包含一个元素的Stream
Stream.of(T... t)
能够从一个变长参列(其实是一个数组)建立一个Stream
使用IntStream/LongStream/DoubleStream
中的方法
IntStream
中的range/rangeClosed (int, int)
能够建立一个包含指定开区间/闭区间中全部元素的Stream
,LongStream
也同样,但DoubleStream
由于区间中的浮点数有无数个所以没有此方法
IntStream.rangeClosed(0, 4).forEach(i -> {
new Thread(() -> {
System.out.println("I am a thread, my name is " + Thread.currentThread().getName());
}, "t-" + i).start();
});
I am a thread, my name is t-0
I am a thread, my name is t-1
I am a thread, my name is t-2
I am a thread, my name is t-3
I am a thread, my name is t-4
复制代码
generate(Supplier s)
,三者都有此方法,经过一个生产策略来建立一个无穷大的Stream
,若是在此Stream
上进行操做那么每处理完一个元素都会经过调用s.get
获取下一个要处理的元素。
final int a = 0;
IntStream.generate(() -> a).forEach(System.out::println);
复制代码
你会发现上述程序会一直打印0
。
经过
generate
建立的stream
,若是在之上进行operation
,那么该processing
会一直进行下去只有遇到异常时才会中止
IntStream.generate(i::getAndIncrement).forEach(e -> {
System.out.println(e);
if (e == 5) {
throw new RuntimeException();
}
});
0
1
2
3
4
5
java.lang.RuntimeException
复制代码
Stream
的数据操纵方法分为terminal
和non-terminal
,non-terminal
操做以后能够继续链式调用其余operation
,造成一个流式管道,而terminal
操做则会终止数据流的移动。Stream
中方法返回Stream
(实际返回的是当前Stream
实例自己)的都是non-terminal option
调用Stream
的filter
并传入一个Predicate
能够对元素序列进行过滤,丢弃不符合条件(是否符合条件根据你传入的Predicate
进行判断)的元素
// print the even number between 1 and 10
IntStream.rangeClosed(1, 10).filter(i -> i % 2 == 0).forEach(System.out::println);
2
4
6
8
10
复制代码
@Test
public void testDistinct() {
Stream.of("a", "b", "a", "d", "g", "b").distinct().forEach(System.out::println);
}
a
b
d
g
复制代码
distinct
会根据equals
方法对“相等”的对象进行去重。
至关于SQL
的limit <offset,rows>
语句中的offset
public void testSkip() {
IntStream.rangeClosed(1,10).skip(6).forEach(System.out::println);
}
7
8
9
10
复制代码
至关于SQL
的limit <offset,rows>
语句中的rows
// 丢弃前6个以后Stream只剩下7~10了,再截取前2个,Stream就只有7和8了
IntStream.rangeClosed(1, 10).skip(6).limit(2).forEach(System.out::println);
7
8
复制代码
map
方法接收一个Function
(接收一个对象,返回一个对象),返回什么对象、需不须要借助传入的对象信息就是映射的逻辑,根据你的所需你能够将Stream
中的全部元素统一换一个类型。
// 从一份菜单(菜品集合)中提取菜品名称清单
List<String> nameList = menu.stream().map(dish -> dish.getName()).collect(Collectors.toList());
System.out.println(menu);
[pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
复制代码
flatMap
能够将你的元素进一步细粒度化,如从某个文件读取文件内容后根据换行符分割建立一个以“行”为元素的Stream
,若是你想进一步将“行”按照“空格”分割获得以“字词”为元素的Stream
,那么你就可使用flatMap
。
你须要传入一个Function<T,Stream>
:传入一个元素,返回一个包含由该元素分割造成若干细粒度元素的Stream
Stream.of("hello", "world").
flatMap(str -> Stream.of(str.split(""))).
forEach(System.out::println);
h
e
l
l
o
w
o
r
l
d
复制代码
sort
须要你传入一个定义了排序规则的Comparator
,它至关于一个BiPredicate
// 将菜品按照卡路里升序排序
menu.stream().
sorted((dish1,dish2)->dish1.getCalories()-dish2.getCalories())
.map(Dish::getCalories)
.forEach(System.out::println);
120
300
350
400
450
530
550
700
800
复制代码
你会发现IDEA提示你(dish1,dish2)->dish1.getCalories()-dish2.getCalories()
能够简化为Comparator.comparingInt(Dish::getCalories)
,使用comparing
你能够直接传入排序字段如getColaries
,它会自动帮咱们封装成一个升序的BiPredicate
,若是你须要降序则再链式调用reversed
便可:
menu.stream().
sorted(Comparator.comparingInt(Dish::getCalories).reversed())
.map(Dish::getCalories)
.forEach(System.out::println);
800
700
550
530
450
400
350
300
120
复制代码
这是一个terminal option
,当Stream
调用match
以后会返回一个布尔值,match
会根据你传入的Predicate
返回true or false
,表示当前Stream
中的全部元素是否存在至少一个匹配(anyMatch
)、是否所有匹配(allMatch
)、是否全都不匹配(noneMatch
)。
这几仅以anyMatch
的使用示例:
// 菜单中是否有卡路里小于100的菜品
boolean res = menu.stream().anyMatch(dish -> dish.getCalories() < 100);
System.out.println(res);
false
复制代码
这也是一个terminal operation
。一般用于在对Stream
进行一系列处理以后剩下的元素中取出一个进行消费。
findAny
,一般用于并行处理Stream
时,获取最早走完整个数据处理流程的元素。
好比对于一个id
,可能会开几个线程并行地调接口、查数据库、查缓存、查ES的方式获取商品数据,但只要有其中一个方式成功返回数据那么就直接消费这个数据,其余方式不予等待。
static Random random = new Random();
public Object queryById(Integer id){
// 随机睡眠0~6秒,模仿从数据库或者调接口获取数据的过程
try {
TimeUnit.MILLISECONDS.sleep(random.nextInt(6000));
String data = UUID.randomUUID().toString();
System.out.println("get data -> " + data + "[id=" + id + "]");
return data;
} catch (InterruptedException e) {
e.printStackTrace();
return e;
}
}
@Test
public void testFindAny() throws InterruptedException {
Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
// 对于每一个id并行获取对应的数据
.parallel()
.map(id -> {
Object res = queryById(id);
if ( res instanceof Throwable) {
throw new RuntimeException();
}
return res;
})
// 有一个拿到了就直接用,后面到的无论了
.findAny();
dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
Thread.currentThread().join();
}
get data -> 6722c684-61a6-4065-9472-a58a08bbc9d0[id=1],spend time : 442 ms
get data -> 1975f02b-4e54-48f5-9dd0-51bf2789218e[id=2],spend time : 1820 ms
get data -> fd4bacb1-a34d-450d-8ebd-5f390167f5f8[id=4],spend time : 4585 ms
get data -> b2336c45-c1f9-4cd3-b076-83433fdaf543[id=3],spend time : 4772 ms
get data -> fcc25929-7765-467a-bf36-b85afab5efe6[id=5],spend time : 5575 ms
consume data : 6722c684-61a6-4065-9472-a58a08bbc9d0
复制代码
上面的输出中consume data
始终都是spend time
最短的数据
findFirst
则必须等到Stream
中元素组织顺序(初始时是数据提供源的元素顺序,若是你调用了sorted
那么该顺序就会变)的第一个元素处理完前面的流程而后消费它:
Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
// 对于每一个id并行获取对应的数据
.parallel()
.map(id -> {
Object res = queryById(id);
if ( res instanceof Throwable) {
throw new RuntimeException();
}
return res;
})
// 先拿到的先用,后面到的无论了
.findFirst();
dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
Thread.currentThread().join();
get data -> d6ac2dd6-66b6-461c-91c4-fa2f13326210[id=4],spend time : 1271 ms
get data -> 560f5c0e-c2ac-4030-becc-1ebe51ebedcb[id=5],spend time : 2343 ms
get data -> 11925a9f-03e8-4136-8411-81994445167e[id=1],spend time : 2825 ms
get data -> 0ecfa02e-3903-4d73-a18b-eb9ac833a899[id=2],spend time : 3270 ms
get data -> 930d7091-cfa6-4561-a400-8d2e268aaa83[id=3],spend time : 5166 ms
consume data : 11925a9f-03e8-4136-8411-81994445167e
复制代码
consume data
始终都是调用findFirst
时在Stream
排在第一位的元素。
reduce
是对当前Stream
中的元素进行求和、求乘积、求最大/小值等须要遍历全部元素得出一个结果的聚合操做。它有以下重载方法:
Optional<T> reduce(BinaryOperator<T> accumulator);
经过accumulator
对Stream
中的元素作聚合操做,返回一个包装了操做结果的Optional
,经过该Optional
能够拿到该结果以及判断该结果是否存在(若是Stream
没有元素,那么天然也就没有聚合结果了)
accumulator
是一个BiFunction
,至关于你在遍历时访问相邻的两个元素得出一个结果,这样Stream
就能依据此逻辑遍历全部元素获得最终结果
Optional<Dish> dishOptional = menu.stream()
.filter(dish -> dish.getCalories() > 600)
.reduce((d1, d2) -> d1.getCalories() < d2.getCalories() ? d1 : d2);
if (dishOptional.isPresent()) {
Dish dish = dishOptional.get();
System.out.println("大于600卡路里的菜品中,卡路里最小的是:" + dish);
} else {
System.out.println("没有大于600卡路里的菜品");
}
大于600卡路里的菜品中,卡路里最小的是:beef
OptionalInt reduce = IntStream.rangeClosed(1, 10)
.reduce((i, j) -> i + j); // -> method inference: Integer::sum
reduce.ifPresent(System.out::println);
55
复制代码
方法逻辑的伪代码以下:
if(stream is emtpy){
return optional(null)
}else{
result = new Element()
foreach element in stream
result = accumulator.apply(element, result);
return optional(result)
}
复制代码
T reduce(T identity, BinaryOperator<T> accumulator);
此方法增长了一个identity
,它的做用是若是调用reduce
时当前Stream
中没有元素了,也应该返回一个identity
做为默认的初始结果。不然,调用空的Optional
的get
会报错:
OptionalInt reduce = IntStream.rangeClosed(1, 10)
.filter(i -> i > 10)
.reduce(Integer::sum);
System.out.println(reduce.isPresent());
System.out.println(reduce.getAsInt());
false
java.util.NoSuchElementException: No value present
复制代码
若是加了identity
,reduce
不管如何都将返回一个明确的结果(若是有元素就返回聚合后的结果,不然返回identity
),而不是一个结果未知的Optional
int res = IntStream.rangeClosed(1, 10)
.filter(i -> i > 10)
.reduce(0, Integer::sum);
System.out.println(res);
0
复制代码
值得注意的是,identity
的值不该该随便给出,给出的规则应该符合:若是Stream
中有元素,那么对于任意元素element
,给定的identity
应该知足accumulator.apply(element, result)
,不然会出现以下使人困惑的现象:
int res = IntStream.rangeClosed(1, 10).reduce(1, (i, j) -> i * j);
System.out.println(res); //3628800
int res = IntStream.rangeClosed(1, 10).reduce(0, (i, j) -> i * j);
System.out.println(res); //0
复制代码
上面给定的identity
为1
、accumulator
逻辑为累乘时,知足对于任意元素e
都有e * 1 == e
,所以不会影响聚合逻辑;而将identity
换成0
,不管Stream
中有什么元素,聚合结果都为0
。这是由reduce(identity,accumulator)
的方法逻辑致使的,其伪代码以下:
if(stream is emtpy){
return optional(identity)
}else{
result = identity
foreach element in stream
result = accumulator.apply(element, result);
return optional(result)
}
复制代码
能够发现,首先将聚合结果result
置为identity
,而后将每一个元素累乘到result
中(result = element * result
),因为任何数乘零都得零,所以聚合结果始终返回0
了。
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
此方法又多了一个combiner
,这个combiner
是为了支持并行的,若是是在串行状态下调用那么传入的combiner
不会起到任何做用:
String reduce = Stream.of("h", "e", "l", "l", "o").reduce("", (a, b) -> a + b);
System.out.println(reduce);
hello
复制代码
但若是是串行状态下调用,那么combiner
会根据ForkJoin
机制和Stream
使用的Splierator
在子任务回溯合并时对合并的两个子结果作一些处理:
String result = Stream.of("h", "e", "l", "l", "o")
.parallel()
.reduce("", (a, b) -> a + b, (a, b) -> a + b + "=");
System.out.println(result); //he=llo===
复制代码
咱们能够在combiner
中查看当前合并的两个子结果:
String result = Stream.of("h", "e", "l", "l", "o")
.parallel()
.reduce("", (a, b) -> a + b, (a, b) -> {
System.out.println("combine " + a + " and " + b + ", then append '='");
return a + b + "=";
});
System.out.println("the final result is :" + result);
combine l and o, then append '='
combine l and lo=, then append '='
combine h and e, then append '='
combine he= and llo==, then append '='
the final result is :he=llo===
复制代码
collect
须要传入一个Collector
,Collectors
为咱们封装了不少Collector
的默认实现。
例如
collect(Collectors.toList())
,能够将元素收集到一个List
中并返回,相似的有toSet
collect(Collectors.joining())
能将Stream
中的字符串元素拼接起来并返回collect(Collectors.groupBy())
可以实现分组SQL
的外貌,你能够很容易理解它下面经过一个业务员Trader
和交易Transaction
的例子来巩固Stream
的运用。
两个类定义以下:
public class Trader{
private final String name;
private final String city;
public Trader(String n, String c){
this.name = n;
this.city = c;
}
public String getName(){
return this.name;
}
public String getCity(){
return this.city;
}
public String toString(){
return "Trader:"+this.name + " in " + this.city;
}
}
public class Transaction{
private final Trader trader;
private final int year;
private final int value;
public Transaction(Trader trader, int year, int value){
this.trader = trader;
this.year = year;
this.value = value;
}
public Trader getTrader(){
return this.trader;
}
public int getYear(){
return this.year;
}
public int getValue(){
return this.value;
}
@Override
public String toString(){
return "{" + this.trader + ", " +
"year: "+this.year+", " +
"value:" + this.value +"}";
}
}
复制代码
需求以下:
/**
* 1. Find all transactions in the year 2011 and sort them by value (small to high).
* 2. What are all the unique cities where the traders work?
* 3. Find all traders from Cambridge and sort them by name.
* 4. Return a string of all traders’ names sorted alphabetically.
* 5. Are any traders based in Milan?
* 6. Print all transactions’ values from the traders living in Cambridge.
* 7. What’s the highest value of all the transactions?
* 8. Find the transaction with the smallest value.
*/
复制代码
代码示例:
//1. Find all transactions in the year 2011 and sort them by value (small to high).
List<Transaction> transactions1 = transactions.stream()
.filter(transaction -> transaction.getYear() == 2011)
.sorted(Comparator.comparing(Transaction::getValue))
.collect(Collectors.toList());
System.out.println(transactions1);
//2. What are all the unique cities where the traders work?
String value = transactions.stream()
.map(transaction -> transaction.getTrader().getCity())
.distinct()
.reduce("", (c1, c2) -> c1 + " " + c2);
System.out.println(value);
//3. Find all traders from Cambridge and sort them by name.
transactions.stream()
.filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
.map(Transaction::getTrader)
.sorted(Comparator.comparing(Trader::getName))
.forEach(System.out::println);
//4. Return a string of all traders’ names sorted alphabetically.
transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.forEach(System.out::println);
//5. Are any traders based in Milan?
boolean res = transactions.stream()
.anyMatch(transaction -> "Milan".equals(transaction.getTrader().getCity()));
System.out.println(res);
//6. Print all transactions’ values from the traders living in Cambridge.
transactions.stream()
.filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
.map(Transaction::getValue)
.forEach(System.out::println);
//7. What’s the highest value of all the transactions?
Optional<Integer> integerOptional = transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::max);
System.out.println(integerOptional.get());
//8. Find the transaction with the smallest value.
integerOptional = transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::min);
System.out.println(integerOptional.get());
复制代码
其实上节介绍reduce
的使用时,就有Optional
的身影,若是你没有给出identity
,那么reduce
会给你返回一个Optional
,如此Optional
的身份(拿到这个类的实例,你会立马条件反射:要在访问对象以前判断一下Optional
包装的对象是否为null
)会提醒你进行非空判断,不至于你拿着reduce
返回的null
去使用从而致使空指针异常。
这种将非空判断交给API的机制,可以让咱们没必要每次拿到对象的时候都要为其是否为空而提心吊胆,又或十分敏感地每拿到一个对象都进行一下if (obj != null)
。
有了Optional
之后,避免空指针的两个点转变以下:
null
的对象,如空字符串""
,空数组等Optional
,如有结果则返回Optional.ofNullable(obj)
,若想返回null
则用Optional.empty()
Optional
,在使用以前它会提醒咱们使用isPresent
或ifPresent
规避空指针说白了,以前须要咱们人为的记住非空判断,但引入
Optional
后,非空判断流程交给API了,卸去了咱们对非空判断的关注点,规范了流程开发。
相关源码以下:
private static final Optional<?> EMPTY = new Optional<>();
private final T value;
private Optional() {
this.value = null;
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
public boolean isPresent() {
return value != null;
}
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
复制代码
你会发现其实他的源码很简单,就是将咱们要使用的对象包了一层
经过of(非空对象)
、ofNullable(可为空对象)
来建立Optional
实例
经过isPresent
能够判断内部对象是否为null
经过get
能够获取内部对象,若是内部对象为null
则会抛异常,所以一般在调用get
前要使用isPresent
判断一下
if(optional.isPresent()){
obj = optional.get();
}
复制代码
orElse
,若是不为null
则返回,不然
orElse(T t)
,返回你传入的对象t
orElseGet(Supplier<T> s)
,调用s.get
获取一个对象orElseGet(Supplier<Throwable> e)
经过ifPresent(consumer)
,能够整合判断和对象访问,若是对象不为null
,那就用传入的consumer
消费它
appleOptional.ifPresent(System.out.println(apple.getName()))
复制代码
此外,Optional
中还提供了filter
、map
两个从Stream
中借鉴的方法,做用相似,可自行查看源码。
其实我还想说
CompletableFutrue
、LocalDate/LocalTime/LocalDateTime
...