コンピュータ基礎および演習II

講義資料

オブジェクト指向プログラミング(1) - クラスを用いたプログラム

前回はプログラムで行う仕事を、 メソッドと呼ばれる処理単位にまとめておき、 必要なときにメソッドを実行する方法を学びました。 しかし、規模の大きなプログラムや複雑な手順の処理を行うプログラムでは、 メソッドによる機能の分割だけでは不十分です。

Java をはじめとする 「オブジェクト指向 (Object Oriented) 」のプログラミング言語では、 関連性の強い複数のメソッドや、 複数のメソッド間で共有したいデータを ひとまとめにする仕組みがあります。 これを「クラス (class) 」と呼びます。

クラスを用いたプログラム

今回は、オブジェクト指向プログラミング言語の機能のうち 「クラス」について学びます。 まず、これまで学んだクラスを使わずにメソッドのみを使った プログラムの例を示しながら、 プログラムの問題点とクラスのメリットを説明していきます。

例題1: オブジェクト指向以前のプログラム

住所録を管理する簡単なプログラムについて考えてみます。 このプログラムは、名前と住所の 2 つの情報を扱うものです。 (ファイル名: AddressNoteNotOO.java)

class AddressNoteNotOO {
    static void printAddress(String address) {
	System.out.println("住所: " + address);
    }

    static void printName(String name) {
	System.out.println("名前: " + name);
    }

    public static void main(String[] args) {
	String address;
	String name;

	address = "東京都千代田区神田錦町2-2";
	name = "電大 メディ男";

	printAddress(address);
	printName(name);
    }
}

main メソッドの 2 つの変数 addressname が、 住所と名前の情報を入れておくための変数であり、 printAddressprintName が それを表示するためのメソッドです。

例題程度の規模のプログラムではそれほど問題になりませんが、 大規模なプログラムでは、 このプログラムのような方法では次のようなことが問題になります。

例題2: 関連する複数の変数をまとめる

クラスを使うと、 複数の変数からなるプログラムの中のデータ処理や計算の対象を 一つにまとめておくことができます。 (ファイル名: AddressNoteWeakOO.java)

class Person {
    String address;
    String name;
}
    
class AddressNoteWeakOO {
    static void printAddress(String address) {
	System.out.println("住所: " + address);
    }

    static void printName(String name) {
	System.out.println("名前: " + name);
    }

    public static void main(String[] args) {
	Person medio;
	medio = new Person();

	medio.address = "東京都千代田区神田錦町2-2";
	medio.name = "電大 メディ男";

	printAddress(medio.address);
	printName(medio.name);
    }
}

このプログラムの実行するには、コマンドプロンプトから、

$ java AddressNoteWeakOO

と入力します。 プログラムはコマンド java に続けて指定された クラス AddressNoteWeakOOpublic static void main の部分から実行が始まります。

プログラムの最初に新たに追加したものが、 一人の住所データをひとまとめにして扱うためのクラス定義です。 この例題では、addressname の 2 つの変数からなる ひとまとまりのデータに Person というクラス名をつけています。

クラスの中で宣言されている変数を、 「(そのクラスの) 属性 (attribute) 」、 または「フィールド (field)」、 「インスタンス変数 (instance variable) 」 などと呼びます。 属性は、これまでの変数宣言と同様の形式で書くことができます。 (他の言語 (C++ など) では属性のことをメンバ変数と呼ぶこともあります。)

これにより main メソッドでは、 次のようにしてクラス Person のデータを使うことができます。

Person medio;

まず上の変数宣言では、クラス Person のデータを 変数 medio で使うことができるようにしています。 このようにクラスは、 int や double のような型の一種として使うことができます。

次に進みましょう。

medio = new Person();

new は指定されたクラスから、 実際にデータを入れたり処理を行ったりできる領域を 新たに作り出すための演算子です。 ここで作られた領域を 「(そのクラスの) インスタンス (instance)」、 または「オブジェクト (object)」 と呼びます。

new 演算子のあとに、 インスタンスを生成したいクラス名と続けてカッコを書きます。 カッコの中にはインスタンスの初期化のための情報を書くこともできます。 これ以降 medio という変数によってクラス Person の インスタンスを使うことができます。

なお、上の 2 行を 1 行にまとめて Person medio = new Person(); のように書くこともできます。

次に進みましょう。

