在Java8中,编写类似Java8风格的代码

总结一下(你在说什么呢)

    コードレビューしていると、Java8のfeature(Stream APIとかOptionalとか)を使ってるんだけど、Java7以前と大差ない書き方をされているコードをよく見かける。
    もっと皆にJava8っぽいコードを書いて欲しい。そうじゃないともったいない。
    (*”Java8っぽい”の基準は割りと(かなり?)私見が入っている)
    言葉を尽くしても伝わりづらいので、例を作ろう。 ← これ

    ついでに、「無理してJava8のfeature使うことで、逆に悪くなってしまっている例」も作ろう。

目标读者

    Java8でコード書いてるけど、Java7以前の書き方が染み付いちゃっていてなかなか抜け出せない人
    「forやnullチェックは絶対使うべきでない」という思いが強すぎて、何でもかんでもStream APIやOptional使ってる人

通知

这篇文章中所写的内容并没有什么特别新奇的,而且除了Java之外的语言也可以有更加精练的表达方式。这篇文章只是针对那些从Java7迁移到Java8但对Stream和Optional的使用方式不熟悉的人所写的。

能够更加符合Java8风格的模式

有一个类似的产品对象,并假设我们要对其进行某种处理。

// 商品クラス
public class Item {
  String name; // 商品名
  BigDecimal price; // 商品の価格
  String currency; // priceフィールドの通貨("JPY"とか"USD")
}

只是将增强for循环用forEach替换了。

想要进行将某些数据转换并存储到另一个列表的处理。
在Java7之前,可以这样写:

// 日本円の商品をUSDに変換
BigDecimal jpyToUsd = BigDecimal.valueOf(100);

List<Item> usdItems = new ArrayList<>();

for(Item jpyItem : jpyItems) {
    Item usdItem = new Item();
    usdItem.setName(jpyItem.getName());
    usdItem.setPrice(jpyItem.getPrice().multiply(jpyToUsd));
    usdItem.setCurrency("USD");
    usdItems.add(usdItem);
}

所以,被告知“既然是Java8,就用Stream而不是使用for循环”之后,结果就是这样。

BigDecimal jpyToUsd = BigDecimal.valueOf(100);

List<Item> usdItems = new ArrayList<>();

jpyItems.forEach(jpyItem -> {
    Item usdItem = new Item();
    usdItem.setName(jpyItem.getName());
    usdItem.setPrice(jpyItem.getPrice().multiply(jpyToUsd));
    usdItem.setCurrency("USD");
    usdItems.add(usdItem);
});

做的事情并没有完全改变,只是将其重写为流的“中间操作”和“终端操作”这样。

BigDecimal jpyToUsd = BigDecimal.valueOf(100);

// forEachを使わず、mapとcollectの組み合わせで実現
List<Item> usdItems = jpyItems.stream()
        .map(jpyItem -> {
            Item usdItem = new Item();
            usdItem.setName(jpyItem.getName());
            usdItem.setPrice(jpyItem.getPrice().multiply(jpyToUsd));
            usdItem.setCurrency("USD");

            return usdItem;
        })
        .collect(Collectors.toList());

关于map中的内容变得臃肿的解决方案将在后面讨论。

通过这样做,

    あるデータを変換して別のデータを生成している(map(…))
    変換されたデータをまとめてCollectionにしている(collect(…))

由于这样做更加明确,可提高可读性。
虽然forEach很方便且能做任何事情,但也应思考是否有其他适合的方法。

lambda很大的

在先前的例子中,map函数中的lambda函数很庞大,而Stream的方法链变得很长,导致整体不够清晰,可读性较低。

如果我们将lambda作为一个变量提取出来,那么就会变成这样。

Function<Item,Item> convertToUsdItem = jpyItem -> {
    Item usdItem = new Item();
    usdItem.setName(jpyItem.getName());
    usdItem.setPrice(jpyItem.getPrice().multiply(jpyToUsd));
    usdItem.setCurrency("USD");

    return usdItem;
};

