タグ「Java」の 記事 4 件中 1 ~ 4 件を表示しています。

業務で触っているシステムがやっと Java 8 に対応したので Java 8 の文法で書いているが、前回 Java 8 を書いたのがもう相当昔なので記法を結構忘れていて焦った。 書いているうちに思い出してきたが、やはり定期的に触っていないと忘れる。 Android アプリ開発は完全に Kotlin に移行してしまっているので Java は使わないし (そもそも Android の JDK が未だに Java 7 相当なのが謎だが)、その他のプロダクトで Java に触る機会もほとんどなく PHP ばかりだったからだ。 Java 8 のラムダ式が => でなく -> なのがちょっと違うし、関数型プログラミング的な書き方をする時に Stream に変換して collect()List などの形式に戻すというのも今見ると冗長に思える。

Android Studio に warning を出されるようになった

Kotlin で以下のような構文を書くと Android Studio に warning を出されるようになった:

if (value?.isEmpty() ?: false)  // Equality check can be used instead of elvis
if (value?.isEmpty() == true)  // これなら OK

この場合は elvis 演算子 ?: ではなく == で代用できるというわけである。 何故私が上の elvis 演算子 を使った構文を使ってしまっていたかというと、紛れもなく Java では false は基本型 boolean であるという事実が頭にあったからであり Java の概念に引きずられて if (value?.isEmpty() == true) の方の例が些か直感的でないように感じてしまったからだ。 以下どういうことなのか考察する。

Java では

Java では以下のような比較は syntax error となる:

if (null == false)  // syntax error. false はオブジェクトではない!!
if (null == Boolean.FALSE)  // これなら OK

Java は歴史的な経緯にて全てのフィールドやメソッドがオブジェクトというわけではなく基本型 (プリミティブ型) とオブジェクト型に分類される。 そもそも基本型が null になることはなく基本型に対する == 比較は基本型且つ型が一致していないと行えない。

基本型はオブジェクトではないので基本型に対するメソッド呼び出しを行うことはできない。 メソッド呼び出しを行いたい場合は対応するラッパー型 (オブジェクト) に変換した上でメソッド呼び出しを行う。 上記の例の場合 boolean のラッパー型は Boolean なので false に対しメソッド呼び出しを行いたいならば Boolean.FALSE を使えば良い。

Boolean.TRUEBoolean.FALSE はそれぞれ java.lang.Boolean クラスに対するインスタンスである。 一方 Java における == 演算子の役割はオブジェクトの参照が等しいことの比較なので以下のような null との比較は false となる:

System.out.println(null == Boolean.TRUE);  // false
System.out.println(null == Boolean.FALSE);  // false

一方 Boolean.equals() の実装も「比較対象が Boolean クラスでない場合 (null も含む) 常に false となる」となっているので以下も合点がいく:

System.out.println(Boolean.TRUE.equals(null));  // false
System.out.println(Boolean.FALSE.equals(null));  // false

まとめると Java では nulltrue でも false でもない第三の値である。 一方以下のように nullnull の同値比較は true となる事に注意する:

System.out.println(null == null);  // true

Kotlin に話を戻す

話を戻すが Kotlin では以下だとコンパイルが通らない:

if (value?.isEmpty())  // 駄目. value が null の場合戻り値が null となるので評価値が Boolean? となる

Kotlin は null-safety な言語なので上記のような書き方をすると valuenull だった場合に NullPointerException がスローされずに isEmpty() メソッド呼び出しが行われないという挙動になる。 よって null の場合を ?: で付け加えるのは自然に見える。

以下再掲する:

if (value?.isEmpty() == true)  // なんとなく null == true がマズそうに見える

Kotlin では全てのローカル変数はオブジェクトの為 null == true の評価が可能である。 しかし ==null オブジェクトに対する equals() のシンタックスシュガーに見えるので何となく null に対するメソッド呼び出しに失敗しそうに見えるのだが、Kotlin 公式に以下のように記載があった:

Structural equality is checked by the == operation (and its negated counterpart !=). By convention, an expression like a == b is translated to a?.equals(b) ?: (b === null)

つまり Kotlin においては前述の if 文は以下と等価である:

if (value?.isEmpty()?.equals(true) ?: (true === null))

これは比較対象の左辺が null だった場合左辺と右辺が共に null の時のみ true となる、と読み解ける。 つまり Java における「nulltrue とも false とも一致しないし null == null = true である」という原則を生かしつつ、常にそのまま第三の値として == で比較できるように言語の文法レベルで配慮が効いているということだろう。

