抽象データ型

インスタンスが持つメソッド

前回の演習で使用した Rectangle クラスと、それを操作する RectangleAction はそれぞれ次のようなものであった。

package j2.lesson01;

public class Rectangle {
    
    // 高さ
    double height;
    
    // 底辺の長さ (幅)
    double width;
}
package j2.lesson01;

public class RectangleAction {

    // プログラム全体 (省略)

    // area-of(rect)
    public static double areaOf(Rectangle rect) {
        // return rect の底辺 * rect の高さ
        return rect.width * rect.height;
    }
}

Rectangle クラスと、それの面積を計算するメソッド areaOf(Rectangle) が別のクラスで定義されているが、実はわざわざ分割する必要はない。次のように書くことができる。

package j2.lesson02;

public class Rectangle {
    
    double height;
    
    double width;

    public static double areaOf(Rectangle rect) {
        return rect.width * rect.height;
    }
}

このようなクラスを使う場合、次のようなプログラムを書ける。

Rectangle rect = new Rectangle();
rect.height = 1.0;
rect.width = 2.0;
System.out.println("面積は" + Rectangle.areaOf(rect));

上記のように、クラス名 . メソッド(実引数) と書いてやることで、別のクラスのメソッドを呼び出すことが可能である。ただし、呼び出し先のクラスが自分自身である場合、クラス名を省略できる (今までのプログラムでは、呼び出し先のクラスが自分自身であったため、全て省略していた)。そう考えると、 Math.sqrt(double) というメソッドは、Math クラスの sqrt(double) というメソッドを呼び出しているだけである。

しかし、まだ上記のプログラムには無駄がある。Rectangle クラス内で定義されている areaOf(Rectangle) というメソッドは、明らかに Rectangle (つまり、その長方形自身) の面積を計算するためのメソッドである。

// Rectangle の area of rect
Rectangle.areaOf(rect)

上記のように、呼び出しの際に 長方形 の意味を含むものが2つ入ってきてしまっている。以下のように書けるのが理想であろうか。

Rectangle rect = new Rectangle();
rect.height = 1.0;
rect.width = 2.0;
// 長方形インスタンス rect の 面積
System.out.println("面積は" + rect.area());

上記のプログラムでは、Rectangle クラスに rect の面積を計算してもらうのではなく、rect 自身にその面積を計算してもらうという意味合いを持たせている。

Java では、上記のようにインスタンス自身にメソッドを持たせるということが可能である。これによってインスタンスフィールドと、それらを使用するメソッドをひとまとめにして宣言することができる。

package j2.lesson02;

public class Rectangle {
    
    double height;
    
    double width;

    // このインスタンスが持つ面積
    public double area() {
        return this.width * this.height;
    }
}

上記のようにメソッドを定義することによって、rect.area() というようにインスタンスが持つメソッドを呼び出すことができるようになる。

インスタンスメソッド

これまでのプログラムと、上記のプログラムを比較してみる。

メソッドの起動方法

まず、メソッドの起動方法が異なっている。

Rectangle rect = new Rectangle();
rect.height = 1.0;
rect.width = 2.0;
// これまでのプログラム
System.out.println("面積は" + Rectangle.areaOf(rect));
                              ~~~~~~~~~~~~~~~~~~~~~~
// 新しいプログラム
System.out.println("面積は" + rect.area());
                              ~~~~~~~~~~~

これまでのプログラムは クラス名 . メソッド(インスタンス) という形で呼び出していたのに対し、新しいプログラムでは インスタンス . メソッド() という形で呼び出している。

このドット演算子「.」は、今までどおり「~の~」と訳してやると読みやすい。新しいプログラムでは「rectの面積」と訳すことができる。

ここでは例を挙げなかったが、インスタンスからメソッドを起動する際にも、他のメソッド呼び出しと同様に引数をとることができる。

static の意味

メソッドの宣言部分を先頭から順に比較していくと、まず最初に目に付く点は、static をつけていないという点である。

// これまでのプログラム
public static double areaOf...
       ~~~~~~
// 新しいプログラム
public double area...

