オブジェクト指向の定義は、専門家の間でも微妙に食い違っているが、 「ソフトウェアで扱う事柄について、データと操作(メソッド)をまとめて1つのオブジェクトとして捉える」 ということは共通している。オブジェクト指向ではオブジェクトを基本単位として考え、 1つのオブジェクトの中には、データとメソッドが備わっているとする。
図: オブジェクトはフィールド(データ)とメソッドを一つにまとめたもの
データはプログラム中で扱う値であり、いわゆる変数である。 Javaではオブジェクトが持つデータのことをフィールド(field)と呼ぶ。 フィールドとして定義できるものは、今まで変数として使ってきたものと同じである。 つまり、int, float, double などの原始型、String型や配列などの参照型のフィールドが 定義できる。
フィールドは、オブジェクトの性質(特性)を持つという意味で、属性(attribute, property)とも呼ばれる。
前回習ったメソッドである。ただし、前回のメソッドには先頭に"static"が付いていたが、 オブジェクトに対して使う場合は、staticを付けない。詳細は次回の演習で述べる。
メソッドの目的はオブジェクト内のフィールド(データ)を操作することと言える。 また見方を変えると、メソッドはオブジェクトに対するメッセージと言える。 例えば、友人に「マンガを貸して」と言う(メッセージを送る)と、マンガを貸してくれる (貸してくれないかもしれないが)ように、例えば、あるオブジェクトに対して、 rent(貸してくれ)というメッセージを送ると、受け取ったオブジェクトの中のrentメソッド が実行され、何かを貸してくれるという動作をするようにプログラムを書くことができる。
図:メッセージを送ると対応するメソッドが実行される
メッセージを送ると返事が返ってくる。返事はメソッドの返り値である。 つまり、int型の返り値なら整数値の返事が返ってくる。void型なら、 「返事なし」である。
メソッドはオブジェクトに挙動を与えるものであるという意味合いから、 メソッドの役割のことを振る舞い(behavior)ということがある。
多くのオブジェクト指向言語では、クラスという仕組みがある。 クラスには、オブジェクトを作る際のデータやメソッドを定義してあり、 いわばオブジェクトの設計図である。 オブジェクトを作る時には、必ずクラス(型)を指定する必要がある。 指定したクラスに定義されたデータとメソッドがオブジェクトに備わるのである。
あるクラスのオブジェクトを作る場合に、そのオブジェクトのことをインスタンス(実例)という場合もある。 本によってはインスタンスという用語を使っているものもあるがオブジェクトと同じ意味だと考えて差し支えない。
クラスは以下のように定義する。
class クラス名 { フィールド(データ)の定義 フィールド(データ)の定義 ... メソッドの定義 メソッドの定義 ... }
以前のメソッドの演習では、上記のメソッドの定義のみがあって、フィールドの定義が 1つもない状態であった。今まで演習で作ってきたプログラムも全てクラスを作っていたということである。
ここで実例を挙げる
class VideoTape { public int position=0; // 現在の位置 public void forward(int t) { // tだけ先に早送りする position += t; } public void back(int t) { // tだけ巻き戻す position -= t; } }
これは、ビデオテープのクラスである。フィールド(データ)としてpositionがあり、 これはテープが現在、先頭からどの位置に巻かれているかを示す。 position=0とあるので、最初は先頭の位置である。 メソッドは2つある、forwardとbackである。forwardは引数mの時間だけテープを早送りする。 backは引数mの時間だけテープを巻き戻す。
次にこのVideoTapeクラスを使ってみよう。
/* VideoExample.java - ビデオテープクラスの使用例 */ class VideoTape { public int position=0; // 現在の位置 public void forward(int t) { // tだけ先に早送りする position += t; } public void back(int t) { // tだけ巻き戻す position -= t; } } public class VideoExample { public static void main(String[] args) { VideoTape vt = new VideoTape(); System.out.println("最初の位置" + vt.position + "秒"); vt.forward(600); // 600秒進める System.out.println("進めた後の位置" + vt.position + "秒"); vt.back(150); // 150秒戻す System.out.println("巻き戻した後の位置" + vt.position + "秒"); } }
上のプログラムには、VideoTapeとVideoExampleという2つのクラスが定義されている。 VideoExampleは、今まで習ったものと同様でmainメソッドが書かれている。 ちなみにこのような場合、javaファイルの名前は、mainメソッドがあるクラスの名前にする。 上の場合は、VideoExample.javaである。
クラスやオブジェクトが持つフィールドとメソッドのことをメンバという。 今後メンバという用語が出てきたら、フィールドあるいはメソッドのことだと理解してほしい。
Javaでは、慣習として、クラス名は頭文字を大文字に、 メンバ名(フィールド名とメソッド名)は頭文字を小文字にする(ただし、定数は大文字)。また、変数名やメソッドの引数の名前も頭文字を小文字にする。
例えば、上記の例でいうと、VideoTapeクラス、forwardメソッド、positionフィールド、forwardメソッドの引数tなどである。
ただし、これはあくまで慣習であって規則ではない。 したがって、守らなくてもエラーが出るようなことはない。 しかし、一般のJavaプログラムはこの慣習を守っているため、 混乱を避けるためにも慣習に従うべきである。
それでは、上記のmainメソッドの中を見ていこう。
まず最初にVideoTapeクラスのオブジェクトを作っている(VideoTape vt = new VideoTape();)。
これは正確に言うと、new VideoTape() でVideoTape型のオブジェクトを生成し、それをvtに代入している。
※ オブジェクトの生成については、参照と配列を見て復習せよ。new演算子を使って指定したクラスに基づいたオブジェクトを作るのである。
次におなじみのprintlnメソッドを使って vt.position を表示している。 vt.positionはvtというオブジェクトが持つ、positionフィールド(データ) を意味する。このように、オブジェクトが持っているフィールドにアクセスするには、 オブジェクト名.フィールド名と書く。間に.を打つわけである。
次の文では、forwardメソッドを呼び出している(vt.forward(600);)。 このようにオブジェクトが持っているメソッドを呼び出すには、 オブジェクト名.メソッド名(引数...)と書く。 (引数...)のところがあるのがフィールドとの違いである。 つまり引数のないメソッドを呼び出す場合は、オブジェクト名.メソッド名()となる。
vt.forward(600);でvtというオブジェクト、つまりVideoTape型の オブジェクトのforwardメソッドが呼ばれる。 VideoTapeクラスで定義されているforwardメソッドが呼ばれるわけである。 forwardメソッドには、position += t;とかかれている。 これで引数として渡した600(秒)がpositionに足されるのである。
mainメソッドからpositionにアクセスするのに、vt.positionと書いたのに、forwardメソッド内では、 単にpositionと書いていることに注目してほしい。そのクラスに定義されているフィールドや メソッドを使う時にはオブジェクト名を書かなくて良い。この場合だと、forwardメソッドと positionフィールドは同じクラス(VideoTape)に属しているからである。対して、自分以外の オブジェクトに属しているメソッドやフィールドを使う場合は、オブジェクト名を書く必要がある。
次の、printlnメソッドで、再び vt.position を表示している。すると、先ほど forwardメソッドが 呼ばれて600秒が加算されたので、vt.positionが600になっていることが分かる。
次に、backメソッドを呼び出している(vt.back(150);)。 もちろん、理屈は上記のforwardメソッドと同様である。backメソッドでは、 position -= t;が実行されるので、引数の150(秒)が positionから引かれる。次のprintln文では、positionの値、つまり450(秒)が表示されるわけである。
ところで、以上のプログラムは別にメソッドを呼び出さなくてもテープの位置を変えられる。 forwardやbackメソッドを呼ばすに、例えば vt.position = vt.position + 600; というふうに直接テープの位置に手を加えれば良い。 しかし、このようなことは望ましくない。
例えば、ビデオテープには、有限の長さがある。120分テープで200分の位置まで進めることはできない。 また、-100分の位置もありえない。しかし、直接positionの値を変えられるようだと、そのようなことが 可能になってしまう。
このようにテープの長さの制限をうまく反映させるには、直接フィールドに手を加えるのではなく、テープの位置を変えるメソッドだけを呼び出すようにすれば良い。 例えば、上記のforwardメソッドを以下のように書く。
public void forward(int t) { if (position+t > 120*60) { position = 120*60; } else { position += t; } }
こうすれば、テープの終わりよりもさらに早送りしようとしてもテープの最後(120分=120×60秒) で止まるようにできる。ただし、ルールを破ってmainメソッドなどから vt.position = 10000; とされれば台無しである。このような場合に、vt.positionを直接触れないようにすることができる。
class VideoTape { private int position=0; public void forward(int t) { 〜〜(途中省略)〜〜 }
このようにprivateにすることで、定義されているクラスの中からしか使えないようにすることが可能である。 つまり、この場合は、VideoTapeクラスからでのみpositionを使うことができ、それ以外、例えばmainメソッドが あるVideoExampleクラスからはpositionを使うことができなくなる。しかし、これでは現在のテープの位置を mainメソッドなどから知ることができなくなる。この場合、現在の位置を知るメソッドをVideoTapeクラスに作れば良い。
/* VideoExample2.java - ビデオテープクラスの使用例2 */ class VideoTape { private int position=0; // 現在の位置 public int get_position() { // 現在の位置を得る return position; } public void forward(int t) { // tだけ先に早送りする position += t; } public void back(int t) { // tだけ巻き戻す position -= t; } } public class VideoExample2 { public static void main(String[] args) { VideoTape vt = new VideoTape(); System.out.println("最初の位置" + vt.get_position() + "秒"); vt.forward(600); // 600秒進める System.out.println("進めた後の位置" + vt.get_position() + "秒"); vt.back(150); // 150秒戻す System.out.println("巻き戻した後の位置" + vt.get_position() + "秒"); } }
上記の場合には、VideoTapeクラスにget_positionメソッドを追加して、 mainメソッドからは vt.get_position() としてテープの現在の位置を得ている。 position は、privateとして宣言したために、mainメソッドから vt.position としてテープの位置を得ることはできなくなった。
このようにメンバ(フィールドとメソッド)には、外部から使える(見える)、使えない(見えない) という制御が可能であり、定義の頭に"private"や"public"といったアクセス修飾子をつけることで行われる。
オブジェクト指向では、オブジェクトの中身で外に見せるべきでないところを隠し、 見せるべきところだけを公開する方法を取る。 これをカプセル化といい、見せるべきでないところを隠すことを情報隠蔽という。 これらを記述するのがアクセス修飾子である。
アクセス修飾子には、他に"protected"と修飾子なし(何も書かない)があるが、 これらを使う場合は、継承やパッケージの概念を学ぶ必要があるため、 現状では、"private"と"public"の2種類を使うこととする。
以上で、オブジェクトがフィールド(データ)を持つことを説明した。 今までの説明では、フィールドとしてintなどの原始型を定義した。 それ以外にも、フィールドとして配列やオブジェクト(への参照)を定義することができる。 つまり、オブジェクトが他のオブジェクトを持つことができる。
例えば、以下のプログラムを見よ。
class Tyre { // タイヤクラス double size; // 大きさ 〜〜〜 } class Engine { // エンジンクラス double displacement; // 排気量 int cylinders; // 気筒数 〜〜〜 } class Car { // 自動車クラス Tyre fl, fr, rl, rr; // タイヤ Engine eg; // エンジン public void run() { // 走行せよ 〜〜〜 } }
Carクラスには、フィールドとしてTyreオブジェクト(への参照)4つとEngineオブジェクト(への参照)1つが定義されている。 つまり、CarオブジェクトはTyreオブジェクト4つとEngineオブジェクト1つを持つことになる。 これで「自動車が4つのタイヤと1つのエンジンを持っている」ということを表現できるのである。
このようにフィールドにオブジェクトを定義することで、「ある物はいくつかの部品が組み合わさったものである」「全体は部分が集まったものである」という実世界であたりまえのことがうまく表現できる。 このような関係を has-a 関係という。
オブジェクト指向が一番活躍できる場は、大規模なソフトウェア開発である。 オブジェクトとして物事をまとめ、適切に外部に見せるもの見せないものを決めると、 巨大なソフトウェアの一部分を作る際に、 知らないといけないことを必要最小限にすることができる。 特にJavaにはクラスライブラリと呼ばれる、 適切に設計されたプログラムが最初から豊富に用意されており、 それらを利用することで大規模なソフトウェアを開発しやすくなっている。
class Point { double x=0, y=0; }このPointクラスに、点の位置を移動するmoveメソッドを定義せよ。 移動量はmoveメソッドに与える引数で指定する。 例えば、以下の場合、p.move(20, -30)で、点pの位置をx方向に20、y方向に-30移動させる。 つまり、moveを呼び出す前の表示が、x=100 y=50、呼び出した後の表示が、x=120 y=30 となれば良い。
public class PointTest { public static void main(String[] args) { Point p = new Point(); p.x = 100; p.y = 60; System.out.println("x=" + p.x + " y=" + p.y); p.move(20, -30); System.out.println("x=" + p.x + " y=" + p.y); } }
class Line { 〜〜〜 Line() { p1 = new Point(); p2 = new Point(); } 〜〜〜 }