Java8 Lambda 学习笔记

函数式接口

什么是函数式接口?

函数式接口,@FunctionalInterface,简称FI,简单的说,FI就是指仅含有一个抽象方法的接口,以@Functionalnterface标注,该注解标注与否对函数式接口没有实际的影响, 不过一般还是推荐使用该注解,就像使用@Override注解一样。

注意,这里的抽象方法指的是该接口自己特有的抽象方法,而不包含它从其上级继承过来的抽象方法,例如:

@FunctionalInterface
Interface FI{
   abstract judge(int a);
   abstract equals();      
}

上面这个接口尽管含有两个抽象方法,但是它仍然是一个FI,因为equals抽象方法是其从超类Object中继承的(当然这里的“接口继承超类Object”的说法很有争议,但是不妨碍咱们这里拿来理解FI这个概念)

详情扩展阅读:JDK8新特性:函数式接口@FunctionalInterface的使用说明

Java SE 7 中已经存在的函数式接口:

除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

  • Predicate<T>——接收 T 并返回 boolean     详情介绍

  • Consumer<T>——接收 T,不返回值      详情介绍

  • Function<T, R>——接收 T,返回      详情介绍

  • Supplier<T>——提供 T 对象(例如工厂),不接收值     详情介绍

  • UnaryOperator<T>——接收 T 对象,返回 T     详情介绍

  • BinaryOperator<T>——接收两个 T,返回 T     详情介绍

除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型(Primitive type)的特化(Specialization)函数式接口,例如 IntSupplier 和 LongBinaryOperator。(我们只为 intlong 和 double 提供了特化函数式接口,如果需要使用其它原始类型则需要进行类型转换)同样的我们也提供了一些针对多个参数的函数式接口,例如 BiFunction<T, U, R>,它接收 T 对象和 U 对象,返回 R 对象。

JDK1.8中提供了一些函数式接口如下:

函数式接口 函数描述符 原始类型特化
Predicate<T> T -> boolean IntPredicate, LongPredicate, DoublePredicate
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function<T,R> T -> R IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>
Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator<T> T -> T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator<T> (T,T) -> T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L,R> (L,R) -> boolean
BiConsumer<T,U> (T,U) -> void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U) -> R ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

上表中的原始类型特化指的是为了消除自动装箱和拆箱的性能开销,JDK1.8提供的针对基本类型的

Lambda表达式和方法引用

有了函数式接口之后,就可以使用Lambda表达式和方法引用了。其实函数式接口的表中的函数描述符就是Lambda表达式,在函数式接口中Lambda表达式相当于匿名内部类的效果。 举个简单的例子:

public class TestLambda {
 
    public static void execute(Runnable runnable) {
        runnable.run();
    }
 
    public static void main(String[] args) {
        //Java8之前
        execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("run");
            }
        });
 
        //使用Lambda表达式
        execute(() -> System.out.println("run"));
    }
}

可以看到,相比于使用匿名内部类的方式,Lambda表达式可以使用更少的代码但是有更清晰的表述。注意,Lambda表达式也不是完全等价于匿名内部类的, 两者的不同点在于this的指向和本地变量的屏蔽上。

Lambda表达式还可以复合,把几个Lambda表达式串起来使用:

Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150).or(a -> “green”.equals(a.getColor()));

上面这行代码把两个Lambda表达式串了起来,含义是选择重量大于150或者绿色的苹果。

Lambda表达式与方法引用和构造器引用

方法引用可以看作Lambda表达式的更简洁的一种表达形式,使用::操作符

  • 静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)

  • 任意类型实例方法的方法引用(例如String的length方法,写作String::length)

  • 现有对象的实例方法的方法引用(例如假设你有一个本地变量localVariable用于存放Variable类型的对象,它支持实例方法getValue,那么可以写成localVariable::getValue)

  • 构造器引用(例如String::new)

举个方法引用的简单的例子:

Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);
 
//使用方法引用
Function<String, Integer> stringToInteger = Integer::parseInt;

方法引用中还有一种特殊的形式,构造函数引用,假设一个类有一个默认的构造函数,那么使用方法引用的形式为:

Supplier<SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.get();
 
//等价于
Supplier<SomeClass> c1 = () -> new SomeClass();
SomeClass s1 = c1.get();

如果是构造函数有一个参数的情况:

Function<Integer, SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.apply(100);
 
//等价于
Function<Integer, SomeClass> c1 = i -> new SomeClass(i);
SomeClass s1 = c1.apply(100);

详情阅读扩展:Java中Lambda表达式与方法引用和构造器引用


Stream

什么是流?

Stream是java8中新增加的一个特性,被java猿统称为流.

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。Java 的并行 API 演变历程基本如下:

1.0-1.4 中的 java.lang.Thread  
5.0 中的 java.util.concurrent  
6.0 中的 Phasers 等  
7.0 中的 Fork/Join 框架  
8.0 中的 Lambda

Stream 的另外一大特点是,数据源本身可以是无限的。

Stream可以分成串行流和并行流,并行流是基于Java7中提供的ForkJoinPool来进行任务的调度,达到并行的处理的目的。 集合是我们平时在进行Java编程时非常常用的API,使用Stream可以帮助更好的来操作集合。