これまでメソッドにはおまじないのように static をつけていたが、static をつけたメソッドはクラスに属するメソッドであるということを表している。

つまり、宣言の public static double areaOf(Rectangle rect) という書き方は、「Rectangle クラスに areaOf(Rectangle) というメソッドを作る」という意味を持つ。そして、メソッドを起動する Rectangle.areaOf(rect) という書き方は、Rectangle クラスに areaOf というメソッドを起動してもらう という意味を持つ。

そして、static をつけていないメソッドはインスタンスに属するメソッドであるということを表している。

つまり、宣言の public double area() という書き方は、「Rectangle クラスから作られたインスタンスに area() というメソッドを作る」という意味を持つ。そして、メソッドを起動する rect.area() という書き方は、rect インスタンスに area というメソッドを起動してもらう という意味を持つ。

インスタンスが持つ (static をつけていない) メソッドを、インスタンスメソッドと呼ぶ。

this 変数

宣言部分で次に目に付く点は、仮引数がなくなっているという点である。

// これまでのプログラム
public static double areaOf(Rectangle rect) {
                           ~~~~~~~~~~~~~~~
// 新しいプログラム
public double area() {
                  ~~

これまでは面積を知りたければ Rectangleクラス (の areaOf メソッド) に長方形オブジェクトを渡してそれを計算してもらっていた。

// Rectangle に任せる
Rectangle.areaOf(rect);


しかしインスタンスメソッド area() を使えば長方形オブジェクトにその面積を直接聞けることになる。繰り返すが、インスタンスメソッドはインスタンスに属する。インスタンスメソッドが呼ばれたときそのメソッドの対象は (呼び出し先の) インスタンスとなるため、Rectangle.areaOf(Rectangle) と違い明示的に実引数として rect (面積を計算する対象) を渡す必要がないのである。

// Rectangle の様なクラス名は出てこない
rect.area();


では area メソッドの書き方をみていこう。static のついている areaOf メソッドでは引数として Rectangle クラスのオブジェクトを受け取っていたため、それを使えばよかった。

// これまでのプログラム
public static double areaOf(Rectangle rect) {
    return rect.width * rect.height;
           ~~~~         ~~~~

static のついていない area() の場合、areaOf での rect のように面積を計算する対象のオブジェクトを表す変数がないように見える。

確かに明示的には出てこないのだがthis という変数にメソッドを定義したクラスと同じ型 (上記の例では Rectangle 型) の、「自分自身」を表すインスタンスが格納されてるのでそれに対する面積を計算すればよい。

// 新しいプログラム
public double area() {
    return this.width * this.height;
           ~~~~         ~~~~

このプログラムを読む際には、this を「これ」と訳してやると日本語として意味が通りやすい。つまり、このメソッドの命令は「(これの幅 * これの高さ) を返す」と訳すことができる。

また、この this 変数は省略することができ、以下のようにも書ける。

public double area() {
    return width * height;
}

慣れないうちはローカル変数と混同しがちなので、明示的に this をつけてやるのもよい。

このように、あるインスタンスに対して何らかの操作を行いたい場合、メソッドをインスタンスメソッドとして作成してやると便利である。

コンストラクタ

ところで、長方形インスタンスを作成する際に、毎回次のように書くのは面倒であるし、読みやすくない。

Rectangle rect = new Rectangle();
rect.height = 1.0;
rect.width = 2.0;

new によって Rectangle 型のインスタンスの場所を確保するだけでなく、同時に底辺の長さと高さを設定できるような仕掛けがあると良い。

それがコンストラクタと呼ばれるものである (英語でconstructor は建設者、ここでは「新しいオブジェクトをつくるもの」)。コンストラクタはメソッドに似ているが、メソッドの名前と戻り値の型の両方をクラスの名前で表したような形である。ただし、コンストラクタには戻り値はなく、return 文もない (新しいオブジェクトを作り、その初期値を設定したものを返す)。これを使うと以下のプログラムのようになる。

package j2.lesson02;

public class Rectangle {
    
    double height;
    
    double width;
    
    // コンストラクタ
    public Rectangle(double h, double w) {
        this.height = h;
        this.width = w;
    }
    
    public double area() {
        return this.width * this.height;
    }
}
// 高さ1.0, 幅2.0 の長方形を作る
Rectangle rect = new Rectangle(1.0, 2.0);
Syste.out.println("面積は" + rect.area());

new Rectangle(1.0, 2.0) とやると、最初に Rectangle クラスのインスタンスを作成し、そのあとに Rectangle(double h, double w) というコンストラクタが実行される。h=1.0, w=2.0 なので、this.height, this.width にはそれぞれ 1.0, 2.0 が代入され、結果として、呼び出し元の rect には高さ 1.0, 幅 2.0 の長方形インスタンスが代入される。

コンストラクタの呼び出し方はメソッドの呼び出し方に似ている。new クラス名(実引数) とやることによって、対象のクラスのコンストラクタが 自動的に呼び出される

コンストラクタのオーバーロード

クラスには必ず1つ以上のコンストラクタがなくてはならない。2つ以上のコンストラクタを宣言したい場合は、コンストラクタのオーバーロード (同じ名前で違う仮引数) をすればよい。

下記のようにコンストラクタのオーバーロードを行うことができる。

package j2.lesson02;

public class Rectangle {
    
    double height;
    
    double width;
    
    // 高さ1.0, 幅1.0 の長方形を作るコンストラクタ
    public Rectangle() {
        this.height = 1.0;
        this.width = 1.0;
    }
    
    // コンストラクタ
    public Rectangle(double h, double w) {
        this.height = h;
        this.width = w;
    }
    
    public double area() {
        return this.width * this.height;
    }
}

この場合 new Rectangle() としてやることによって、高さ1.0, 幅1.0 の長方形インスタンスを作ることができる。これは、 new Rectangle(1.0, 1.0) と同じ意味を持つ。

暗黙のコンストラクタ

クラスには必ず1つ以上のコンストラクタがなくてはならない。と書いたが、今まではコンストラクタをひとつも宣言しなかった。

このように、コンストラクタを一つも作らない場合、仮引数なしの中身が空であるコンストラクタが暗黙にひとつ用意される。逆に、ひとつでもコンストラクタを用意した場合、暗黙のコンストラクタは用意されなくなる。

オブジェクト指向 (1)

メッセージ

Java はオブジェクト指向言語であるが、上記のメソッド呼び出しの形、たとえば、

rect.area()

がオブジェクト指向言語の特徴を表している。

オブジェクト指向とは、オブジェクト (特にクラスのインスタンス) 主体でプログラムを考えることである。

プログラムは、一般にデータとデータを処理するアルゴリズムからなる。アルゴリズムは実行文の列やそれをまとめた手続き (procedure: Javaではメソッド(method)と呼ばれる。C や Fortran では関数 (function)とかサブプログラムなどと呼ばれる) で表現される。

C やFortran などの従来の言語ではプログラムの主体は手続きであった。データはその手続きの対象物に過ぎなかった。したがって、rect というデータからその面積を得る手続き呼び出しの形は

area(rect)

であった。

オブジェクト指向言語ではデータとそのデータを扱うメソッドとをまとめたものをオブジェクトとする。オブジェクト指向の考えをより強調した言い方としては

rect.area()

を実行することを、次のような2段階で表現することができる。

抽象データ型

「自動車」という物体は現実世界にあるが、それを Java のオブジェクトとして考えてみる。

まず、自動車の持つ属性について考える。

と、上記のように無数の構成要素からなるが、自動車を操作する側から意識するものは、自動車の構成要素ではなく、自動車の振る舞いである。

自動車 (ここではATを考える) を操作する際に、操縦者が行う操作は主に以下のようなものがある。

これらの操作によって自動車は適切に振る舞い、さまざまな動作を行うことが可能になる。例えばアクセルペダルを踏むとエンジンの回転数が増大し、必要ならトランスミッションを変更してエンジンシャフトを回転させ、…といった具合で自動車が前進する。

しかし、アクセルペダルを踏む際に毎回そのようなことを考える必要はなく、アクセルペダルを踏むことによって「アクセラレートしろ」というメッセージを自動車に伝え、自動車がそのメッセージに対して適切に振る舞うことによって自動車が操縦される。

オブジェクト指向でプログラムを行う場合、データの内部表現ではなく、オブジェクトの振る舞いを中心に考えることが多い。上記の例では、操縦者は自動車の振る舞いを中心に自動車の操作を行い、自動車の内部表現を気にしていない。その操作の基点となるものが各種ペダルを踏んだり、ハンドルを回したりするなどの自動車に対する「メッセージ」であった。

メッセージを中心に考えることによって、Java で自動車クラス Car を作成することができる。この Car クラスに必要なものはメッセージを受け取るためのインスタンスメソッドであり、また、メッセージを受け取った際の振る舞いである。

package j2.lesson02;

public class Car {
    
    // 現在の速度 (km)
    double speed;
    
    // 現在の方向 (度)
    double direction;
    
    // アクセルペダルを踏む
    public void pressGasPedal() {
        // アクセルペダルを踏んだ際の振る舞い
        // 速度を 40.0km/h に設定する
        this.speed = 40.0;
    }
    
    // ブレーキペダルを踏む
    public void pressBreakPedal() {
        // ブレーキペダルを踏んだ際の振る舞い
        // 速度を 0.0km/h に設定する
        this.speed = 0.0;
    }
    
    // ハンドルを右に回す
    public void turnRightHandle() {
        // ハンドルを右に回した際の振る舞い
        // 方向を 90.0度傾ける
        this.direction += 90.0;
    }
    
    // ハンドルを左に回す
    public void turnLeftHandle() {
        // ハンドルを左に回した際の振る舞い
        // 方向を -90.0度傾ける
        this.direction -= 90.0;
    }

    // 速度メーターを確認する
    public double getSpeed() {
        // 現在のスピードを取得する
        return this.speed;
    }
}

上記の例では簡単に作成したが、現実の自動車により近づけるにはそれぞれの振る舞いを変更してやればよい。

このように、データ構造 (ここではオブジェクト) をその表現とそれを扱う手続きの集まりで表現したものを「抽象データ型 (Abstract Data Type, ADT)」と呼ぶ。

Java について考えてみると、「データ構造の表現」というのは「属性(→インスタンスフィールド)」のことであり、「データ構造を扱う手続き」というのは「振る舞い(→インスタンスメソッド)」のことである。

ADTでは、データの操作を主に振る舞いに任せ、内部のデータ構造の表現は使う側に意識させないのが普通である。

まとめ

今回の内容は、Java やその他のオブジェクト指向言語を習得する上で非常に重要なものなので、簡単にまとめておく。

インスタンスメソッド

それぞれのインスタンスが持つメソッド。それぞれのインスタンスの振る舞いを定義するために使われる。

例えば、「長方形の面積を計算する」メソッドは、それぞれの長方形インスタンスに「自分自身の面積を計算する」メソッドを持たせれば長方形インスタンスの面積を計算することができるようになる。

インスタンスメソッドの宣言

今までのメソッド宣言から static を取り除いて宣言する。

public 戻り値の型 メソッドの名前(仮引数リスト) {
    メソッド本体
}

例えば、戻り値が String, 名前が toString, 引数なしのインスタンスメソッドを宣言する場合、次のように書く。

public String toString() {
    return "文字列";
}

インスタンスメソッドの呼び出し

インスタンス . メソッド(実引数) の形で呼び出せる。

例えば、rect というインスタンス変数の area というメソッドを引数なしで呼び出す場合は以下のようにする。

rect.area()

this 変数

インスタンスメソッド内で、自分自身のインスタンスを使用するときに使われる。

static をつけたメソッド (クラスメソッド) 内では使用することができない。

コンストラクタ

new によって新しいインスタンスを作成するときに呼び出されるメソッド (のようなもの)。

次のように宣言できる。

public クラス名(仮引数リスト) {
    コンストラクタ本体
}

また、次のように new を使って新しいインスタンスを生成するときに自動的に呼び出される。

// Rectangleクラスの、引数(double, double)のコンストラクタが呼び出される
new Rectangle(2.0, 3.0);