[索引]
クラスの例 |
コンストラクタ |
インスタンス |
アクセス修飾子 |
final修飾子 |
staticメンバー |
thisキーワード |
クラスの配列 |
コラム
クラスはJavaの学習で最も重要な概念です。 Javaのプログラムはクラスで構成されています。 簡単なプログラムは1つのクラスで構成され、複雑なプログラムは、互いに関連した複数のクラスで構成されています。 クラスはユーザが独自に作れるデータ型です。 本章ではクラスの基本的なことがらについて説明します。
まずはクラスの例を示します。 クラスは変数とメソッドとから構成されています。 変数はフィールドと呼ばれることもあります。 変数とメソッドをまとめてメンバーと言います。 以下のPersonクラスはnameおよびageという変数と、それを処理するメソッド群からなっています。
class Person {
String name = ""; // 変数:氏名
int age = 0; // 変数:年齢
void setName(String n) { // メソッド:変数nameに氏名をセットする
name = n;
}
void setAge(int a) { // メソッド:変数ageに年齢をセットする
age = a;
}
String getName() { // メソッド:変数nameから氏名を取得する
return name;
}
int getAge() { // メソッド:変数ageから年齢を取得する
return age;
}
}
先頭のclassはこれからクラスを記述することを示すキーワードです。 次にクラス名を書きます。 クラス名はJavaの識別子の命名規則(1章)に従ってさえいれば、自由な名前を付けることができます。 1章で説明したように、クラス名は大文字で始めるのが慣用です。 行末に中括弧始め { を書き、次行からクラスの内容、すなわち変数とメソッドを書いていきます。 最後に行頭の中括弧終わり } で終了します。
Circleというクラスを作成してください。
Circleに半径の値を保存するint型の変数radiusと、「半径xxの円です」を表示するメソッドdisplayを装備させてください。
radiusのデフォルト値は 0 にしてください。
(ヒント) displayの戻り値の型はvoidです。
クラスにはそのクラスと同名のメソッドを書くことができます。 これをコンストラクタと言います。 コンストラクタはそのクラスの生成時に必ず呼び出され、クラスの変数の初期化などを行います。 コンストラクタには戻り値がありません。 したがって、戻り値の型も指定しません。 1つのクラスには引数の異なる複数のコンストラクタを定義できます。
以下の例では、3種のコンストラクタを定義しています。 クラスにコンストラクタが1つも定義されていないと、コンパイラが自動的に以下の「コンストラクタ(3)」のような、空(から)のコンストラクタを生成します。 これをデフォルトコンストラクタと言います。
class Person {
String name = "";
int age = 0;
Person(String n, int a) { // コンストラクタ(1)
name = n;
age = a;
}
Person(String n) { // コンストラクタ(2)
name = n;
}
Person() { // コンストラクタ(3)
}
}
デフォルトコンストラクタは何もしていないように見えますが、次章(7章)で述べるように、superというメソッドによって上位のクラスを呼び出し、クラス生成の際に必要な準備を整えるという重要な仕事を行っています。 これはデフォルトコンストラクタ以外のコンストラクタでも同じです。
それぞれのコンストラクタは次のようにして使われます。 「オーバーロード」(4章)のしくみにより、引き数の形態に対応するコンストラクタが呼ばれます。
Person("田中", 20) // コンストラクタ(1)が呼ばれる
Person("田中") // コンストラクタ(2)が呼ばれる
Person() // コンストラクタ(3)が呼ばれる
× Person(20) // 対応するコンストラクタが無いのでエラー
クラスは単なる鋳型(設計図)にすぎず、クラスを記述してもそれだけでは利用でません。 使えるようにするにはメモリ領域を確保して、そこに展開し、後で使えるように固有の名前を付ける必要があります。 この作業を一度で指示するのが、これまで何度か出てきたnew文です。 newの結果メモリ上に展開されるデータの実体をインスタンスまたはオブジェクトといいます。
インスタンスの生成は、実際には次のように行います。 まず以下の 01: のようにtanakaという変数のデータ型がPersonであることを宣言します。 次に 02: のようにnew文でメモリ領域を確保しデータを展開します。 newの後ろでは、Personのコンストラクタを呼んでます。 これは前項のコンストラクタ(1)を呼んでいることになります。
01: Person tanaka;
02: tanaka = new Person("田中",20);
上記 01: は宣言文であり、int ageなどの型宣言文と同じ構造になっています。 つまりPersonはintなどと同じようなデータ型の1種であるわけです。 Javaには基本データ型は8種類しかありませんでしたが、クラスを作ることで、いくらでもデータ型を定義することができます。
配列と同じように、01: と 02: をまとめて次のように1行で書くこともできます。
Person tanaka = new Person("田中",20);
インスタンスの生成後、そのメンバー(変数とメソッド)を指定するには、次のように、インスタンス名の後ろにピリオド( . )を付けて、その後にメンバー名を記述します。
tanaka.age; // 変数の呼び出し
tanaka.getName(); // メソッドの呼び出し
※ 従来のプログラムに慣れた方ですと、なぜ getName(tanaka) ではなくて tanaka.getName() にするのかと思われるかも知れません。 これはもちろんオブジェクトを強調するためですが、この両者に本質的な違いがあるわけではありません。 最初は違和感があるかもしれませんが、この書き方に慣れてください。
次の機能を実行するmainメソッドを含むクラスExを作ってください。
前述のようにクラスのメンバーは「インスタンス名.メンバー名」で他のクラスからアクセスすることができました。 しかし、次のようなアクセス修飾子をクラス定義の先頭に付加することで、アクセスの範囲を制限することができます。 アクセス修飾子の種類は以下の通りです。 前章で述べたようにpublic修飾子はクラス自身にも付けることができます。 protectedとprivateが付けられるのはクラスメンバーのみです。
※ 「protected(保護された)」の方が、(標準用法と考えられる)「修飾子無し」よりも、保護されていない(すなわちアクセスされる範囲が広い)のは奇妙な事実です。 これはprotectedが、オブジェクト指向言語の初期の段階から命名されていた用語であるのに対し、修飾子無しはJava言語の設計者が、後から作ったものだからです。
一般に変数にはprivate修飾子を付けて外部からの直接のアクセスを制限します。 そして変数の書き込みメソッドで値をチェックすることで、不正な値が入るのを防ぐことができます。 以下の例では氏名の長さや年齢の値を確認してから書き込んでいます。
class Person {
private String name = ""; // private修飾子付加
private int age = 0; // private修飾子付加
void setName(String n) {
name = n.substring(0,13); // 長さが13以上なら後部をカット
} // substring(a,b)は位置aから位置(b-1)
// までを切り出すメソッド
void setAge(int a) {
if(a<0) { a=0 }; // 値がマイナスなら0をセット
age = a;
}
}
このように変数とそれを処理するメソッドとをまとめて扱うことで、外部からの干渉や誤用を減らすことができます。 この考え方をカプセル化と言います。 カプセル化はオブジェクト指向の重要な概念の一つです。 うまくカプセル化をすると、外部の利用者は内部の構造を知らずに操作が可能であり、逆に内部の開発者は、操作(メソッド)の仕様さえ守れば内部を自由に作成、改変することができます。
前問(6.3)で作ったCircleクラスのradiusにprivate修飾子をつけてください。 そして半径をセットするコンストラクタを改造して、セット値が負なら 0 に修正して代入するようにしてください。
変数、メソッド、クラスにfinalと言う修飾子を付けることができます。 final修飾子は変更を許さないということです。 それぞれ次のような意味になります。
変数 :値を変更できない(つまり定数と同じ扱いになる)
メソッド:オーバーライドできない(オーバーライドは次章参照)
クラス :継承できない(継承は次章参照)
クラスを使用するにはnewしなければならないと言いましたが、例外があります。 それはメンバーにstaticと言う修飾子を付ける方法です。 staticを付加されたメンバーはnewしなくてもクラス定義が実行された時点でメモリ上に展開されます。 staticでないメンバーをインスタンスメンバー(インスタンス変数/インスタンスメソッド)と言います。 1つのクラスにstaticメンバーとインスタンスメンバーとを混在して記述することができます。
class Person {
static int total = 0; // static変数
private String name = "";
private int age = 0;
以下省略…
上の定義が実行されると、static変数totalが、メモリ上に展開されます。 その後Personをnewするとtotal以外の部分だけがメモリ上に展開されます。 すなわち、インスタンスが複数個newされてもtotalは1つだけになります。 前にも触れましたが、mainメソッドもstaticメンバーです。 mainメソッドの実行時には、誰もそれを含むクラスをnewしてくれないので、staticにしておく必要があるのです。
インスタンスメンバーは「インスタンス名.メンバー名」と呼び出しますが、staticメンバーは、インスタンスが生成されないので「クラス名.メンバー名」で呼び出します。
tanaka.age; // インスタンスメンバーはインスタンス名tanakaを使う
Person.total; // staticメンバーはクラス名Personを使う
クラスの中で自分自身を示したいときにはthisを使います。 例えばPersonクラスのsetNameメソッドで、引き数を n ではなくフルネームでnameと書くと、クラスの変数であるnameと名前が衝突してしまいます。 このようなときは、次のようにthisを付けて書くとクラスの変数のnameであることを明示できます。
String name = ""; // (1)このnameはクラスの変数
void setName(String name) { // (2)このnameは引き数
this.name = name; // this.nameは(1)のname、右辺のnameは(2)のname
}
コンストラクタ本体の先頭にthis(引き数)と書くと、引き数の型と数に対応した同じクラスの対応するシグニチャのコンストラクタが呼び出されます。 例えばコンストラクタの項で示した例をthisを使って書くと次のようになります。
Person(String n, int a) {
this(n); // こう書くとすぐ下のコンストラクタが
// 呼ばれ name = n; が実行される
age = a;
}
Person(String n) {
name = n;
}
1つのクラスから生成したインスタンスを配列にして扱うことができます。 例えばPersonクラスから複数のインスタンスを生成したときは、配列にして管理すると便利です。 この配列は次のようにして生成します。
Person[] p = new Person[3];
p[0] = new Person("田中", 27);
p[1] = new Person("山田", 20);
p[2] = new Person("佐藤", 34);
このように配列の生成と、Personクラスの生成のそれぞれにnewを使うところがポイントです。 ちょっと複雑ですが、クラスの配列は初心者が最初につまずくところです。 上の例を良くみて理解してください。 クラスを配列にすると、次のようにして容易に全員の氏名を出力することができます。
for (int i=0; i<p.length; i++) {
System.out.println(p[i].getName());
}
出力結果 田中
山田
佐藤
※ 配列は繰り返しデータを定義できますが、基本データ型の配列ではint型なら全部int型となり、同じ要素しか扱えません。 一方COBOLのOCCURS句では異なった型のデータをまとめて、それを繰り返すことができました。 クラスの配列を使うと、このように均一でない構造を持ったデータの繰り返しが可能になります。
以前(6.3)に作ったCircleクラスを生成するmainメソッドを改造して、半径が 1、2、…、10 の円を生成し、displayメソッドですべての円の様子を表示するようにしてください。 配列を使って作ってください。
以下の文で正しいものを全部選択してください。
(1) 1つのクラスから複数のインスタンスを生成することができる。 (2) protected修飾子は同一パッケージのメソッドのみからアクセス可能である。 (3) コンストラクタはシグニチャが異なればいくつでも定義できる。 (4) クラスには必ず変数とメソッドがなければならない。 (5) staticメンバーはnewしなくてもメモリに展開される。
オブジェクト指向言語の初心者は、次のようなプログラムを書いてしまうことがよくあります。
public class Sample {
public static void main(String[] args) {
open(args[0]); // 前処理
proc(); // 主処理
close(); // 後処理
}
void open (String[] s) { … }
…以下略
しかしこれでは、openメソッドがメモリ上に展開されていないので、コンパイルエラーになってしまいます。 これを避けるには、openメソッドにもstatic修飾子を付けてstaticメソッドにすればよいのですが、こうすると、procやcloseにも全部staticを付けることになり、オブジェクト指向プログラムらしからぬものになってしまいます。
このような場合には、自分(Sampleクラス)自身をnewしてインスタンス化します。 すなわち、次のようにするのです。 こうするとオブジェクト指向プログラムっぽくなります。
public class Sample {
public static void main(String[] args) {
Sample sp = new Sample(); // Sampleクラスをnewして、インスタンスspを生成
sp.open(args[0]); // 前処理
sp.proc(); // 主処理
sp.close(); // 後処理
}
void open (String[] s) { … }
…以下略
自分自身をnewすると、恐ろしい無限地獄に陥りそうな気がしますが、そんなことはありません。 それはnewしているのがstaticメソッドの中であり、staticメソッドはそれを含むクラスをnewしてもその中には含まれないからです。