medio.address = "東京都千代田区神田錦町2-2";
medio.name = "電大 メディ男";
printAddress(medio.address);
printName(medio.name);

インスタンスの属性を使うためには、 インスタンスの変数名に続けてピリオド (.) を書き、 属性名を書きます。属性は通常の変数と同じ様に使うことができます。

今回のクラスを作るのにあたり注目したのは、 一人の住所データを一つのクラスで表そうということです。 このように、プログラムの中の処理の対象物をクラスとし、 対象物の複数のデータを属性としてまとめておくことにより、 プログラムの中でのデータの構造をわかりやすく表すことができます。

例題3: データとそのデータに関する処理をまとめる

前の例題では以下のような medio に対するデータの操作を行っていました。 2 つの代入は、medio に対して各属性の値を登録する処理、 次の 2 つのメソッドの実行は各属性の値を表示する処理です。

medio.address = "東京都千代田区神田錦町2-2";
medio.name = "電大 メディ男";
printAddress(medio.address);
printName(medio.name);

これらの処理はクラス Person の内部データに関する処理であることから、 直接 main メソッドに書くのではなく クラス Person の定義の中に併せて書くことができます。 すなわち、属性の値を登録するメソッド (setAddress, setName) と、 属性の値を表示するメソッド (printAddress, printName) をクラス Person の定義の中にまとめておけば良いのです。 (ファイル名: AddressNote.java)

class Person {
    String address;
    String name;

    void printAddress() {
	System.out.println("住所: " + address);
    }

    void printName() {
	System.out.println("名前: " + name);
    }

    void setAddress(String a) {
	address = a;
    }

    void setName(String n) {
	name = n;
    }
}
    
class AddressNote {
    public static void main(String[] args) {
	Person medio = new Person();

	medio.setAddress("東京都千代田区神田錦町2-2");
	medio.setName("電大 メディ男");

	medio.printAddress();
	medio.printName();
    }
}

前の例題と良く見比べてください。

クラス Person に新たに追加したのは、 メソッド setAddress, setName, printAddress, printName です。 前の例題では、クラス Person に関するデータ処理が、 main メソッドに存在していたわけですが、 これをクラス Person の中に持ってきたというわけです。 これで、データとそのデータに対する処理を クラス Person の中に整理することができました。

一方 main メソッドでは、 メソッド setAddress, setName, printAddress, printName を実行しているだけです。 インスタンスに対してメソッドを実行するためには、 属性へのアクセスと同様に、インスタンスの変数名に続けてピリオド (.) を書き、 メソッド名を書きます。 ここでは、 インスタンスである medio に対しての 操作を行うメソッドを実行しているというわけです。

このようなメソッドの実行形態は、 あるプログラムの中での処理対象 (インスタンス) に対して指令 (メッセージ) を送っていると考えると良いでしょう。 例えば、インスタンス medio に対し、 「名前を登録せよ (setName) 」や、 「住所を表示せよ (printAddress) 」 というメッセージを送っているという具合いです。

さて、 以前は変数 addressname への代入で medio の属性に値を登録していた部分を、 メソッド setAddress, setName の実行に書き換えました。 このように全てメソッドを介してインスタンスへのアクセスをすることで、 クラス Person の内部状態に依らず目的の仕事を行うことができます。 つまり、クラスを使う側から見ると、 メソッドの実行方法だけを知っていれば、 あとはクラスが所定の仕事をやってくれると信頼して任せてしまえば良いのです。 クラスの内部状態やデータの構造などは関知する必要はありません。

一方クラス内では、クラス内の属性へアクセスが 必ずメソッドを介して行うようにすれば、 メソッドの中で値のチェックをしたり、 アクセス制限を行うといった属性の値の管理が可能になります。

ところで、メソッドを Person の中に書く際に、 キーワード static をつけませんでした。 インスタンスを生成してから利用するメソッドは、 static をつけて宣言してはいけません。

この例題を通して、 オブジェクト指向プログラミングにおけるクラスを用いた プログラムの考え方が理解できたはずです。 これらをまとめると次のようになります。

例題4: 複数のデータを簡単に扱う

最初に示した例題のプログラムでは、 複数の人の住所データを扱うのは難しいという問題点がありました。

クラスは「型」の一種であり、 クラスからインスタンスを生成しましたね。 int 型の変数がいくつでも宣言できるのと同様に、 一つのクラスからいくつでもインスタンスを生成し利用することができるのです。 (ファイル名: AddressNote2.java)

