入門書が教えてくれないJava スコープ編

入門書ではあまりとりあげられない部分を解説するコンセプトの「入門書が教えてくれないJava」シリーズの第二弾。前回は変数についてだった。今回はそのスコープについて取り上げたい。

スコープとは

スコープとは大雑把に言えば変数やメソッドなどが見える範囲のことを指す。

Javaの変数のスコープで一番簡単なのはローカル変数で、これは{から}まで(これをブロックという)の中で、宣言した位置より後ろで参照することができる。

public void main(String[] args) {
  int i1 = 0;
  // i1はここからmainの}までの間で参照できる
  if (true) {
    int i2 = 0;
    // i2はこのifの}までの間で参照できる
  }
  // i2はifの}を過ぎると参照できなくなる
  i2 = 1; // ← コンパイルエラー
}

なお、Javaの言語仕様では{}のブロックは結構いろんなところに書けるが、ブロックとは違うものがちょっとだけある。

  • クラス宣言等の{}
  • 配列の初期化に用いる{}

逆説的だがローカル変数宣言ができるやつがブロックと覚えてもよい。ブロック内では文(このあたりは踏み込むとボリュームがあるので割愛。そのうち別稿を用意するかもしれない)を書けるところならifなどを伴わずに置くこともできる。

public void main(String[] args) {
  int x1 = 0;
  int x1 = 0; // ← 変数名が被るのでコンパイルエラー
  
  {
    int x2 = 0;
  }
  {
    int x2 = 0; // ← 先のx2はスコープ範囲外なのでOK
  }
}

上記例ではx2をブロック内においてみた。x2が有効なスコープはブロックの内側だけなので、下で同じようにx2を宣言しているが名前が被らないのでOKとなる。

一般に長いメソッドを記述するのは良くないとされるが、どうしても長くなる場合は一時的に用いる変数をブロックを用いて局所的にしておくことで整理することもできる。ただし、そうした整理ができる人は、おそらくそのブロックを独立したprivateメソッドに切り出したくなるだろう。ブロックを用いた変数スコープの制御はそういう意味であまり脚光は浴びないものである。

スコープの全体像

さて、簡単な事例でスコープの概要がつかめただろうと思うので、スコープについての全体のイメージをここで抑えておこう。
Javaの場合、スコープについて考える場合、影響する要素は以下の様なものがある。

今回、Java9のモジュールについては述べない。当エントリの時点ではまだJava9はリリースされていないが、新機能については試作版が提供されているので興味がある人は試してみて欲しい。

なぜスコープを用いるのか

あの変数を参照したいのに、コンパイルエラーになって参照できずに困ったことがあることだろう。Javaオブジェクト指向に触れた時、必ず通る道だと思う。なんでわざわざ参照できなくするんだ。全部参照できたら便利なのに!

教室を表現したclassを例にしてみよう。クラスと書くと紛らわしいので「教室」としている。JavaのクラスはClassと書くので混乱しないようにして欲しい。朗読して読み聞かせをしている人は諦めて文字で文章を読んでもらおう :-P
なお、オブジェクト指向の比喩というのは往々にして正しくオブジェクト指向を表現できないものである。あくまで比喩であることを予め断っておく。

教室があったとして、ここでは適当に3年B組としておこう、出席番号3番の人を探そう。
出席番号3番の人は3年B組には一人かも知れないが、3年A組にもいることだろう。2年生にも1年生にもいるだろうし、他の学校にもいるだろう。

いやいや、今話しをしているのは3年B組のことだった。よその出席番号3番は関係無い!関係無いんだから引っ込んでいろ!

これがスコープで「見えなくする」動機付けなのである。

要するに、世の中のありとあらゆる出席番号3番が同じスコープにいると、ごちゃごちゃになるのだ。「今話題にしているところ」だけをスコープにして、関係無いところは無視したいのである。ごく小さいプログラムを組んでいる時には変数は全部参照できたら便利なのにと思うかもしれないが、ちょっと規模が大きくなってくると、余計なところは無視したい!という要求が強くなってくる。

大きなプログラムを組むにはプログラムの整理術が必要になる。その整理術のひとつがスコープの管理なのである。

ローカル変数、インスタンスフィールド、static フィールド

ローカル変数については先に述べた。ローカル変数はローカルであることが大事である。ローカル変数は中の仕組みみたいなもんだから外からそれを意識ないで済むのがよい。家電の類はだいたいあんまり中の仕組みを知らなくても使えるように設計されている。トースターでパンを焼くのにいちいち中の仕組みなんて考えないで済むのが理想なのである。

