一、前言
记录这篇博客的目的主要是因为不太熟悉Lambda表达式,也就顺便瞅一瞅JAVA8有哪些好玩好用的东西
基本上可以有个大致的概念
- JAVA8的大部分新特性都是针对函数式编程和流式编程
- JAVA8的大部分新特性都是为了编写可读性更高的代码
二、Lambda表达式
lambda表达式的目的是能够将函数作为表达式和参数来进行传递
重要特征
- 可选类型申明:不需要声明参数类型,编译器可以统一识别参数值
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号
- 可选的返回关键字:如果主体只有一个表达式返回值,则编译器会自动返回值,大括号需要指明表达式返回了一个数值
注意事项
lambda表达式不绑定this
(this
是外层作用域的this
)
JS的箭头函数
箭头函数作为函数体
const logFunc = (name, age) => {
console.log(name + "" + age);
};
logFunc("教主", 20);
箭头函数作为参数
const vm = new Vue({
data: function() {
return { students: null };
},
mounted: function() {
axios.get('localhost:1000/hello/students').then((response) => {
this.studentList = response.data;
});
}
});
JAVA的Lambda表达式
在JAVA中,lambda表达式实现的本质是匿名内部类,使用到的局部变量必须是final
限定的。因此lambda表达式对引用的变量只读不写。但同时JAVA8中编译时又默认会给使用到的局部变量加上final
限定,并不需要显式指出
lambda表达式简化匿名内部类
public static void main(String[] args) {
// 匿名类内部类对象作为参数
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("启动线程1");
}
});
t.start();
// Lambda表达式作为参数
new Thread(() -> {
System.out.println("启动线程2");
}).start();
}
lambda表达式简化遍历
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// for-each语句
for (Integer a : list) {
System.out.println(a);
}
// lambda表达式的for-each
list.forEach((a) -> {
System.out.println(a);
});
}
三、函数式接口
函数式接口在JAVA中指有且仅有一个抽象方法的接口。将函数式接口的匿名内部类对象作为参数传递时,由于只有一个抽象方法就可以简化为lambda表达式
只有确保接口中有且仅有一个抽象方法, JAVA中的Lambda才能顺利地进行推导
@FunctionalInterface
注解
和@Override
注解一样,都只起标识作用。编译器将会强制检查使用该注解的接口是否确实有且仅有一个抽象方法,否则将会报错
JAVA8之前已经有的函数式接口
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
JAVA8中新增的四大函数式接口
函数式接口 | 方法原型 |
---|---|
消费型接口Consumer<T> |
void accept(T var1) |
供给型接口Supplier<T> |
T get() |
功能型接口Function<T, R> |
R apply(T var1) |
断言型接口Predicate<T> |
boolean test(T var1) |
自定义函数式接口
目前有参无返回、无参有返回、有参有返回、无参无返回的常见类型都可以直接四大函数式接口和java.lang.Runnable
接口实现。
lambda表达式传参就是函数式接口的匿名内部类对象传参
@FunctionalInterface
public interface ExtraFunction<R, P1, P2> {
R apply(P1 var1, P2 var2);
}
public static void func(Integer var1, Integer var2, ExtraFunction<Integer, Integer, Integer> action) {
Integer result = action.apply(var1, var2);
}
四、方法引用
lambda表达式组合四种函数式接口已经可以满足绝大部分的函数传参的需求,但是我们也注意到
- lambda表达式更适合于传递临时的函数
- 如果某些函数已经存在,那么用Lambda表达式还是会显得代码冗余
为了传递已有的函数,方法引用应运而生
方法引用的分类
方法引用 | 语法 | 对应的Lambda表达式 |
---|---|---|
静态方法引用 | [类型名]::[静态方法名] |
([参数]) -> { [类调用静态方法]([参数]); } |
构造方法引用 | [类型名]::new |
([参数]) -> { [调用该类型构造方法]([参数]); } |
对象的实例方法引用 | [对象]::[实例方法] |
([参数]) -> { [实例].[调用实例方法]([参数]); } |
类的实例方法引用 | [类名]::[实例方法] |
([参数]) -> { [参数].[调用实例方法](); } |
示例
public static void main(String[] args) {
// 数组的构造方法引用,本质是Function<T, R>型接口的匿名内部类对象
Function<Integer, Integer[]> constructor = Integer[]::new;
constructor.apply(10);
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 对象的实例方法引用传参
list.forEach(System.out::println);
// 等价于
list.forEach((a) -> {
System.out.println(a);
});
// 类的实例方法引用传参(不太恰当的例子)
list.forEach(Object::toString);
// 等价于
list.forEach((a) -> {
a.toString();
});
}
五、流式API
参考自《Java8中的Streams API详解》
大致认识
Stream
不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的。它更像一个高级版本的Iterator
原始版本的Iterator, 用户只能显式地一个一个遍历元素并对其执行某些操作
高级版本的Stream, 用户只要给出需要对其包含的元素执行的操作
流式API的一个好处就是好写又好看。比如一端没什么意义的链式调用
public static void main(String[] args) {
Random random = new Random();
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(random.nextInt(100) + "");
}
list.stream()
// 中间操作————将String转换为Integer
.map(Integer::parseInt)
// 中间操作————过滤相同值
.distinct()
// 中间操作————跳过10个升序排序
.skip(10)
.sorted()
// 中间操作————跳过10个降序排序
.skip(25)
.sorted((a, b) -> -(a - b))
// 中间操作————只要大于60的部分
.filter(a -> a >= 60)
// 终端操作————输出
.forEach(System.out::println);
}
Stream
的使用步骤
- 创建
Stream
——从一个数据源(如集合、数组)中获取流 - 中间操作——一个操作的中间链,对数据源的数据进行操作
- 终止操作——一个终止操作,执行中间操作链,并产生结果
- 关闭流
常用的创建流方式
最常用的是对集合和数组,调用stream()
方法获取一个流
常用的中间操作
筛选与切片
filter(predicate)
从流中排除某些操作limit(n)
截断流使其元素不超过给定对象nskip(n)
返回跳过前n个元素的流。若流中元素不足n个,则返回一个空流,与limit(n)
互补distinct()
通过流所生成元素的hashCode()
和equals()
去除重复元素
映射
map(mapper)
将元素转换成其他形式flatMap(mapper)
将流中的每个元素都换成另一个流,然后把所有流连接成一个流
排序
sorted()
自然升序sorted(comparator)
定制排序
常用的终端操作
遍历
forEach(action)
遍历流中所有元素
输出
toArray()
将流转换为数组collect(collector)
输出其它集合。比如collect(Collectors.toList())
输出成一个List
reduce(accumulator)
合并流中的元素。比如累加运算
判断
anyMatch(predicate)
流中是否至少包含一个符合特定条件的元素allMatch(predicate)
流中是否至少每个元素都符合特定条件noneMatch(predicate)
流中是否所有元素都不符合特定条件
查询
min()
max()
count()
findFirst()
六、总结
JAVA8还有很多新特性也是挺实用的。比如
- 接口的
default
方法和static
方法更易于使用模板方法模式了 Optional
类的包装简化了空指针的判断- 更好的类型推断(如若不然,现在的泛型接口岂不是要把人写死嘛)
但是最主要的还是函数式编程和流式API,基本上革命性的改变了代码编写的风格,可读性更高了,编写和修改更加容易了…
不过对教主来说其实能够使用的也就那么几个,毕竟接触到的函数式接口基本都是最常用到的,而常用的函数式接口基本也就几个,传参基本也就消费型逻辑的函数、供给型逻辑的函数、断言型逻辑的函数、功能型逻辑的函数…经常使用总是可以记住的嘛