SEチャンネル

ITについてできる限り書くメモ

あなたの知らないJava staticの使い方

f:id:tkmtys:20151013232552p:plain

今回はJavaプログラミング初心者向けに、Java文法の「static修飾子」を解説する。Java初心者と中級者の違いの1つが、static修飾子を正しくかつ効果的に使用できているかどうかである。というのもstaticなしでも大体の処理は記述できるが、staticを使えばコードが構造化され、可読性が上がる&再利用しやすくなるからである。

クラスとインスタンス

まずいきなりだが、以下のHelloworldを用意してmainメソッドを実行した場合、メモリ上ではどのような処理が行われるだろうか?

public class Helloworld {
    
    private String message;
    
    public Helloworld(String msg) {
        this.message = msg;
    }
    public void echoMessage () {
        System.out.println(message);
    }
}
public static void main (String[] args) {
    Helloworld inst = new Helloworld("Helloworld");
    inst.echoMessage();
}

jvmがmain実行を命令された際には、まずクラスパス上からHelloworld.classを探しだしHelloworldクラスをメモリにロードする。これがJavaでいうクラス(類型)であり、インスタンスの元になるものだ。無事Helloworldのクラスがロードできた後に、最初の処理「new Helloworld("Helloworld");」を実行する。このnewはロードしたクラスを別のメモリ空間にコピーしインスタンスを作成する、というまた非常に重要な処理である。このコピーされたインスタンスへの参照ポインタがコード中に示されているinstである。

動きを図示すると以下のようになる。

f:id:tkmtys:20151013231051p:plain

jvmはまず最初にクラスをロードし、newを行うたびにコピーしてインスタンスを作成している、という動きが重要だ。インスタンスの元になるクラスは1jvm上に1つしか作成されないが、インスタンスは複数作成されるということである。

通常の(staticでない)変数やメソッドは、インスタンスごとにコピーされてそれぞれのインスタンスが専有して使用できる。

public static void main (String[] args) {
    Helloworld hello1 = new Helloworld("hello1");
    Helloworld hello2 = new Helloworld("hello2");
    hello1.echoMessage(); // hello1が出力される
    hello2.echoMessage(); // hello2が出力される
}

sponsor

static変数

解説

Java文法上static修飾子は様々な意味を持つが、基本的には、インスタンスではなくクラス領域に定義するという意味を理解しておけば良い。static修飾子で最も簡単なのは変数である。次の例を見てほしい。

public class Helloworld {
    
    public static String message;  // static変数化

    public void echoMessage () {
        System.out.println(message);
    }
}

先ほどのmessage変数にstaticが追加されている。このmessage変数は先ほどとことなり、各インスタンスではなくHelloworldクラス上に定義されるため、1jvmプロセス上にたった1つしか存在しない。

以下のmain文で実行してみると、2つのインスタンスが同じメモリ上の値を参照しているのがわかる。

public static void main (String[] args) {
    Helloworld.message = "msg 1"; //messageに「msg 1」が格納される
    Helloworld hello1 = new Helloworld();
    Helloworld hello2 = new Helloworld();
    hello1.echoMessage(); // msg 1が出力される
    hello2.echoMessage(); // msg 1が出力される
    Helloworld.message = "msg 2"; //messageに「msg 2」が格納される
    hello1.echoMessage(); // msg 2が出力される
    hello2.echoMessage(); // msg 2が出力される
}

使いドコロ

初心者(またはC/C++経験者)は、グローバル変数を使用するためによくstatic変数を使用するが、Javaではグローバル変数は原則使わない。Javaは構造化プログラミング言語であり、分割統治論を採用すれば、メソッド中の処理は「そのクラスのクラス変数」、「インスタンス変数」、「メソッド引数」のみで実現されるべきだからだ。他のクラスに定義されたグローバル変数を更新する処理は書くべきでない。

ではstatic変数の使いドコロは何なのか?static変数の正しい使い道は「(1)定数」と「(2)クラス内でのみ使用される共用変数」である。「(1)定数」は簡単でfinal修飾子をつけて上書きできないようにすれば実現できる。具体例はいろいろあるが、jdk自体ではjava.util.Collections.EMPTY_LISTがfinal static指定された空のListとなっている。「(2)クラス内でのみ使用される共用変数」はアクセス修飾子をprivateにして、そのクラス内からしか参照・更新できなくすればよい。

public class Helloworld {
    public static final String Message = "Hello World !";  // (1)定数
    private static String innerMessage; // (2)クラス内でのみ使用される共用変数
}

とはいっても、クラス内でも変数の共用は出来る限り避けたほうがよいだろう。変数を複数インスタンスで共有するということは、別インスタンスからの変数更新を常に意識しないといけないということだ。複雑な更新処理がある場合は、MediatorやObserverなどのデザインパターンを参考にして共用変数を管理・更新を行うクラスを別に作ったほうが良いだろう。

