一直感觉Lambda表达式是十分“高大上”的一种技术,不管是冲着其本身可以将代码量缩短至变态的缘故,还是希望拿来当做装逼神器的工具(开玩笑!),都值得好好学习一波。
好了,言归正传!Lambda表达式的格式不外乎如下:
(parameters) -> expression 或 (parameters) ->{ statements; }
语法格式和传统的方法其实一样,箭头左边就是参数列表,当然也可以是为空;箭头右边就是表达式或者代码段了。之所以推出lambda表达式,就是允许你通过表达式来代替功能接口。
需要特别注意的是,lambda表达式使用是有前提的,那就是只有函数式接口的方法才能能用lambda作为其调用时的方法。具体什么意思呢?首先来看下什么是函数式接口?
函数式接口其实本质上还是一个接口,但是它是一种特殊的接口:SAM类型的接口(Single Abstract Method)。也就是说,该接口只有一个抽象方法,多了不行,少了也不行。可以有静态方法和默认方法,因为这两种方法都是已经实现的了。同时,这个抽象方法一定不能跟Object类中的方法同名。Java8新特性提供了函数式接口,用于更好的支持函数式编程。(具体了解函数式接口可以参考另一篇博客Java 8 函数式接口和Lambda学习笔记(二))
下面将从简单到复杂的过程来系统学习一下lambda表达式,希望能揭开那对我来说异常神秘的面纱!
首先,是lambda表达式简单的示例,通过这几个小栗子可以加深对其概念的理解。
// 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)
以第3个例子,我们尝试实现它,代码如下:
package com.wjb.lambda;
/**
* 函数式接口:
* 1.只能有一个抽象方法
2.可以有静态方法和默认方法,因为这两种方法都是已经实现的了
3.这个抽象方法一定不能跟Object类中的方法同名。
* @author Administrator
*
*/
@FunctionalInterface
interface Fun{
//只设置一个抽象方法 ------可以正常运行
abstract int spend(int remaining,int money);
//加入静态方法和默认方法 ----可以正常运行
static void staticMethod() {
System.out.println("There is a static method!");
}
default void defaultMethod() {
System.out.println("There is a default method!");
}
//设置Object中可以重写的抽象方法 ----编译报错
// abstract boolean equals(Object obj);
}
public class FunInterface{
public static void main(String[] args) {
Fun fun = (a,b) -> a-b;
System.out.println(fun.spend(100, 5));
}
}
这样看来,lambda表达式真的很简洁。
下面,我们看看lambda表达式的常用场景:
1. 列表迭代
对一个列表的每一个元素进行操作,不使用 Lambda 表达式时如下:
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer integer : list) {
System.out.println(list);
}
使用Lambda表达式的话是怎么呢?
List<Integer> list = Arrays.asList(1,2,3,4,5);
list.forEach((integer2) -> System.out.println(integer2));
一行就搞定了!但是需要注意的是,java 8提出的foreach方法其底层就是实现了Consumer接口的accept方法,所以可以用lambda表达式当做参数。
如果只需要调用单个函数对列表元素进行处理,那么可以使用更加简洁的 方法引用
代替 Lambda 表达式:
list.forEach(System.out::println);
这里的方法引用属于引申,感兴趣的可以看这篇博客 Java 8 方法引用
下面列举一些真实开发可能会遇到的场景,看看lambda表达式真的是神奇:
(1) 比如:从List<Object>对象列表中获取对象单个字段组成的新List,
当我们要从List<User> userList中获取所有用户的Id列表并组成新的List要怎么做,寻常方法可以实现但是过于复杂了
List<Long> userIdList = userList.stream.map(User::getUserId).collect(Collectors.toList());
Stream也是java 8提出的一个非常重要的特性,详情可以看 Java 8 Stream 学习笔记
(2)修改List<User> userList中对象的某个字段值
List<User> userList;//获取所有用户的Id列表
userList.foreach(User -> User.setUserId(User.getUserId+1));
(3)修改List<User> userList中多个参数值
userList.foreach((x) -> {x.setUserId(0L); x.setUserName(""); x.setUserSex(0);})
(4) List过滤值
List<String> list
//过滤null值
list.stream.filter(x -> x != null).collect(Collectors.toList());
//过滤特定值(包括空字符串"")
list.stream.filter(x -> !"str".equals(x)).collect(Collectors.toList());
(5)逗号分隔的字符串转List<Long>
List<Long> listIds = Arrays.asList(ids.split(",")).stream().map(s -> Long.parseLong(s.trim())).collect(Collectors.toList());
代码功能是先将字符串切割成数组后转列表,然后遍历列表时去掉空格并转成Long类型的元素,最后转成新的列表。
(6) List转Map(假设列表是List<FlowNodeTimeoutRuleUser> userList)
//1.List对象中两个字段对应
Map<Long, String> userMap1 = userList.stream().collect(Collectors.toMap(FlowNodeTimeoutRuleUser::getUserId, FlowNodeTimeoutRuleUser::getUserName));
//2.List对象中字段和对象本体对应
Map<Long, String> userMap2 = userList.stream().collect(Collectors.toMap(FlowNodeTimeoutRuleUser::getUserId, x -> x));
//本体表达式可以用lambda表达式x->x 也可以使用接口Function.identity()
Map<Long, String> userMap3 = userList.stream().collect(Collectors.toMap(FlowNodeTimeoutRuleUser::getUserId, Function.identity()));
//3. List对象中key字段重复导致错误,增加一个lambda表达式(key1, key2) -> key2,后者覆盖前者解决key重复问题
Map<Long, String> userMap4 = userList.stream().collect(Collectors.toMap(FlowNodeTimeoutRuleUser::getUserId, Function.identity(), (key1, key2) -> key2));
具体某个方法不懂的可以查看下源码就知道了。
(7)统计List中重复数量:
List<Integer> list = Arrays.asList(1,1,2,5,2,3,4,5);
//统计List中重复数量
Map<Integer,Long> map = list.stream().collect(Collectors.groupingBy(x -> x,Collectors.counting()));
其中的lambda表达式x->x就是list元素本身。
(8)List去除重复值:
List.stream().distinct().collect(Collectors.toList());
(9)根据list对象属性值去除重复值
List<Person> unique = persons.stream().collect(
Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getName))), ArrayList::new)
);
(10)List排序 按照从小到大排序
List<User> userList;
userList.sort((User u1, User u2) -> u1.getAge().compareTo(u2.getAge()));
2. 事件监听
不使用 Lambda 表达式:
button.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
//handle the event
}
});
这里是采用的匿名内部类的方式实现的。
使用 Lambda 表达式,需要编写多条语句时用花括号包围起来:
button.addActionListener(e -> {
//handle the event
});
3. Predicate 接口
java.util.function 包中的 Predicate 接口可以很方便地用于过滤。如果你需要对多个对象进行过滤并执行相同的处理逻辑,那么可以将这些相同的操作封装到 filter 方法中,由调用者提供过滤条件,以便重复使用。
不使用 Predicate 接口,对于每一个对象,都需要编写过滤条件和处理逻辑:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<String> words = Arrays.asList("a", "ab", "abc");
numbers.forEach(x -> {
if (x % 2 == 0) {
//process logic
}
})
words.forEach(x -> {
if (x.length() > 1) {
//process logic
}
})
使用 Predicate 接口,将相同的处理逻辑封装到 filter 方法中,重复调用:
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<String> words = Arrays.asList("a", "ab", "abc");
filter(numbers, x -> (int)x % 2 == 0);
filter(words, x -> ((String)x).length() > 1);
}
public static void filter(List list, Predicate condition) {
list.forEach(x -> {
if (condition.test(x)) {
//process logic
}
})
}
filter 方法也可写成:
public static void filter(List list, Predicate condition) {
list.stream().filter(x -> condition.test(x)).forEach(x -> {
//process logic
})
}
4.Map 映射
使用 Stream 对象的 map 方法将原来的列表经由 Lambda 表达式映射为另一个列表,并通过 collect 方法转换回 List 类型:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> mapped = numbers.stream().map(x -> x * 2).collect(Collectors.toList());
mapped.forEach(System.out::println);
将列表的元素乘以2之后赋给一个新的列表,用Stream的方式实现,代码量少了好多。
5. Reduce聚合
reduce 操作,就是通过二元运算对所有元素进行聚合,最终得到一个结果。例如使用加法对列表进行聚合,就是将列表中所有元素累加,得到总和。
因此,我们可以为 reduce 提供一个接收两个参数的 Lambda 表达式,该表达式就相当于一个二元运算:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce((x, y) -> x + y).get();
System.out.println(sum);
都是用Stream的方式实现的!
6. 代替Runnable
这个也是特别见的场景,下面以创建线程为例,寻常使用 Runnable 类的代码如下:
Runnable r = new Runnable() {
@Override
public void run() {
//to do something
}
};
Thread t = new Thread(r);
t.start();
使用 Lambda 表达式:
Runnable r = () -> {
//to do something
};
Thread t = new Thread(r);
t.start();
或者使用更加紧凑的形式:
new Thread(()->System.out.println("run thread...")).start();
参考文献