在Java世界里面,面向对象还是主流思想,对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式我们都得学。
这种新的抽象方式还有其他好处。很多人不总是在编写性能优先的代码,对于这些人来说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更多地表达了业务逻辑,而不是从机制上如何实现。易读的代码也易于维护、更可靠、更不容易出错。
在写回调函数和事件处理器时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,只有在真正需要的时候,才初始化变量的值。
面向对象编程是对数据进行抽象;函数式编程是对行为进行抽象。
核心思想: 使用不可变值和函数,函数对一个值进行处理,映射成另一个值。
对核心类库的改进主要包括集合类的API和新引入的流Stream。流使程序员可以站在更高的抽象层次上对集合进行操作。
lambda表达式
- lambda表达式仅能放入如下代码: 预定义使用了 @Functional 注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传lambda表达式。
- lambda表达式内可以使用
方法引用
,仅当该方法不修改lambda表达式提供的参数。本例中的lambda表达式可以换为方法引用,因为这仅是一个参数相同的简单方法调用。
list.forEach(n -> System.out.println(n)); list.forEach(System.out::println); // 使用方法引用 @pdai: 代码已经复制到剪贴板
然而,若对参数有任何修改,则不能使用方法引用,而需键入完整地lambda表达式,如下所示:
list.forEach((String s) -> System.out.println("*" + s + "*")); @pdai: 代码已经复制到剪贴板
事实上,可以省略这里的lambda参数的类型声明,编译器可以从列表的类属性推测出来。
- lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获。
- Lambda表达式在Java中又称为闭包或匿名函数,所以如果有同事把它叫闭包的时候,不用惊讶。
- Lambda方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。可以使用JDK中的 javap 工具来反编译class文件。使用 javap -p 或 javap -c -v 命令来看一看lambda表达式生成的字节码。大致应该长这样:
private static java.lang.Object lambda$0(java.lang.String); @pdai: 代码已经复制到剪贴板
- lambda表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量。
List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7}); int factor = 2; primes.forEach(element -> { factor++; }); @pdai: 代码已经复制到剪贴板
Compile time error : "local variables referenced from a lambda expression must be final or effectively final" 另外,只是访问它而不作修改是可以的,如下所示:
List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7}); int factor = 2; primes.forEach(element -> { System.out.println(factor*element); }); @pdai: 代码已经复制到剪贴板
分类
惰性求值方法
lists.stream().filter(f -> f.getName().equals("p1")) @pdai: 代码已经复制到剪贴板
如上示例,这行代码并未做什么实际性的工作,filter只是描述了Stream,没有产生新的集合。
如果是多个条件组合,可以通过代码块{}
及早求值方法
List<Person> list2 = lists.stream().filter(f -> f.getName().equals("p1")).collect(Collectors.toList()); @pdai: 代码已经复制到剪贴板
如上示例,collect最终会从Stream产生新值,拥有终止操作。
理想方式是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果。与建造者模式相似,建造者模式先是使用一系列操作设置属性和配置,最后调用build方法,创建对象。
stream & parallelStream
stream & parallelStream
每个Stream都有两种模式: 顺序执行和并行执行。
顺序流:
List <Person> people = list.getStream.collect(Collectors.toList()); @pdai: 代码已经复制到剪贴板
并行流:
List <Person> people = list.getStream.parallel().collect(Collectors.toList()); @pdai: 代码已经复制到剪贴板
顾名思义,当使用顺序方式去遍历时,每个item读完后再读下一个item。而使用并行去遍历时,数组会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。
parallelStream原理:
List originalList = someData; split1 = originalList(0, mid);//将数据分小部分 split2 = originalList(mid,end); new Runnable(split1.process());//小部分执行操作 new Runnable(split2.process()); List revisedList = split1 + split2;//将结果合并 @pdai: 代码已经复制到剪贴板
大家对hadoop有稍微了解就知道,里面的 MapReduce 本身就是用于并行处理大数据集的软件框架,其 处理大数据的核心思想就是大而化小,分配到不同机器去运行map,最终通过reduce将所有机器的结果结合起来得到一个最终结果,与MapReduce不同,Stream则是利用多核技术可将大数据通过多核并行处理,而MapReduce则可以分布式的。
stream与parallelStream性能测试对比
如果是多核机器,理论上并行流则会比顺序流快上一倍,下面是测试代码
long t0 = System.nanoTime(); //初始化一个范围100万整数流,求能被2整除的数字,toArray()是终点方法 int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray(); long t1 = System.nanoTime(); //和上面功能一样,这里是用并行流来计算 int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray(); long t2 = System.nanoTime(); //我本机的结果是serial: 0.06s, parallel 0.02s,证明并行流确实比顺序流快 System.out.printf("serial: %.2fs, parallel %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9); @pdai: 代码已经复制到剪贴板
Stream中常用方法如下:
stream()
,parallelStream()
filter()
findAny()
findFirst()
sort
forEach
voidmap(), reduce()
flatMap()
- 将多个Stream连接成一个Streamcollect(Collectors.toList())
distinct
,limit
count
min
,max
,summaryStatistics
看下所有API:
常用例子
匿名类简写
new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start(); // 用法 (params) -> expression (params) -> statement (params) -> { statements } @pdai: 代码已经复制到剪贴板
forEach
// forEach List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API"); features.forEach(n -> System.out.println(n)); // 使用Java 8的方法引用更方便,方法引用由::双冒号操作符标示, features.forEach(System.out::println); @pdai: 代码已经复制到剪贴板
方法引用
构造引用
// Supplier<Student> s = () -> new Student(); Supplier<Student> s = Student::new; @pdai: 代码已经复制到剪贴板
对象::实例方法 Lambda表达式的(形参列表)与实例方法的(实参列表)类型,个数是对应
// set.forEach(t -> System.out.println(t)); set.forEach(System.out::println); @pdai: 代码已经复制到剪贴板
类名::静态方法
// Stream<Double> stream = Stream.generate(() -> Math.random()); Stream<Double> stream = Stream.generate(Math::random); @pdai: 代码已经复制到剪贴板
类名::实例方法
// TreeSet<String> set = new TreeSet<>((s1,s2) -> s1.compareTo(s2)); /* 这里如果使用第一句话,编译器会有提示: Can be replaced with Comparator.naturalOrder,这句话告诉我们 String已经重写了compareTo()方法,在这里写是多此一举,这里为什么这么写,是因为为了体现下面 这句编译器的提示: Lambda can be replaced with method reference。好了,下面的这句就是改写成方法引用之后: */ TreeSet<String> set = new TreeSet<>(String::compareTo);