staticメソッド

解説

staticメソッドはstatic変数と同じく、クラスに定義されるメソッドである。個々のインスタンスの変数は参照できないため「クラス変数」と「メソッド引数」のみで処理を記述することになる。

使いドコロ

staticメソッドの使いみちはかなり広いが、代表例を挙げるなら「(1)生成などのインスタンス変数に依存しない処理」「(2)副作用を発生させない処理」をstatic化する場合が多い。「(1)生成などのインスタンス変数に依存しない処理」の具体的なコードは以下のようになる。

public class Helloworld {
    
    private String[] messages;
    
    public Helloworld(String[] messages) {
        this.messages = messages;
    }

    public static Helloworld newHelloworld() {
        return new Helloworld(new String[]{});
    }
    public static Helloworld newHelloworld(String[] msgs) {
        return new Helloworld(msgs);
    }
    public static Helloworld newHelloworld(List<String> msgs) {
        return new Helloworld(msgs.toArray(String.class));
    }
}

Helloworldクラスはメンバ変数にStringの配列を持つ。そして、引数なし、Stringの配列、StringのListのそれぞれの引数に対応してHelloworldのインスタンスをnewして返却するstaticメソッド定義している。

この3通りのインスタンス生成は3つのコンストラクタを用意しても実現できるが、複数のコンストラクタを定義するのは避けるべきである。理由は、同一引数で別の生成処理を書きたい場合に対応できないこと、コンストラクタは名前を変更できないこと(生成ロジックの内容を名前に付与できない)、インスタンス変数の初期化処理が複数箇所に分散すること(ヌルポの温床)、「new Helloworld();」より「Helloworld.newXXX」のほうが記述しやすい(eclipseのコンテンツ・アシストが活用できる)などいろいろある。

staticメソッドでインスタンス生成を実装している代表例はgoogle guavaのListsやMapsである。以下のように様々な引数、実装クラスでListやMapを生成できる。

  1. Lists.newArrayList()
  2. Lists.newArrayList(E ... elements)
  3. Lists.newArrayList(Iterable<? extends E> elements)
  4. Maps.newHashMap()
  5. Maps.newLinkedHashMap()
  6. Maps.newTreeMap()

次に「(2)副作用を発生させない処理」だが、インスタンスの状態を変更するメソッドはインスタンス自身が持ち、インスタンスの状態を変更しないメソッドは外部クラスのstaticメソッドで定義しても良い、とおぼえておけば良いだろう。インスタンス自体が副作用がないメソッドを持ってはいけないという意味ではない

public static void main (String[] args) {
    ArrayList<String> list = Lists.newArrayList("msg1", "msg2");
    list.add("msg3"); // インスタンスの状態を変更するメソッドはインスタンス自身が持つ
    /**
    * Listを逆にしたListを返すメソッド
    * listインスタンスの状態を変更しないため、外のクラスのstaticメソッドで定義している。
    */
    List<String> rev = Lists.reverse(list);
}

副作用的にListの順番を逆にするメソッドなら、ArrayList#reverseというメソッドをArrayList自身が持つべきだろう。(そんなメソッド無いけど)

static内部クラス

解説

static内部クラスはstaticが指定された内部クラスである。この内部クラスは親クラスの個々のインスタンスの変数は参照できないから、別ファイルに定義した普通のクラスと全く同じ扱いである。コードは以下の様になる。

public class AddCalc {
    
    /**
    * static内部クラス
    * Result.javaとして別ファイルに切り出しても同じ
    */
    public static class Result {
        private int xAddy;
        
        public Result(int xAddy) {
            this.xAddy = xAddy;
        }       
        public int getxAddy() {
            return xAddy;
        }
    }
    
    private int x, y;

    public AddCalc(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public Result calc (){
        return new Result(x+y);
    }
}

親のCalcクラスはx, yをメンバ変数として持ち、子クラスのResultはx+yという計算結果を持つクラスである。Calc#calcでこのResultインスタンスを取得できる。

使いドコロ

別ファイルに定義しても普通のクラスと全く同じであるにもかかわらず、static内部クラスを使用する意味は「(1)クラス間の結びつきの強さを表現できる」点と「(2)同じクラス名でも、親クラスを含めて意味を表現できる」点である。

上の例で言えば、Resultクラスを別ファイルにすると一体何の計算結果を意味するクラスかわかりづらくなってしまうが、AddCalcクラス内に定義することによりAddCalcのResultであることを表現することができる。また別の計算ロジッククラス(SubCalcなど)があった場合にも、AddCalc.Result、SubCalc.Resultとしてアクセスすることにより、何のResultかを表現することができる。

(補足) 内部クラス

ちなみに普通の内部クラスは親クラスのインスタンス変数・インスタンスメソッドにもアクセスできるという違いがある。下のコードでは、親インスタンスが持っているx, y変数を返すメソッドも定義している。

public class AddCalc {
    