class Person {
    String address;
    String name;

    void printAddress() {
	System.out.println("住所: " + address);
    }

    void printName() {
	System.out.println("名前: " + name);
    }

    void setAddress(String a) {
	address = a;
    }

    void setName(String n) {
	name = n;
    }
}
    
class AddressNote2 {
    public static void main(String[] args) {
	Person medio = new Person();
	Person denko = new Person();

	medio.setAddress("東京都千代田区内神田1-14-8");
	medio.setName("電大 メディ男");
	denko.setAddress("東京都千代田区神田錦町2-2");
	denko.setName("電大 デン子");

	medio.printAddress();
	medio.printName();
	denko.printAddress();
	denko.printName();
    }
}

このプログラムの前半部分、クラス Person の定義は前の例題と同様です。

main メソッドでは、mediodenko の 2 つのインスタンスを宣言・生成しています。 一旦インスタンスを生成すると、 それぞれのインスタンスは独立したデータとして振る舞います。 すなわち medio の属性やメソッドが denko のデータに影響を与えることや、その逆はないのです。

このように、プログラムの中に登場する処理の対象の性質を一旦クラスとして定義してしまえば、 具体的な対象物ごとにインスタンスをいくつでも生成できるのです。

オブジェクト指向の概念

Java は「オブジェクト指向 (Object Oriented) 」のプログラミング言語です。 オブジェクト (Object) とは日本語で「モノ」のことです。 オブジェクト指向の概念においては すべてがモノを中心とした考え方をします。 オブジェクト指向概念について詳しく考えてみましょう。

オブジェクト

「オブジェクト」はオブジェクト指向を理解する上で重要な概念です。

現実世界では、犬、机、パソコン、自転車など身の回りにオブジェクトの例を 見ることができます。 このような現実世界のオブジェクトには、 すべて「状態」と「振る舞い」を持つという特徴を備えています。 例えば、犬には状態 (名前、毛色、種類、空腹) があり、 振る舞い (吠える、歩く、お手をする、ひっかく) があります。

プログラムの世界では、オブジェクトは変数で状態を持ち、 メソッドと呼ばれるオブジェクトに関する処理をひとまとめにした処理単位で 振る舞いを実現します。

定義: オブジェクトは、変数とメソッドをまとめたプログラムの部分単位である。

プログラムの世界でオブジェクトを用いると、 プログラムの中で扱う様々な対象物をプログラムの中に書くことができます。 住所録の個人データを表すオブジェクトや、 RPGにおける登場人物やアイテムを表すオブジェクトなどです。 また、オブジェクトは画面上のアイコン、ボタンやウィンドウなど GUI (Graphical User Interface) の表示部品を表すためにも使われます。 さらに、抽象的な概念を表すのにも使われます。 マウスのボタンが押されたという行為、 プログラムでエラーが起ったという事態、などです。

ここでは携帯電話の例を用いてオブジェクト指向の考え方を説明しましょう。 エッセンシャル Java pp.118-119 を参照してください。

どのようなものがオブジェクトになるかを見出し、 その特徴を分析することは重要な問題です。 ここでは、オブジェクトになるための 2 つの特性について説明します。

1. 属性

オブジェクトは、固有の姿、形、性質などを持っています。 このようなオブジェクトの状態は、属性として表現されます。

物理的なオブジェクトの属性としては、大きさ、色、重さなどがあります。 携帯電話をオブジェクトとして考えると、機種名、色、電話会社、 電話番号、アドレス帳などが属性です。

これらの属性の値がそのオブジェクトのアイデンティティとなるわけです。 オブジェクトは属性の値によって 他のオブジェクトに対して明確に区別することができます。 オブジェクトは固有の属性値を持つ唯一の存在であると 考えることができます。 このような「固有である」という性質も オブジェクトの重要な特性の一つなのです。

概念的なオブジェクトについて考えてみましょう。 マウスを操作したという行為については、 マウスの位置、押されたボタン、クリックかダブルクリックかドラッグかの別 という属性が考えられます。 また、エラーという事態については、 エラーの原因、エラーの起こったプログラム上の場所などの属性が考えられます。

Java では属性を変数として表します。

2. 操作

各オブジェクトは固有の振る舞いを持ちます。 これを操作 (operation) と呼びます。 操作は一般的に動詞で表現されます。

