Javaのオブジェクト指向の本質とは

オブジェクト指向プログラミング言語とは、継承や多態化などの便利な要素が使えるプログラミング言語のことだ。正しく理解し、使用すればオブジェクト指向プログラミングはとても便利なものなのだが、オブジェクト指向のプログラミング言語に入門している人の中にはこのオブジェクト指向を理解する段階で挫折してしまう人もいるようだ。もしかしたら、あなたもそうかもしれない。それはほとんどの場合、オブジェクト指向プログラミングの本質を理解できていないからだ。オブジェクト指向プログラミングの本質を理解すれば、自然とオブジェクト指向のプログラミング言語も理解できるようになる。そこで今回は、Javaのオブジェクト指向の本質について書きたいと思う。

オブジェクト指向プログラミングとは

オブジェクト指向プログラミングとは何だろうか。おそらく、オブジェクト指向プログラミングについて検索などをしてみれば「オブジェクトを定義する」などという小難しい定義が出てきて、結局オブジェクト指向とはどういうものなのかということがよく理解できないことだろう。これからオブジェクト指向プログラミングを覚えようとしている方は一度この定義を忘れよう。オブジェクト指向は、ここでは「便利な機能が使えてコードの無駄を省ける便利なもの」と覚えておこう。厳密には少し違うのであるが、それは後半で詳しく解説する。

これまでの言語は「手続き型プログラミング言語」と呼ばれ、ソースは指示を手続き形式で書かれたものだった。この手続き型言語は簡単なプログラムであれば便利なのだが、複雑なプログラムを組む場合、従来の手続き型言語では保守が難しくなる。なぜなら、ある機能を修正したい場合も、その機能のコードがどれであるかがわからないため、上から順番にコードを精査しなければならないからだ。

ここで、少しプログラミングに関する話をしておこう。プログラミングはほとんどの場合、現実世界の何かをコンピュータで再現するために行うことだ。つまり、ほとんどのプログラムは現実世界の何かを機械に代わりにやらせるためのものだ。例えば、お小遣い管理プログラムであれば、今までお小遣い帳に支出と収入を書いていたことをコンピュータ上で再現するものだ。ストップウォッチのプログラムは、今まで砂時計で計っていた時間をコンピュータ上で再現するものだ。銀行のプログラムでも、ネットショップでも、すべて昔は人が手作業で行ってきたことを機械にやらせるためのものだ。

このように、大半のプログラムの内容は今まで現実世界で行ってきたことをプログラムにしたものだ。オブジェクト指向プログラミングとは、このようにプログラムの内容を現実世界の内容と合致させようとする指向のプログラミングのことだ。今までの手続き型プログラミングでは、クラスやメソッドを開発者の都合で作ってきた。「main()」メソッドや「sub()」メソッドなどは、現実世界には存在しない。オブジェクト指向プログラミングは、それを現実世界に合致させるプログラミングの指向のことなのだ。

例えば、RPGゲームを作る場合は、「Yushaクラス」などが挙げられるだろう。このYushaクラスに「attack()」や「defense()」などのメソッドを作成し、このYushaクラスをJavaの仮想世界に生み出し、Javaで勇者を使用するのだ。

なお、「インスタンス」とは、それぞれのクラスから生成し、Javaの仮想世界に生み出された実体のことをいう。「オブジェクト」とは、先述のインスタンスを生成するためのクラスのことをいう。次からはこの用語を使用するため、確実に理解しておいてほしい。

しかし、なぜ、インスタンスを生成するためにオブジェクトを作成しなければならないのだろうか。直接インスタンスを定義すれば、オブジェクトを作成するよりも楽なのではないだろうか。

なぜ、オブジェクトを定義しなければならないか。それは、オブジェクトを定義することにより、そのインスタンスを大量に生成することができるからだ。例えば、RPGゲームでは、同じ敵キャラクターが一度に何体も出現することがあるかもしれない。その際、コピペなどを使って同じ敵キャラクターのインスタンスを定義していれば、コードの可読性が下がってしまう。しかし、オブジェクトを定義し、そこからインスタンスを生成すれば、同じインスタンスを大量に生成することができ、コピペなどが必要なく、コードがすっきりする。また、さまざまなクラスから同じインスタンスを生成することができるようになるため、コピペなどを使っている場合に比べてインスタンスの改良などが容易になる。