かなり手間をかけた全面刷新

コジごみカレンダー自体は 5 年近く前に初版をリリースした歴史ある Android アプリなのだが、アプリの構造が Android 2.2 時代のもののままなので更新するにも何かと制約がありモチベーションが保てなくなっていた。 なので現代の技術で全面リプレースするという構想は実は 2, 3 年前から持っていたのだが、既にかなり複雑なアプリに仕上がってしまっていたので移植しようとすると全ての機能を網羅しなければならず、なかなか最後まで続かない結果になっていた。 作っては中断、作ってはまた中断を繰り返しており、開発は頓挫状態となっていた。

Kotlin に感謝

では何故今回モチベーションが保てたのかというと間違いなく Kotlin のお陰だ。 既存の冗長な Java コードを Kotlin で書き換えるのが非常に気持ちよく、地味な移植作業を続けるのがそれ程苦ではなくなっていた。

今回 In-app Billing の既存のコード以外全て Kotlin で書き直している。 業務で Kotlin を使うのは流石に憚られたが、自分のアプリなら自由に使えるのでなかなか楽しい。

ただ 「Android 開発経験がそれほどない技術者は素直に Java で書いたほうがいい」と思った。 所々 Kotlin の文法ミスなのか Android SDK の API の使用方法が間違っているのか判断がつかない場面があったので、恐らく経験が浅いと解決できないのではないかと思われたからだ。

これが特に良かったという点

実際にコジごみカレンダーのコードを一部引用し述べる。

Fragment で Activity のインスタンスを使用する場合コンストラクタ等で行おうとするとまだ Activity に attach() されてないので getActivity()null になる。 これを避ける為には Fragment#onAttach() で Activity のインスタンスを参照するか Fragment#onActivityCreated()getActivity() するのが普通なのだが、そうすると以下のように退屈な代入を書かなければならない:

private ContentResolver mResolver;  // ここで代入できないから final にできない!!
...
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mResolver = getActivity().getContentResolver();
}

Kotlin なら by lazy { ... } を使えば実際に使用する箇所でコードブロックが走る (遅延実行) ので大丈夫だ:

// mResolver を onActivityCreated() 以降でしか参照しないならこれで問題ない
private val mResolver: ContentResolver by lazy { activity.contentResolver }

多数の分岐を持った変数代入を Java でやると以下だ:

// 読みやすいけど退屈
final int backgroundColor;
if (year == currentYear && month == currentMonth && date == currentDate) {
    backgroundColor = getResources().getColor(R.color.calendar_today);
} else if (targetMonth != month) {
    backgroundColor = getResources().getColor(R.color.super_light_gray);
} else if (j == 0 || dayType.isHoliday(cal) || dayType.isExtra(cal)) {
    backgroundColor = getResources().getColor(R.color.calendar_sunday);
} else if (j == 6) {
    backgroundColor = getResources().getColor(R.color.calendar_saturday);
} else {
    backgroundColor = Color.WHITE;
}
columnLayout.setBackgroundColor(backgroundColor);

Kotlin だと when 文があるのでかなり簡潔になる:

columnLayout.setBackgroundColor(when {
    year == currentYear && month == currentMonth && date == currentDate -> resources.getColor(R.color.calendar_today)
    targetMonth != month -> resources.getColor(R.color.calendar_not_target_month)
    j == 0 || Utils.isHoliday(cal) || Utils.isExtra(cal) -> resources.getColor(R.color.calendar_sunday)
    j == 6 -> resources.getColor(R.color.calendar_saturday)
    else -> Color.WHITE
})

後は拡張メソッド (エクステンション) やトップレベル関数、インライン関数やラムダ式なども勿論便利だった。 もうプライベートで行うコーディングは Java で行う気がしない。

Stream API を割と使っている

今参画しているプロジェクトでは Java8 を使用しているのもあって遠慮無く Lambda Expression や Stream API を使用している。 特に Stream API は以前だと簡単な絞り込みをしたい場合でも for ループを回して面倒くさい書き方をしなければいけなかったのがシンプルに書けるようになっていて素晴らしいと思う。思うがやはり Scala なんかと比べると冗長に感じて仕方がないところはある……。

特に以下の collect(Collectors.toList()) は長い。もうすこし何とかならなかったのだろうかと感じる:

public static void main(String[] args) {
    // 途中で List<E> にするのに boxing する必要があるのも地味に辛い
    final List<Integer> list = IntStream.range(1, 11).map(x -> x * x).boxed().collect(Collectors.toList());
}

