LoginSignup
7
7

More than 5 years have passed since last update.

Eclipse で Null 解析しつつ guava を使いたい

Last updated at Posted at 2013-06-04

@yuichielectric さんがこちらで書かれていますが, 最近の Eclipse では静的な Null チェックが出来るみたいです。これは素晴らしいと思ったので早速試してみたのですが, guava と組み合わせて使おうとすると少々問題が発生することに気付きます……。

セットアップ

まず @yuichielectric さんの記事を参考に Eclipse (Juno) のセットアップを行います。次に, javax.annotation.Nullable が必要なので JSR305 を依存関係に追加します。

// build.gradle
dependencies {
  compile com.google.code.findbugs:jsr305:2.0.1
  compile com.google.guava:guava:14.0.1
}

これで準備は完了です。きちんと静的解析が効いていることが分かると思います(詳細は @yuichielectric さんの記事をご覧ください……)。

guava を使ってみる

ここ数年, Java を使って開発する場合は guava を使うことが多いと思います。guava は javax.annotation.Nullable 等を使ってアノテートされているので, Eclipse の Null 解析対象を javax.annotation.Nullable にすれば Function 等を利用する際に静的に Null チェックが出来るはずです。早速使ってみましょう。

public class Main {

    public static void main(String[] args) {

        List<Integer> list = Lists.newArrayList(1, 2, 3, null, 5);
        System.out.println(Joiner.on(", ").join(Collections2.transform(list, new Function<Integer, Integer>() {
            @Override
            public Integer apply(@Nullable Integer input) {
                return input == null ? -1 : input * 2;
            }
        })));

    }

}

ところがこれでは次のようなコンパイルエラーが発生してしまいます……。

The method equals(Object) from Object cannot implement the corresponding method from Function<Integer,Integer> due to incompatible nullness constraints

Guava の Functionequals を以下のようにオーバーライドしています。

boolean equals(@Nullable Object object);

equals の引数が @Nullable 指定されているので, 匿名クラスの定義と競合してしまうのですね。このエラーを回避しようと思うとこんな感じになってしまいます……。

public class Main {

    public static void main(String[] args) {

        List<Integer> list = Lists.newArrayList(1, 2, 3, null, 5);
        System.out.println(Joiner.on(", ").join(Collections2.transform(list, new Function<Integer, Integer>() {
            /** {@inheritDoc} */
            @Override
            public Integer apply(@Nullable Integer input) {
                return input == null ? null : input * 2;
            }
            /** {@inheritDoc} */
            @Override
            public boolean equals(@Nullable Object obj) {
                return super.equals(obj);
            }
        })));

    }

}

だ、ダサい……。更に今度は Eclipse が「equals をオーバーライドしたのに hashCode をオーバーライドしてないよ?」という警告を出力してきます。関数をその場で作りたいだけなのに, apply / equals / hashCode の実装が必要になってしまいました……。

どうすれば?

この問題は既に guava でも Eclipse でも認識されているようですが, 特に結論は出ていないみたいです。自分の場合, 流石にここまで冗長な記法は受け入れ難い & 取り敢えずアプリケーションレイヤだけでもチェックしたいので, サードパーティライブラリを利用する部分の静的解析は捨てて, アプリケーションレイヤで別の @Nullable / @Nonnull アノテーションを定義して利用することにしました。ただ, これはあくまで妥協案なので, 暫く利用してみて問題があったりより良い方法が見つかったら別の方法にしようと思っています(この機能自体を今日知ったところなので……)。誰か良い解決策をご存知の方がいらっしゃれば教えて頂きたいところです。

ちなみに自分の場合, Function は大抵ラップしてしまうので, こんな感じでアプリケーション用の Function を定義して使うことにしました。

// ココの @Nullable と @Nonnull はアプリケーションで定義した独自型(javax.annotation ではない)
public interface ExceptionalFunction<S, T, E extends Exception> {

    @Nullable
    T apply(@Nullable S input) throws E;

}

public interface Function<S, T> extends ExceptionalFunction<S, T, RuntimeException>, com.google.common.base.Function<S, T> {

    @Override
    @Nullable
    T apply(@Nullable S input);

}

