いろいろガンガンいこうぜ。体も大事に

普段はJavaでAndroidアプリ開発しているプログラマーのブログです。

「Java魂」の2章,finalストーリーを4での感想

論理エラーをコンパイルエラーに置き換える

コンパイルエラーはある程度簡単に修正することがほとんどです。 それに対して論理エラーは発見するのが難しく,致命的な不具合を起こすことも多いです。

そのような内容で始まる,「Java魂」の2章,finalストーリーを4での感想です。

2..1 final 定数

2.1.2 public 基本型と置き換え

バージョニング問題。

自分もこの節で紹介されている問題で痛い目を見たことがあります。

package com.mrstar.sample.final_story.mock_library;

public class Constants {
    public static final String STRING_VALUE = "Hello World!";

    private Constants() {
    }
}
package com.mrstar.sample.final_story;

import com.mrstar.sample.final_story.mock_library.Constants;

public class Main {
    public static void main(String[] args) {
        System.out.println(Constants.STRING_VALUE);
    }
}

Constantsクラスで定義したpublic static fianalな文字列定数を,他のクラスMainから利用しています。一見問題がなさそうに見えます。 これらのクラスを普通に使う分には問題ないと思います。

次のような手順で問題が発生します。

  1. ConstansクラスとMainクラスをコンパイルする。
  2. 実行する。ここでは期待通り「Hello World!」と表示される。
  3. Constantsクラスの STRING_VALUEを「HELLO WORLD!!!!!」に書き換える。
  4. Constantsクラスのみコンパイルする。
  5. 実行する。「HELLO WORLD!!!!!」表示されることを期待するが,「Hello World!」と表示されてしまう。

この問題は,Constantsクラス内の定数の値を書き換えたビルドした時に,その利用側のMainクラスも再ビルドすることで発生しなくなります。 そもそも,public static finalな定数でなくて,private static finalであれば問題が発生しません。 これは,static finalなプリミティブ型の値や文字列の変数はリテラルで置き換えられ,コンパイルされた利用側のクラスは定数の値・リテラルが埋め込まれ,定数クラスの変数という情報がなくなるため発生するようです。

2.1.2 多すぎる定数

private static finalの定数は一つのメソッド内でしか使わないならば,メソッド内の定数として定義しましょう。とのこと。 確かにクラス内で定数が多数あると,読みづらいですよね。 クラス内のスコープということは,そのクラス内からであればどこからでも参照できるので,publicな定数ほどではないですが広いスコープを持っていますよね。

本では,複数のメソッドが定数にアクセスしている場合はprivate static finalの定数にすればいい。と書いてあります。

ScalaやKotlinなどでは,メソッド内にメソッドを定義できます。 複数のメソッド内メソッドで共通な定数もがあると思います。 その場合は外側のメソッドで定数を定義するのが有効なのかと思いました。

2.2 final変数,fialパラメーター

scalaなどのval。 初めて存在を知った時にはそれにどれくらい価値があるのだろうと思ったのですが,便利ですね。

この章の冒頭の論理エラーをコンパイルエラーに変える,という言葉。 valな変数,Javaではfinalな変数は、再代入するとコンパイルエラーになります。

このような変数は,参照しているオブジェクトは変わりません。値型ならば値は変わりません。 本来は再代入しないはずなのに,誤って再代入してしまった論理エラー。 これはvalな変数やfinalな変数をもちいることでコンパイルエラーにすることができます。

少し話は変わって初期化。 finalな変数は宣言時に初期化しなくても良いですね。 ただし,フィールドの場合はコンストラクタにおいては初期かしないといけません。クラス定数は,static{}で初期化しなくてはいけません。

2.4 finalコレクション

finalなコレクション。これも気をつけないといけないことを再確認しました。 以下のクラスを使ってみます。

package com.mrstar.sample.final_story;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class IntListWrapper {

    private final List<Integer> list;

    public IntListWrapper(Integer... values) {
        list = new ArrayList<Integer>(Arrays.asList(values));
    }

    public List<Integer> getList() {
        return list;
    }
}

IntListWrapperのフィールドlistにはfinalですね。 これを次のような感じで使います。

package com.mrstar.sample.final_story;

public class Main {
    public static void main(String[] args) {
        IntListWrapper intListWrapper = new IntListWrapper(1, 2, 3, 4, 5);
        System.out.println(intListWrapper.getList().toString());
    }
}

これを実行すると,[1, 2, 3, 4, 5]と表示されます。 List型のフィールドにはfinalがついているので,listが保持している中身は変わりませんね。 めでたしめでたし。

というのは間違いです。

list変数はfinal修飾子がついています。 ですが,不変なのはあくまでlist変数が参照するインスタンスです。 listの中身は不変ではありません。 次のようなことをしてみます。

package com.mrstar.sample.final_story;

public class Main {
    public static void main(String[] args) {
        IntListWrapper intListWrapper = new IntListWrapper(1, 2, 3, 4, 5);

        intListWrapper.getList().clear();
        intListWrapper.getList().add(1);

        System.out.println(intListWrapper.getList().toString());
    }
}

実行結果は,[1]です。 listの中身が変わってしまっています。

当然ですね。 list変数の参照先のインスタンスは,clearもremoveもaddも呼べるのですから。中身も変わります。

また,addやremoveを呼び出すとUnnsupportedOperationExceptionがなげられるListやSet,Mapを実装したインスタンスを返すCollectinsのメソッドが紹介されました。これは,addを呼ぶことに関してはコンパイルエラーにはなりませんが,UnnsupportedOperationExceptionが投げられることで,実行時エラーにはなりますが,分かりずらい論理エラーではないので,早く解決・検知できることも多いと思います。

Scalaでのコレクションは,不変なものと可変なものがあります。一般的に不変なものを使うことが多いらしいです。 不変なコレクションであれば,うっかりリストの中身が変わってしまうということも無いので,安心ですね。

2.5 インスタンススコープ変数

finalがついているフィールドは,

という性質があります。便利ですよね。

2.6 finalクラス,2.7 finalメソッド

Kotlinというプログラミング言語では,デフォルトでfinalクラスで,デフォルトでfinalメソッドらしいです。 もっというと,openという修飾子をクラスにつけないと継承できないし,openをつけないとメソッドはオーバーライドできないようですね。 一見面倒に思えますね。 Effective Javaという本には,「継承の為に設計及び文書化する。でなければ継承を禁止する。」という項目があります。 継承をする際にはしっかりと考え,設計をした後にコーディングを行うと思いますので,それに加えたらopen修飾子をつけるのは対したコストではない,ということでしょうか。

さてさて,final修飾子をつけるとクラスは継承ができなくなります。 また,privateなコンストラクタを作った場合,サブクラスはスーパークラスコンストラクタを呼べないので,継承禁止のクラスとなる,とあります。

2.8 条件付きコンパイル

public static final boolな変数のインスタンスを用いて,本番環境に残したくない処理を除くテクニックが紹介されてました。

finalというキーワードのみで構成されていた章。 苦い思い出が蘇り,大切さを再確認し,なぜそれがいいかをもう一度考えるきっかけとなりました。