新人エンジニア研修で知っておきたいインスタンスの使い方
なぜ、インスタンスの理解が重要なのか、その理由。
この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。
前回はstaticメソッドについて解説しました。今回はインスタンスの活用について解説します。
いままでは、話を単純にするためにインスタンスの話題はできるだけ避けてきました。しかし、ここからはいよいよオブジェクト指向の本丸、クラスからインスタンスを作成するということについて学んでいきます。
クラスは設計図、インスタンスは設計図から作られた実物のイメージでしたね。前回はstaticメソッドを学びました。クラス名.メソッド名()で呼び出すメソッドでした。実はJavaでメソッドを扱う際にはインスタンス・メソッドを使うほうがより一般的なのです。ですから今回のお話も非常に重要です。おそらく本連載の中で最も長い章となるでしょう。
1.オブジェクト指向とは何だったか?
Objectとは、モノのことです。
この記事の1回目でお話ししたように、オブジェクト指向とは他の工業製品と同じように部品を組み合わせることでプログラム全体を作り上げるという考え方でした。
例えば、新人エンジニアの皆さんがお使いのパソコンも多くの部品から構成されています。私たちは、それぞれの部品の内部構造を知らなくても、それらを組み合わせて使うことができます。大多数の人は“CPU”と“メモリ”の区別も曖昧なままスマホやパソコンを使っています。それでもスマホとパソコンをつないでデータを交換したりインターネットを見たりすることはできます。また、それぞれの部品単位でアップデートすることもできます。例えば、古くなったハードディスクを新しいものに交換する、などといったことができます。
この考え方をプログラムに応用して、再利用性やメンテンス性の高いプログラム部品でシステムを組むのがオブジェクト指向です。
つまり、オブジェクトはプログラムの部品です。そして、オブジェクトの設計図がクラスです。クラスには、オブジェクトに共通する属性(情報や機能)を定義します。この設計図をもとに作成した実際の部品がインスタンスです。
人によって言葉の使い方に多少のずれがありますが、この研修では、
オブジェクト(抽象概念) = クラス(設計図) + インスタンス(具体的なもの)
という言葉で使い分けています。
単にオブジェクトといった場合には、それがクラスのことを指すこともあれば、インスタンスのことを指すこともあります。ですから本書ではオブジェクトという言葉の使用は極力避けて、クラスとインスタンスという言葉を使うようにしています。
例えば、今現在あなたが見ているこの画面も、ウインドウというインスタンスの上にメニューというインスタンスが乗っていて、クリックの情報を受け取るインスタンスがある、という風になっているというと少しはイメージの助けになりますでしょうか?
論より証拠、以下のプログラムを実行していただくとお分かりただけると思います。
package chap09;
import javax.swing.JFrame;
public class Example00 {
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setSize(1000, 500);
frame.setVisible(true);
}
}
例えばウインドウサイズを変更したいとき、どこをいじれば良いか分かりますか?
これは比喩的な表現ですが、パソコンクラスというものを作ったとして、あなたや隣の人の机に乗っている具体的なパソコンがインスタンスです。新入社員クラスがあったとして、あなたやあなたの隣の人がそのインスタンスです。
例えば新入社員の皆さんで下図の名簿を作ったとします。名簿の項目はクラスに当たります。そして一人一人の行はインスタンスです。
身近な例を思いつくままに挙げてみてください。
あなたの思いついた例: |
2.フィールド(インスタンス変数)を持ったクラスの定義
例えば、新人エンジニアのクラスを作成してみましょう。話を単純化するために新人エンジニアクラスは社員番号と名前だけを持っているとします。以下のNewEngineer1を見てください。
package chap09;
class NewEngineer1 {
int id;
String name;
}
このクラスを実行しようとすると、「main()メソッドがありません」といったメッセージが表示され実行できません。それでもこれで立派にクラスを定義できました。
NewEngineer1クラスは二つの情報idとnameを持っています。これらの情報を総称してフィールド(field)と呼ぶのでした。なお、フィールドでは変数の宣言と初期化までが可能です。
したがって
String name = "tabuchi";
のような記述は可能です。
ただし、フィールドに処理を書くことはできません。処理はコンストラクタやメソッドに書くということはこの後すぐ学びます。
では、さっそく一人のエンジニアを誕生させてみましょう。
NewEngineer1クラスを使うExample01というクラスを作成します。
以下のExample01はフィールドの初期値を説明するプログラムです。
package chap09;
public class Example01 {
public static void main(String[] args) {
NewEngineer1 se1 = new NewEngineer1();
System.out.println(se1.id);
System.out.println(se1.name);
}
}
実行する前に上から順番に解説していきます。
NewEngineer se1;
ここでは、NewEngineer型のローカル変数se1を宣言しています。いままでも「String str」といった宣言を目にしてきましたが「型 変数名」という同じ形ですね。
se1 = new NewEngineer();
ここで、変数se1に代入して初期化しています。何を代入したのかというとNewEngineerクラスのインスタンスです。イメージとしては一人のエンジニアを作り出したわけです。この時、newという演算子を使っています。new演算子は配列のところでも出てきましたね。実は、new演算子はクラスの全てのインスタンスメンバをメモリー(ヒープ領域)にコピーするための演算子です。
また、NewEngineer()というのはコンストラクタというものです。コンストラクタは文字列のところでも出てきました。コンストラクタはクラス名と同じ名前のメソッドです。観察眼が鋭い人は先頭文字が大文字であることに気づかれたことでしょう。コンストラクタ自体の作り方は後で学びます。
ここでは、コンストラクタを利用するには、
new クラス名(実引数);
と書くのだということをおさえましょう。
なお、以下のように宣言と初期化を1行で書くこともできます。
NewEngineer se1= new NewEngineer();
実行してみます。
<実行結果>
0 null |
実は、フィールドは初期値が下表9.1のように決まっているのです。
真偽値 | false |
整数型 | 0 |
浮動小数点型 | 0.0 |
文字型 | \u0000 ※ |
参照型 | null |
※ユニコードのNUL(空文字)、Javaのnullではない。
それに対して、ローカル変数は初期化してから使う必要があったことをここで思い出してください。では、あらためてフィールドに値を代入してみます。
package chap09;
public class Example02 {
public static void main(String[] args) {
NewEngineer1 se1;
se1 = new NewEngineer1();
se1.id = 1;
se1.name = "yamada";
System.out.println(se1.id);
System.out.println(se1.name);
}
}
<実行結果>
1 yamada |
ここで使用したidやnameといったフィールドを個々のインスタンス固有の変数という意味でインスタンス変数(またはインスタンスフィールド)と呼びます。
インスタンス変数とフィールドは同じものですが、インスタンス変数といったときにはインスタンス生成後の値の入ったものを指すのに対して、フィールドはクラスの状態のものを指します。また、このあとクラス共通の変数であるstatic変数というのが出てきますのでそれと区別できるようにしましょう。
ここからはローカル変数やstatic変数と区別してインスタンス変数という表現を積極的に使っていきます。
3.メソッドを持ったクラスの定義
先の新人エンジニアクラスにメソッドを加えたNewEngineer2クラスを作ってみましょう。
NewEngineer1では2つのprintln()メソッドを使ってIDと名前をそれぞれ表示させていました。
以下のNewEngineer2では一度にIDと名前を表示するshow()メソッドを加えることにします。
package chap09;
class NewEngineer2 {
int id;
String name;
void show() {
System.out.println("私のIDは" + id + "、名前は" + name + "です。");
}
public static void main(String[] args) {
NewEngineer2 se2 = new NewEngineer2();
se2.id = 2;
se2.name = "tabuchi";
se2.show();
}
}
<実行結果>
私のIDは2、名前はtabuchiです。 |
インスタンスメソッドを呼び出すときは、
変数名.メソッド名(実引数)
でしたね。
ただし、ここでフィールド(インスタンス変数)にあるデータをわざわざ実引数で渡す必要はありません。上記の例ではidやnameをshow()メソッドの実引数にする必要はないのです。
ちなみに、このように自分で作成したクラスにmain()メソッドを書き足して簡易的に実行するとコードを確認しながら実行できて、試行錯誤の多い初学者には便利です。ただし、1つのプロジェクトにエントリーポイントが複数存在する状態になってしまいます。納品するプログラムからは削除して下さい。
自クラスの中で自分自身のインスタンスを生成している部分が腑に落ちない方もいらっしゃるかもしれません。その点に関しては、下図の3つのメモリ領域の図を思い出してください。main()メソッドは、インスタンスが生成される前にスタティック領域にロードされるのでしたね。main()メソッドはこのクラスのインスタンスとは別の領域で実行されているので、このインスタンスの生成処理はインスタンスの外から実行されているのと同じ事になるのです。
※常にクラスの中にmain()メソッドを書くJavaの書き方には多くの人が違和感を持つのか、Javaの後継言語とも目されているKotlinでは、main()メソッドはクラスの外にも書けます。
4.インスタンスの生成
上記サンプルプログラムに以下のように書き足して、登場人物を2人にしてみましょう。
package chap09;
public class Example03 {
public static void main(String[] args) {
NewEngineer2 yamazaki = new NewEngineer2();
yamazaki.id = 1;
yamazaki.name = "yamazaki";
yamazaki.show();
NewEngineer2 imai = new NewEngineer2();
imai.id = 2;
imai.name = "imai";
imai.show();
}
}
※ここには記述されていない先のNewEngineer2クラスを再度利用してインスタンス生成しています。
<実行結果>
私のIDは1、名前はyamazakiです。 私のIDは2、名前はimaiです。 |
なお、デバッガを使ってインスタンスを見ると以下のようになりますので確認しておいてください。
では、以下のプログラムExample04を実行したら何が表示されるでしょうか?
package chap09;
public class Example04 {
public static void main(String[] args) {
NewEngineer2 yamazaki = new NewEngineer2();
yamazaki.id = 1;
yamazaki.name = "yamazaki";
System.out.println(yamazaki);
}
}
<実行結果>
chap09.NewEngineer2@7852e922 |
上記のように表示されました。これは、配列のところで見た参照(クラスの型@ハッシュ値)ですね。ここで、参照についてまとめましょう。
5.参照型とプリミティブ型
実は、Javaで使用できる変数の型にはプリミティブ型(基本型)と参照型があります。プリミティブ型の変数には、boolean、int、 doubleなど値そのものが入っています。一方、参照型の変数には、その名の通りインスタンスへの参照(ヒープメモリのアドレス)が入っています。決してインスタンスそのものが入っているわけではありませんので注意しましょう。
ここでJavaの型についてもう一度整理します。これから学ぶのはクラス型なのですが、これは配列と同じ参照型に分類されます。
NewEngineer2 yamazaki = new NewEngineer2();
と書いたときに、変数yamazakiにはNewEngineer2クラスのインスタンスへの参照が代入されているのです。したがって、一つのインスタンスを2つの参照が指している状態を作ることも可能です。
以下のExample05はse1とse2という二つの変数が同一のインスタンスを参照している例です。
package chap09;
public class Example05 {
public static void main(String[] args) {
NewEngineer2 se1 = new NewEngineer2();
se1.id = 1;
se1.name = "yamazaki";
NewEngineer2 se2 = se1;
se1.show();
se2.show();
}
}
<実行結果>
私のIDは1、名前はyamazakiです。 私のIDは1、名前はyamazakiです。 |
イメージは下図になります。
6.参照型の配列
参照型の配列を作成することもできます。
以下のExample06は参照型の配列の例です。
package chap09;
public class Example06 {
public static void main(String[] args) {
NewEngineer2[] se = new NewEngineer2[3];
se[0] = new NewEngineer2();
se[1] = new NewEngineer2();
se[2] = new NewEngineer2();
se[0].id = 1;
se[0].name = "tabuchi";
se[1].id = 2;
se[1].name = "shinohara";
se[2].id = 3;
se[2].name = "kokubun";
for (int i = 0; i < se.length; i++) {
se[i].show();
}
}
}
実行結果の予想: |
ただし、その作られ方には違いがあります。
以下の下図のようにプリミティブ型は配列に直接値が格納されるのに対して、オブジェクト型の場合には配列からまた参照をたどった先にインスタンスがあります。
7.NullPointerException
このとき、新人エンジニアのみなさんが起こしがちなミスとして、配列は作ったものの、インスタンスを作り忘れるというものがあります。
例えば、以下のExample07はインスタンスを作り忘れてNullPointerExceptionが発生する例です。
package chap09;
public class Example07 {
public static void main(String[] args) {
NewEngineer2[] se = new NewEngineer2[3];
se[0].id = 1;
se[0].name = "tabuchi";
se[1].id = 2;
se[1].name = "shinohara";
se[2].id = 3;
se[2].name = "kokubun";
for (int i = 0; i < se.length; i++) {
se[i].show();
}
}
}
<実行結果>
Exception in thread "main" java.lang.NullPointerException: Cannot assign field "id" because "se[0]" is null at chap09.Example07.main(Example07.java:7) |
7行目で「NullPointerException」が発生しました。Pointer(参照)がNull(空)であるというException(例外)です。
※ポインタとはC言語等で使われる参照(のようなもの)です。用語が先祖返りしているのが興味深いですね。
se[0]には何も入っていないのに、その参照を使おうとしたことが原因です。先のNullPointerExceptionが起きなかった場合と図解で比較してみると下図のようになります。
オブジェクト型の配列の場合は、最初は参照に何も入っていないnullという状態なのです。例えるなら、リモコンはあるけれども肝心のTVやエアコンがない状態と言ったら良いでしょうか?参照はあってもインスタンスがないとNullPointerExceptionが起きるのです。
おそらく、これから新人エンジニアの皆さんが学習を進めていくうえで、一番多く遭遇するのがこのNullPointerExceptionです。
しかも、このNullPointerExceptionは非検査例外といってプログラマの責任で必ず対処しておかなければならない例外なのです。よって、IDEもチェックしてくれません。
※例外のところで詳述します。
nullは、特別な値で参照型の変数に代入することができます。
package chap09;
public class Example08 {
public static void main(String[] args) {
NewEngineer2 se = null;
System.out.println(se);
}
}
<実行結果>
null |
参照先が未設定(null)で実体がない参照型を使おうとしたときに発生するのがNullPointerExceptionです。
以下のサンプルコードを見て下さい。
package chap09;
public class Example09 {
public static void main(String[] args) {
String str1 = null;
System.out.println(str1.length());
}
}
<実行結果>
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str1" is null at chap09.Example09.main(Example09.java:7) |
nullは文字列の"null"とも、""(空文字)とも数値の0とも違います。参照型なのに参照先が未設定な場合に、つまり実体がまだ無い参照型が持つ特別な値なのです。
以下のExample10を見て下さい。
package chap09;
public class Example10 {
public static void main(String[] args) {
String str1 = null;
System.out.println(str1 == null);
System.out.println(str1 == "null");
System.out.println(str1 == "");
}
}
実行結果の予想: |
ちなみに、Javaにはガーベージコレクション(Garbage collection:ゴミ集め)という機構があり、どこからも参照されなくなったインスタンスはプログラマが何もしなくても自動的にメモリから消去されます。new演算子でメモリが確保され、ガーベージコレクションが自動でメモリを解放してくれるのです。このおかげで、メモリの確保と開放という作業が不要になりました。
この仕組みがない言語(C、C++等)では、プログラマが都度必要なメモリをOSから借り、不要になったら解放しなければなりません。メモリを借りっぱなしにしておくと、メモリリーク (memory leak:メモリ漏れ) という下手をすればシステムを破壊しかねない深刻なエラーが起こります。
今では当たり前の機能ですが、Javaが登場した当時はガーベージコレクションの仕組みを備えていたことも画期的なことでした。
8.参照による値渡しと値渡し
インスタンスの参照をメソッドの引数として渡すことができます。引数にインスタンスを渡すことでたくさんのデータを一つの引数で渡すことができるようになります。「参照による値渡し」【pass-by-value of the reference】といいます。
例えば、以下のExample11は参照による値渡しの例です。
package chap09;
public class Example11 {
static void show(NewEngineer1 se) {
System.out.println(se.id + " : " + se.name);
}
public static void main(String[] args) {
NewEngineer1 yamazaki = new NewEngineer1();
yamazaki.id = 1;
yamazaki.name = "yamazaki";
NewEngineer1 imai = new NewEngineer1();
imai.id = 2;
imai.name = "imai";
show(yamazaki);
show(imai);
}
}
<実行結果>
1 : yamazaki 2 : imai |
参照による値渡しのイメージを図解すれば下図のようになります。
参照による値渡しの場合のインスタンスは同一のものを指していますから、呼び出し元のインスタンスと呼び出し先のメソッドのインスタンスは同じです。つまり、メソッド側でインスタンスに変更を加えた場合は、元のインスタンスにも変更が及びます。
例えれば、参照による値渡しの変数が持っているのは地図で言えば住所です。その住所にある家を改装したとして住所を知っている人が見に行けば誰でも新しくなった家を見れますよね。
その参照による値渡しの説明をしているのが以下のサンプルプログラムです。
package chap09;
public class Example12 {
static void show(NewEngineer1 se) {
se.name += "_san";
System.out.println(se.id + " : " + se.name);
}
public static void main(String[] args) {
NewEngineer1 yamazaki = new NewEngineer1();
yamazaki.id = 1;
yamazaki.name = "yamazaki";
show(yamazaki);
System.out.println(yamazaki.name);
}
}
<実行結果>
1 : yamazaki_san yamazaki_san |
これは、プリミティブ型を引数に渡した時とは違う結果です。プリミティブ型を引数に渡した場合は、値はコピーされて呼び出し元の値と呼び出し先のメソッドの値は別物(コピー)になります。
以下のExample13は値渡しの例です。
package chap09;
public class Example13 {
static void plusOne(int i) {
i++;
System.out.println("呼び出し先のi:" + i);
}
public static void main(String[] args) {
int i = 10;
System.out.println("呼び出し元のi:" + i);
plusOne(i);
System.out.println("呼び出し元のi:" + i);
}
}
<実行結果>
呼び出し元のi:10 呼び出し先のi:11 呼び出し元のi:10 |
この引数の渡し方を値渡しといいます。Javaだけではなく、多くのプログラミング言語で共通の概念です。値渡しは契約書のコピーを渡したようなものです。どちらか一方が契約書を破っても、もう一枚は手つかずのままなのです。
参照による値渡しと単なる値渡しを図解すると以下のとおりです。
くどいようですが、また例え話をすると、参照は貸し金庫の鍵のようなものです。つまり、私があなたに1トンの金塊を渡すとして、(重すぎる)金塊そのものを渡すのではなく、鍵を渡しているのです。ですから、もしも、私も鍵を持っているとしたら、ふたりともその金塊にアクセスできるというわけです。どちらか一方が金塊を削れば、金塊は小さくなります。インスタンスも巨大になる可能性があるため参照による値渡しです。
9.メソッドの戻り値に参照を使う
参照をメソッドの戻り値にすると複数のデータを一度に戻すことができます。メソッドの戻り値は一つのみでしたが、この方法を使うことで実質は複数の値を同時に返すことが可能になります。
以下のExample14はメソッドの戻り値に参照を使う例です。compare(比較)というメソッドでNewEngineer2のインスタンスを返すことで、結果としてidとnameという2つの値を返しています。
package chap09;
public class Example14 {
static NewEngineer2 compare(NewEngineer2 se1, NewEngineer2 se2) {
return se1.id > se2.id ? se1 : se2;
}
public static void main(String[] args) {
NewEngineer2 se1 = new NewEngineer2();
NewEngineer2 se2 = new NewEngineer2();
se1.id = 8;
se1.name = "shinohara";
se2.id = 2;
se2.name = "imai";
System.out.println("idの大きい方のSEを表示します。");
NewEngineer2 se3 = compare(se1, se2);
se3.show();
}
}
<実行結果>
idの大きい方のSEを表示します。 私のIDは8、名前はshinoharaです。 |
上記の例ではフィールドがidとnameの2つだけなので、メソッドで参照を返せることのメリットはあまり感じられなかったかもしれません。しかし、インスタンスにいろいろなフィールドを付与していけば、その恩恵を感じられることでしょう。
10.コンストラクタでインスタンスの初期化
英語で【constructor】には「建設者」という意味があります。コンストラクタとはインスタンスを作り出す特別なメソッドのことです。インスタンスが生成されるときに自動的に実行される特別なメソッドです。
コンストラクタを定義する基本構文は以下の通りです。
クラス名(引数列) {
命令文;
}
つまり、コンストラクタはクラスと同じ名前になります。
コンストラクタに戻り値はありません。(あえて言えばインスタンスが戻り値です)もしも、コンストラクタに戻り値の型を書いてしまうとそういう名前のメソッドだとコンパイラには解釈されてしまいますので気をつけてください。
コンストラクタには引数を渡せますので、インスタンスを初期化するのに使うことができます。
以下のNewEngineer3とExample15の2つのサンプルプログラムを見てください。インスタンス変数をコンストラクタが呼び出されるたびに異なる値で初期化できるようにしてます。中身が空っぽの新人エンジニアインスタンスを作れなくしたわけです。
package chap09;
public class NewEngineer3 {
int id;
String name;
public NewEngineer3(int id, String name) {
this.id = id;
this.name = name;
}
}
※なお、IDEを使えばコンストラクタの挿入も1クリックでできます。
package chap09;
public class Example15 {
public static void main(String[] args) {
NewEngineer3 se = new NewEngineer3(3, "tabuchi");
System.out.println(se.id + " : " + se.name);
}
}
実行結果の予想: |
このとき、
this.id = id;
のようにthis参照を使っていることに注目してください。
【this】=「この」ということで記述されている自分自身のインスタンスを指してます。this.を省略すると、ローカル変数や仮引数にも同じ名前の変数があった場合、ローカル変数が優先されてしまうという問題があります。つまり、フィールドとローカル変数の名前が同じ場合はローカル変数が優先されるというルールがあります。
このthisは、以下のように仮引数の名前を変えることで付けずに済ますことも可能です。
NewEngineer2(int a, String b) {
id = a;
name = b;
}
しかし、先のthisを付ける方が分かりやすいことはご理解いただけると思います。
なお、今までコンストラクタを作成することなくインスタンスを作成してきました。それは、Javaによってデフォルト・コンストラクタ【default constructor】というものが補われていたからなのです。
デフォルト・コンストラクタは、以下のような形をしています。
NewEngineer1() {
}
つまり、引数も命令文もない形です。
インスタンスをヒープ領域に作成するだけのコンストラクタです。デフォルトコンストラクタは明示的なコンストラクタがない場合のJavaの親切機能です。明示的にコンストラクターを定義すると作成されなくなります。
11.コンストラクタのオーバーロード
this(引数)と記述すると自分自身のコンストラクタを呼び出すことができます。コンストラクタでも、this(引数)を使ってメソッドのときに学んだオーバーロードを実現できます。ただし、この記述ができるのはコンストラクタの最初の1行目でなくてはいけないというルールがあります。
以下の2つのサンプルコードNewEngineer4とExample16を見てください
package chap09;
class NewEngineer4 {
int id;
String name;
NewEngineer4(int id, String name) {
this.id = id;
this.name = name;
}
NewEngineer4() {
this(0, "名無し");
}
}
package chap09;
public class Example16 {
public static void main(String[] args) {
NewEngineer4 se = new NewEngineer4();
System.out.println(se.id + " : " + se.name);
}
}
<実行結果>
0 : 名無し |
引数2つのコンストラクタと引数のないコンストラクタを用意して、後者の中で前者を呼び出しています。
オーバーロードとは、同じクラスの中でメソッド名と戻り値の型が同じで、引数の型や数、並び順が違うメソッドを複数定義することをいいました。コンストラクタもメソッド同様、呼び出し時に指定される引数によって実行されるコンストラクタが区別されるのです。また、コンストラクタから自分のメソッドを呼び出すこともできます。
show()メソッドの使われ方に着目して、以下のNewEngineer5を見てください。
package chap09;
class NewEngineer5 {
int id;
String name;
NewEngineer5(int id, String name) {
this.id = id;
this.name = name;
this.show();
}
void show() {
System.out.println("My id is " + id + ".\nMy name is " + name + ".");
}
}
<メインメソッドを持つクラス>
package chap09;
public class Example17 {
public static void main(String[] args) {
NewEngineer5 se = new NewEngineer5(4, "imai");
}
}
実行結果の予想: |
これまで見てきたとおり、Javaにはshow()メソッドのようにインスタンスごとに存在するメンバと、main()メソッドのようにクラスで唯一のメンバがありました。では、これらはどのように使い分けるのが良いのでしょうか?
いまからその点について学んでいきましょう。まずは、static変数からです。
12.インスタンス変数とstatic変数の使い分け
インスタンス変数は、個々の実体ごとに固有の情報を保持する目的で使用します。今までの例ではidやnameです。
static変数(クラス変数ともいう)は、クラス全体で共通の1つの情報を保持する目的で使用します。static変数の代表例を探せば、クラスMathのフィールドPI(標準API)があります。
Mathクラスの146行目を御覧下さい。
public static final double PI = 3.14159265358979323846;
その名の通り、円周率を表すフィールドです。円周率はいつ、誰が、どこで使っても同じでないと困りますね。それゆえ、static変数なのです。
詳しくPIの定義を見ます。publicはこのフィールドがクラス外に公開されていて他のどのクラスからも利用できることを示していました。【static】というキーワードがstatic変数であることの宣言になります。staticというのは英語で「静的な」という意味がありました。動的にヒープメモリにインスタンスを作らなくても良いということを表しています。
また、finalというのは定数ということでした。finalというのは英語で「最後」という意味ですから、これ以上変化しない、最後の値であるということを表現しているわけです。
以下のExample18はJavaがあらかじめ用意しているstatic変数Math.PIを使って円周率を表示させます。
package chap09;
public class Example18 {
public static void main(String[] args) {
System.out.println(Math.PI);
}
}
※なお、定数によってプログラムが読み解きやすくなっている点にも注目ください。「3.141592653589793」というリテラルをプログラマが円周率のつもりで使っても、プログラムを読む人には伝わりません。"PI"と書かれているからこそ「円周率なのだ」というメッセージを発信することができるのです。
<実行結果>
3.141592653589793 |
皆さんが作成するクラスでも、クラス共通の情報としたいもの、インスタンスごとの個別情報としたいものがあると思います。
例えば、車を例にとって考えてみましょう。車といっても抽象的な“車”ではなく、みなさんの家にあるような具体的な車です。例えば、ここではプリウスとしましょう。static変数はすべてのプリウスで共通にしたいデータです。static変数の例:ドアの数、ターボの有無、ガソリンタンクの大きさ、などでしょうか。対して、インスタンス変数は個々のプリウスで違うデータです。インスタンス変数の例:ナンバープレートの情報、現在のドライバー名、現在のガソリンの残量、など。このような例が思い浮かぶと思います。
例えば、今まで作成してきたNewEngineerクラスにおいてstatic変数にすべきものを考えてみましょう。新人エンジニアの総人数はどのインスタンスが保持すべきでしょうか?
人数の情報は個々のインスタンスが持つべき情報ではありませんね。static変数にした方がしっくりきます。
以下のExample19とNewEngineer6を見てください。
package chap09;
class NewEngineer6 {
static int count;
int id;
String name;
NewEngineer6(int id, String name) {
this.id = id;
this.name = name;
count++;
this.show();
}
void show() {
System.out.println(id + ":" + name + ":" + count + "人目です。");
}
}
package chap09;
public class Example19 {
public static void main(String[] args) {
new NewEngineer6(4, "imai");
new NewEngineer6(3, "shinohara");
new NewEngineer6(2, "tabuchi");
System.out.println("クラスの総人数:"+ NewEngineer6.count);
}
}
<実行結果>
4:imai:1人目です。 3:shinohara:2人目です。 2:tabuchi:3人目です。 クラスの総人数:3 |
このように個々のインスタンスに持たせることのできない変数はstatic変数にします。すべてのクラスで共有する変数なのでクラス変数と呼ばれることもあります。
しかし、static変数を使いすぎると様々なクラスから同一の変数にアクセスできてしまいます。同じstatic変数を2つのプログラムで使用していると、一方が知らないうちに他方が変更を加えているということが起こり得ます。
変数にはスコープ(有効範囲)という概念があります。スコープは狭い順から、ローカル変数、インスタンス変数、static変数です。そして、できるだけスコープは狭くすべきなのです。スコープを狭くすることで作成者が意図しない変数の使用ができないようにすべきです。
もう一度、3つのメモリ領域の図で確認します。
13.インスタンスメソッドとstaticメソッドの使い分け
次に、staticメソッドはどんな時に使ったらよいでしょうか?
結論から言うと個々のインスタンス変数の情報を使うメソッドはインスタンスメソッド、そうではない場合はstaticメソッドとして設計すると良いでしょう。
ここでもJavaの標準APIにその範を求めましょう。
Mathクラスのrandom()メソッド(標準API)を見てみましょう。このクラスの説明を読むと以下のようになっています。
0.0
以上で1.0
より小さい、正の符号の付いたdouble
値を返します。戻り値は、この範囲からの一様分布によって擬似乱数的に選択されます。
ゲームなどで有効そうなメソッドですね。
以下のExample20はstaticメソッドの例です。サイコロのように1~6の整数値を出力するプログラムです。
package chap09;
public class Example20 {
public static void main(String[] args) {
System.out.println((int)(Math.random() * 6) + 1);
}
}
<実行結果の例>
1 |
実際の結果は様々な乱数が表示されます。
この場合に、例えばMathのインスタンスを作ってそのインスタンスのrandom()メソッドを呼び出すというのはどうでしょうか?
少しまどろっこしい気もしますね。
Mathクラスではできませんが、別のクラスを使えば同じようなことができます。その名もRandom(標準API)という名前のクラスです。
説明文には、以下の記述もあります。
多くのアプリケーションの場合、
Math.random()
メソッドを使うほうが簡単です。
※ただし、Randomクラスにはboolean値や一定範囲のint値だけを生成するメソッドがあるため便利だったりします。
このRandomクラスを使ったサンプルも載せておきます。
以下のExample21はインスタンスメソッドを使った例で、同じく6面のサイコロのイメージです。
package chap09;
import java.util.Random;
public class Example21 {
public static void main(String[] args) {
Random r = new Random();
System.out.println(r.nextInt(6) + 1);
}
}
これはプログラミングというよりは設計の問題になりますが、乱数を発生させるためだけにインスタンスを生成して、その都度使い捨てるのは、メモリやCPU時間の無駄です。そこで、Javaの設計者はMath.randomをクラスから直接呼び出せるstaticメソッドにした訳です。
ちなみに、Mathクラスはその名の通り数学のためのクラスです。数学でよく使う様々な計算ツールが揃っています。このようなクラスを便利屋的なクラスという意味でユーティリティクラスと呼ぶことがあります。Mathクラス(標準API)で見てみましょう。
コンストラクタの定義が見当たりませんね。
こんどはJavaのソースコードを見てみましょう。方法は講師から説明を受けてください。クラスの定義のすぐ下(133行目)に以下のようなコンストラクタが見えますか?
private Math() {}
コンストラクタがprivateで宣言されています。privateはpublicの反対の意味で「非公開」ということですね。つまり、このコンストラクタは他のクラスからは呼び出せないようになっているのです。その意図は、インスタンスを作らせない純粋なユーティリティクラスであるという宣言です。Mathクラスはインスタンスを作れないクラスなのですね。
最後にメソッドの呼び出し方を下表9.2にまとめておきます。
同じクラス内に定義されているメソッド | メソッド名(実引数)※1 |
インスタンスに定義されているメソッド | 変数名.メソッド名(実引数) |
staticメソッド | クラス名.メソッド名(実引数)※2 ただし、変数名.メソッド名(実引数)と書いてもコンパイラが左記の通りに解釈する |
スーパークラスのメソッド | super.メソッド名(実引数)※3 次の継承を参照のこと |
※1に関してstaticメソッドからインスタンスメンバへのアクセスはできません。なぜなら、どのインスタンスの持っているメンバかを特定できないからです。
インスタンスメソッドからクラスメンバのアクセスはできます。なぜなら、staticの付いたメンバは同じクラスから作られる全インスタンスで共有されるものだからです。(下図参照)
ちなみに、お気づきの方も多いと思いますが、IDEでソースコードを書いている最中に
変数名.
とドッドを入れて少し待つと、いろいろなメンバがウィンドウに表示されるかと思います。
自分で作成したフィールドやメソッドはもちろんですが、中には作成した覚えのないequals()メソッドやtoString()メソッドもありますね。この点に関しては次回の継承で重要になってくる事実ですので、記憶にとどめておいてください。
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「インスタンスでデータと処理を再利用可能部品にする」方法について見てきました。
次回は、「継承を使ってクラスをグループ化する」です。
これも、オブジェクト指向特有のテーマです。スーパークラスを拡張してサブクラスを作る仕組みを解説します。
IT企業向け新人研修おすすめ資料 無料公開中 (saycon.co.jp)