Java 8 最佳实践
by @王爵nice
biezhi.me@gmail.com
我们在探讨什么?
- 编写简洁的代码
- 函数式编程思想
- 避免副作用
- 并行流的大跃进
- 更快的完成事情
Java 8 best practices
你的时间非常重要,即便是坐在这里。
Agenda
- λ Lambdas
- f(x) Functional Interfaces
- ! Exceptions
- ? Optional
- ♒ Streams
- ⏱ DateTime API
- 🛫 Extras
Java 8 best practices
Lambdas
- 一段代码块
- 作用于一个函数式接口
- 自动类型推断
什么是 lmabda ?
Java 8 best practices
Lambdas
public interface Comparator<T> {
int compare(T obj1, T obj2);
}
// Java 7
Collections.sort(peoples, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name);
}
});
Java 7 的时代
Java 8 best practices
Lambdas
public interface Comparator<T> {
int compare(T obj1, T obj2);
}
// Java 8
people.sort((p1, p2) -> p1.name.compareTo(p2.name));
// prefer
(p1, p2) -> p1.name.compareTo(p2.name);
// avoid
(Person p1, Person p2) -> p1.name.compareTo(p2.name);
- 利用参数类型推断
- 只在编译器需要的时候指定类型
Java 8 best practices
Lambdas
// prefer
str -> str.toUpperCase(Locale.US);
// avoid
(str) -> str.toUpperCase(Locale.US);
当小括号可选的时候不要写小括号
Java 8 best practices
Lambdas
public UnaryOperator<String> upperCaser(Locale locale) {
return str -> str.toUpperCase(locale);
}
这里无需声明 final
- 不要将局部变量声明为 “final”
- 实际上在 lambda 表达式中局部变量默认就是 final 的
Java 8 best practices
Lambdas
// prefer
str -> str.toUpperCase(Locale.US);
// use with care
str -> {
return str.toUpperCase(Locale.US);
}
- 使用表达式,而不是块代码,减少冗余
- 在必要的时候单独写一个方法
Java 8 best practices
Lambas for Abstraction
private int doFoo() {
// 很长的一段代码
// foo 的核心逻辑
// 又是很长一串代码
}
private int doBar() {
// 很长的一段代码
// bar 的核心逻辑
// 又是很长一串代码
}
- 重复的代码
- 除了中间的一点逻辑
- 使用lambda来表示差异
Java 8 best practices
private int doFoo() {
return doFooBar( lambdaOfFooSpecificLogic );
}
private int doBar() {
return doFooBar( lambdaOfBarSpecificLogic );
}
private int doFooBar(Function<A, B> fn) {
// 很长一段代码
result = fn.apply(arg);
// 很长一段代码
}
Functional interfaces
Java 8 best practices
- 使用只有一个抽象方法的接口
- Runnable
- Comparable
- Callable
- Java 8 中添加了很多这样的接口
- Function<T, R>
- Predicate<T>
- Supplier<T>
- Consumer<T>
- 可以查看 java.util.function 包
Functional interfaces
Java 8 best practices
- 如果自定义的函数式接口是有价值的、有意义的那么你就可以自己实现一个,否则别做
- 比如大量的参数或者原始类型、对象的混合
- 如果你写了一个函数式接口,记得加上 @FunctionalInterface 注解
@FunctionalInterface
public interface FooBarQuery {
public abstract Foo findAllFoos(Bar bar);
}
Higher order methods
Java 8 best practices
private String nameGreet(Supplier<String> nameSupplier) {
return "Hello " + nameSupplier.get();
}
// 使用 lambda 调用
String greeting = nameGreet(() -> "空中金融");
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
Avoid method overloads
Java 8 best practices
- lambda使用目标类型
- 与方法重载会导致冲突
// avoid
public class Foo<T> {
public Foo<R> apply(Function<T, R> fn);
public Foo<T> apply(UnaryOperator<T> fn);
}
Avoid method overloads
Java 8 best practices
使用不同的方法名称以避免冲突
// prefer
public class Foo<T> {
public Foo<R> applyFunction(Function<T, R> fn);
public Foo<T> applyOperator(UnaryOperator<T> fn);
}
Functional interface last
Java 8 best practices
- 将函数式接口置尾
- 尤其当函数式接口和非函数式接口混用时
- 保持整体风格
- IDE会提示你做的更好
// prefer
public Foo parse(Locale locale, Function<Locale,Foo> fn);
// avoid
public Foo parse(Function<Locale,Foo> fn, Locale locale);
Checked exceptions
Java 8 best practices
- 大多数函数式接口不会声明异常
- 在 lambda 中没有简单的方法来检查异常
// 编译不通过
public Function<String, Class> loader() {
return className -> Class.forName(className);
}
Checked exceptions
Java 8 best practices
- 编写一个工具方法
- 编写一个 Unchecked 类
- 将受检异常转换为非受检异常(运行时异常)
public Function<String, Class> loader() {
return Unchecked.function(Class::forName);
}
Optional and null
Java 8 best practices
说起空指针你们会想到什么?
服务器又炸了
没错,是的!
Optional and null
Java 8 best practices
- Java8 新增加了一个 Optional 类
-
这是一个两极分化的观点
- 有人说它是救世主也有人说这功能并无卵用
- 以务实的方式使用,可能很有用
-
概念简单,只有两种状态
- 目前是空的
- 就像非空引用和空引用一样
- 在查询之前必须检查它处于哪个状态
String a = "AB";
String b = null;
Optional<String> a = Optional.of("AB");
Optional<String> b = Optional.empty();
Optional and null
Java 8 best practices
Optional 有什么需要注意的 ?
- Optional 变量本身不可以为NULL
- 永远都不行
- Never, never, never, never!
String a = "AB";
String b = null;
Optional<String> a = Optional.of("AB");
Optional<String> b = Optional.empty();
Optional<String> c = null; // NO NO NO
Optional and null
Java 8 best practices
// 当某个库、API 没找到会返回 NULL
public Foo findFoo(String key) { … }
// 程序中必须记住要对那些可能为 NULL 的变量做检查
Foo foo = findFoo(key);
if (foo == null) {
foo = Foo.DEFAULT; // 或者抛出一个异常
}
// 当某个库、API 没找到会返回 Optional
public Optional<Foo> findFoo(String key) { … }
// 获取值的代码
Foo foo = findFoo(key).orElse(Foo.DEFAULT);
// 或者
Foo foo = findFoo(key).orElseThrow(RuntimeException::new);
← 以往判断 NULL 的方式
Optional 的方式 →
Optional and null
Java 8 best practices
- 首选函数式方法,比如 orElse
- 使用 isPresent 方法大多数情况下是滥用这个功能了,多此一举
// prefer
Foo foo = findFoo(key).orElse(Foo.DEFAULT);
// avoid ifPresent()
Optional<Foo> optFoo = findFoo(key);
if (optFoo.ifPresent()) { … }
Optional and null
Java 8 best practices
你会选什么 ?
A. 在哪里都使用 Optional
B. 在公共 API、入参和返回值上使用而不是NULL
C. 在公共返回类型上使用而不是 NULL
D. 在几个选定的地方使用
E. 不用
Optional and null
Java 8 best practices
- Optional 是一个类
- 使用它会有一些内存/性能成本
- 无法直接得到最终类型
- 不可序列化
- 在getter中将 NULL 实例变量转换为Optional
- JDK 为了返回一个类型添加了它
- 在参数中使用通常会让调用者感到厌烦
- 通过返回类型获取真正需要的值
Optional and null
Java 8 best practices
存在即返回,否则提供默认值
//而不是 return user.isPresent() ? user.get() : null;
return user.orElse(null);
return user.orElse(UNKNOWN_USER);
存在即返回, 无则由函数来产生
return user.orElseGet(() -> fetchAUserFromDatabase());
//而不要 return user.isPresent() ? user: fetchAUserFromDatabase();
user.ifPresent(System.out::println);
//而不要下边那样
if (user.isPresent()) {
System.out.println(user.get());
}
存在才对它做点儿什么
Optional and null
Java 8 best practices
使用 map 转换结果
return user.map(u -> u.getOrders()).orElse(Collections.emptyList())
// 上面避免了我们类似 Java 8 之前的做法
if(user.isPresent()) {
return user.get().getOrders();
} else {
return Collections.emptyList();
}
return user.map(u -> u.getUsername())
.map(name -> name.toUpperCase())
.orElse(null);
map 是无限层级的,可以在深一层获取大写用户名
Streams
Java 8 best practices
- 大多数循环都是一样的
- 重复设计模式
- Streams 库提供了一个抽象
- 使用 lambda 传递有趣的信息
Streams
Java 8 best practices
List<Trade> trades = loadTrades();
List<Money> valued = new ArrayList<Money>();
for (Trade t : trades) {
if (t.isActive()) {
Money pv = presentValue(t);
valued.add(pv);
}
}
List<Trade> trades = loadTrades();
List<Money> valued =
trades.stream()
.filter(t -> t.isActive())
.map(t -> presentValue(t))
.collect(Collectors.toList());
BEFORE
AFTER
Streams
Java 8 best practices
// avoid
strings.stream().filter(s -> s.length() > 2).sorted()
.map(s -> s.substring(0, 2)).collect(Collectors.toList());
// prefer
strings.stream()
.filter(s -> s.length() > 2)
.sorted()
.map(s -> s.substring(0, 2))
.collect(Collectors.toList());
每行最多有一个流方法调用。
像 map、filter、sorted 这样的操作更容易识别
Streams
Java 8 best practices
// avoid
strings.stream()
.sorted(Comparator.reverseOrder())
.limit(10)
.collect(Collectors.toMap(Function.identity(), String::length));
// prefer
strings.stream()
.sorted(reverseOrder())
.limit(10)
.collect(toMap(identity(), String::length));
导入 Stream API 中的所有静态方法,减少视觉干扰。
这将使你的代码变得更加简洁、容易阅读和理解。
Streams
Java 8 best practices
// avoid
strings.stream()
.map(s -> s.length())
.collect(toList());
// prefer
strings.stream()
.map(String::length)
.collect(toList());
方法引用更容易阅读,因为我们避免了 -> 和 () 操作符产生的所有视觉干扰。像 s -> s.length() 这样的 lambda 表达式被编译为一个私有静态方法和一个 invokedynamic 指令。
// s -> s.lenght() 被翻译为下面的代码
private static Integer lambda$main$0(String s) {
return s.length();
}
Streams
Java 8 best practices
Stream<Object> objects = Stream.of(
"a string",
42,
new String[] { "an array" },
"another string");
List<String> strings = objects
.filter(String.class::isInstance)
.map(String.class::cast)
.collect(toList());
如何获取该流中的所有字符串类型,收集为一个 List ?
Streams
Java 8 best practices
// 转换 Entity 为 Map
Map<Integer, Entity> entityById = entities.stream()
.collect(toMap(Entity::getId, identity()));
// 经常使用的情况
Map<Integer, Entity> entityById = entities.stream()
.collect(ExtraCollectors.toByIdMap());
private static class ExtraCollectors {
public static Collector<Entity,?,Map<Integer,Entity>> toByIdMap() {
return Collectors.toMap(Entity::getId, identity());
}
}
为经常使用的收集器起一个有意义的名称,
同时可以将收集器提取为一个方法。
Streams
Java 8 best practices
- 不要用滥用它
- 流并不总是比循环更可读
- 对集合有好处,对 Map 则不太好
- 通过专用包装器在 Map 上进行流式处理
- 查看 MapStream 类
- 关键部分性能的基准测试
- 并行流必须非常小心地使用
- 对方法引用的过度使用要谨慎
- IntelliJ 有一个无益的暗示
Streams
Java 8 best practices
public List<Money> value(List<Trade> trades, Data data) {
return trades.stream()
.filter(t -> t.isActive(data))
.map(valueFn)
.collect(Collectors.toList());
}
List<Trade> trades = loadTrades();
Predicate<Trade> activePredicate = t -> t.isActive();
Function<Trade, Money> valueFn = t -> presentValue(t);
List<Money> valued =
trades.stream()
.filter(activePredicate)
.map(valueFn)
.collect(Collectors.toList());
lambda 中的常见问题
如果难以编译,就把他们提取出来
Streams
Java 8 best practices
- 调试流可能会很痛苦
- 代码行定位是模糊的
- JDK调用堆栈会非常大
- 返回 Stream 的方法使情况变得更糟
java.lang.IllegalArgumentException: Oops
at com.opengamma.strata.calc.DefaultCalculationRunner.lambda$2(DefaultCalculationRunner.java:98)
at java.util.stream.ReferencePipeline$11$1.accept(ReferencePipeline.java:372)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Iterator.forEachRemaining(Iterator.java:116)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at com.opengamma.strata.calc.DefaultCalculationRunner.calculate(DefaultCalculationRunner.java:100)
at com.opengamma.strata.calc.DefaultCalculationRunner.lambda$0(DefaultCalculationRunner.java:86)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Iterator.forEachRemaining(Iterator.java:116)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at com.opengamma.strata.calc.DefaultCalculationRunner.calculate(DefaultCalculationRunner.java:87)
at com.opengamma.strata.calc.DefaultCalculationRunnerTest.calculate(DefaultCalculationRunnerTest.java:49)
java.lang.IllegalArgumentException: Oops
at com.opengamma.strata.calc.DefaultCalculationRunner.calculate(DefaultCalculationRunner.java:102)
at com.opengamma.strata.calc.DefaultCalculationRunner.calculate(DefaultCalculationRunner.java:87)
at com.opengamma.strata.calc.DefaultCalculationRunnerTest.calculate(DefaultCalculationRunnerTest.java:49)
Streams
Java 8 best practices
- 流并不总是比循环更可读。
- 流异常可能更糟。
-
我的建议
- 使用流进行小型的,本地化的逻辑
- 谨慎使用流进行大规模逻辑
- 不要从方法返回流(至少不是最初)
-
在顶层使用 for-each 循环
- 仅用于较短的堆栈追踪
Date and Time
Java 8 best practices
- 新的日期时间 API - JSR 310
- 包括日期、时间、瞬间、周期、持续时间
- 80% 的设计来自Joda-Time
Date and Time
Java 8 best practices
- 远离 Joda-Time
- 避免 java.util.Date 和 java.util.Calendar
- 必要时可以尝试 ThreeTen-Extra 库
- 关注四种最有用的类型
- LocalDate, LocalTime, ZonedDateTime, Instant
- 像XML/JSON这样的网络格式使用偏移类型
- OffsetTime, OffsetDateTime
- Temporal 是比较底层的
- 使用具体的类型
Date and Time
Java 8 best practices
// prefer
LocalDate date = LocalDate.of(2018, 05, 10);
// avoid
Temporal date = LocalDate.of(2018, 05, 10);
Other features
Java 8 best practices
- Base64
- 没有数值算术溢出
- 无符号算术
- StampedLock
- CompletableFuture
- LongAdder/LongAccumulator
- 增强操作系统的控制过程
- 注解增强
- 反射获取方法参数名
- Hotspot JVM中没有PermGen
- Nashorn JavaScript
- 可能还有一些其他骚操作...
Summary
Java 8 best practices
- Java 8 是好的开始
- 它和 Java 7 之前的版本有很大的不同
- 重新思考编码风格和标准至关重要
- 接口方法有很大的不同
- 对函数式编程要谨慎
- java不是纯粹的函数式编程语言
- 我们需要将知识应用到Java中
- java的函数式编程库通常没必要
- OpenGamma Strata是一个很好的范例项目
- 由 Java 8 开发
- 许多优秀的Java 8技术和实用工具
Java 8 最佳实践
By biezhi
Java 8 最佳实践
- 1,202