List<Item> usdItems = jpyItems.stream()
    .map(convertToUsdItem)
    .collect(Collectors.toList());

尽管这样也能让流程更清晰,但是如果以后想要测试convertToUsdItem的处理过程时,可测试性会很低。所以将其设为通常的方法会更方便,而且个人觉得更易读。 (至于可读性可能是个人喜好的问题)

public Item convertToUsdItem(Item jpyItem) {
    Item usdItem = new Item();
    usdItem.setName(jpyItem.getName());
    usdItem.setPrice(jpyItem.getPrice().multiply(jpyToUsd));
    usdItem.setCurrency("USD");

    return usdItem;
}

List<Item> usdItems = jpyItems.stream()
    .map(this::convertToUsdItem)
    .collect(Collectors.toList());

如果使用方法引用,可以像使用lambda一样简洁地编写Stream。

只使用了对象的部分字段,但没有进行映射。

当考虑像”以”A”开头的商品名称为例,将商品名称输出到标准输出”这样的处理时,偶尔会看到类似这样的代码。

items.steam()
    .filter(item -> item.getName().startsWith("A"))
    .forEach(item -> System.out.println(item.getName()));

只需使用商品名称,不需要一直传递item对象,所以最好在开始时使用map提取出name字段,这样后续处理会更简单。

items.stream()
    .map(Item::getName)
    .filter(name -> name.startsWith("A"))
    .forEach(System.out::println);

通过这样做,

    ”商品名のみを扱う”ということが早い段階で理解できる
    メソッド参照によりコードをよりシンプルにできる

拥有以下的好处。

在创建一个返回null的方法,并在调用该方法的地方进行null检查。

在处理Stream API的findFirst方法的返回值时,以及使用Java8的库方法返回Optional的情况下,不管是否愿意,都需要使用Optional。
然而,在刚开始学习Java8时,很容易发现当我们试图自己实现方法时,很少会意识到“这里应该将返回类型设为Optional”。

假设我们创建了一个类似的方法:“从缓存中获取汇率信息,如果缓存不存在,则返回默认值1”。


public RateInfo getFromCache(String fromCurrency, String toCurrency) {
    // キャッシュに存在すればそのRateInfoオブジェクトを、なければnullを返す
}

public BigDecimal retrieveRate(String fromCurrency, String toCurrency) {

    RateInfo rateInfo = getFromCache(fromCurrency, toCurrency);

    if(rateInfo != null) {
        return rateInfo.getRate();
    }

    return BigDecimal.ONE;
}

在这里,getFromCache方法的行为是“如果缓存中存在,则返回RateInfo对象;如果不存在,则返回null”。
因此,在调用处需要进行null检查的if语句。
有时候会忘记进行这个null检查,导致出现NullPointerException的情况。

所以,现在是Optional出场的时候了。

public Optional<RateInfo> getFromCache(String fromCurrency, String toCurrency) {
    // キャッシュからRateInfoオブジェクトを取り出そうとした結果を、Optional<RateInfo>オブジェクトとして返す
}

public BigDecimal retrieveRate(String fromCurrency, String toCurrency) {
    Optional<RateInfo> rateInfo = getFromCache(fromCurrency, toCurrency);

    return rateInfo.map(RateInfo::getRate).orElse(BigDecimal.ONE);
}

通过这样做,

    nullチェックというかオブジェクトの存在チェックをメソッドの呼び元に強制できる

    rateInfo.get()でチェックをせずに無理やり値を取り出すこともできるが、そういったコードは静的解析で弾ける

    orElseなどのメソッドを使用することでコードをすっきり書ける

有诸多益处。

最好避免強行使用Java8的特性

陷入类似于成为”为了使for循环和null检查绝对无法通过”的情况时容易陷入困境。与其他语言相比,Java8的Stream和Optional经常存在不好的地方,当我们强行试图解决这些问题时可能会陷入困境。

