春なのでJava入門的なことを書こうと思い立ったので、入門書ではあまりとりあげられない部分を解説するコンセプトの入門記事を書いてみようと思う。(←ひねくれ者)
対象読者としては、Java言語の基礎を学んだがもう一歩踏み込んだ話が知りたいぐらいの初学者〜中級者を想定してる。上級者の方は記述に誤りがないかチェックしていただければ幸いだが、説明を簡単にするためにいろいろ端折っている点はご理解いただきたい。
今回は変数・フィールド編とした。筆者のやる気次第だがこのシリーズでいくつか書こうと考えている。
- 入門書が教えてくれないJava 変数編 (本稿)
- 入門書が教えてくれないJava スコープ編 - プログラマーの脳みそ
初期値を指定しない変数宣言
変数宣言に際して初期値を設定しないことができる。
int i;
この場合、このint i;がフィールドであるか、ローカル変数であるかで扱いが変わってくる。
iがフィールドであれば、intなどのプリミティブ型であれば0 (boolean型ならfalse) で参照型ならnullの扱いとなる。(インスタンスフィールド、staticフィールドともに同じ)
メソッド内などのローカル変数の場合、使用する前に値を代入して初期化しなければならない。
if文などの分岐で初期化がされない可能性がある場合はコンパイルエラーとなる。
int i; if (flag) { i = 1; } System.out.println(i);
上記の例だとflagがfalseの場合、初期化されずにSystem.out.println(i);へと進むことになるのでエラーとなる。
細かいことを言えば、初期化がされてなくても使用されない変数であればコンパイルエラーにはならない。(が無駄なので書かないほうがよい)
ローカルfinal変数の初期化
通常の変数と基本的に変わらないが、2回目の代入をしようとするとコンパイルエラーになる。
static final フィールドの初期化
static final なフィールドの初期化は、クラスの初期化が終わるまでにされなければならない。*1
final変数の初期化ではフィールド宣言と同時に
public static final String NULL_PO = "GA!";
といったように値を設定する使い方が多いと思うが、時には複数行にまたがる演算によって初期値を設定したくなることもある。
public static final String START_DATE; public static final String END_DATE; static { LocalDate d1 = LocalDate.of(2016, 4, 1); START_DATE = d1.format(DateTimeFormatter.ISO_DATE); LocalDate d2 = d1.plus(75, ChronoUnit.DAYS); END_DATE = d2.format(DateTimeFormatter.ISO_DATE); }
このような時は、クラスの初期化が終わるまでにfinal変数に値を代入すればよい。具体的にはstatic初期化ブロックを用いる。
final変数への代入は一度きりである必要がある。2回代入しようとすればコンパイルエラーになるし、初期化忘れもコンパイルエラーとなる。
インスタンスの final フィールドの初期化
インスタンスのfinalフィールドの初期化は、インスタンスの初期化が終わるまでにされなければならない。
これは3つのやり方がある。
インスタンス初期化ブロックは先にあげたstatic初期化ブロックと似ている。static {} からstaticを取り除くとインスタンス初期化ブロックになる。
public class Hoge { final int i; { // ここがインスタンス初期化ブロック i=1; } }
配列の[]
Javaは言語の構文はC言語に似せてある。その関係で配列の変数の宣言は少々特殊な書き方ができる。
int[] i1 = null; int i2[] = null; int[] i3[] = null;
Javaの構文は一般に最初に宣言型を書き、次に変数名を書き、それから = 初期値となるが、C言語の影響で変数名の後に[]を記述して配列を宣言することができる。そのためint i2[]のような記述でint配列の宣言を行うこともできるのだが、Cから受け継いだ負の遺産だと思って使用しないようにしよう。
i3の場合はint[][]型となるのだが、紛らわしいので当然書くべきではない。
変数の初期化時だけで使える構文
配列の初期化について、変数の初期化時だけで使える便利構文がある。
String[] s0 = new String[2]; String[] s1 = new String[]{"one", "two"}; String[] s2 = {"one", "two"};
通常、配列を作成する場合はnew String[2]のように添え字に配列のサイズを指定する。
s1の例のように添え字なしでnew String[] とし、直後に{}で初期値を記述する書き方もできる。
さらに、変数の宣言への代入に限りs2のようにnewと型の記述を省略していきなり{"one", "two"}といった記述をすることができる。
変数のtransient
比較的マイナーなJavaの予約語にtransientというものがある。変数宣言に付記することができるのだが
transient String text;
これは、オブジェクトを直列化する際にこのフィールドは無視するという設定である。
直列化とは何か?という話は別途調べていただきたいが、大雑把に言えばオブジェクトをbyte配列に変換して保存したり別のJavaVMに送信したりするこのできる機能である。(参考:Java直列化メモ(Hishidama's Java Serializable Memo))
冗長だが扱いやすいように加工したデータをフィールドに持っているような場合、それらをtransientキーワード指定することがある。もっとも直列化機能を使わない限りはあまり気にすることはないのだが。
変数のvolatile
比較的マイナーなJavaの予約語にvolatileというものがある。これはマルチスレッドに関連しており、端的に言えばこのフィールドを操作するにあたってメモリの同期化がされるようになる。
Javaはマルチスレッドで動作している場合にメインメモリからコピーをもってきて操作するような動きをする。そのため、複数のスレッドの間で違う値を見ているということが起きる。じゃあvolatileをつければとにかく大丈夫なのかというと、そんなに単純な話ではないので別途マルチスレッドの解説を読んでいただきたい。
変数のハイディング
変数・フィールドは同名のものを宣言できるケースがある。
同名の宣言で覆い隠すことをハイディングという。
- 親クラスのstaticフィールドと同名のstaticフィールド
- 親クラスのインスタンスフィールドと同名のインスタンスフィールド
- 親クラスのstaticフィールドと同名のインスタンスフィールド
- 親クラスのインスタンスフィールドと同名のstaticフィールド
- 同クラス/親クラスのstaticフィールドと同名のローカル変数
- 同クラス/親クラスのインスタンスフィールドと同名のローカル変数
- 外部クラスのインスタンスフィールドと同名の内部クラスのインスタンスフィールド
基本的にstaticフィールドはクラス名.フィールド名でアクセスできるので、親クラスのstaticフィールドをハイディングしてもアクセスしたいときは明示的に親クラス名.フィールド名を書けば済む。
そのオブジェクトでのインスタンスフィールドをアクセスしているのだ、ということを明示的に表現したい場合はthis.フィールド名といったようにthisキーワードを用いて明示することができる。
親クラスのインスタンスフィールドと現在のクラスのインスタンスフィールドが同名となった場合、super.フィールド名で親クラスのフィールドを参照することができる。現クラスのほうはthis.フィールド名で明示するとよいだろう。superによるアクセスは当クラスからのみで、外部からオブジェクトの親のフィールドをsuperで参照するようなことはできない。
また、3段以上に継承している場合、superが参照するのはひとつ上の階層となる。多重ハイディングしている場合に階層ごとに呼び分けるようなことはできない。
内部クラスから外部クラスの同名フィールドを参照したい場合、外部クラス名.this.フィールド名とすることで呼び出すことができる。まれにこの記法を知らないがために迂回路となるアクセサ・メソッドを用意したりしているソースコードを見ることがある。ダサいといえばダサいが、そんなに実害のあるものでもない。
おわりに
入門書ではスルーされがちな部分をいくつかピックアップしてみたが、楽しんでいただけたであろうか。訂正はもちろん、リクエストなどもあればコメントいただきたい。
*1:このあたりクラスロードとClassLoaderについてはざっくり割愛しているので上級者の皆様におかれましてはblogエントリなどでの解説を試みていただきたく