Java8中的函数式编程
Posted 灵派coder
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8中的函数式编程相关的知识,希望对你有一定的参考价值。
1.Lambda表达式与函数式接口
1.Lambda表达式简介
Lambda表达式的作用:使得Java可以把 函数 像对象一样作为语言的 一等公民 来对待。从而:
能够编写出 可读性更强、更加紧凑、抽象级别更高的代码
易于编写出可在多核CPU上高效运行、线程安全 的代码
在编写 回调函数 和事件处理程序时,可以摆脱匿名内部类的冗繁
…
Java8的lambda表达式其实是 匿名类 的 语法糖 ,本质上仍然是对象。
函数成为 一等公民 的含义:可以将函数 赋值 给变量,可以在 参数 中传递函数,可以让一个方法的 返回值 是函数…
即,凡是对象可以出现的地方,函数 都可以出现。
2.Lambda表达式的用法
一个lambda表达式可由 用逗号分隔的参数列表 、 ->符号 、 函数体三部分组成,例如:
Arrays.asList("a", "b", "c").forEach((String item) -> System.out.println(item));
因为 参数 item的 类型 可由编译器 推测 出来,因此也可简写成:
Arrays.asList("a", "b", "c").forEach(item -> System.out.println(item));
上面的两种写法,等价于在Java8之前的版本中,使用 匿名类 的实现:
Arrays.asList("a", "b", "c").forEach(
new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
}
);
如果lambda的函数体 非单一语句 ,需要把函数体放在 一对花括号 中:
Arrays.asList("a", "b", "c").forEach((String item) -> {
System.out.println(item);
System.out.println(item);
});
lambda可以使用外部作用域的 常量 (如果是变量而非常量,会 隐式地 转为常量,建议显式地声明为常量):
public class Test {
private int num = 1;
public static void main(String[] args) {
String separator = ",";
Test test = new Test();
Arrays.asList("a", "b", "c").forEach(
item -> System.out.println(item + separator + test.num));
//separator = ";";
//编译期错误 Variable used in lambda expression should be final or effectively final
//test = new Test();
//编译期错误 Variable used in lambda expression should be final or effectively final
}
}
lambda有可能会返回一个值。返回值 的类型也可由编译器 推断 出来。如果lambda的 函数体只有一行 的话,可以不显式使用 return语句,下面两个代码是等价的:
Arrays.asList("a", "b", "c").sort((s1, s2) -> s1.compareTo(s2));
Arrays.asList("a", "b", "c").sort((s1, s2) -> {
return s1.compareTo(s2);
});
3.函数式接口简介
所有的lambda表达式都可以替换成匿名类的实现方式,但并非所有的匿名类都能用lambda来表示。那么什么样的匿名类一定能用lambda来表示呢?那就是 实现于只有一个abstract方法的接口的匿名类 (不算Object中的方法,以及static方法和default方法)。而这种特殊的接口,在Java8中称为 函数式接口 ,用注解 @FunctionalInterface 表示:
@FunctionalInterface
public interface TestFunctional{
void method();
}
但为什么只有这样的接口,才能和lambda配合使用呢?其实不难理解。因为lambda本质是 没有显式重写方法、省略了重写方法名 的匿名类,而一个lambda只能对一个方法进行重写。
Java中所有 lambda表达式 的 类型 都是某个具体的函数式接口。
一个 函数式接口 即使不加 @FunctionalInterface 注解,也可以与lambda配合使用,但这样的函数式接口是 容易出错 的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种 脆弱性 并且能够 明确声明 接口作为函数式接口的意图,建议显式使用该注解 。
4.默认方法与静态方法
Java 8用 默认方法 与 静态方法 这两个新概念来扩展接口的声明。
默认方法 使用 default 关键字定义,默认方法与抽象方法不同,不需要被实现类来具体实现,但是可以被实现类继承或重写。默认方法的出现使Java可以 在扩展接口功能的同时保证向后兼容 (只要是Java1到Java7写出的代码,在Java8中依然可以编译通过)。例如Collection中的stream方法,如果Java8没有对默认方法的支持,那么所有的子类都需要对stream方法提供实现,显然无法保证向后兼容。
静态方法 使用 static 关键字定义,与一般java类中的静态方法一样。静态方法的出现是为了 让工具方法和相关的类或接口放在一起,而不是放到另一个工具类中。例如下面的代码,如果Java8没有对静态方法的支持,那么ofAll方法必须放到另外的工具类Rulers中,显然不如直接放到Ruler中友好。
示例:
@FunctionalInterface
public interface Ruler<T> {
/**
* 校验T
*/
void check(T checkTarget);
/**
* 或操作
*/
@SuppressWarnings("unchecked")
default Ruler<T> or(Ruler<T>... rulers) {
return checkTarget -> {
try {
check(checkTarget);
} catch (CheckException e) {
ofAll(rulers).check(checkTarget);
}
};
}
/**
* Ruler整合
*/
@SafeVarargs
static <T> Ruler<T> ofAll(Ruler<T>... rulers) {
return (checkTarget -> Arrays.stream(rulers).forEach(ruler -> ruler.check(checkTarget)));
}
}
5.四大常用的函数式接口
消费型接口 Consumer<T>
/**
* @name 消费型接口
* @use Consumer<T>
* @param T 传入参数
* @fun 接受一个参数 无返回值
* */
Consumer<String> con = (str) -> System.out.println(str);
con.accept("我是消费型接口!");
//输出:我是消费型接口!
供给型接口 Supplier<R>
/**
* @name 供给型接口
* @use Supplier<R>
* @param R 返回值类型
* @fun 无参数 有返回值
* */
Supplier<Date> supp = () -> new Date();
Date date = supp.get();
System.out.println("当前时间:" + date);
//输出:当前时间:Wed Jul 04 08:05:10 CST 2018
函数型接口 Function<T,R>
/**
* @name 函数型接口
* @use Function<T,R>
* @param T 传入参数
* @return R 返回值类型
* @fun 接受一个参数 有返回值
* */
Function<String, String> fun = (str) -> "hello," + str;
String str = fun.apply("tom");
System.out.println(str);
//输出:hello,tom
断定型接口 Predicate<T>
/**
* @name 断定型接口
* @use Predicate<T>
* @param T 传入参数
* @return Boolean 返回一个Boolean型值
* @fun 接受一个参数 返回Boolean型值
* */
Predicate<Integer> pre = (num) -> num > 0;
Boolean flag = pre.test(10);
System.out.println(flag);
//输出:true
还有更多功能丰富的函数式接口,可自行了解。
2.方法引用
在Java8中,我们可以直接通过 方法引用 来 简写lambda表达式 ,使语言的构造更紧凑简洁,减少冗余代码。但注意,方法引用与lambda表达式 并不完全等价 ,详见:java8中一个超易踩的坑:方法引用与lambda表达式的区别
方法引用 有如下四种用法:
类型 | 示例 |
---|---|
构造器引用 | Class::new |
静态方法引用 | Class::static_method |
任意对象的方法引用 | instance::method |
特定对象的方法引用 | Class::method |
下面,我们以定义了4个方法的Car这个类为例子,区分Java8中支持的4种不同的方法引用。
public static class Car {
public static Car create(final Supplier<Car> supplier) {
return supplier.get();
}
public static void collide(final Car car) {
System.out.println("Collided " + car.toString());
}
public void follow(final Car another) {
System.out.println("Following the " + another.toString());
}
public void repair() {
System.out.println("Repaired " + this.toString());
}
}
1.构造器引用
第一种方法引用是构造器引用,它的语法是Class::new,或者更一般的Class<T>::new。请注意构造器 没有参数。
final Car car1 = Car.create(() -> new Car()); //lambda表达式
final Car car2 = Car.create(Car::new); //方法引用
2.静态方法引用
第二种方法引用是静态方法引用,它的语法是Class::static_method。请注意这个方法 接受一个Car类型的参数。
final List<Car> cars = Arrays.asList(car1, car2);
cars.forEach((Car it) -> Car.collide(it)); //lambda表达式
cars.forEach(Car::collide); //方法引用
3.任意对象的方法引用
第三种方法引用是特定类的任意对象的方法引用,它的语法是Class::method。请注意,这个方法 没有参数。
cars.forEach((Car it) -> it.repair()); //lambda表达式
cars.forEach(Car::repair); //方法引用
4.特定对象的方法引用
第四种方法引用是特定对象的方法引用,它的语法是instance::method。请注意,这个方法 接受一个Car类型的参数。
final Car police = new Car();
cars.forEach((Car it) -> police.follow(it)); //lambda表达式
cars.forEach(police::follow); //方法引用
3.Optional
Java8引入 Optional 来通过一系列的 链式调用, 优雅地 解决 null安全问题。
1.Optional的三种构造方式
Optional.of(obj) :它要求传入的obj不能是null值, 否则会抛出NPE。
Optional.empty() :返回一个持有null的Optional实例。
Optional.ofNullable(obj) :它以一种智能的,宽容的方式来构造一个Optional实例。传null进到就得到Optional.empty(),非null就调用Optional.of(obj)。
2.Optional的错误使用
使用 isPresent() 方法:
isPresent() 与 obj != null 没有任何分别,并不会使代码变得优雅使用 get() 方法:
没有 isPresent() 作铺垫的 get() 调用在IDEA中会收到警告
Reports calls to java.util.Optional.get() without first checking with a isPresent() call if a value is available. If the Optional does not contain a value, get() will throw an exception
调用 Optional.get() 前不事先用 isPresent() 检查值是否可用. 假如 Optional 不包含一个值, get() 将会抛出一个异常
Optional 类型作为 类/实例属性 或 方法参数 时:
把 Optional 类型用作属性或是方法参数在 IntelliJ IDEA 中是强力不推荐的
Reports any uses of java.util.Optional, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”. Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.
使用任何像 Optional 的类型作为字段或方法参数都是不可取的. Optional 只设计为类库方法的, 可明确表示可能无值情况下的 返回类型 . Optional 类型不可被序列化, 用作字段类型会出问题的
错误示例:
//不要这么写:
Optional<User> userOpt = selectUserById(id);
if (userOpt.isPresent()) {
return userOpt.get().getName();
} else {
return "none";
}
//这其实与我们以前不使用Optional时的代码没有任何区别:
User user = getUserById(id);
if (user != null) {
return user.getName();
} else {
return "none";
}
//正确的写法:
return selectUserById(id).map(User::getName).orElse("none");
3.Optional的常见用法
存在即返回,无则提供默认值:
public T orElse(T other)
return opt.orElse(null);
//而不是 return opt.isPresent() ? opt.get() : null;
存在即返回, 无则由函数来产生:
public T orElseGet(Supplier<? extends T> other)
return user.orElseGet(() -> fetchAUserFromDatabase());
//而不要 return user.isPresent() ? user: fetchAUserFromDatabase();
存在才对它做点什么:
public void ifPresent(Consumer consumer)
user.ifPresent(System.out::println);
//而不要
if (user.isPresent()) {
System.out.println(user.get());
}
Optional最重要的用法:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
map方法通常会搭配 orElse orElseGet orElseThrow 方法 使用:
return selectUserById(id)
.map(u -> u.getUsername())
.map(name -> name.toUpperCase())
.orElse(null);
//在java8之前的版本,等价的写法是这样的:
User user = getUserById(id);
if (user != null) {
String name = user.getUsername();
if (name != null) {
return name.toUpperCase();
} else {
return null;
}
} else {
return null;
}
flatMap filter 方法的使用与map类似,这里就不展开了。
4.Stream
1.Stream简介
Stream是用函数式编程方式,在 集合类 上进行复杂操作的工具:
public static void main(String[] args) {
long count1 = Stream.of(1, 2, 3)
.filter(it -> it > 2)
.count();
long count2 = 0;
List<Integer> list = Arrays.asList(1, 2, 3);
for (Integer it : list) {
if (it > 2) {
count2++;
}
}
System.out.println("count1:" + count1 + "---count2:" + count2);
//输出:count1:1---count2:1
}
上述代码实现了一种“获取集合中值大于2的元素个数”的功能。整个过程被 分解 为两种更简单的操作:过滤 和 计数。看似进行了两次遍历操作,事实上,类库巧妙的设计使得只对集合进行了一次遍历:Stream方法的返回值并不是一个新集合,而是一个Stream对象,是用来创建新集合的 配方(算法)。
像filter这样只 描述 Stream,而不产生新集合的方法叫做 惰性求值方法;而像count这样用来从Stream中产生值的方法叫做 及早求值方法。
只有调用了 及早求值方法 ,Stream中之前注入的 惰性求值方法 才会被真正的运行,否则,惰性求值方法 只会像 势能 一样,悬而不发:
// 势能存储
Stream<Integer> potentialEnergy = Stream.of(1, 2, 3)
.filter(it -> {
System.out.println("hello");
return it > 2;
});
System.out.println("world");
// 势能释放
long count = potentialEnergy.count();
System.out.println("count:" + count);
// 尝试将势能再次释放(抛出异常)
long countAgain = potentialEnergy.count();
System.out.println("countAgain:" + countAgain);
//输出:
//world
//hello
//hello
//hello
//count:1
//java.lang.IllegalStateException: stream has already been operated upon or closed
判断一个方法是 惰性求值 的还是 及早求值 的很简单:看它的 返回值。如果是 Stream 类型的就是惰性求值;否则就是及早求值。
对Stream的使用方式是:形成一个惰性求值的 链,最后调用一个及早求值的方法返回想要的结果,或执行预期的操作。
为方便演示,这里定义了一个字段、两个实体类、一个main方法。下文的示例将放到main方法中,会用到这部分代码。
public class Test {
public static void main(String[] args) {
//示例代码
}
private static List<Business> businessList = Arrays.asList(
new Business(1L, "家政", Arrays.asList(
new Category(11L, "保洁"),
new Category(12L, "保姆")
)),
new Business(2L, "速运", Arrays.asList(
new Category(21L, "搬家"),
new Category(22L, "货运")
))
);
@Data
@AllArgsConstructor
static class Business {
private long id;
private String name;
private List<Category> categoryList;
}
@Data
@AllArgsConstructor
static class Category {
private long id;
private String name;
}
}
2.常用惰性求值方法
map:一对一映射
businessList.stream()
.map(Business::getName)
.forEach(System.out::println);
//输出:
//家政
//速运
flatMap:一对多映射
businessList.stream()
.flatMap(business -> business.getCategoryList().stream())
.forEach(System.out::println);
//输出:
//Test.Category(id=11, name=保洁)
//Test.Category(id=12, name=保姆)
//Test.Category(id=21, name=搬家)
//Test.Category(id=22, name=货运)
filter:过滤
businessList.stream()
.filter(business -> business.getId() > 1)
.forEach(System.out::println);
//输出:
//Test.Business(id=2, name=速运, categoryList=[Test.Category(id=21, name=搬家), Test.Category(id=22, name=货运)])
3.常用及早求值方法
forEach:对每个元素执行指定操作(演示略)
reduce:归约
long sumOfCategoryId = businessList.stream()
.flatMap(business -> business.getCategoryList().stream())
.map(Category::getId)
.reduce((x, y) -> x + y).orElse(0L);
System.out.println(sumOfCategoryId);
//输出:66
collect:收集
List categoryIdList = businessList.stream()
.flatMap(business -> business.getCategoryList().stream())
.map(Category::getId)
.collect(Collectors.toList());
System.out.println(categoryIdList);
//输出:[11, 12, 21, 22]
5.Collector
Collector(收集器) 是一种通用的,从流生成复杂值的结构。
标准类库java.util.stream.Collectors,已经提供了一些常用的收集器
如果标准类库中的收集器无法满足需求,也可 定制 一个收集器。
接下来先对标准库中的收集器做简要介绍,然后再说明如何定制收集器。
1.转换成其他集合
toList(),toSet(),toCollection()
//当希望使用集合对象/实现来收集值时,可以使用toCollection
TreeSet rst = businessList.stream()
.map(Business::getName)
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(rst);
//输出:[家政, 速运]
2.转换成值
按某种特定顺序生成一个值:maxBy(),minBy()
//获取id值最大的业务线
Optional<Business> rst = businessList.stream()
.collect(Collectors.maxBy(Comparator.comparing(Business::getId)));
//等价于 .max(Comparator.comparing(Business::getId));
System.out.println(rst.map(Business::getId).orElse(0L));
//输出:2
实现一些常用的数值计算:averagingXX(),summingXX()
//求所有业务线的id均值
Double rst = businessList.stream()
.collect(Collectors.averagingDouble(Business::getId));
System.out.println(rst);
//输出:1.5
3.数据分块
将一个流根据指定的Predicate对象,分解成两个集合
//根据业务线id是否大于1分块
Map<Boolean, List<Business>> rst = businessList.stream()
.peek(business -> business.setCategoryList(null))
.collect(Collectors.partitioningBy(business -> business.getId() > 1));
System.out.println(rst);
//输出:{false=[Test.Business(id=1, name=家政, categoryList=null)], true=[Test.Business(id=2, name=速运, categoryList=null)]}
4.数据分组
数据分开只能将数据分成两部分,而数据分组不受此限制,可以更自然地分割数据(类似与SQL中的 group by 操作)
@Data
@AllArgsConstructor
static class Custom {
private long id;
private String name;
private String city;
}
public static void main(String[] args) {
List<Custom> customList = Arrays.asList(
new Custom(1L, "张三", "北京"),
new Custom(2L, "李四", "北京"),
new Custom(3L, "tom", "纽约"),
new Custom(4L, "jerry", "纽约"),
new Custom(5L, "thomas", "纽约")
);
//根据城市分组
Map<String, List<Custom>> rst = customList.stream()
.collect(Collectors.groupingBy(Custom::getCity));
System.out.println(rst);
}
//输出:{纽约=[Test.Custom(id=3, name=tom, city=纽约), Test.Custom(id=4, name=jerry, city=纽约), Test.Custom(id=5, name=thomas, city=纽约)], 北京=[Test.Custom(id=1, name=张三, city=北京), Test.Custom(id=2, name=李四, city=北京)]}
5.字符串
通过指定 分隔符、前缀、后缀 的方式生成一个格式化后的字符串:joining()
String rst = businessList.stream()
.map(Business::getName)
.collect(Collectors.joining(",", "[", "]"));
System.out.println(rst);
//输出:[家政,速运]
6.组合收集器
组合收集器分为 主收集器 和 下游收集器,主收集器会用到下游收集器,下游收集器是用来生成最终values的 配方。
List<Custom> customList = Arrays.asList(
new Custom(1L, "张三", "北京"),
new Custom(2L, "李四", "北京"),
new Custom(3L, "tom", "纽约"),
new Custom(4L, "jerry", "纽约"),
new Custom(5L, "thomas", "纽约")
);
Map<String, List<Long>> rst = customList.stream()
.collect(Collectors.groupingBy
(Custom::getCity,
Collectors.mapping(Custom::getId, Collectors.toList())));
System.out.println(rst);
//输出:{纽约=[3, 4, 5], 北京=[1, 2]}
7.定制收集器
收集器接口的方法介绍
supplier():用来创建容器的工厂方法,和reduce操作的第一个参数类似,是后续操作的 初值。
accumulator():该方法的作用和reduce操作的第二个参数一样,用来 结合 之前操作的结果和当前值,生成并返回新的值。
combiner():如果有多个容器,会通过该方法 合并为一个容器。
finisher():对容器进行 转换 以得到预期的结果值。
定制一个字符串拼接收集器
public static void main(String[] args) {
List<Custom> customList = Arrays.asList(
new Custom(1L, "张三", "北京"),
new Custom(2L, "李四", "北京"),
new Custom(3L, "tom", "纽约"),
new Custom(4L, "jerry", "纽约"),
new Custom(5L, "thomas", "纽约")
);
String rst = customList.parallelStream()
.map(Custom::getName)
.collect(new MyStrCollector());
System.out.println(rst);
//输出:[张三,李四,tom,jerry,thomas]
}
//泛型含义:<待收集元素的类型,累加器的类型,最终结果的类型>
static class MyStrCollector implements Collector<String, StringBuilder, String> {
private final String prefix = "[";
private final String separator = ",";
private final String suffix = "]";
@Override
public Supplier<StringBuilder> supplier() {
return StringBuilder::new;
}
@Override
public BiConsumer<StringBuilder, String> accumulator() {
return (stringBuilder, item) -> {
stringBuilder.append(separator).append(item);
};
}
@Override
public BinaryOperator<StringBuilder> combiner() {
return StringBuilder::append;
}
@Override
public Function<StringBuilder, String> finisher() {
return (stringBuilder -> prefix + stringBuilder.toString().substring(1) + suffix);
}
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
end
图片来自于:《Java8函数式编程》
以上是关于Java8中的函数式编程的主要内容,如果未能解决你的问题,请参考以下文章