Lambda
class List<E> {
? static <Z> List<Z> nil() { ... };
? static <Z> List<Z> cons(Z head, List<Z> tail) { ... };
? E head() { ... }
}
//通过方法赋值的目标参数来自动推断泛型的类型
List<String> l = List.nil();
//而不是显示的指定类型
//List<String> l = List.<String>nil();
//通过前面方法参数类型推断泛型的类型
List.cons(42, List.nil());
//而不是显示的指定类型
//List.cons(42, List.<Integer>nil());
一、forEach
本文由 JavaGuide 翻译,原文地址:https://www.baeldung.com/foreach-java
1 概述
在Java 8中引入的forEach循环为程序员提供了一种新的,简洁而有趣的迭代集合的方式。
在本文中,我们将看到如何将forEach与集合一起使用,它采用何种参数以及此循环与增强的for循环的不同之处。
2 基础知识
public interface Collection<E> extends Iterable<E>
Collection 接口实现了 Iterable 接口,而 Iterable 接口在 Java 8开始具有一个新的 API:
void forEach(Consumer<? super T> action)//对 Iterable的每个元素执行给定的操作,直到所有元素都被处理或动作引发异常。
使用forEach,我们可以迭代一个集合并对每个元素执行给定的操作,就像任何其他迭代器一样。
例如,迭代和打印字符串集合的for循环版本:
for (String name : names) {
? ?System.out.println(name);
}
我们可以使用forEach写这个 :
names.forEach(name -> {
? ?System.out.println(name);
});
3.使用forEach方法
3.1 匿名类
我们使用 forEach迭代集合并对每个元素执行特定操作。要执行的操作包含在实现Consumer接口的类中,并作为参数传递给forEach 。
所述消费者接口是一个功能接口(具有单个抽象方法的接口)。它接受输入并且不返回任何结果。
Consumer 接口定义如下:
@FunctionalInterface
public interface Consumer {
? ?void accept(T t);
}
任何实现,例如,只是打印字符串的消费者:
Consumer<String> printConsumer = new Consumer<String>() {
? ?public void accept(String name) {
? ? ? ?System.out.println(name);
? };
};
可以作为参数传递给forEach:
names.forEach(printConsumer);
但这不是通过消费者和使用forEach API 创建操作的唯一方法。让我们看看我们将使用forEach方法的另外2种最流行的方式:
3.2 Lambda表达式
Java 8功能接口的主要优点是我们可以使用Lambda表达式来实例化它们,并避免使用庞大的匿名类实现。
由于 Consumer 接口属于函数式接口,我们可以通过以下形式在Lambda中表达它:
(argument) -> { body }
name -> System.out.println(name)
names.forEach(name -> System.out.println(name));
3.3 方法参考
我们可以使用方法引用语法而不是普通的Lambda语法,其中已存在一个方法来对类执行操作:
names.forEach(System.out::println);
4.forEach在集合中的使用
4.1.迭代集合
任何类型Collection的可迭代 - 列表,集合,队列 等都具有使用forEach的相同语法。
因此,正如我们已经看到的,迭代列表的元素:
List<String> names = Arrays.asList("Larry", "Steve", "James");
names.forEach(System.out::println);
同样对于一组:
Set<String> uniqueNames = new HashSet<>(Arrays.asList("Larry", "Steve", "James"));
uniqueNames.forEach(System.out::println);
或者让我们说一个队列也是一个集合:
Queue<String> namesQueue = new ArrayDeque<>(Arrays.asList("Larry", "Steve", "James"));
namesQueue.forEach(System.out::println);
4.2.迭代Map - 使用Map的forEach
Map没有实现Iterable接口,但它提供了自己的forEach 变体,它接受BiConsumer。*
Map<Integer, String> namesMap = new HashMap<>();
namesMap.put(1, "Larry");
namesMap.put(2, "Steve");
namesMap.put(3, "James");
namesMap.forEach((key, value) -> System.out.println(key + " " + value));
4.3.迭代一个Map - 通过迭代entrySet
namesMap.entrySet().forEach(entry -> System.out.println(entry.getKey() + " " + entry.getValue()));
二、 内置函数式接口(Built-in Functional Interfaces)
函数式接口 | 函数描述符 |
---|
Runnable | ()->void | Predicate<T> | T->boolean | Consumer<T> | T->void | Function<T,R> | T->R | Supplier<T> | () -> T | UnaryOperator<T> | T -> T | BinaryOperator<T> | (T,T)->T | BiPredicate<L,R> | (L,R)->boolean | BiConsumer<T,U> | (T,U)->void | BiFunction<T,U,R> | (T,U)->R |
JDK 1.8 API包含许多内置函数式接口。 其中一些借口在老版本的 Java 中是比较常见的比如: Comparator 或Runnable ,这些接口都增加了@FunctionalInterface 注解以便能用在 lambda 表达式上。
但是 Java 8 API 同样还提供了很多全新的函数式接口来让你的编程工作更加方便,有一些接口是来自 Google Guava 库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。
Predicates
Predicate 接口是只有一个参数的返回布尔类型值的 断言型 接口。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非):
译者注: Predicate 接口源码如下
package java.util.function;
import java.util.Objects;
?
@FunctionalInterface
public interface Predicate<T> {
? ?
? ?// 该方法是接受一个传入类型,返回一个布尔值.此方法应用于判断.
? ?boolean test(T t);
?
? ?//and方法与关系型运算符"&&"相似,两边都成立才返回true
? ?default Predicate<T> and(Predicate<? super T> other) {
? ? ? ?Objects.requireNonNull(other);
? ? ? ?return (t) -> test(t) && other.test(t);
? }
? ?// 与关系运算符"!"相似,对判断进行取反
? ?default Predicate<T> negate() {
? ? ? ?return (t) -> !test(t);
? }
? ?//or方法与关系型运算符"||"相似,两边只要有一个成立就返回true
? ?default Predicate<T> or(Predicate<? super T> other) {
? ? ? ?Objects.requireNonNull(other);
? ? ? ?return (t) -> test(t) || other.test(t);
? }
? // 该方法接收一个Object对象,返回一个Predicate类型.此方法用于判断第一个test的方法与第二个test方法相同(equal).
? ?static <T> Predicate<T> isEqual(Object targetRef) {
? ? ? ?return (null == targetRef)
? ? ? ? ? ? ? ?? Objects::isNull
? ? ? ? ? ? ? : object -> targetRef.equals(object);
? }
示例:
Predicate<String> predicate = (s) -> s.length() > 0;
?
predicate.test("foo"); ? ? ? ? ? ? ?// true
predicate.negate().test("foo"); ? ? // false
?
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
?
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
Functions
Function 接口接受一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen):
译者注: Function 接口源码如下
package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Function<T, R> {
? ?
? ?//将Function对象应用到输入的参数上,然后返回计算结果。
? ?R apply(T t);
? ?//将两个Function整合,并返回一个能够执行两个Function对象功能的Function对象。
? ?default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
? ? ? ?Objects.requireNonNull(before);
? ? ? ?return (V v) -> apply(before.apply(v));
? }
? ?//
? ?default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
? ? ? ?Objects.requireNonNull(after);
? ? ? ?return (T t) -> after.apply(apply(t));
? }
? ?static <T> Function<T, T> identity() {
? ? ? ?return t -> t;
? }
}
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); ? ? // "123"
Suppliers
Supplier 接口产生给定泛型类型的结果。 与 Function 接口不同,Supplier 接口不接受参数。
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); ? // new Person
Consumers
Consumer 接口表示要对单个输入参数执行的操作。
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
Comparators
Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法:
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
?
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
?
comparator.compare(p1, p2); ? ? ? ? ? ? // > 0
comparator.reversed().compare(p1, p2); ?// < 0
三、Optionals
Optionals不是函数式接口,而是用于防止 NullPointerException 的漂亮工具。这是下一节的一个重要概念,让我们快速了解一下Optionals的工作原理。
Optional 是一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是有时却什么也没有返回,而在Java 8中,你应该返回 Optional 而不是 null。
译者注:示例中每个方法的作用已经添加。
//of():为非null的值创建一个Optional
Optional<String> optional = Optional.of("bam");
// isPresent(): 如果值存在返回true,否则返回false
optional.isPresent(); ? ? ? ? ? // true
//get():如果Optional有值则将其返回,否则抛出NoSuchElementException
optional.get(); ? ? ? ? ? ? ? ? // "bam"
//orElse():如果有值则将其返回,否则返回指定的其它值
optional.orElse("fallback"); ? ?// "bam"
//ifPresent():如果Optional实例有值则为其调用consumer,否则不做处理
optional.ifPresent((s) -> System.out.println(s.charAt(0))); ? ? // "b"
推荐阅读:[[Java8]如何正确使用Optional]
四、方法引用
ambda表达式 | 方法引用 | |
---|
(args) -> ClassName.staticMethod(args) | ClassName::staticMethod | 静态方法方法引用 | (arg0, params) -> arg0.instanceMethod(params) | ClassName::instanceMethod | 内部实例方法引用 | arg0 (params) -> arg0.instanceMethod(params) | arg0.instanceMethod | 外部实例方法引用 |
五、 lambda表达式的写法
六、 重构重复代码 ?
Java8?由Oracle在2014年发布,是继Java5之后最具革命性的版本。 Java8吸收其他语言的精髓带来了函数式编程,lambda表达式,Stream流等一系列新特性,学会了这些新特性,可以让你实现高效编码优雅编码。
烂代码登场
首先引入一个实际的例子,我们常常会写一个dao类来操作数据库,比如查询记录,插入记录等。
下面的代码中实现了查询和插入功能(引入Mybatis三方件):
public class StudentDao {
? ?/**
? ? * 根据学生id查询记录
? ? * @param id 学生id
? ? * @return 返回学生对象
? ? */
? ?public Student queryOne(int id) {
? ? ? ?SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
? ? ? ?SqlSession session = null;
? ? ? ?try {
? ? ? ? ? ?session = sqlSessionFactory.openSession();
? ? ? ? ? ?// 根据id查询指定的student对象
? ? ? ? ? ?return session.selectOne("com.coderspace.mapper.student.queryOne", id);
? ? ? } finally {
? ? ? ? ? ?if (session != null) {
? ? ? ? ? ? ? ?session.close();
? ? ? ? ? }
? ? ? }
? }
? ?/**
? ? * 插入一条学生记录
? ? * @param student 待插入对象
? ? * @return true if success, else return false
? ? */
? ?public boolean insert(Student student) {
? ? ? ?SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
? ? ? ?SqlSession session = null;
? ? ? ?try {
? ? ? ? ? ?session = sqlSessionFactory.openSession();
? ? ? ? ? ?// 向数据库插入student对象
? ? ? ? ? ?int rows = session.insert("com.coderspace.mapper.student.insert", student);
? ? ? ? ? ?return rows > 0;
? ? ? } finally {
? ? ? ? ? ?if (session != null) {
? ? ? ? ? ? ? ?session.close();
? ? ? ? ? }
? ? ? }
? }
}
睁大眼睛观察上面的代码可以发现,这两个方法有很多重复的代码。
除了下面这两行,其他的代码都是一样的,都是先获取session,然后执行核心操作,最后关闭session。
// 方法1中核心代码
return session.selectOne("com.coderspace.mapper.student.queryOne", id);
// 方法2中核心代码
int rows = session.insert("com.coderspace.mapper.student.insert", student);
作为一个有追求的程序员,不,应该叫代码艺术家,是不是应该考虑重构一下。
获取session和关闭session这段代码围绕着具体的核心操作代码,我们可以称这段代码为模板代码。
假如又来了一个需求,需要实现删除student方法,那么你肯定会copy上面的获取session和关闭session代码,这样做有太多重复的代码,作为一名优秀的工程师肯定不会容忍这种事情的发生。
开始重构烂代码
怎么重构呢?现在请出我们的主角登场:环绕执行模式使行为参数化。
名字是不是很高大上,啥叫行为参数化?上面例子中我们已经观察到了,除了核心操作代码其他代码都是一模一样,那我们是不是可以将核心操作代码作为入参传入模板方法中,根据不同的行为分别执行。
变量对象很容易作为参数传入,行为可以作为参数传入吗?
答案是:当然可以,可以采用lambda表达式传入。
下面开始重构之前的例子,主要可以分为三步:
(1)定义函数式接口;
(2)定义模板方法;
(3)传递lambda表达式
所有的环绕执行模式都可以套用上面这三步公式。
第一步:定义函数式接口
@FunctionalInterface
public interface DbOperation<R> {
? ?/**
? ? * 通用操作数据库接口
? ? * @param session 数据库连接session
? ? * @param mapperId 关联mapper文件id操作
? ? * @param params 操作参数
? ? * @return 返回值,R泛型
? ? */
? ?R operate(SqlSession session, String mapperId, Object params);
}
定义了一个operate抽象方法,接收三个参数,返回泛型R。
第二步:定义模板方法
DbOperation是一个函数式接口,作为入参传入:
public class CommonDao<R> {
? ?
? ?public R proccess(DbOperation<R> dbOperation, String mappperId, Object params) {
? ? ? ?SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
? ? ? ?SqlSession session = null;
? ? ? ?try {
? ? ? ? ? ?session = sqlSessionFactory.openSession();
? ? ? ? ? ?// 核心操作
? ? ? ? ? ?return dbOperation.operate(session, mappperId, params);
? ? ? } finally {
? ? ? ? ? ?if (session != null) {
? ? ? ? ? ? ? ?session.close();
? ? ? ? ? }
? ? ? }
? }
}
第三步:传递lambda表达式
// 根据id查询学生
String mapperId = "com.coderspace.mapper.student.queryOne";
int studentNo = 123;
CommonDao<Student> commonDao = new CommonDao<>();
// 使用lambda传递具体的行为
Student studentObj = commonDao.proccess(
? ? ? (session, mappperId, params) -> session.selectOne(mappperId, params),
? ? ? ?mapperId, studentNo);
// 插入学生记录
String mapperId2 = "com.coderspace.mapper.student.insert";
Student student = new Student("coderspace", 1, 100);
CommonDao<Boolean> commonDao2 = new CommonDao<>();
// 使用lambda传递具体的行为
Boolean successInsert = commonDao2.proccess(
? ? ? (session, mappperId, params) -> session.selectOne(mappperId, params),
? ? ? ?mapperId2, student);
实现了上面三步,假如要实现删除方法,CommonDao里面一行代码都不用改,只用在调用方传入不同的参数即可实现。
总结
环绕执行模式在项目实战中大有用途,如果你发现几行易变的代码外面围绕着一堆固定的代码,这个时候你应该考虑使用lambda环绕执行模式了。
环绕执行模式固有套路请跟我一起大声读三遍:
第一步:定义函数式接口
第二步:定义模板方法
第三步:传递lambda表达式
|