@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 の Function
は equals
を以下のようにオーバーライドしています。
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
)については警告に留めるしかなさそうです。他の問題は後々……。