Stream提供了非常丰富的操作,包括筛选、切片、映射、查找、匹配、归约等等, 这些操作又可以分为中间操作和终端操作,中间操作会返回一个流,因此我们可以使用多个中间操作来作链式的调用,当使用了终端操作之后,那么这个流就被认为是被消费了, 每个流只能有一个终端操作。

Collection的流方法以及流接口中的工厂方法

java.util.Collection<T>:该接口中的默认方法也许是最常使用的生成流的方式

  • stream()    Stream<T>

  • parallelStream()    Stream<T>

java.util.stream.Stream<T>:该接口公开了大量的静态工厂方法,并且带有默认实现。

//筛选后收集到一个List中
List<Apple> vegetarianMenu = apples.stream().filter(Apple::isRed).collect(Collectors.toList());
 
//筛选加去重
List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);

以上都是一些简单的例子,Stream提供的API非常丰富,可以很好的满足我们的需求。

操作 类型 返回类型 使用的类型/函数式接口 函数描述符
filter 中间 Stream<T> Predicate<T> T -> boolean
distinct 中间 Stream<T>

skip 中间 Stream<T> long
limit 中间 Stream<T> long
map 中间 Stream<R> Function<T,R> T -> R
flatMap 中间 Stream<R> Function<T, Stream<R>> T -> Stream<R>
sorted 中间 Stream<R> Comparator<T> (T,T) -> int
anyMatch 终端 boolean Predicate<T> T -> boolean
noneMatch 终端 boolean Predicate<T> T -> boolean
allMatch 终端 boolean Predicate<T> T -> boolean
findAny 终端 Optional<T>

findFirst 终端 Optional<T>

forEach 终端 void Consumer<T> T -> void
collect 终端 R Collector<T,A,R>
reduce 终端 Optional<T> BinaryOperator<T> (T,T) -> T
count 终端 long

与函数式接口类似,Stream也提供了原始类型特化的流,比如说IntStream等:

//maoToInt转化为一个IntStream
int count = list.stream().mapToInt(list::getNumber).sum();

并行流与串行流的区别就在于将stream改成parallelStream,并行流会将流的操作拆分,放到线程池中去执行,但是并不是说使用并行流的性能一定好于串行的流, 恰恰相反,可能大多数时候使用串行流会有更好的性能,这是因为将任务提交到线程池,执行完之后再合并,这些本身都是有不小的开销的。关于并行流其实还有非常多的细节, 这里做一个抛砖引玉,有兴趣的同学可以在网上自行查找一些资料来学习。




parallelStream

parallelStream是什么

parallelStream其实就是一个并行执行的流.它通过默认的ForkJoinPool,可能提高你的多线程任务的速度.

parallelStream的作用

Stream具有平行处理能力,处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作,因此像以下的程式片段:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
       .forEach(out::println);

你得到的展示顺序不一定会是1、2、3、4、5、6、7、8、9,而可能是任意的顺序,就forEach()这个操作來讲,如果平行处理时,希望最后顺序是按照原来Stream的数据顺序,那可以调用forEachOrdered()。例如:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
       .forEachOrdered(out::println);

注意:如果forEachOrdered()中间有其他如filter()的中介操作,会试着平行化处理,然后最终forEachOrdered()会以原数据顺序处理,因此,使用forEachOrdered()这类的有序处理,可能会(或完全失去)失去平行化的一些优势,实际上中介操作亦有可能如此,例如sorted()方法。

parallelStream于Stream的区别?

Stream:有序

parallelStream

1、无序的

2、并不是并行流一定性能好,如果简单处理,并行流处理再加上合并时间,未必比stream效果好

3、并行流,有可能会造成数据的丢失,相见如下解释


流可以是无限的、有状态的,可以是顺序的,也可以是并行的。在使用流的时候,你首先需要从一些来源中获取一个流,执行一个或者多个中间操作,然后执行一个最终操作。中间操作包括filter、map、flatMap、peel、distinct、sorted、limit和substream。终止操作包括forEach、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst和findAny。 java.util.stream.Collectors是一个非常有用的实用类。该类实现了很多归约操作,例如将流转换成集合和聚合元素。 使其对集合操作更加灵活。


Java8中 Parallel Streams 的陷阱 [译]

JAVA使用并行流(ParallelStream)时要注意的一些问题

深入浅出ParallelStream


小结

以上只是对Java8的新特性进行了一个非常简单的介绍,由于近年来函数式编程很火,Java8也受函数式编程思想的影响,吸收了函数式编程好的地方, 很多新特性都是按照函数式编程来设计的。关于Java8还有非常多的细节没有提到,这些需要我们自行去学习,推荐一本学习Java8非常好的书籍——《Java8实战》, 看完这本书对Java8的使用可以有一个比较清楚的了解。

现在已经是2017年了,据说今年会推出Java9,Java9会推出什么新特性,让我们拭目以待吧。

阅读参考


未完待补充。。。




赞(52) 打赏
未经允许不得转载:优客志 » JAVA开发
分享到:

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