インスタンスフィールドは、classのインスタンスが持つ情報である。先ほどの教室の例でいえば、3年B組というのがインスタンスで、教室に在籍している生徒はインスタンスフィールド(どういう形で格納するかは別途考えて欲しい)に持つ情報ということになる。

対してstaticフィールドというのは比喩が難しい。「教室」というclassそのものに保持する情報ということになる。一般にはこのclassを扱う場合に必要となる定数などの定義をすることはあるが、static「変数」を定義することは稀。

参照箇所がインスタンスメソッドか、staticメソッドか

さてそんなフィールドたちだが、どこからアクセスできるかを考える場合、参照しようとするコードがインスタンスメソッドか、staticメソッドかというのが影響してくる。

が、まずは一般論としてのルール。インスタンスフィールドにアクセスするには、オブジェクト.フィールド名でアクセスする。staticフィールドにアクセスするにはクラス名.フィールド名でアクセスする。

  • オブジェクト.フィールド名
  • クラス名.フィールド名

この時、インスタンスメソッドでは自分自身のインスタンスを指し示すthisというキーワードが使える。

  • this.フィールド名

そして、this.は省略可能だ。

  • フィールド名

また、staticフィールドも、自分自身のclassのstaticフィールドだとクラス名を省略することができる。

  • フィールド名

このように、省略した結果、インスタンスフィールドへのアクセスと、staticフィールドへのアクセスが同じ形になってしまう。なので、コードを読み解く場合は逆のルールでこのフィールド名はインスタンスのものか、staticなものかを探すことになる。

IDEを使っている場合はだいたい色分けしてインスタンスフィールドかstaticフィールドかわかるようにしてくれるので混同することはないと思う。

インスタンス内からは同一インスタンスのフィールドをthisを省略してアクセスできるが、慣れるまではthisを補ったほうがスコープについてはわかりやすいと思う。3年B組がthisである場合に、3年A組のフィールドにアクセスする、といった事例でも省略形のフィールド名だけのアクセスで考えるのではなく 3年B組.担任 と 3年A組.担任 であると考えるほうが混乱がないように思う。

インスタンス間でデータが混ざらないということは整理術として非常に重要なポイントとなる。

継承階層

前段ではごく簡単にインスタンスフィールドとstaticフィールドのアクセスについて書いたが、実際には継承階層がある場合、親クラスおよびインターフェースにあるものも参照できるので話がもうちょっとややこしい。

基本を言えば

  • 親のクラスで宣言されたインスタンスフィールドは、子供のクラスで参照できる
  • 親のクラスで宣言されたstaticフィールドは、子供のクラス名.フィールド名でアクセスできる

このうち、特にstaticフィールドについては実際にstaticフィールドが宣言されているクラス.フィールド名でアクセスすることがよしとされている。なお、子供のクラス内からだとクラス名.が省略できるが、この省略をしたいがために継承をするというのは一般にNGとされる。

なおJava5からはstaticインポートという機能があるので、継承階層にないstaticフィールドをフィールド名だけでアクセスしていることがある。

アクセス修飾子

アクセス修飾子については入門書で必ず記載があることだろう。public, protected, private, パッケージプライベートというやつである。

ごく小規模なプログラムだとパッケージプライベートを使わないことも多いかもしれないが、いくつかのクラスの集まりをパッケージにまとめる設計をし始めると、むしろパッケージプライベートを多用することになる。

とは言え、パッケージをひとつのモジュールとして中の仕組みを隠蔽しよう、中の仕組みを知らなくても使えるようにしようとするには、この4つのアクセスレベルでは力不足なのであって、Java9で導入されるモジュール機能というのはそれを改善するためのものなのだ。

内部クラス

スコープの話で特殊かつ入門書ではスルーされるものとして本稿で解説しなければならないものに内部クラスが挙げられる。
内部クラスについては拙稿Javaのクラス宣言5種+α - プログラマーの脳みそでも取り上げているが、ここの内部クラス(Inner class)の部分がこの話題となる。

内部クラスは教室の例でいえば、班みたいなものとしてイメージしてもらいたい。3年B組という教室のインスタンスがあって、そこに属する班が複数ある。3年B組の1班、2班といった形だ。班というインスタンスが教室というインスタンスにぶら下がっている形なのが特徴だ。

