Java
java8

Javaで失敗を表現する

はじめに

Javaを扱う上で失敗を表現するのは意外と難しいことです。
Java8でOptionalが登場し大分扱いやすくはなりましたが、それでも他の言語に比べるとというのが現状です。
最近ずっと頭を悩ませているので、ここで自分なりに失敗をどう扱っているというのをまとめてみました。

環境

Java8以降(後のバージョンの方がより良いです)

Nullで扱う

王道といえば王道である取得できない場合はNullを返却するパターンです。
Java8でOptionalが登場するまではNullチェックをするのが当たり前でしたが、Optionalがとって変わったような印象です。
アプリケーション内の戻り値としてNullでは余り扱わさせないようにしています。

古い処理で良く残っているのはしょうがないので、リファクタリング出来る日までNullで扱っています。

Main.java
public class Main {
    public static void main(String[] args) {
        String first = getFirstArg(args);
        if(first == null) {
            System.out.println("引数はないよ");
            return;
        }

        System.out.print("最初の引数は[ " + first + "]です");
    }

    private static String getFirstArg(String[] args) {
        if(args == null || args.length == 0) {
            return null;
        }
        return args[0];
    }
}

Booleanで扱う

こちらも王道です。
ishascanを表現することが多いかと思います。
この場合はObject型であるBooleanよりプリミティブ型のbooleanを使うことが多いです。
データの有無ではなくtruefalseで表現できるので、このチェックは今後も残っていくものだとして使っています。

Main.java
public class Main {
    public static void main(String[] args) {
        if(hasFirstArg(args)) {
            System.out.println("引数あるよ");
        } else {
            System.out.println("引数はないよ");
        }
    }

    private static boolean hasFirstArg(String[] args) {
        return args != null && 0 < args.length;
    }
}

Exceptionで表現する

例外を発生させることでエラーを表現することも可能です。
外部ライブラリとして処理を作った時にメソッドの目的を達成出来ない場合は使いますが、それ以外はあまり使いません。
問題なさそうなら下から投げられる例外と上にそのまま投げるということはよくやりますが、最終的に捨てないように気を付けています。

捨てられたりなんだりがあって難易度も高い表現方法だと思いますが、上手く使っていきたい所です。
エラー統制難しいのが難点ですが・・・

Main.java
public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("最初の引数は[" + getFirstArg(args) + "]です");
        } catch(NullPointerException e) {
            System.out.println("引数がNullです");
        } catch(IllegalArgumentException e) {
            System.out.println("不正な引数です");
        }
    }

    private static String getFirstArg(String[] args) {
        if(args == null) {
            throw new NullPointerException();
        }
        if(args.length == 0) {
            throw new IllegalArgumentException();
        }
        return args[0];
    }
}

これをレビューに出したら刺されても文句言えないと思います。

Optinalで表現する

先ほどから出ているOptionalで表現するのが、現状のJavaの基本機能だとスタンダートだと思います。
Optionalを使うことで、この戻り値が存在しないかもしれないというのが分かりやすくなっているのが特徴です。
今までの表現では成功か失敗かは一目で分からず、Nullをチェックするまで分からない上にNullチェックを忘れてアクセスするとみんな大好きNullPointerExceptionが発生してしまうという状態でした。
Optinalの登場でこの状況から抜け出せることが出来るようになり大変うれしく思います。

詳しくはQiitaで記事化していますので、そちらをご覧いただければと思います。
https://qiita.com/tasogarei/items/18488e450f080a7f87f3
https://qiita.com/tasogarei/items/f289d125660af6fe5ba6

Main.java
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> arg = getFirstArg(args);
        arg.ifPresent(first ->System.out.print("最初の引数は[ " + first + "]です"));
    }

    private static Optional<String> getFirstArg(String[] args) {
        if(args == null || args.length == 0) {
            return Optional.empty();
        }
        return Optional.ofNullable(args[0]);
    }
}

ただし、Java8ではifPresentOrElseが存在しないため、成功した場合と失敗した場合の処理がうまく表現できないことがあるので注意が必要となります(Java9で初登場)。
Java8の場合はisPresentで存在チェックしてifで処理を書くという悲しいことをしなければなりません。

arg.ifPresentOrElse(first -> System.out.println("最初の引数は[ " + first + "]です"),
            () -> System.out.println("引数はありません"));

もしくはmaporElse(Get)で特定の型に変換しながら処理させる方法もあります。
やりすぎるとOptionalネスト地獄で可読性が下がる可能性もあるので、ここらへんを上手く書くのは想像以上に難しいです。

Main.java
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> arg = getFirstArg(args);
        System.out.println(arg.map(first -> "最初の引数は[" + first + "]です").orElse("引数はありません"));
    }

    private static Optional<String> getFirstArg(String[] args) {
        if(args == null || args.length == 0) {
            return Optional.empty();
        }
        return Optional.ofNullable(args[0]);
    }
}

Eitherで表現する

Optionalの登場によってNullからの脱却を果たすことが可能になりましたが、Optionalは成功か失敗は分かるもののその理由について述べることは出来ません。
そこで登場するのがEitherです。

Eitherは言語でサポートされているケースもありますが、残念ながらJavaではサポートされてません。
自力で実装出来るレベルではありますが、vavrというライブラリで実装されているので(他の機能もいっぱいありますが、今回はEither目的で使います)それを使っています。

【公式】
http://www.vavr.io/

ということでpom.xmlに依存関係を追加して実装してみます。

pom.xml
    <dependency>
        <groupId>io.vavr</groupId>
        <artifactId>vavr</artifactId>
        <version>0.9.2</version>
    </dependency>
Main.java
import java.util.Optional;

import io.vavr.control.Either;

public class Main {
    public static void main(String[] args) {
        Either<Integer,String> arg = getFirstArg(args);
        if(arg.isRight()) {
            System.out.println("最初の引数は[" + arg.get() + "]です");
        } else {
            System.out.println("引数エラー。種別" + arg.getLeft());
        }
    }

    private static Either<Integer,String> getFirstArg(String[] args) {
        if(args == null) {
            return Either.left(1);
        }
        if(args.length == 0) {
            return Either.left(2);
        }
        return Either.right(args[0]);
    }
}

EitherはRightに値が存在してれば成功でLeftに値が存在してれば失敗です。
どちらかにしか値を持つことが許されていないので、成否についてはどちらに値が存在しているか確認出来れば問題ありません。

ただしJavaではScalaのmatchのような存在がないため、ここまでやってどちらに値を持つかをifで表現するのが悲しい感じになっています。
もしかしたらmatchと似たようなことが出来るのかもしれませんが、まだ分かってません。

しかし、これで失敗の種類を表現出来るようになり、それによって処理を変えたりエラーを表現しながら値を取得したりなんだりが簡単に出来るようになりました。

最後に

最近はOptionalを中心にしてEitherも使い始めた感じで色々悩みながら実装しています。
失敗を表現することは難しいことですが、良い実装方法があったら教えてください。