Scala だと以下で凄くシンプルで羨ましい:

val seq = 1.to(10).map(x => x * x).toSeq

まぁ無い物ねだりをしても仕方がない。Java で飯を食っている以上 Java の枠組みの中でなるべく綺麗なコードを書くように気をつけるしかない。

他の言語でよくやるメソッド内関数が簡単に書けるようになったけどやっぱり辛いという話

例えば PHP や Python などだと lambda 構文もあるし関数内で関数を定義する事も簡単なのでよく使う:

# f(x) 内でしか使用しないが何度も出てくるような関数を f(x) 内で g(y) として定義する事で影響範囲がわかりやすい
def f(x):
    def g(y):
        return y ** 2
    return g(g(x) + 1)

これが Java でやろうとすると Java8 以前だと無名クラスを使用しなければならなくてこれがもうとんでもなく面倒くさかった、というかやろうとも思わなかった:

public static void main(String[] args) {
    final Func<Integer, Integer> f = new Func<Integer, Integer>() {
        @Override
        public Integer apply(Integer x) {
            return x * x;
        }
    };
    System.out.println(f.apply(f.apply(1) + 1));
}

public static interface Func<X, R> {
    R apply(X x);
}

Java8 からは Lambda Expression を用いて以下で書ける:

public static void main(String[] args) {
    final Func<Integer, Integer> f = x -> x * x;
    System.out.println(f.apply(f.apply(1) + 1));
}

public static interface Func<X, R> {
    R apply(X x);
}

更にこのような 1 つの引数を受け取り 1 つの結果を返すような Interface は java.util.function パッケージに用意されているのでわざわざ自分で Interface を定義しなくてもよい:

public static void main(String[] args) {
    final IntUnaryOperator f = x -> x * x;
    System.out.println(f.applyAsInt(f.applyAsInt(1) + 1));
}

OK. これはシンプル。素晴らしい。……とは言い難い。IntUnaryOperator とはなにか?

FunctionalInterface 群の型が覚えられない...

Scala だと全部型推論できるので左辺は val と書けば良い。つまり右辺がどうなっていようが関係ない:

val f = (_: Int) * (_: Int)
val g = (x: Int) => x * x
val h = () => print("Hello")
val i = (x: String) => print(s"Hello $x")

一方、これを Java8 で書こうとすると以下になる:

public static void main(String[] args) {
    final IntBinaryOperator f = (x, y) -> x * y;
    final IntUnaryOperator g = x -> x * x;
    final Runnable h = () -> System.out.println("Hello");
    final Consumer<String> i = x -> System.out.println(String.format("Hello %s", x));
    i.accept("Baka");
}

この BinaryOperator だとか Consumer だとかを覚えないと書けない。 とりあえずひしだまさんの Functional Interface Memo を見て確認すると、以下の 5 つに大別されるのがわかる:

クラス名 概要
Supplier 引数はなく 1 つの結果を返す。供給する (supply) と覚える。
Consumer 1 つの引数を受け取り何も返さない。消費する (consume) と覚える。2 つ受け取って何も返さない BiConsumer というのもある。
Predicate 1 つの引数を受け取りその判定結果 (boolean) を返す。2 つの引数を受け取り判定する BiPredicate というのもある。Stream API の filter() に渡す際によく使う。
Operator 引数の型と同じ型の戻り値を返す。引数の個数により UnaryOperator, BinaryOperator と用意されている。
Function 引数の型と別の型の戻り値を返す。引数の個数により Function, BiFunction と用意されている。

番外で引数も戻り値もない、ただ単に決まった処理をしたいといったものは Runnable を使う。まぁこれはいいだろう。

上記 5 つの大別の中で更に Java には int, long, double, boolean 等のプリミティブ型があるので、その組み合わせぶん全て用意されている。 そこが便利ではあるが分かりにくくしている。例えば DoubleToLongFunction とか IntBinaryOperator とか、そういうものだ……。 しかし、定義しようとしている関数のシグネチャを考えて、上の表に当てはめてみれば多少は考えやすくなるのではないだろうか。つまり、

  1. 引数はあるか? -> 無いなら Supplier
  2. 戻り値はあるか? -> 無いなら Consumer
  3. 戻り値は boolean か (何らかの判定をしたいのか) ? -> そうなら Predicate
  4. 引数と戻り値の型が一緒か? -> そうなら Operator, そうでないなら Function

このチャートで適切なものが選択できるはずだ。