班型からみた教室型のことをエンクロージング型(Enclosing type)と呼ぶ。3年B組の1班から見た3年B組はエンクロージングインスタンスというわけだ。このとき、1班からは3年B組の情報が直接参照できる。

ただし、1班から2班を直接参照することはできない。変数名.フィールド名といった形で2班.メンバーといった参照であれば参照可能だが、このときはまず1班はなんらかの形で2班インスタンスを持ってこなくてはならない。やりたければ直接参照可能な3年B組インスタンスから2班インスタンスを得て参照するようなオブジェクトの引き回しが必要になる。

内部クラスのスコープはちょっと独特だ。

内部クラスの内部クラスといったものも宣言することができる。この場合は内部クラスの内部クラスから外側の内部クラスのインスタンスを直接参照することができる。また一番外側のクラスのインスタンスも参照できる。

匿名クラス・ラムダ式

匿名クラス(Anonymous class)についても拙稿Javaのクラス宣言5種+α - プログラマーの脳みそで取り上げているがここでも簡単に触れておこう。

匿名クラスの宣言時には、外側のブロックで宣言されているfinalな変数を参照できる。finalでなくてはならない。

public static void main(String[] args) {
  final int i = 42;
  
  Runnable r = new Runnable() {
    @Override
    public void run() {
      System.out.println(i);
    }
  };

  // 上記のrun()はここで呼び出される
  r.run();
}

なぜfinalじゃないといけないのかという話もいろいろ議論があるのだが、Javaのスタンスの現れと捉えておくのが無難だろう。finalな変数は再代入はできないが、代入されたオブジェクトの内容を更新することはできる。なので以下の様なコードで匿名クラス内部から外側のスコープの変数を更新することも可能ではある。

public static void main(String[] args) {
  final int i[] = {42};
  
  Runnable r = new Runnable() {
    @Override
    public void run() {
      i[0] += 10;
    }
  };

  // 上記のrun()はここで呼び出される
  r.run();

  System.out.println(i[0]); // 結果は52
}

しかし、このようなコードは「整理術」としてはNGであるというのが主流の考え方だ。コード的に実装可能であっても避けるべきである。

さて、ラムダ式についても触れておこう。ラムダ式でも匿名クラスと似たように外側のブロックで宣言されている変数を参照できる。
しかし、ラムダ式の場合はfinalではない変数でも参照できる。

  int i = 42;
  Runnable r = () -> System.out.println(i);

しかし、これは外側のブロックの変数を変更できるという意味ではない。該当の変数にfinal修飾子をつけたとしても大丈夫な場合にのみ、ラムダ式内で外部のブロックの変数を参照できるようになっている。これは実質的finalと呼ばれる。

  int i = 42;
  Runnable r = () -> System.out.println(i++); // コンパイルエラー

そのため、ラムダ式で外側のブロックの変数を変更した場合、上記の例ではi++となっているが、その場合はコンパイルエラーとなるし、

  int i = 42;
  i = i+10;
  Runnable r = () -> System.out.println(i); // コンパイルエラー

変数が宣言されたあとに再代入されている場合(上記の例ではi+10で再代入されている)もコンパイルエラーとなる。

なので、Java言語の立ち位置は変わらず、ただシンタックスシュガーとして構文を簡素で便利にしているだけ、と捉えるのが妥当ではないかと思う。

メソッド、型変数、etc.

ここまで変数・フィールドのスコープを中心に見てきたが、スコープというと何も変数に限って使われる言葉ではない。

メソッドを参照できる、できないといった場合にもスコープと表現されるし、ジェネリクスの型変数についても参照範囲についてはスコープと表現する。またマイナーではあるが、ローカル内部クラス(Javaのクラス宣言5種+α - プログラマーの脳みそ参照)に至っては宣言されたメソッド内だけが有効なスコープとなる。

いずれも、基本的には「関係ないものは見えなくする、関係あるものだけが見える」という状態を保つことで、プログラムを整理する技なのである。大量のハズレの選択肢から当たりを探すような作業はプログラミングの妨げでしかない。

ごく小規模なプログラムならいざしらず、ちょっと大きくなってくると途端に整理整頓が必要になってくる。変数のスコープは小さくという先人の知恵をぜひ活かして欲しい。納得が行かない人は心ゆくまで試行錯誤してみるのも良いかもしれない :-)