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 上进行流式处理
  • 关键部分性能的基准测试
  • 并行流必须非常小心地使用
  • 对方法引用的过度使用要谨慎
  • 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,159