また、コピペによって同じコードを何度も使用している場合、人為的ミスが発生する温床になりかねない。なぜなら、何度もコピペして使用しているプログラムの一部を編集した場合、コピペして使用している全コードを編集しなければならないからだ。もし、どこかのコードを更新することを忘れてしまったら、どうなるだろうか。もしかしたら、整合が取れなくなり、異常終了してしまうかもしれない。これは致命的なミスだ。

そこで、オブジェクト指向プログラミングの便利な機能を使用することで、そのようなソースのコピペなどをなくすことができる。そして、人為的ミスが発生する環境をなくすことができ、ソースコード内のミスを減らすことができる。

また、従来の手続き型の言語では、ほとんどの場合はメインクラスがすべての処理を行ってきたが、オブジェクト指向では一部の処理を部品クラスに任せることによって、問題が発生した際にはその機能の部品クラスのコードを精査すれば良いだけになるので、バグなどを発見しやすくなり、全コードを精査する必要がなくなり、保守などが容易になる。

オブジェクト指向の三大要素

では、オブジェクト指向の三大要素と呼ばれている要素とは何なのだろうか。これからは実際にオブジェクト指向の三大要素について書きたいと思う。

継承

これが、オブジェクト指向プログラミングで最も知られていると考えられている要素だ。継承とは、抽象的なクラスを作成し、そのクラスを使用してオブジェクトを作成することをいう。例えば、「ShoyuRamen」クラスと「ShioRamen」クラスを作成するとしよう。二つのクラスはどちらもラーメンのインスタンスを生成するためのオブジェクトであるから、ラーメンに関する情報の共通点などにより二つのクラスの間には一部の重複が生じる。もし、新しく「MisoRamen」クラスを作る際は、「ShoyuRamen」クラスや「ShioRamen」クラスなどからコピペなどを行い、味噌ラーメン用に書き換えなければならない。これでは、重複している部分に修正が発生した場合はすべてのラーメンのクラスを編集しなければならないため、コピペによるミスの温床になりかねない。

継承とは、このようなクラスの重複する部分のみが記述されているクラスを使用してクラスを作成する機能のことだ。なお、重複する部分のみが記述されているクラスを「抽象クラス」という。例えば、RPGを作っている場合はたくさんのキャラクターを作るだろう。そして、それぞれのキャラクターのクラスにはフィールド変数HPやMP、メソッドattack()などが宣言されているだろう。しかし、このようなフィールド変数、メソッドはすべてのキャラクターに共通である。HeroクラスにもWitchクラスにもそれらの機能は実装されるだろう。そこで、Characterクラスを作成し、そこにフィールド変数やメソッドなどを宣言し、HeroクラスなどでCharacterクラスを継承することで、Heroクラスにそれらのフィールド変数やメソッドが実装されているのと同じ機能を得ることができるようになる。

では、実際に継承を行うにはどう記述すればよいのだろうか。

継承を行うには、クラスの宣言部分に「extends 継承するクラス名」と追加に記述する。例えば、先ほど例に挙げたRPGの例では、「public class Hero extends Character」や「public class Witch extends Character」と記述する。

抽象クラスのための安全機能

先ほどは抽象クラスの便利な機能を解説した。しかし、本来は継承して使用するはずの抽象クラスを誤ってインスタンス化してしまったらどうなるだろうか。抽象クラスは実際のクラスを作るための型のクラスであるから、正常に動作しない。もしその抽象クラスを誤ってインスタンス化し、そのまま本番の環境で稼働させてしまったらどうなるだろうか。間違いなくプログラムは異常な動作を起こしてしまう。

