目录
LocalDateTime、LocalDate、LocalTime
一、Lambda 表达式
1)业务场景
假设我们现在有一个业务场景,需要从一个 List 集合中查询符合条件的客户出来,并将对应的客户进行相应的业务处理。人的抽象我们用一个 Person 类表示,如下:
public class Person {
private String name;
private Integer age;
private Sex gender;
private String emailAddress;
public void printPerson() {
// ...
}
public enum Sex {
MALE, FEMALE
}
// getter、setter method...
}
Person 为客户基础抽象类,随着系统的逐步升级,Person 会被赋予更多的客户属性,而相应的条件查询筛选也会随着变化,下面将使用传统的普通方法、接口、匿名内部类和 Lambda 表达式四种方式,逐步递进来表现 Lambda 表达式的魅力。
2)普通方法
最传统粗暴的方式,就是给每一种不同的条件查找单独写一个方法来处理,例如要获取所有年龄大于 age 的客户,代码如下:
/**
* 处理所有年龄大于 age 的 Person
*/
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
如果现在需求变更,需要处理某一个年龄范围的客户,那就需要新增一个方法,比如:
/**
* 处理所有年龄在 [low, high] 的 Person
*/
public static void printPersonsWithinAgeRange(List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() <= high) {
p.printPerson();
}
}
}
没错,也就是每次查询需求有一点小变化,我们都要为此编写一个新的方法来处理,但除了 if 条件逻辑语句之外的大部分代码都是重复的,所以这种方式会导致系统存在大量的冗余代码。
3)接口
为了解决传统的普通方法导致的大量冗余代码问题,其实我们已经发现,每次条件的变化影响的都是 if 的逻辑语句,那么只需要把经常变化的逻辑语句抽象成一个 CheckPerson 接口,后续的业务变化就交给具体的实现类来处理,就可以防止频繁改动方法的源代码,复用现有的查找处理方法。
CheckPerson 接口
public interface CheckPerson {
boolean test(Person p);
}
CheckPersonEligibleForSelectiveService 实现类
public class CheckPersonEligibleForSelectiveService implements CheckPerson {
@Override
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
print 方法
public static void printPersons(List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
最后获取合适的客户只需要传递具体的实现类,后续修改也可以不改动方法源代码,如下:
printPersons(roster, new CheckPersonEligibleForSelectiveService());
这种方式比起传统方法可以不修改源代码,但是如果业务要筛选的条件非常多,就需要很多的实现类,但这些类往往只是一次性的,用过之后就很少再使用,所以也是一种浪费。
4)匿名内部类
接口实现类的方式解决了方法源代码频繁改动冗余的问题,但会增加很多类,为了进一步优化,可以使用匿名内部类,直接实现接口并当做参数传入方法中使用,最终代码的优化工作又更进了一步。
printPersons(roster, new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25;
}
});
5)Lambda 表达式
尽管匿名内部类的方式已经很优雅,节省了大量冗余代码,提高了代码的扩展性和复用性。但还存在着些许冗余信息,比如 CheckPerson 接口的 new 创建语句,接口的 test 方法,我们希望只关注不同点,也就是指具体的方法体,而忽略接口名和接口方法这些相同点,这时 Lambda 表达式语法应运而生,它完成了我们的期望,匿名内部类中的写法用 Lambda 表达式就变成下面这样:
printPersons(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
最终的代码变得非常的简洁和优雅,只保留了我们需要专心关注的方法体信息。
Lambda 表达式与匿名内部类相似,你可以认为它是一种匿名方法,因为它省略了方法名、返回值和参数等信息,会根绝类型自行推断最优的方法调用。
Lambda 表达式的特征
- 参数列表,即上述方法中的 “p”,如果有多个,则以逗号分割,并且用“() ”包括起来,比如 (p1, p2);
- 箭头,即“->”符号,表示方法体的开始;
- 方法体,如果方法体只有一行代码,可以不加中括号"{}"和分号。如果有多行,则需要加中括号"{}"和分号,比如:{ String p1 = 1; return p1 + "2"; }。
二、函数式接口
函数式接口即指接口中有且仅有一个抽象方法,这样的接口称为函数式接口,你也可以使用 @FunctionInterface 标记当前接口为函数式接口,限制当前接口只能有一个抽象方法,如果有多个抽象方法就会报错。
Lambda 表达式使用的基础其实就是基于函数式接口,只有是含有单一抽象方法的函数式接口,才可以使用 Lambda 表达式的语法糖。
Java 8 的 JDK 中也自带有很多的现成的函数式接口,常用的比如 Predicate、Consumer、Function 和 Supplier,这些函数式接口都放在 java.util.function 包下。
1)Predicate
在上一章的 Lambda 表达式案例中,我们使用到了 CheckPerson 接口的 boolean test(Person person) 方法来抽象不同业务场景下的 Person 筛选条件,其实在 Java 8 中,有自带 boolean test(T t) 方法的函数式接口 Predicate。
你可以把 Predicate 理解为对任意一个对象进行逻辑判断,最终返回 Boolean 结果的一个抽象接口,基于 Predicate 可以继续优化上一章案例的 printPersons 方法,而方法的最终使用方法与 CheckPerson 接口的方式相同,只不过连接口的定义这一步都节省了。
print 方法
public static void printPersons(List<Person> roster, Predicate<Person> predicate) {
for (Person p : roster) {
if (predicate.test(p)) {
p.printPerson();
}
}
}
调用方式与原来的相同,如下:
printPersons(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
大部分情况下,Predicate 接口的 test() 方法就能抽象大部分的逻辑表达式语句,如果需要更细粒度的逻辑划分,可以使用 Predicate 接口的默认方法和静态方法来组合。
Predicate 接口的默认和静态方法
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// and
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
// or
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
// ! 取反
default Predicate<T> negate() {
return (t) -> !test(t);
}
// 通过两个对象的 equals 方法来对比
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
2)Consumer
经过我们的 Lambda 表达式和 Predicate 函数式接口优化后,我们对 Person 目标客户群的筛选处理方法已经很优雅了。但业务需求总是多变复杂的,现在我们希望对筛选后的 Person 客户做多样的处理,那么与筛选条件类似,需要把处理的方法也抽象成一个接口,那就可以使用 JDK 8 自带的 Consumer 接口的 accept(T t) 方法,如下:
public static void processPersons(List<Person> roster, Predicate<Person> predicate, Consumer<Person> consumer) {
for (Person p : roster) {
if (predicate.test(p)) {
consumer.accept(p);
}
}
}
方法的调用方式
processPersons(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
Person::printPerson
);
我们只需要将不同的处理方法同 Lambda 的写法传递给方法,可以不修改源代码。
Consumer 接口可以表示接受一个对象,并对对象进行处理,Consumer 的抽象方法和默认方法如下:
@FunctionalInterface
public interface Consumer<T> {
// 处理对象 t
void accept(T t);
// 可以组成消费处理链 c.andThen(...).accpet(..)
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
所以当我们需要应付对某个对象多变的处理需求时,就可以通过 Consumer 来抽象处理的方法,并通过 andThen 方法来自由组成处理的先后顺序。
3)Function
Function 接口如字面意思,代表一个接受类型 T 的参数并返回一个为非 void 的值方法的抽象,它提供了比 Predicate 和 Consumer 接口更加宽泛的意义。
Function 接口方法
@FunctionalInterface
public interface Function<T, R> {
// 调用方法,接受 T 类型参数,返回 R 类型返回值
R apply(T t);
// 合并两个方法,传递前置处理方法
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;
}
}
在日常开发中我们经常会获取某个对象的字段值,但这个对象可能会有 NPE 问题,我们需要避免发生 NPE,而且对象为 Null 时就设置字段为一个默认值。
那么我们可以借鉴一下 python 语言中的 dict.get(obj, filedName, defaultValue) 方法,写一个 ObjectUtil 的 safeGet 方法,如下:
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ObjectUtil {
public static <T, R> R safeGet(T obj, Function<T, R> getter, R defaultValue) {
if (obj == null) {
return defaultValue;
}
return getter.apply(obj);
}
public static void main(String[] args) {
Person person = null;
Integer age = safeGet(person, Person::getAge, 0);
// 相当于
// Integer age = 0;
// if (person != null) {
// age = person.getAge();
// }
}
}
对于对象 NPE 的优雅处理,Java 8 中还可以使用 Optional,这个放到第四章再介绍。
4)Supplier
Supplier 接口为获取某类型对象的方法的抽象,接口中就只有一个 T get() 方法,比较简单,Supplier 主要是用来结合 Optional 和 Stream API 使用,等到第四章再做演示。
Supplier 接口方法
@FunctionalInterface
public interface Supplier<T> {
T get();
}
三、接口改动
在第二章中,我们看到的几个函数式接口中,有 default 默认方法和 static 静态方法的身影,其实这也是 Java 8 中接口的一个新特性。
Predicate 类回顾
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// 默认方法
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
// 默认方法
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
// 默认方法
default Predicate<T> negate() {
return (t) -> !test(t);
}
// 静态方法
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
Java 8 以前的接口只能定义抽象方法和全局静态常量,而 Java 8 中的接口增加了 default 和 static 方法的定义,缩小了接口与抽象类的差距,使接口的使用场景更多,毕竟在 Java 中类只能继承一个父类,但可以实现多个接口,所以接口的使用会更加的灵活。
四、Optional
Optional 是 Java 8 中用来优雅处理对象 Null 值判断,避免 NPE 的一个新类,合理的利用 Optional 可以使你的代码更加简洁,并且不至于因为判断而导致 if 多层嵌套。下面将通过几个实例来展示合理使用 Optional 带来的魅力,并与 if 语句实现的代码做对比。
场景一:新客户处理
假设我们的业务系统在客户登录时,需要通过客户名获取客户的信息 Person,如果客户为空,则为新顾客,需要为其创建新数据,如果不为空直接获取信息返回,这是很常见的业务场景。通过 Optional 的 ofElseGet 方法来实现,只需要一行代码。
public static void ofElseGet(Person exist) {
// 不为空直接获取,为空创建新的 Person 数据
Person p = Optional.ofNullable(exist).orElseGet(Person::new);
// 相当于下面 6 行的效果
// Person p;
// if (exist!= null) {
// p = exist;
// } else {
// p = new Person();
// }
}
Optional 的 orElseGet 方法,其实就是通过第三章中的函数式接口 Supplier 来实现,如下:
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
Supplier 接口抽象了对象的获取和创建方式,交由使用者自己定制。上面的例子在实际使用中,我们也不会直接只是调用对象构造器就完成创建,更好的方法其实是结合 Builder 设计模式来创建,如下:
public static void ofElseGetWithBuilder(Person exist) {
Person p = Optional.ofNullable(exist).orElseGet(
() -> Person.builder().age(1)
.emailAddress("address")
.name("shq")
.gender(Person.Sex.MALE)
.build());
}
通过 Optional 结合 Builder 可以让代码看起来更连贯,也更加简洁,关于 Builder 的使用,可以搜索一下 lombok。
场景二:判断 DTO 的 errCode 属性
很多时候,我们还要获取一个对象的属性值,并做后续的处理,比如很多第三方服务接口返回的 DTO 数据的字段,有可能是 Null 值,常见的比如 errCode 和 errMsg。
这种情况可以通过 Optional 的 map 方法获取对应的属性字段,如下:
static class Result {
private Integer errCode;
private String errMsg;
private String data;
public Result() {
}
public Result(Integer errCode, String errMsg, String data) {
// ....
}
// getter、setter method...
}
public static void map(Result res) {
Integer errCode = Optional.ofNullable(res).map(Result::getErrCode).orElse(0);
// 相当于下面 6 行的效果
// Integer errCode;
// if (res != null) {
// errCode = res.getErrCode() == null ? res.getErrCode() : 0;
// } else {
// errCode = 0;
// }
}
map 方法其实是通过 Function 函数式接口来完成这一处理,可以自己查看 Optional 的源码做深入的理解。
场景三:根据 DTO 的 errCode 做对应的处理
上个例子中,我们获取了 DTO 类的 errCode 之后,其实还需要判断 errCode 对应的值,再做对应的判断,比如 errCode 为 0 或者 null 时,一般都代表着接口调用成功,而 errCode 不为 null 并且值为非 0 通常代表着调用失败,我们经常需要通过判断 errCode 值来做对应的处理。这种情况可以结合 Optional 的 filter 和 ifPersent 方法,通过 filter 判断 DTO 的字段值,再通过 ifPersent 做相应的后续处理,比如调用成功时,输出 data 的值,调用失败时,输出 errMsg 的值。
具体代码如下:
private static final Integer SUCCESS = 0;
public static void consumeByErrCode(Result result) {
Optional<Result> resultOpt = Optional.ofNullable(result);
// 给太长的逻辑表达式取一个代表含义的名字,提高代码可读性
Predicate<Result> successPre = r -> SUCCESS.equals(resultOpt.map(Result::getErrCode).orElse(0));
// 调用成功
resultOpt.filter(successPre)
.ifPresent(r -> System.out.println(r.getData()));
// 调用失败
resultOpt.filter(successPre.negate())
.ifPresent(r -> System.out.println(r.getErrMsg()));
}
小结
通过上述几个例子,我们可以看到合理使用 Optional 并结合几个内置函数式接口,可以大大的避免 if (obj != null) 这类的逻辑判断出现,使代码看起来更加专注于业务的处理细节,推荐多利用 Optional 来处理对象的 NPE 问题。
不过在使用 Optional 的 filter 做对象状态属性这类判断需要逻辑上的一些转变,可能不如 if 语句那么直接,大家可以基于团队的习惯灵活使用,自己做取舍。
五、方法引用
方法引用其实在前面几张已经出现过数次了,就比如上一章中的 Result::getErrCode 其实就是方法引用。方法引用其实就是在 Lambda 表达式的基础上,进一步简化其代码的方式,比如把一个已存在的类或实例的方法当做函数式接口方法的入参时,就可以使用方法引用。
方法引用一共有 4 种:
- 静态方法引用,ContainingClass::staticMethodName;
- 实例对象方法引用,containingObject::instanceMethodName;
- 任意对象特定类型的实例方法引用,ContainingType::methodName;
- 构造器引用,ClassName::new;
方法引用的特征就是“::”,左边是类名或者实例对象名,右边是方法名或者 new。
六、Stream API
Stream API 是 Java 8 中给 Collection 类新增的一种类 SQL 的集合操作方式。集合是我们平时用得很多的类,Collection 中例如 List、Set 和 Queue,在新增了 Stream API 之后,我们可以很方便的对这些集合类进行查找、排序、求和、获取最大/小值、转成 Map 和 Set 等这些操作,Stream API 封装了大量此类业务经常要使用到的方法,节省了我们重复封装编程的工作。
下面将通过几个案例来介绍常用的 Stream API,Stream API 对应 java.util.stream.Stream 这个接口,其他的方法用途就根据例子举一反三,选择合适的场合使用。
组成
一个 Stream API 的使用,通过是通过 Collection 接口的 stream 方法开始,然后中间使用 Stream API 提供的各种过滤、排序、映射、去重等处理组合,处理顺序根据具体业务组成,最后再通过 collect 方法和 Collector 类返回一个新的集合类,或者通过 findFirst、min、max 等求得最小/大值。
例如:
public static void component(Collection<Person> personCollection) {
// 开始:获取 Stream API
Stream<Person> stream = personCollection.stream();
// 中间处理:查找 Person 为 Male
Stream<Person> filterHandlerStream = stream.filter(p -> Person.Sex.MALE == p.getGender());
// 结尾:获取过滤后的 List 结果
List<Person> result = filterHandlerStream.collect(Collectors.toList());
}
集合转换
public static void collectAndCollector(Collection<Person> personCollection) {
// collect(Collectors.toSet())
// 获取 Person 名字的 set 集合
Set<String> names = personCollection.stream().map(Person::getName)
.collect(Collectors.toSet());
// collect(Collectors.toMap(Person::getName, v -> v, (k1, k2) -> k1))
// 以 Person 名字为 KEY,Person 为 VALUE 返回一个 Map
Map<String, Person> namePersonMap = personCollection.stream()
.collect(Collectors.toMap(Person::getName, v -> v, (k1, k2) -> k1));
// collect(Collectors.groupingBy(Person::getName))
// 以 Person 名字为 KEY 进行分组,把相同 name 的 Person 放在同一个 List 作为一个 VALUE, 返回一个 Map
Map<String, List<Person>> namePersonListMap = personCollection.stream()
.collect(Collectors.groupingBy(Person::getName));
}
通过 Stream API,Collection 集合可以调用方法直接转换成其他的集合或者 Map 类。
如上面的案例所示,常用的一般有:
- 转换 List:collect(Collectors.toList());
- 转换 Set:collect(Collectors.toSet());
- 转换 Map:collect(Collectors.toMap(Person::getName, v -> v, (k1, k2) -> k1)),Person::getName 指定 Map 的 KEY,v -> v 指定 Map 的 VALUE,而最后的 (k1, k2) -> k1,指定 KEY 相同时,以哪个为准,强烈要求传入 (k1, k2) -> k1 方法,否则 KEY 冲突时会报错;另外也可以用 collect(Collectors.groupingBy(Person::getName)) 对集合进行分组。
求最大/小值、求和、求均值
public static void calculate(Collection<Person> personCollection) {
// 求最大年龄
Optional<Integer> maxAge = personCollection.stream()
.map(Person::getAge).max(Integer::compareTo);
// 效果同上
// OptionalInt max = personCollection.stream().mapToInt(Person::getAge).max();
// 求最小年龄
Optional<Integer> minAge = personCollection.stream()
.map(Person::getAge).min(Integer::compareTo);
// 效果同上
// OptionalInt min = personCollection.stream().mapToInt(Person::getAge).min();
// 年龄求和
int sum = personCollection.stream()
.mapToInt(Person::getAge).sum();
// 年龄求平均值
OptionalDouble average = personCollection.stream()
.mapToInt(Person::getAge).average();
}
上面的例子展示了通过 Stream API 来对集合求最大/小值、求和、求均值等操作,主要区别我都特地换行了,方便区分。主要就是要通过 map 方法先映射对象中你想要计算的字段值,然后再调用相应的求值方法,比如 max。另外如果调用的是 map 方法,则返回的依旧是 Stream,而调用了 mapToXXX(比如 mapToInt) 则返回的是相应的 XXXStream(比如 IntStream),IntStream 主要是返回的 Optional 类不同和求值方法(max、min 等)不用传参。
七、新的时间类
Java 8 中,新增了不少功能齐全的时间类,常用的比如 LocalDateTime、LocalDate、LocalTime 等,这些新的时间类都放在 java.time 包下,感兴趣的可以自己到这个包下研究底层源码。
优点
- 旧的 Date、Calendar 类线程非安全,而新的 LocalDateTime、LocalDate、LocalTime 等线程安全,官方注释中明确的写明了:Implementation Requirements: This class is immutable and thread-safe;
- 旧的 Date 类月份从 0 开始,也就是 1~12 月份对应的值其实是 0 - 11,一般要使用 Calendar 中的月份枚举,不然很容易搞错;
- 新的 LocalDateTime(年月日 时分秒)、LocalDate(年月日) 和 LocalTime (时分秒)将时间拆分得更加具体,并且三个类之间可以互相转换,可以根据业务需求使用对应的类;
- 新的 LocalDateTime 等时间类,API 更加齐全,封装了对应的年与日,时分秒的获取方法,还有加(plus)减(minus)方法。
LocalDateTime、LocalDate、LocalTime
public static void convert() {
// 2022-03-28T00:00
LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2022, 3, 28), LocalTime.MIN);
// 2022-03-28
LocalDate localDate = dateTime.toLocalDate();
// 00:00
LocalTime localTime = dateTime.toLocalTime();
System.out.println(dateTime);
System.out.println(localDate);
System.out.println(localTime);
}
日期计算
public static void datetimeComputer() {
LocalDateTime current = LocalDateTime.now();
// 加法
LocalDateTime nextYear = current.plusYears(1);
LocalDateTime nextMonth = current.plusMonths(1);
LocalDateTime tomorrow = current.plusDays(1);
LocalDateTime nextHour = current.plusHours(1);
LocalDateTime nextMinute = current.plusMinutes(1);
LocalDateTime nextSecond = current.plusSeconds(1);
// 对应的减法就是 minusXXX,获取就是 getXXX,此处不在重复累赘
LocalDateTime lastYear = current.minusYears(1);
// ...
int currentYear = current.getYear();
// ...
}
日期解析和格式化
日期的解析和格式化都需要依赖 DateTimeFormatter 类,DateTimeFormatter 类主要定义了日期的格式,如下:
public static void parseAndFormat() {
LocalDateTime current = LocalDateTime.now();
// 定义日期模板
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化
String dateTimeString = current.format(formatter);
// 解析
LocalDateTime parseTime = LocalDateTime.parse(dateTimeString, formatter);
// 获取秒级时间戳
long timestampSec = parseTime.atZone(ZoneId.systemDefault()).toEpochSecond();
// 获取毫秒级时间戳
long timestampMilliSec = parseTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
还有其他新的时间类,此处就不再展开介绍,后续自行了解。
参考资料:https://www.oracle.com/java/technologies/javase/8-whats-new.html,Oracle Java 8 官方文档