Java 8での課題

Java 8の課題

1. 概要

Java 8では、ラムダ式の使用を中心にいくつかの新しい機能が導入されました。 この簡単な記事では、それらのいくつかの欠点を見ていきます。

また、これは完全なリストではありませんが、Java8の新機能に関する最も一般的で人気のある苦情の主観的なコレクションです。

2. Java8ストリームとスレッドプール

まず、Parallel Streamsはシーケンスの簡単な並列処理を可能にすることを目的としており、単純なシナリオではまったく問題なく動作します。

ストリームは、デフォルトの一般的なForkJoinPoolを使用します–シーケンスをより小さなチャンクに分割し、複数のスレッドを使用して操作を実行します。

しかし、キャッチがあります。 specify which ForkJoinPool to useを実行する良い方法はありません。したがって、スレッドの1つがスタックした場合、共有プールを使用して他のすべてのスレッドがスタックすると、長時間実行されるタスクが完了するのを待つ必要があります。

幸いなことに、そのための回避策があります。

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() -> /*some parallel stream pipeline */)
  .get();

これにより、新しい個別のForkJoinPoolが作成され、並列ストリームによって生成されたすべてのタスクは、共有のデフォルトプールではなく、指定されたプールを使用します。

OracleのJavaおよびOpenJDK開発者であるStuartMarksによると、別の潜在的な問題が発生する可能性があることに注意してください。“this technique of submitting a task to a fork-join pool, to run the parallel stream in that pool is an implementation ‘trick' and is not guaranteed to work”です。 このテクニックを使用する際に留意すべき重要なニュアンス。

3. デバッグ可能性の減少(Decreased Debuggability)

The new coding style simplifies our source code, yetcan cause headaches while debugging it

まず、この簡単な例を見てみましょう。

public static int getLength(String input) {
    if (StringUtils.isEmpty(input) {
        throw new IllegalArgumentException();
    }
    return input.length();
}

List lengths = new ArrayList();

for (String name : Arrays.asList(args)) {
    lengths.add(getLength(name));
}

これは、自明の標準的な命令型Javaコードです。

空のStringを入力として渡すと(結果として)、コードは例外をスローし、デバッグコンソールで次のように表示されます。

at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)

それでは、Stream APIを使用して同じコードを書き直し、空のStringが渡されたときに何が起こるかを見てみましょう。

Stream lengths = names.stream()
  .map(name -> getLength(name));

呼び出しスタックは次のようになります。

at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)

これは、コード内の複数の抽象化レイヤーを活用するために支払う代償です。 ただし、IDEはJavaストリームをデバッグするための堅牢なツールをすでに開発しています。

4. NullまたはOptionalを返すメソッド

Optionalは、オプションを表現するためのタイプセーフな方法を提供するためにJava8で導入されました。

Optionalは、戻り値が存在しない可能性があることを明示的に示します。 したがって、メソッドを呼び出すと値が返される可能性があり、Optionalを使用してその値を内部にラップします。これは便利であることがわかりました。

残念ながら、Javaの下位互換性のために、2つの異なる規則を混在させたJava APIが作成されることがありました。 同じクラスで、nullを返すメソッドとOptionals.を返すメソッドを見つけることができます。

5. 機能インターフェイスが多すぎます

java.util.functionパッケージには、ラムダ式のターゲットタイプのコレクションがあります。 それらを次のように区別してグループ化できます。

  • Consumer –いくつかの引数を取り、結果を返さない操作を表します

  • Function –いくつかの引数を取り、結果を生成する関数を表します

  • Operator –いくつかの型引数に対する演算を表し、オペランドと同じ型の結果を返します

  • Predicate –いくつかの引数の述語(boolean値関数)を表します

  • Supplier –引数を取らず、結果を返すサプライヤーを表します

さらに、プリミティブを操作するための追加のタイプがあります。

  • IntConsumer

  • IntFunction

  • IntPredicate

  • IntSupplier

  • IntToDoubleFunction

  • IntToLongFunction

  • …およびLongsDoublesの同じ代替

さらに、アリティが2の関数の特別な型:

  • BiConsumer

  • BiPredicate

  • BinaryOperator

  • BiFunction

その結果、パッケージ全体には44個の機能タイプが含まれており、紛らわしくなります。

6. チェックされた例外とラムダ式

チェック済みの例外は、Java 8がすでに登場する前に、問題があり論争の的となっていた問題です。 Java 8の登場以来、新しい問題が発生しました。

チェック済みの例外は、すぐにキャッチするか、宣言する必要があります。 java.util.functionの機能インターフェイスは例外のスローを宣言していないため、チェックされた例外をスローするコードはコンパイル中に失敗します。

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws IOException
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));

この問題を解決する1つの方法は、チェックされた例外をtry-catchブロックでラップし、RuntimeExceptionを再スローすることです。

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        writeToFile(i);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

これはうまくいくでしょう。 ただし、RuntimeExceptionをスローすると、チェックされた例外の目的と矛盾し、コード全体が定型コードでラップされます。これは、ラムダ式を利用して削減しようとしています。 ハッキーな解決策の1つはto rely on the sneaky-throws hack.です

別の解決策は、コンシューマー機能インターフェースを作成することです。これにより、例外がスローされる可能性があります。

@FunctionalInterface
public interface ThrowingConsumer {
    void accept(T t) throws E;
}
static  Consumer throwingConsumerWrapper(
  ThrowingConsumer throwingConsumer) {

    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };
}

残念ながら、チェックされた例外はまだランタイム例外でラップされています。

最後に、問題の詳細な解決策と説明については、次の詳細を調べることができます:Exceptions in Java 8 Lambda Expressions

8. 結論

この簡単な記事では、Java 8の欠点について説明しました。

それらのいくつかはJava言語アーキテクトによって行われた意図的な設計選択でしたが、多くの場合、回避策または代替ソリューションがあります。考えられる問題と制限を認識する必要があります。