Javaでは、このようなミスを犯さないための安全機能が備わっている。それは、クラスの宣言のコードに「abstract」をつける方法だ。例えば、「public class Example」という抽象クラスのインスタンス化を禁止する場合は、「publicabstract class Example」というように、「class」の前に「abstract」を付けることで、インスタンス化を禁止することができる。newしてインスタンス化しようとしても、コンパイルエラーになってしまう。これは、オブジェクト指向の「保守性を上げ、安全に」という考えに基づいた機能の一つだ。

カプセル化

カプセル化とは、プログラマーのミスを減らし、プログラムをより安全にする機能のことだ。例えばRPGの例でいえば、RPGのHPを格納する変数に負の値が格納されることはありえない。RPGのシステムでは通常、HPが負の数になりそうになった場合はHPを0にし、戦闘不能にする。そのため、HPに負の値が格納されることはありえない。しかし、代入を使えば簡単に負の値が格納できてしまう。これは、プログラマーのミスにより起きてしまうかもしれない。HPなどの変数を代入などで書き換えている場合は、プログラマーがミスを起こさないよう注意する以外の対策はない。そこで、変数に値を代入する際、入力チェックを施すことで、おかしな値が格納されることがなくなる。

Javaには、書き換え元によってフィールド変数の書き換えの許可を設定できる機能がある。具体的には次の通りだ。

名前 記述 許可
プライベート private 自身のクラスのみ
プロテクト protected 自身のクラスとそのクラスを継承した子クラス
パッケージプライベート (記述なし) 同一パッケージのクラス
パブリック public 制限なし

カプセル化を行うクラスでは、そのクラスのすべてのフィールド変数にこのprivateを設定し、GetterとSetterを作成する。Getterとは、対象のフィールド変数の中身を取得するためのメソッドの名称だ。そして、Setterとは、対象のフィールド変数の中身を書き換えるためのメソッドの名称だ。基本的に、GetterやSetterは「get*****()」(「*****」は変数名)や「set*****(int *****)」(「*****」は変数名)のように使う。「nameのGetter」と言われたら「getName()」を指すことが多い。

Getter


class Getter {
  private int value = 0;
  public int getValue() {
    return value; // 値を返して終了
  }
}

public class Main {
  public static void main(String[] args) {
    Getter getter = new Getter(); // newしてインスタンス化
    String s = getter.getValue(); // 変数sには0が代入された。
    System.out.println(getter.getValue()); // 0と表示された。
  }
}

直接フィールド変数を参照すればよいだけなので、Getterは必要ではないと思われる方もいらっしゃるかもしれないが、カプセル化によってフィールド変数がprivateになっており、外部からフィールド変数を直接参照することができないため、Getterを設置する必要がある。なお、フィールド変数をpublicなどに変更した場合、フィールド変数を直接書き換えることができるようになり、不正な値も代入できるようになり、カプセル化の意味が失われてしまうため、決して行ってはならない。

Setter


class Setter {
  private int value = 0; // 負の値はあり得ない変数
  public void setValue(int value) {
    if (value < 0) throw new IllegalArgumentException(); // もし引数が負の値であれば例外をスローする(安全機能)。
    this.value = value; // if文による検査に通過した場合のみ、フィールド変数に引数の値を代入する。
  }
}

public class Main {
  public static void main(String[] args) {
    Setter setter = new Setter(); // newしてインスタンス化
    setter.setValue(123); // 先ほどnewしたインスタンスのフィールド変数valueの値が123に変更された。
    setter.setValue(-123); // 不正な値を引数に指定した。この行が実行された際に例外がスローされ、プログラムは異常終了する。
  }
}

フィールド変数にprivateを指定した場合はそのフィールド変数が宣言されているページからしか変数に対して代入を行うことができなくなる。そこで、変数に対して代入を行う代替策として、Setterを作成し、そのSetter内で代入されそうになった値の入力チェックを行うことで、不正な値が代入されることを防ぐことができる。privateにした場合はフィールド変数の参照も行えなくなるため、フィールド変数を直接参照する代替策としてGetterを設置する。

多態化

