参考oracle官网:https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
目录
函数式编程、lambda
函数式编程,可以将函数作为方法参数传入另一个函数,也能作为函数的返回值返回。
以List.foreach为例
foreach方法参数是Consumer接口,可以用匿名内部类作参数。
List<String> list = new ArrayList();
list.forEach(new Consumer<String>() {
public void accept(String s) {
System.out.println(s);
}
});
官网说法,如果您的匿名类的实现非常简单,例如仅包含一种方法的接口,那么匿名类的语法似乎笨拙且不清楚。在这些情况下,您通常会尝试将功能传递为另一个方法的参数,例如在某人点击按钮时应采取的操作。lambda表达式使您可以执行此操作,以将功能视为方法参数,或代码为数据。匿名类,向您展示了如何在不给出名称的情况下实现基类。虽然这通常比命名类更简洁,但对于只有一种方法的课程,即使是一个匿名的课程似乎有点过度和繁琐。Lambda表达式让您更加紧凑地表达单方法类的实例。
lambda简化了只有一个抽象方法的接口。当一个接口只有一个方法,可以用lambda表达式实现,代码更简洁。
List<String> list = new ArrayList();
//(参数)->{}
list.forEach(a-> {System.out.println(a);});
//只有一个语句,省去{}
list.forEach(a-> System.out.println(a));
如果接口方法需要返回值
FutureTask<String> futureTask = new FutureTask(()->{return "";});
FutureTask<String> futureTask2 = new FutureTask(() -> "");
new Thread(futureTask).start();
在Lambda表达式中能访问域外的局部非final变量、但不能修改Lambda域外的局部非final变量。因为在Lambda表达式中,Lambda域外的局部非final变量会在编译的时候,会被隐式地当做final变量来处理。
访问lambda外变量可以。
String a = "";
list.forEach(l -> System.out.println(a));
修改lambda外变量不行,这样编译错误,提示把变量改成final数组 或Automic ,然后再修改
String a = "";
list.forEach(l -> {
a="12";
System.out.println(a);
});
双冒号
参考官网,方法引用,https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
当lambda表达式中的内容,仅仅是调用另一个线程的方法,可以用双冒号继续简化代码。有四种用法。但注意,一旦使用双冒号,lambda中的参数一定要被使用、并且只能被用一次,否则编译错误。
定义一个类试一试。
static class Fruit {
String name;
public Fruit(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void hi(Fruit fruit) {
System.out.println(fruit.getName());
}
public static void hello() {
}
}
双冒号调用静态方法
- 注意静态方法 hi(Fruit f) 是有参数的,foreach把遍历的元素作参数 传给hi()。等同于 list.forEach(a->Fruit.hi(a)); 如果hi方法没有参数、或参数不是一个Fruit,会编译错误
- 有意思的地方,list.forEach(Fruit::hello)编译错误,list.forEach(a->Fruit.hello())编译通过,这是因为hello是静态方法并且没有参数,想用双冒号必须要使用lambda中的参数,list.forEach(Fruit::hello)中没有使用 接口参数。但lamda表达式很明显用不用参数不影响。
Fruit fruit1 = new Fruit("1");
Fruit fruit2 = new Fruit("2");
Fruit fruit3 = new Fruit("3");
List<Fruit> list = new ArrayList<Fruit>() {{
add(fruit1);
add(fruit2);
add(fruit3);
}};
list.forEach(Fruit::hi);//编译通过
list.forEach(Fruit::hello);//编译错误
list.forEach(a->Fruit.hello());//编译通过
双冒号调用一个对象的实例方法
把list每个元素元素打印。
//双冒号,调用实例方法
list.forEach(System.out::print);
out是System类的类变量,是个对象,print实际上是调用out对象的实例方法。
双冒号调用 指定类型的对象 的实例方法。
- 一开始说了,使用双冒号,lambda接口参数必须要被使用。getName方法虽然没参数,但实际上接 把接口参数作为 调用方,lambda接口参数是 list中的一个元素,调用这个元素对象的 getName方法。如果这里是setName肯定错误,因为setName方法的参数没有,接口参数只能用一次,被用做了调用方
list.forEach(Fruit::getName);
list.forEach(a->a.getName());//相当于
因为foreach只能消费一个参数,再举个消费两个参数 例子。
加一个静态方法,意思是执行BiFunction,把参数a,b传入BiFunction,得到一个结果。
public static String print(String a, String b, BiFunction<String,String,String> biFunction){
String apply = biFunction.apply(a, b);
return apply;
}
BiFunction接口方法参数有两个
public interface BiFunction<T, U, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
执行结果打印出“HelloWorld”,String的concat实例方法参数只有一个,BiFunction接口方法参数有两个,为什么能使用?
Fruit.print("Hello","World",String::concat);
上面语句等价于,看到这应该明白了,(a,b)->a.concat(b) 简写成了 String::concat。接口参数a和b,第一个参数a作为了函数调用方,a调用自己concat,第二个接口参数作为concat的参数。
Fruit.print("hello","world",(a,b)->a.concat(b));
所以在用 双冒号 调用 对象的实例方法时,一定要理解 第一个lambda接口参数被用做了 实例方法调用方,其他接口参数 会被用作 实例方法的参数。
再举个例子,跟上面意思一样
Stream<String> stream = Stream.of("a", "b", "c");
stream.reduce((a,b)->a.concat(b));
stream.reduce(String::concat);
双冒号调用构造方法
构造方法也一样,一定要使用接口参数,
假设Fruit构造方法传参是Fruit
public Fruit(Fruit name) {
}
就能这么用
Fruit fruit1 = new Fruit("1");
Fruit fruit2 = new Fruit("2");
Fruit fruit3 = new Fruit("3");
List<Fruit> list = new ArrayList<Fruit>() {{
add(fruit1);
add(fruit2);
add(fruit3);
}};
list.forEach(Fruit::new);
<?extends T> <? super T>
参考 Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同? - 知乎
假设Fruit是基类,Apple、Banana是子类。
如果没有<? extends Fruit>, List<Fruit>list是不能指向子类集合。
List<? extends Fruit> list 代表list中的元素是Fruit的子类,好处是让list既可以指向 apple集合,也能指向Banana集合。
List<? extends Fruit> list = new ArrayList<Apple>();
list = new ArrayList<Banana>();
<? extends Furit>的副作用,list不能add元素,因为你不知道list指向的是apple集合还是banana。
<? extends Furit>只能从中get元素,拿到的是Fruit类型或者Fruit的父类。
List<? super Fruit> list,代表集合中的元素是Fruit的父类,副作用是list只能add Fruit及其子类,get只能拿到Object类型。
Stream
参考 java8 Stream的实现原理 (从零开始实现一个stream流) - 小熊餐馆 - 博客园
Java8的Stream流详解_长风-CSDN博客_java stream流操作
stream更方便的处理合数据。
先看一些接口
只能举一些例子。
Function,入参一个泛型,出参另一个泛型,类似于 y = F(x)
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
BiFunction 类似于 z = F(x,y)
@FunctionalInterface
public interface BiFunction<T, U, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
条件判断
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
数据容器
public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();
/**
* If non-null, the value; if null, indicates no value is present
*/
private final T value;
stream接口定义挺清晰,Stream接口继承BaseStream,BaseStream还有其他子类,IntStream、LongStream、DoubleStream都差不多,Stream流中的元素是泛型,IntStream里元素是Integer。
生成流
Collection接口的stream()或parallelStream()方法
静态的Stream.of()、Stream.empty()方法
Arrays.stream(array, from, to)
静态的Stream.generate()方法生成无限流,接受一个不包含引元的函数
静态的Stream.iterate()方法生成无限流,接受一个种子值以及一个迭代函数
Pattern接口的splitAsStream(input)方法
静态的Files.lines(path)、Files.lines(path, charSet)方法
静态的Stream.concat()方法将两个流连接起来
中间操作
filter(Predicate)将结果为false的元素过滤掉
map(fun)转换元素的值,map常用在 把流中类型T变为类型R,一个T变为一个R,如Stream<String> s =Stream.of(new Student()).map(Student::getName),学生类变为String类,流中是学生姓名。
flatMap(fun)转换元素的值,和map一样把 流中T类转为R类,区别是flatmap接口参数 要求把一个T元素转为Stream<R>,这样如果有多个T元素就会得到多个Stream<R>对象,flatMap又会把多个Stream<R>合并为一个Stream<R>。如Stream<Student> s= Stream.of(new School()).flatMap(school->school.getStudents().stream()),一个School有很多Student,把School变为Student流,流中是学校里的所有学生。
limit(n)保留前n个元素
skip(n)跳过前n个元素
distinct()剔除重复元素
sorted()将Comparable元素的流排序
sorted(Comparator)将流元素按Comparator排序
peek(fun)流不变,但会把每个元素传入fun执行,可以用作调试
收集操作
iterator()
forEach(fun)
forEachOrdered(fun)
可以应用在并行流上以保持元素顺序
toArray()
toArray(T[] :: new)
返回正确的元素类型
collect(Collector)
collect(fun1, fun2, fun3)
fun1转换流元素;fun2为累积器,将fun1的转换结果累积起来;fun3为组合器,将并行处理过程中累积器的各个结果组合起来
reduce和并行流
Optional<T> reduce(BinaryOperator<T> accumulator);把元素按照accumulator 计算,如下面,这种Stream流中未必有元素,所以计算结果可能是空,结果需要用Optional接收(一个数据容器,可以判断容器中是否空、得到里面的值)。
Stream<Integer> stream = Stream.of(3, 6, 5, 8, 7, 4);
Optional<Integer> reduce = stream.reduce((a, b) -> a + b);
T reduce(T identity, BinaryOperator<T> accumulator)也是按照accumulator计算,但会有个初值。先计算初值和第一个元素,因为必然有初值,所以结果肯定不是空,直接会得到Integer,不需要Optional。
Stream<Integer> stream = Stream.of(3, 6, 5, 8, 7, 4);
Integer reduce = stream.reduce(1, (a, b) -> a + b);
最后,<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);需要用并行流才有效果。
流可以通过stream.parallel()设置并行,sequential设置串行。设置为并行后,会用多线程去执行。
流分为多个线程执行,就涉及到了多个线程结果的合并。
Stream<Integer> stream = Stream.of(3, 6, 5, 8, 7, 4);
stream.parallel().reduce(0, (a, b) -> a + b, (c, d) -> c + d);
第三个参数就是 combiner 怎么合并两个线程的结果,上面的代码是把线程结果相加,当然根据业务的需要对 两个线程结果合并,比如要求 两个线程要乘2,stream.parallel().reduce(0, (a, b) -> a + b, (c, d) -> 2 * (c + d)); 这样会把两个线程结果相加后成2,再继续和剩下的线程结果计算。
并行流的代码简单debug了下,大概是跟ForkJoinTask有关。