本篇Blog开始学习和实践Java8中的新特性,主要分为两大部分:语言新特性和库函数新特性,重点落在工作中经常会用到的几个重大特性:
- 语言新特性:Lambda表达式,方法引用,接口的默认方法和静态方法,重复注解
- 库函数新特性:Optional,Streams,Date/Time API(JSR 310),base64,并行数组
接下来按照如下几个结构分别介绍和学习以上知识点:基本概念,解决问题,语法范式,实践操作,我发现虽然经常听到函数式编程这样的名词,但是好像从来不知道具体是什么,所以这次一并了解下函数式编程的概念。
函数式编程什么是函数式编程?函数式编程是一种编程范式,除了函数式编程之外还有 命令式编程,声明式编程 等编程范式
- 命令式编程:命令式编程 是面向计算机硬件的抽象,有变量、赋值语句、表达式、控制语句等,可以理解为 命令式编程就是冯诺伊曼的指令序列。 它的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么,其实就是代码脚本段
- 声明式编程:声明式编程 是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子,SELECt * FROM collection WHERe num > 5,其实就是表达式结果。它不需要创建变量用来存储数据,它也不包含循环控制的代码如 for, while
- 函数式编程:函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。
函数式编程的本质就是:函数式编程中的函数不是指计算机中的函数,而是指数学中的函数,即自变量的映射。函数的值取决于函数的参数的值,不依赖于其他状态,比如abs(x)函数计算x的绝对值,只要x不变,无论何时调用、调用次数,最终的值都是一样
函数式编程特点函数式编程有两个特点:函数是第一等公民,函数是纯函数
- 函数是第一等公民:是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值
// 赋值
var func1 = function func1() { }
// 函数作为参数
function func2(fn) {
fn()
}
// 函数作为返回值
function func3() {
return function() {}
}
- 函数是纯函数:纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。纯函数的两个特点:相同的输入必有同输出,函数无副作用
这两个特点的示例如下:
// 是纯函数
function sum(x,y){
return x + y
}
// 输出不确定,不是纯函数
function random(x){
return Math.random() * x
}
// 有副作用,不是纯函数
function setFontSize(el,fontsize){
el.style.fontsize = fontsize ;
}
// 输出不确定、有副作用,不是纯函数
let count = 0;
function addCount(x){
count+=x;
return count;
}
函数式编程优劣
综合来理解就是函数式编程,不变、不变、不变,固定的输入产生固定的输出且对外部没有任何影响,那么好处显而易见,所有的操作都是幂等的:
- 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
- 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
- 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
- 隐性好处。减少代码量,提高维护性
缺点也就是命令式编程可以发挥作用的:
- 性能差:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
- 资源占用:为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
- 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作
大致了解了函数式编程后再来看看Java是如何使用Lambda表达式来应用函数式编程这一理念的。
Lambda表达式Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑
解决问题核心解决的问题就是:让程序员使用更少的代码实现同样的功能。虽然看着很先进,其实Lambda表达式的本质只是一个语法糖,由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能
语法范式lambda表达式允许通过表达式来代替功能接口。lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体【body可以是一个表达式或一个代码块】
左侧:Lambda表达式的参数列表 -> 右侧:Lambda表达式中要执行的功能
右侧实现可以是表达式,也可以是代码块
(parameters) -> expression
或
(parameters) ->{ statements; }
对于左侧的参数列表而言:
- 可选参数个数:参数可以没有,也可以只有一个或多个。
- 可选参数类型声明:不需要声明参数类型,编译器可以统一识别参数值
- 可选参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
对于右侧功能实现来说
- 可选返回主体:如果返回主体是表达式,那么编译器自动返回表达式值,如果主体是单行语句,那么返回类型可以看做void,如果主体是代码块,需要用return关键字指定返回值
- 可选返回关键字:如上所述,如果主体是代码块,需要用return关键字指定返回值
- 可选大括号:如果主体只包含单行语句(表达式),就不需要使用大括号,如果主体是多行代码块,需要使用大括号
对于右侧功能,总结而言:单行语句或表达式,可以省略return关键字和{},几个简单的例子:
// 1. 不需要参数,返回值为 5 () -> 5 // 2. 接收一个参数(数字类型),返回其2倍的值 x -> 2 * x // 3. 接受2个参数(数字),并返回他们的差值 (x, y) -> x – y // 4. 接收2个int型整数,返回他们的和 (int x, int y) -> x + y // 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) (String s) -> System.out.print(s)实践操作
既然一个新的事物出现,一定有其原因,也就是之前一定有痛点,那么新事物才有价值,这里只列举当前我看到的Lambda解决的问题
1 快速处理集合我们一般在一些集合操作时使用Lambda表达式,没有lambda之前:
public static void main(String[] args) {
List list = Arrays.asList("t", "m", "l");
for (String s : list) {
System.out.println(s);
}
}
有了lambda之后,只需要一行代码,将参数和功能组合成一个入参。
Arrays.asList( "t", "m", "l" ).forEach(e -> System.out.println( e ) );
如同上面说的理论能力,上面这个代码中的参数e的类型是由编译器推理得出的,e也可以自定义类型:
Arrays.asList( "t", "m", "l" ).forEach( ( String e ) -> System.out.println( e ) );
如果Lambda表达式需要更复杂的语句块,则可以使用花括号将该语句块括起来,类似于Java中的函数体:
public static void main(String[] args) {
Arrays.asList("t", "m", "l").forEach((String e) ->
{
String a = e + "帅";
System.out.println(a);
}
);
}
2 取代匿名内部类
关于内部类可以看我的这篇Blog【Java SE基础 九】Java内部类,所谓匿名内部类就是:匿名内部类是直接使用 new 来生成一个对象的引用,匿名内部类没有类的名称,很多场景下可以简化我们代码,但是Lambda出现后告诉我们可以更简化:它只聚焦于核心实现:
public class Demo01Inner {
public static void main(String[] args) {
//使用匿名内部类的方式实现多线程。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行了");
}
}).start();
//使用Lambda表达式实现多线程
new Thread(() -> System.out.println(Thread.currentThread().getName() + "执行了")).start();
}
}
有了Lambda,不需要再定义接口、不需要new实例、不需要run()的方法声明等,只care核心实现即可。这就是函数式或者声明式编程的魅力,不是万物皆对象的,有些时候实现可以如此简单。
3 匿名方法的使用只要是接口函数也都可以使用lambda,如果不使用lambda,我们调用参数和比较器来进行排序:
public class Person {
// ...
LocalDate birthday;
public int getAge() {
// ...
}
public LocalDate getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
// ...
}
Person[] rosterAsArray = roster.toArray(new Person[roster.size()]); class PersonAgeComparator implements Comparator{ public int compare(Person a, Person b) { return a.getBirthday().compareTo(b.getBirthday()); } } Arrays.sort(rosterAsArray, new PersonAgeComparator());
其中sort的方法签名为:
staticvoid sort(T[] a, Comparator super T> c)
该接口是一个功能接口。因此可以使用 lambda 表达式,而不是定义然后创建一个实现以下内容的类的新实例ComparatorComparator
Arrays.sort(rosterAsArray,
(Person a, Person b) -> {
return a.getBirthday().compareTo(b.getBirthday());
}
);
这种比较两个实例的出生日期的方法我们可以在Person中定义,并且已经有了,所以上述写法还可以再简化:
Arrays.sort(rosterAsArray,
(a, b) -> Person.compareByAge(a, b)
);
总结一下
总而言之,通过写这篇博客刷新了一些关于函数式编程模式的一些认知,最深刻的感受就是我们可能太OOP了,有时候感觉没有对象就什么也干不成了,做一些逻辑实现时总要定义一些冗余的类,接口,其实有些时候只需要关注最核心的实现可以减少大量的代码编写,忘记实例创建、忘记方法声明、忘记new吧,只关注核心实现!当然这也不是说别OOP了,其实无论是命令式编程还是函数式编程都有其具体的应用场景,Java这样的OOP语言也不会把函数式编程模式完全隔绝出去,这才引入了Lambda。所以一切不以应用场景为前提的优劣比较都是耍流氓