使用流(Stream)来替换带有索引的for循环。

比方说,

for(int i=0; i < items.size(); i++) {
    System.out.println(
        String.valueOf(i) + " : " + items.get(i).getName());
}

我充满斗志地尝试了一种类似的处理方式,没有使用for循环,结果却很成功!

// IntStreamでループカウンタ生成
IntStream.range(0, items.size())
    .forEach(i -> System.out.println(
        String.valueOf(i) + " : " + items.get(i).getName());

// AtomicIntegerとforEachで頑張る
AtomicInteger i = new AtomicInteger();
items.forEach(item -> System.out.println(
        String.valueOf(i.getAndIncrement()) + " : " + item.getName());

就像变成这样了。
前者本来想要作为items的Stream处理表示,但是由于在用于生成index的IntStream中使用了get(i)进行访问,所以和普通的for循环并没有区别,也很难读懂。
(作者以前曾经采用过这种写法,但是因为上述的理由而停止了。)

后者根本就是胡说,”不能在lambda内部递增原始类型int,所以要使用AtomicInteger”,这明显偏离了AtomicInteger的原本使用方法,我不建议这样做。

如果Java有支持索引的集合操作(例如Kotlin的那样),那就可以直接使用它,但很不幸目前还没有实现,所以比起强行使用Stream API,现阶段还是直接使用for循环更好。

只是为了进行null检查而创建Optional.ofNullable

在古老的库和Java7之前也可以使用的方法中,有些方法可能会返回null作为返回值。
对于这样的方法,当只想要”仅仅检查返回值是否为null”时,我们会产生一种”无论如何不要写if (a != null)!”的想法,然后就会写出下面这样的代码。


// getText()はnullを返し得る
String text = getText();

// Optionalで包んでチェック。中身の値は使わない
if(Optional.ofNullable(text).isPresent()) {
    return "OK";   
}

return "NG";

使用Optional.isPresent()虽然不太好,但相比普通的空值检查,代码会变得更长并稍显繁琐。
我认为直接使用if(text != null)就可以了。

有些地方正在煩惱中-像默認引數一樣使用。

Optional作为一种设计思想,主要是用作“方法返回值”,不推荐将其用于其他用途(比如作为参数的类型)。
(引自r.f stackoverflow – 为什么java.util.Optional不可序列化,如何对具有这种字段的对象进行序列化)

虽然确实可以使用Optional类型作为参数,但如果传入的Optional对象为null,那么就没有太多的好处了。
然而,在参数方面,有一种“可以这样使用Optional”的方式。
那就是像使用默认参数一样。

// 対象商品を指定した通貨の価格に変換したものを返す
public Item convert(Item item, String toCurrency) {
     // 通貨の指定がなければUSDを使用する
     String currency = Optional.ofNullable(toCurrency).orElse("USD");

     ...
     ...
}

也许有像「使用overload」这样的观点,但是在第三方库的接口实现方法中指定为回调方法的情况下无法使用overload。所以我正在考虑这种用法是否可行。

备注 (2017/08/30)

可能需要在另一个方法中进行相应的改进,正如您在评论中所指出的。

// 対象商品を指定した通貨の価格に変換したものを返す
public Item convert(Item item, String toCurrency) {
     // java.util.Objects.toString
     String currency1 = Objects.toString(toCurrency,"USD");

     // org.apache.commons.lang3.ObjectUtils.defaultIfNull
     String currency2 = ObjectUtils.defaultIfNull(toCurrency, "USD");

     ...
     ...
}

最终

    Stream APIもOptionalも、「単なるメソッド・クラスの追加」ではなく「今までよりも効率的で理解しやすい書き方ができる」と捉えて、色々な書き方を探ってみると面白い。
    とは言え、Java8時点ではイケてない部分も多いので、盲目的に使用するのではなく「丁度良い使い方」を探っていくのが肝要。

bannerAds