クライアントはこんな感じで書けますね(以下の実装例は適当です)。例えば Option 型を導入したいときって結局 Option の参照自体に null が入る可能性があってイマイチだなーと思うのですが, この機能を使えば Option の参照自体を @Nonnull で注釈出来るのが嬉しいですね。

public class Main {

    public static void main(String[] args) {

        @Nonnull List<Integer> list = Lists.newArrayList(1, 2, 3, null, 5);
        Option<Integer> found = Collections3.find(list, new Predicate<Integer>() {
            @Override
            public boolean apply(@Nullable Integer input) {
                return input == null ? false : input == 3;
            }
        });
        found.get(3); // Nonnull !

    }

}

public final class Collections3 {

    /**
     * コレクションから条件を満たす値を取得します。
     * <p>
     * 複数の値が見つかった場合, 先頭の要素が返ります。
     * 
     * @param <E> 値の型
     * @param collection コレクション
     * @param predicate 条件
     * @return 値
     */
    @Nonnull
    public static <E> Option<E> find(@Nonnull Collection<E> collection, @Nonnull Predicate<E> predicate) {
        Collection<E> filtered = Collections2.filter(collection, predicate);
        return Option.apply(filtered.isEmpty() ? null : filtered.iterator().next());
    }

    private Collections3() {
    }

}

public abstract class Option<T> implements Serializable {

    @Nonnull
    public static <T> Option<T> apply(@Nullable T value) {
        return value == null ? Option.<T> none() : some(value);
    }

    @Nonnull
    public static <T> Option<T> some(@Nonnull T value) {
        return new Some<>(value);
    }

    @Nonnull
    public static <T> Option<T> none() {
        return new None<>();
    }

    public abstract boolean isDefined();

    @Nonnull
    public abstract <D> Option<D> map(@Nonnull Function<T, D> f);

    @Nonnull
    public abstract <D> Option<D> bind(@Nonnull Function<T, Option<D>> f);

    @Nullable
    public abstract T getOrNull();

    @Nonnull
    public T getOrElse(@Nonnull T defaultValue) {
        T value = getOrNull();
        return value == null ? defaultValue : value;
    }

    public static class Some<T> extends Option<T> implements Serializable {

        @Nonnull
        private final T value;

        @Override
        public boolean isDefined() {
            return true;
        }

        @Override
        @Nonnull
        public <D> Option<D> map(@Nonnull Function<T, D> f) {
            return Option.apply(f.apply(value));
        }

        @Override
        @Nonnull
        public <D> Option<D> bind(@Nonnull Function<T, Option<D>> f) {
            Option<D> option = f.apply(value);
            return option == null ? Option.<D> none() : option;
        }

        @Override
        @Nullable
        public T getOrNull() {
            return value;
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean equals(Object obj) {
            if (obj instanceof Some) {
                return Objects.equal(value, ((Some<T>) obj).value);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(value);
        }

        private Some(@Nonnull T value) {
            this.value = value;
        }

    }

    public static class None<T> extends Option<T> implements Serializable {

        @Override
        public boolean isDefined() {
            return false;
        }

        @Override
        @Nonnull
        public <D> Option<D> map(@Nonnull Function<T, D> f) {
            return none();
        }

        @Override
        @Nonnull
        public <D> Option<D> bind(@Nonnull Function<T, Option<D>> f) {
            return none();
        }

        @Override
        @Nullable
        public T getOrNull() {
            return null;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof None) {
                return true;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return None.class.hashCode();
        }

        private None() {
        }

    }

    private Option() {
    }

}

Eclipse 4.3 になったら改善されてるのかなーと期待しつつ……。今まで特にこの手の機能は使っていなかったので、より良い方法があれば是非是非教えて頂きたいです。


追記

ちょっと使ってみました。幾つか問題がありますがそのうちのひとつが フィールドに付加した @Nonnull アノテーションは処理されない ということです。なので例えば @Nonull のプロパティを返す Getter を、同じく @Nonnull として定義するとエラーになってしまいます。これは Eclipse 4.3 で修正されるそうです。次に、アノテーションが付加されていないサードパーティライブラリの取り扱いについてですが、これは現状どうしようもなく Eclipse 4.2 では取り敢えず暗黙の型変換(@Nullable@Nonnull)については警告に留めるしかなさそうです。他の問題は後々……。

7
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7