多態化とは、あるオブジェクトをあいまいにみて便利に使う要素のことだ。例えば、車にはいろいろな車種がある。ロボットはそれを一つ一つ厳密にとらえ、違うものとして扱っている。そのため、ロボットは新しい車を操作しようとした際には再度学習する必要がある。それに比べて人間は、無意識のうちにさまざまな種類の車を単に「車」とあいまいにとらえている。そのため、さまざまな車をほかの車と同じように運転することができる。

このように、オブジェクトをあいまいに見ればプログラムの世界でもメリットが生じる。例えば、RPGの開発でattack()メソッドを開発していたとしよう。そして、このattack()メソッドが持つ攻撃の機能はキャラクター全員が持っているべきであるため、すべてのキャラクターが継承しているCharacterクラスに宣言したとしよう。ここまでは順調なのだが、Characterクラスに宣言した際、問題が生じる。それは、キャラクター一つ一つのためにメソッドを作成しなければならないということだ。つまり、HeroクラスもWitchクラスも攻撃が行えなければならないため、「public void attack(Hero h)」「public void attack(Witch w)」と何度も宣言しなければならず、無駄な時間が生じる。また、キャラクターが増えた際にはまた新しくメソッドを宣言しなければならない。もし宣言を忘れた場合、攻撃できないキャラクターが誕生してしまい、大問題だ。また、この方法はコピペを行わなければならないため、ミスの温床になりかねない。

そのような場合、多態化を使用することが有効だ。HeroもWitchも、GhostもDragonも、あいまいに見ればすべてCharacterだ。そこで、「public void attack(Character c)」と宣言することで、一発の宣言で終わらせることができる。

ただし、多態化を使用することで呼び出せるメソッドも減ってしまう。なぜなら、「Character c = new Hero();」などのインスタンス化を行うことで、もともとHeroであったことは忘れてしまい、そのあとはCharacterクラスを継承している何かという情報しか残らなくなり、Characterクラスで宣言されているフィールド変数やメソッドしか使用することができなくなるからだ。この点には注意しなければならない。

instanceof

特定のインスタンスである場合のみ対象の処理を行いたい場合はinstanceofを使用すればよい。instanceofとは、その変数の中身が指定する型と一致するかどうかを確認するためのものだ。instanceofは「変数 instanceof 型名」のように使用する。例えば、「if (c instanceof Witch);」で変数cの中身がWitch型であるかどうかを調べることができる。「if (c instanceof Witch) System.out.println(“これは魔法使いです。”);」などのように使用することができる。

Object型

Javaが用意しているクラスの一つに「Objectクラス」がある。このクラスは、どのようなクラスでも継承している。「extends java.lang.Object」などを行わなくてもこれが記述されているものとみなされるのだ。そのため、どのようなクラスでも「Object value = new ……….(…..)」のような使い方ができる。Object型の変数には何でも代入することができる。例えば、「Object value = “Example”;」や「Object value = 123」、「Object value = new Hero();」のような使い方もできる。これは便利だと思われる方もいらっしゃるかもしれない。確かに、何でも代入できるという点では便利だ。しかし、デメリットも大きい。例えば、「public static void method(Object number, Object string)」のようなメソッドがあったとする。この場合、第一引数に数値、第二引数に文字列だということが読み取れるが、もしかしたら次のようなミスを犯すかもしれない。

  • method(“Example”, 123); // 引数の順番が違う
  • method(new Hero(), new Witch()); // 引数にするべき型が違う

このようなミスは、Object型を使用している場合はコンパイルが通り、実行できてしまう。実際に型を使用していればこのようなミスを犯した際にコンパイルエラーが出て気づくことができるのだが、Object型を使用している場合は文法的には何の問題もないため、そのままコンパイルできてしまう。そして、実行した際に例外が出て異常終了し、引数の順番の間違いに気づく。もしかしたら、重要なプログラムでこのようなミスを犯し、異常終了してしまうこともあるかもしれない。そのようなことを避けるためにも、Object型を使用することは極力避けるようにしよう。

最後に

今回は、Javaのオブジェクト指向プログラミングについて書いた。このブログForchiveでは、他にもJavaに関するさまざまな情報が掲載されている。このブログに掲載されているJavaに関する情報を見るには、Javaのカテゴリページを参照していただきたい。