    public class Result {
        private int xAddy;
        
        public Result() {
            this.xAddy = x+y;
        }       
        public int getxAddy() {
            return xAddy;
        }
        public int getX() {
            return x;
        }
        public int getY() {
            return y;
        }
    }
    
    private int x, y;

    public AddCalc(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public Result calc (){
        return new Result();
    }
}

あまり使う機会はないがクラス外部から子クラスをnewする場合は、「親インスタンス.new 子クラス名」という少し変わった構文となる。というのも、子クラスは親への参照ポインタを持つ必要があるので、どの親インスタンスから生成したかを宣言する必要があるからである。

public static void main (String[] args) {
    AddCalc calc = new AddCalc(100, 200);
    AddCalc.Result result = calc.new Result(); //インスタンス.new クラスで生成
    System.out.println(result.getxAddy());  
}

static イニシャライザ

解説

このあたりから別の意味でstatic修飾子が使用され始める。staticイニシャライザはクラスがメモリ上にロードされた後に実行される処理である。(したがってstatic コンストラクタだと言えなくもない。)実際のプログラミングでは単純代入では実現できないstatic変数の初期化にしか使われない。代表的なのはHashMapなどのCollection系を初期化する場合である。

public class Helloworld {
    
    private static String msg = "initial msg"; // 単純代入で初期化できる場合
    private static HashMap<String, String> codemap
        = new HashMap<>(); //単純代入で初期化できない
    
    static {
        codemap.put("東京都", "01");
        codemap.put("大阪府", "02");
        codemap.put("北海道", "03");
        codemap.put("沖縄県", "04");
        codemap.put("和歌山県", "05");
    }
}

ただ、このようなstaticで使われる変数は大概が実行パラメタやコード値であり、Spring Frameworkなどを使って外部ファイルからDI(Dependency Injection)するほうが正しいため、実際にstaticイニシャライザが使われることはほとんどない。

初期化処理は以下の順に実行される。

  1. クラスロード
  2. static変数初期化(jvm
  3. static変数初期化(ユーザ定義)
  4. staticイニシャライザ
  5. new によるインスタンスメモリ割り当て
  6. インスタンス変数初期化(jvm
  7. インスタンス変数初期化(ユーザ定義)
  8. インスタンスのコンストラクタ実行

コードで示すと以下の通りになる。

public class Helloworld {

    private static int value1; // (2)
    private static int value2=100; // (3)
    private int value3; // (6)
    private int value4=100; // (7)

    static {
        //(4)
    }
    
    public Helloworld () { //(8)
        this.value3 = 500;
        this.value4 = 600;
    }
}

staticイニシャライザの終了を持ってクラスロードの完了となるため、当たり前だがstaticイニシャライザ内でstaticメソッドをコールすることはできない。

使いドコロ

Collection系などの単純代入では実現できないstatic変数の初期化にしか使わないが、staticイニシャライザを使うほど複雑な変数初期化は別の設定ファイルに定義してSpring Frameworkなどを使用してDIするほうが正しい。したがって使いドコロとしては(1)Frameworkを使用するまでもない変数初期化(他の人と共有しない単体テストなど)、(2)POJO信奉者でFrameworkを使用できない場合となる。

static import

解説

もはやstatic修飾子である意味は完全にない。用途としてはstaticメソッドを使用する場合にほんのちょっとだけ簡単になるだけだ

static import使用しない場合

import com.google.common.collect.Lists;

public class Helloworld {
    public static void main (String[] args) {
        ArrayList<Object> list = Lists.newArrayList();
    }
}

static import使用した場合

import static com.google.common.collect.Lists.*;

public class Helloworld {
    public static void main (String[] args) {
        ArrayList<Object> list = newArrayList();
    }
}

使いドコロ

import文はeclipseが自動で挿入してくれる点、staticメソッドは「クラス名.メソッド名」で意味のある名前として命名されていることが多い点を考慮すると、使う機会はほぼない。むしろ使ってる人いるの?

まとめ

今回はJava初心者向けにstatic修飾子を解説した。static修飾子はjvmでのメモリの動きや内部クラスの動作など知っていないと理解できない部分もあり、とっつきにくかったかもしれない。しかしstaticを使うことでコードを構造化し、「見せるコーディング」が可能となるため、いろいろ使ってみてほしい。

enum型についても書いたので参考にどうぞ tkmtys.hatenablog.com

sponsor