携帯電話というオブジェクトの振る舞いとして、 電話に出る、電話をかける、メイルを送信する、 アドレス帳を検索するといった操作を挙げることができます。

概念的なオブジェクトの場合は、 例えば、会議をオブジェクトとすると、 開催場所を設定する、開催日時を設定する、議題を設定する といった操作を考えることができます。

Java では操作をメソッドとして表します。 メソッドとはプログラムの一部分をひとまとまりにして、 ある機能を実現するように一つの単位としてまとめたものです。 (メソッドのことをまれにメンバ関数と呼ぶこともありますが、 これは C++ 等の別の言語での呼び方)

メソッドを実行するためには、以下の情報が必要です。

  1. メソッドを実行するオブジェクト名
  2. 実行するメソッドの名前
  3. メソッドに必要なパラメータ

携帯電話の例を見てみましょう。

  1. 携帯電話というオブジェクトに対して、
  2. 電話をかけるというメソッドを実行するように指示する。
  3. その際のパラメータは相手の電話番号である。

このように、メソッドを実行する際には、 オブジェクトに対してメッセージを送るという考え方をします。

クラスとインスタンス

クラス

我々は、実体のモノをある特徴によりグループ化し、 そのグループに対して共通的な名前をつけることで、 モノに対する共通の認識を得ています。 このグループのことが「クラス」です。 クラスは、同じ特性を持つオブジェクトの集合に名前をつけたものです。 オブジェクトの特性を抽象化したものとも言えます。

これまで説明してきた携帯電話は、 ある特定のひとつの携帯電話を想定してきました。 しかし、世の中には携帯電話は何万台もあります。 それらひとつひとつに、電話会社や電話番号、カメラの有無など 固有の属性がありますが、電話としての性質は共通です。

このようなオブジェクトは携帯電話というクラスに 属していると考えることができます。

Javaでは、オブジェクトを利用するためには、 まずプログラムの中でクラスを記述し、 次にクラスに属するオブジェクト (インスタンス) を生成します。 このため、クラスはオブジェクトの設計図である と考えられます。

現実世界において設計図からたくさんの製品が作られるように、 クラスという設計図から、 多くのオブジェクトを生成し利用することが可能になります。

インスタンス

インスタンスとは、 クラスから生成されたそのクラスに属するオブジェクトのことです。 このため、オブジェクト指向プログラミングにおいては、 インスタンスとオブジェクトという言葉は同じ意味で使われます。

クラスからオブジェクトと生成することを インスタンス化すると言います。

一般に、 クラスを定義しただけでは、そのオブジェクトを利用することはできません。 そのクラスの実体であるインスタンスを生成して、 はじめてオブジェクトとして利用可能になるのです。

Javaによるオブジェクト指向プログラミング

例題として、電大生の成績処理を行うプログラムを考えてみましょう。 このためには、一人の電大生の成績情報を持つクラスが必要だと考えられます。 クラスの名前は TDUStudent とします。 このクラスには次の属性があるものとします。

また、次のメソッドがあるものとします。

このようなプログラムは以下のように書くことができます。 (ファイル名 TDUStudentMain.java)

class TDUStudent {
    int id;
    String name;
    int math;
    int english;
    int physics;

    void setIdentifier(int i, String n) {
        id = i;
        name = n;
    }

    void setScore(int m, int e, int p) {
        math = m;
        english = e;
        physics = p;
    }

    int average() {
        int avg;

        avg = (math + english + physics) / 3;
        return avg;
    }

}

class TDUStudentMain {
    public static void main(String[] args) {
        TDUStudent alice = new TDUStudent();
        TDUStudent bob = new TDUStudent();

        alice.setIdentifier(1, "Alice");
        alice.setScore(80, 90, 100);

        bob.setIdentifier(2, "Bob");
        bob.setScore(90, 70, 60);

        int a, b;
        a = alice.average();
        System.out.println("Alice's average is " + a);
        b = bob.average();
        System.out.println("Bob's average is " + a);

    }
}

クラス TDUStudent のオブジェクトの利用

まず

TDUStudent alice = new TDUStudent();

alice をクラス TDUStudent のインスタンスを格納する変数として宣言し、 new 演算子でインスタンスを新たに生成しています。 同様に、別の新たなクラス TDUStudent のインスタンスを生成し、 変数 bob に代入しています。

alice.setIdentifier(1, "Alice");

この行はインスタンス alicesetIdentifier というメソッドを、 1"Alice" という情報を与え実行する、という意味です。 メソッド setIdentifier によって、 学籍番号と名前を alice の属性に登録するようにメッセージを送っているのです。

1"Alice" はメソッド setIdentifier での処理に必要な引数です。

次に、

alice.setScore(80, 90, 100);

この行では同様に、alice に対してメソッド setScore を実行することで、 数学、英語、物理の各点数が alice の属性に登録されます。 bob についても同様です。

a = alice.average();

この行では、alice に対してメソッド average を実行し、 その結果を変数 a に代入しています。 メソッド average では alice の平均点を計算する処理が行われています。

クラス TDUStudent のクラス定義

TDUStudent という名前のクラス定義を見てみましょう。

    int id;
    String name;
    int math;
    int english;
    int physics;

これらの行で id, name, math, english, physics の各属性を宣言しています。

    void setIdentifier(int i, String n) {

から始まる中カッコで囲まれた部分がメソッド setIdentifier の宣言です。

setIdentifier は、メソッドの実行された相手から、 int 型整数 i と文字列 n を受け取り、 i を属性 id に、n を属性 name に代入しています。

    void setScore(int m, int e, int p) {

も同様です。 int 型整数の m, e, p を受け取り、 それぞれを属性 math, english, physics に代入しています。

    int average() {

は、属性 math, english, physics の各値の平均値を計算するメソッドです。 このメソッドの計算結果である平均値を、 return 文でメソッドを実行した相手に返しています。

オブジェクト指向のプログラムの特長

クラスからオブジェクトを生成すれば、 同じ性質のオブジェクトをいくつでも生成でき、 何度でも利用することができるようになります。 また、メソッドの実行によって、 複雑な処理を簡単に何度でも実行できるようになります。

オブジェクト指向のメリットはこれだけではありません。

オブジェクトを使うことによって、 モノを中心にプログラムを書くことができ、 プログラムで扱うデータとそれに対する操作を わかりやすく記述することができます。

一般に、大きなプログラムをひとまとめに書くと理解が難しくなります。 人間はある程度以上に複雑なものを、 すべてを一度に理解することは困難なのです。 このような場合の原則は、全体をいくつかの部分に分けて、 それぞれを別々に理解することです。 オブジェクト指向プログラムはプログラムの中で登場するモノに着目し、 プログラムをオブジェクトの単位で分けることができます。 オブジェクト指向は、人間が現実世界で物事を理解する時の考え方に 近い概念であるため、プログラムの理解が容易になるのです。

オブジェクトは他のプログラムとは独立して仕事をします。 オブジェクトを利用する際は、 オブジェクトが所定の仕事をしてくれると信頼して、 いわばブラックボックスとして考えてしまえば良いのです。

カプセル化と情報隠蔽

メソッド setIdentifier では、 特別な計算処理を行っているわけではなく、 与えられたデータを属性に登録しているだけです。 このような場合、 main の中でメソッド setIdentifier を実行する代わりに、

alice.id = 1;
alice.name ="Alice"

と書くこともできたわけです。 しかし、この書き方は勧められるものではありません。 名前を変更せずに、学籍番号だけを変更してしまったり、 その逆を行うこともできてしまいます。 これは多くの場合間違いです。 同じ学籍番号に間違った名前が登録されたり、 勝手に、学生の学籍番号を変更することができてしまうからです。

属性を直接操作するのではなく、 必要な操作だけをメソッドとして用意することで、 想定しないような使われ方をして問題が生じることのないように することが可能です。 オブジェクトは、 データとその操作が一体化されたものなのです。 このような概念をカプセル化と言います。

また、処理を行う際にメソッドを用いることで、 オブジェクトの中にどういう変数があり、 どのような計算が行われているかといった、 オブジェクトの内部構造や処理手順を把握する必要はありません。 知っていれば良いのはメソッドの使い方だけです。 このような概念を情報隠蔽と言います。

文法のまとめ

クラス定義の文法

実は、今まで class 〜 で始めていたプログラム自身もクラスのひとつなのです。 一般に、 Java のプログラムはいくつかのクラス定義を並べた形をしています。

クラス定義の文法は次のとおりです。

class クラス名 {

    int member;                      ←属性とする変数の宣言
    ...                                (属性のないクラスの場合省略可)


    int method(int arg, ... ) {      ←メソッドの宣言
        ...                            (メソッドのないクラスの場合省略可)
    }
    ...
}