現代のプログラミング言語に必須の概念「オブジェクト指向」。オブジェクト指向とは、プログラムに必要な要素を細分化し、それらを組み合わせて機能を実装する手法です。オブジェクト指向の導入により、効率的で拡張性が高いプログラムが作れます。
しかし、オブジェクト指向は複雑な印象を一般的に抱くため、苦手意識を感じてしまう人も少なくありません。実は、オブジェクト指向の概念自体は単純。私たちの日常生活に置き換えることもできる、身近なものでもあります。そこを理解したうえで、実際のプログラミングに落とし込んでいけば、オブジェクト指向は決して困難なものではないのです。
今回は、オブジェクト指向の基本的な概念やメリット、実際のプログラミング方法などについて解説します。オブジェクト指向の習得でお悩みの方は、ぜひ参考にしてみてください。
目次
「オブジェクト指向(object-oriented)」とは、プログラムを「部品の集まり」であると考えて、細かな部品を組み合わせることでプログラムを構成する手法です。その「部品」のことを「オブジェクト」と呼びます。
椅子の組み立てをイメージしてみましょう。椅子には脚部や座面、ボルトやナットなどさまざまなパーツがあります。それら細かな部品はすべて「オブジェクト」であり、それぞれに「役割」があります。部品を組み合わせれば、人が座れるという機能を持つ「椅子」の完成です。
さらに、組み立てた椅子そのものも「オブジェクト」になり得ます。たとえば、椅子をあと3つ組み立てたうえで、新たに食卓テーブルも追加したらどうでしょうか。「最大4人で使える食卓テーブルセット」という、新たな機能を持つオブジェクトになりますよね。
オブジェクト指向のプログラミング言語でも同様に、細かな部品を組み立てて機能を作り出します。詳細は後述しますが、プログラミングの世界では各部品を「クラス」というまとまりに分類し、そのクラスを組み合わせてさまざまな機能を実現していきます。
「オブジェクト」や「指向」という用語自体は、1950年代後半から1960年代前半に、MIT(マサチューセッツ工科大学)においてすでに考案されていました。しかし、オブジェクト指向の概念がプログラミング言語に採用されたのは、1960年代後半に登場した「Simula(シミュラ)」というプログラミング言語が初めてです。
Simulaの大きな特徴は、後述するオブジェクト・クラス・継承など、オブジェクト指向に欠かせない要素を導入したことです。それ以降、計算機科学者アラン・ケイ氏らがさまざまな改良を加え、オブジェクト指向は発展を続けてきました。現在ではさまざまなプログラミング言語において、オブジェクト指向が当たり前のように採用されています。
オブジェクト指向のプログラミング言語がこれほど注目を集めたのは、その効率性や拡張性の高さが理由です。プログラムの要素を機能や役割ごとに分類することで、従来より遥かに少ない工数で仕様変更や新機能の追加などに対応できるようになりました。そのため、オブジェクト指向は大規模プロジェクトに欠かせないのです。
オブジェクト指向を理解するうえで、まず知っておくべき用語は以下4つです。
前述したように、オブジェクト指向プログラミングにおいては、システムを構成する要素・部品を「オブジェクト」と呼びます。
たとえば、パソコンには、「CPU」「GPU」「HDD(SSD)」「マザーボード」「電源ユニット」など、大小さまざまなパーツが必要です。これらすべてがオブジェクトであり、それぞれに重要な役割があります。
言い換えれば、オブジェクト指向プログラミングを行ううえで、すべてのベース・起源となるのがオブジェクトだということです。オブジェクト指向では、基本的にオブジェクトを主軸として、プログラムの構成を考えていきます。
プログラミング言語には、実際にオブジェクトという単位があるわけではなく、あくまで「部品」を示す抽象的な概念です。一方で「クラス」は、実際にソースコード上でオブジェクトを表現するための、「設計図」としての役割を果たします。
現実世界の店舗をイメージしてみましょう。店舗には同じ商品がいくつも並んでおり、すべて同じ形・仕様です。これは製品を作るための設計図があり、同じものをいくらでも量産できるからです。オブジェクト指向プログラミングでは、クラスという設計図を基にして、オブジェクトの「インスタンス(実体)」を生成できます。
しかし、同じ設計図から作られた製品であっても、それぞれの個体は「別物」です。現実世界の製品でも、最初はすべて同じであっても、使い方や環境によって徐々に個体差が出てくるはずです。同じように、インスタンスもプログラムで使用する過程で、それぞれ中身のデータは異なるものになっていきます。
「プロパティ」はオブジェクトの「情報」のことで、基本的にはクラス内の変数を指します。たとえば「自動車」というオブジェクトの場合は、色・シート数・ドア数などの情報が、プロパティに格納されます。
なお、プロパティの内容はインスタンスごとに異なることがほとんどです。たとえば、同じ車種の車でも、個体によってさまざまな色があります。プログラムに置き換えるなら、プロパティに保存されるデータは「白」「黒」「赤」など、インスタンスによって変わるということです。
また、プロパティに保存されるのは、必ずしも数値や文字列だけではありません。プロパティには、ほかのオブジェクトが格納されることもあります。この場合は、プロパティの変数を介して、ほかのオブジェクトにアクセスできます。
「メソッド」は、オブジェクトの「動作」を意味し、プログラミング言語によっては「関数」とも呼ばれます。たとえば自動車の場合は、「アクセルを踏むと進む」「ブレーキをかけると止まる」など、複数の動作があります。こうした動作の内容を詳しく記載するのが、メソッドの役割です。
なお、メソッドは自分自身のインスタンスの参照と変更を行えます。つまり、メソッドは自分自身を変化させて、「自律的な動作」ができるということです。たとえば、自動車はアクセルを踏むと自動的に加速し、ブレーキをかけると止まります。プログラム上では、メソッドやプロパティなどを活用すると、同じことを再現可能です。
たとえば、アクセルとブレーキのペダルを踏んだときの速度変化をプロパティに設定したうえで、アクセルやブレーキのメソッドにペダルをどれくらい踏み込んだかを渡します。こうすることで、各メソッドで移動後の座標値の演算と反映を行い、自律的な動作を実現できます。
こうした「オブジェクトの自主性」は、オブジェクト指向プログラミングに欠かせない概念です。自主性を実現することで、プログラマが細かな処理をその都度書かなくても、最小限の情報を渡すだけでオブジェクトを操作できます。
実際にオブジェクト指向プログラミングを行うためには、以下3つのテクニックを身につけておく必要があります。
「カプセル化」は、各オブジェクトの情報を外部から見えないように「隠蔽(いんぺい)」して、互いに干渉し合うことを防ぐ仕組みです。これは現実世界の「アクセス権限」に相当します。たとえば、パソコンの「システムファイル」は、管理者権限がなければ基本的には変更できないようになっています。意図しない変更が行われて、コンピューターに深刻な問題が発生してしまうのを防ぐためです。
プログラミングの世界でも、クラスのインスタンスのデータをどこからでも変更できると、意図しないタイミングでデータを書き換えてしまうことがあります。その結果、プログラムのバグやエラーが増えて、全体的な安全性・安定性が低下してしまいます。カプセル化を導入すると、外部クラスからのアクセスを制限し、より安全なプログラムを作ることが可能です。
「継承」は、あるクラスの要素をすべて受け継ぎ、発展的なクラスを作るための仕組みです。継承先のクラスには、継承元の要素に加えて新たな機能を追加できます。継承というと難しく感じるかもしれませんが、実は現実世界にもさまざまな「継承」があります。
たとえば「乗り物」を考えてみましょう。乗り物とは、人を乗せて移動するものを指します。乗り物には、自転車・船舶・自動車・航空機などさまざまな種類がありますが、いずれも「人を乗せて移動する」点では同じです。これをプログラミングに落とし込むと、「自動車クラス」や「航空機クラス」は、「乗り物クラス」を継承していることになります。
さらに、自動車には原動機の動力で路上を走るという機能がありますが、自動車にも乗用車・スポーツカー・トラックなどさまざまな種類があります。つまり、「乗用車クラス」や「トラッククラス」は、「自動車クラス」を継承しているということです。
こうしたクラスの「継承関係」を構築することで、オブジェクト同士の関連性を明確化でき、より効率性・メンテナンス性の高いプログラムが作れます。なお、継承元のクラスは一般的に「親クラス(スーパークラス)」、継承先の新しいクラスは「子クラス(サブクラス)」と呼ばれるので、この際に覚えておきましょう。
「ポリモーフィズム」とは、「同じ名称の動作でありながら、異なる振る舞いをすること」を実現するための仕組みです。ポリモーフィズムは多様性を意味し、実は現実世界ではごく一般的な概念です。
たとえば、多くの動物は鳴きますが、その「鳴き声」や「鳴き方」は動物ごとにまったく異なります。ネコは「ニャー」・イヌは「ワン」・ライオンは「ガオー」のように鳴くでしょう。同じ「鳴く」という動作でも、その中身の振る舞いは異なるということです。
ポリモーフィズムは、前述した継承によって実現できます。たとえば、動物の「Animalクラス」に鳴き声を出す「Soundメソッド」があるとしましょう。Animalクラスを継承したのが、「Catクラス」「Dogクラス」「Lionクラス」です。これら3つの子クラスにも、親クラスと同じSoundメソッドがあります。
しかし、Soundメソッドの中身は、それぞれのクラスで異なることがポイントです。CatクラスのSoundメソッドは「ニャー」・Dogクラスは「ワン」・Lionクラスは「ガオー」という鳴き声を表現します。つまり、Soundメソッドで「鳴き声を表現する」ことは同じでも、その振る舞いがクラスごとに異なるということです。
このように、オブジェクト指向のポリモーフィズムを活用すれば、オブジェクトごとの特徴を正確に表現でき、より現実に即した振る舞いを実現できるようになります。
オブジェクト指向プログラミングを導入することで、以下5つのメリットが得られます。
オブジェクト指向は、各オブジェクトに固有の「役割」を与えます。そのため、「どの部分が何を担当しているのか」を明確化できます。つまり、オブジェクト指向で構成されたプログラムは、機能ごとに分類しやすいということです。
たとえば、プレイヤーキャラクターの動作を変更したいときは、「プレイヤークラスの動作メソッドを変更すればいい」と判別できます。このように、それぞれの役割が明確化されていることで、プログラムの全体像を把握しやすくなります。まずは大まかな全体像を作り、あとから詳細を煮詰めていくことも容易です。
オブジェクト指向プログラミングは、大規模かつ多人数で開発しやすいことも魅力です。前述したように、オブジェクト指向はプログラムを部品ごとに「クラス」として細分化し、それらを組み合わせて機能を実装します。そのため、まるで工業製品の部品のように、複数の担当者で部品の製造を分担できるのです。
たとえば、自動車の挙動を再現するプログラムを作る場合、Aさんはアクセル、Bさんはブレーキのようにプログラマの役割分担を明確化できます。このように、オブジェクト指向ではプログラムを切り分けることができるので、従来手法よりはるかに容易に分業ができ、大規模なプロジェクトでも効率的に進められます。
オブジェクト指向プログラミングは、改良や修正など、ソースコードに変更を加えやすいことも魅力です。これは、プログラムがクラスというパーツごとに、細分化されているからです。たとえば、現実世界で自動車のタイヤを変更したいとき、タイヤだけ外して付け替えます。もし車両本体とタイヤが一体化していれば、タイヤの交換に多くの時間と工数がかかるでしょう。
プログラミングの世界でも同じで、非オブジェクト指向のプログラミングでは、仕様変更を加えるときにメインプログラム全体を変更しないといけないことがあります。しかし、オブジェクト指向プログラミングでは、該当する処理を行っているクラスやメソッドを変更するだけで、仕様変更が簡単にできます。
オブジェクト指向プログラミングの導入により、プログラムのバグやエラーを特定しやすくなります。前述したように、オブジェクト指向プログラミングでは、各要素の役割分担がはっきりしているため、「どの部分が何を行っているか」がわかります。
そのため、バグやエラーが発生しても、どこを修正すべきかすぐに見当をつけることが可能です。たとえば、キャラクターの動作がおかしい場合は、キャラクタークラスの動作メソッドをチェックすれば、問題のある場所が見つかるでしょう。
また、カプセル化によって互いに影響を及ぼす範囲が少ないことも、オブジェクト指向プログラミングの利点です。たとえば、プログラムAにバグやエラーがあっても、プログラムBはAと分離しているため、広範囲に悪影響が及ぶことを防げます。業務用システムなど、安定的な連続稼働が求められる分野では、非常に心強いメリットになります。
オブジェクト指向の導入により、「ソースコードの再利用」が容易になるため、プログラムの開発効率が上がります。オブジェクト指向プログラミングでは、プログラムを構成する要素が、クラス単位で細かく分類されています。つまり、ほかのプログラムと共通する部分も、それだけ多くなるということです。
たとえば、パソコンにはCPUやメモリなどのパーツが搭載されていますが、規格さえ合えばほかのパソコンにも流用できます。プログラミングでも同じで、うまく細分化して設計されたクラスは使いまわし可能です。そのため、プロジェクトごとにゼロから開発し直す必要がなくなり、プログラムの開発効率が大幅に高まります。
また、そもそもオブジェクト指向は、適切な設計がなければ実現できません。オブジェクト指向のソースコードは、必要な部分だけが抽出された簡潔なものです。これは、複雑に入り組んでわかりにくい「スパゲッティコード」の予防にもつながります。ソースコードが簡潔で読みやすくなるため、チーム全体の開発効率が高まります。
さまざまなメリットがあるオブジェクト指向プログラミングですが、これらの魅力は「適切な設計」がなければ発揮されません。たとえば、余分な要素が多くあるクラスや、継承関係が適切ではないクラスでは、カプセル化やポリモーフィズムの実現は困難です。事前の設計がよくなければ、開発段階でさまざまな問題が発生してしまうでしょう。
オブジェクト指向のポイントは、オブジェクトごとに役割を与えて、それぞれを独立させることです。うまく設計されたオブジェクト指向のプログラムは、現実世界に置き換えても違和感がありません。慣れるまでは、こうした感覚をつかむのが難しいかもしれませんが、まずは書籍やインターネット上のサンプルコードに触れ、コーディングしてみましょう。
オブジェクト指向を採用したプログラミング言語として、以下6つのものが有名です。ここでは、各言語の特徴を見ていきましょう。
「C++」は、1983年に登場した比較的古いプログラミング言語で、「C言語」にオブジェクト指向の概念を導入したことが特徴です。C++は機能性・汎用性・実行速度いずれも優れているため、各種システムやソフトウェア、組み込みやゲームなど幅広い分野で採用されています。
C++の機能は膨大で言語仕様も複雑なので、習得難易度はかなり高いといえます。しかし、C++を習得すればエンジニアとして活躍できる幅が広がるうえに、ほかのプログラミング言語も容易に習得しやすくなることが魅力です。
「Java」は1995年に登場した、比較的新しいプログラミング言語です。前述したC++をさらに発展させて、オブジェクト指向を追求した言語仕様になっています。Javaの魅力は、機能性が高いことや習得難易度が低いことです。ハードウェアに近い複雑な部分が隠蔽されているため、C++と比べて安全なプログラムが作りやすいことも魅力です。
「C#」は、2000年に登場した比較的新しいプログラミング言語で、Windowsで有名なMicrosoft社が開発したことで有名です。C#の特徴は、「C++とJavaの良いとこ取り」をした言語仕様になっていることです。ハードウェアに近い操作と、安全性の高いプログラミングの双方に対応しているため、幅広い分野で採用されています。
また、C#は「Unity」というゲームエンジンで開発言語として採用されていることも、大きなメリットです。開発環境についても、「Microsoft Visual Studio」の無料版が使えるので、環境構築に悩む心配もありません。オブジェクト指向を学ぶ最初のプログラミング言語として、C#は非常に魅力的だといえるでしょう。
「Python」は、1991年に登場したプログラミング言語で、AI関連の最先端分野で活用されていることで有名です。プログラミングの構文自体は、C言語やC++と似ていますが、より簡潔で効率的なコードが書けるようになっています。短時間で高度な機能を実現できるからこそ、専門的な分野での導入実績が豊富なのです。
「Ruby」は、Javaと同時期の1995年に登場したプログラミング言語で、日本人によって開発されたことで有名です。前述したPythonから派生した言語で、「エンジニアが楽しくプログラミングできる言語」がコンセプト。オブジェクト指向を採用し、シンプルなソースコードが書けることが特徴です。RubyはWeb系の分野で広く活用されています。
「PHP」はJavaやRubyと同時期の、1995年に登場したプログラミング言語です。言語仕様はC言語に影響を受けた部分が多いですが、構文や記法はC言語とは大きく異なります。主にWebサイトやWebアプリ開発の分野で活用されており、Web系の分野で活躍したいプログラマにとっては「必修科目」といっても過言ではありません。
オブジェクト指向の概要を学んだところで、実際にオブジェクト指向を活用してプログラミングをしてみましょう。今回は、Javaを題材にしてソースコードやテクニックを紹介していきますが、前述したどのプログラミング言語でも基本的な部分は同じです。
まずは、そもそも「オブジェクト指向ではないプログラム」とはどのようなものか知るために、不十分なオブジェクト指向のサンプルコードから見ていきましょう。プログラムの内容は、自動車のデータと動作を表示するというごく簡単なものです。
//サンプルプログラム
class CarA { // 車種名 public String name; // 販売価格 public int value; // コンストラクタ public CarA() { name = "自家用車A"; value = 2000000; } // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); } } class CarB { // 車種名 public String name; // 販売価格 public int value; // コンストラクタ public CarB() { name = "自家用車B"; value = 3000000; } // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); } } class Truck { // 車種名 public String name; // 販売価格 public int value; // 積載重量 public int capacity; // コンストラクタ public Truck() { name = "トラック"; value = 5000000; capacity = 10; } // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // 荷物を載せる public void Load(int weight) { // 重量をチェックする if (weight > capacity) { System.out.println("最大積載量" + capacity + "トンを超過しています!"); } else { System.out.println(name + "に" + weight + "トンの貨物を積載します!"); } } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); System.out.println("最大積載量:" + capacity + "トン"); } } public class Main { public static void main(String[] args) { // 各クラスをインスタンス化する CarA carA = new CarA(); CarB carB = new CarB(); Truck truck = new Truck(); // 各クラスのメソッドを実行する carA.Accelerator(); carA.Break(); carA.Data(); System.out.println(); carB.Accelerator(); carB.Break(); carB.Data(); System.out.println(); truck.Accelerator(); truck.Break(); truck.Load(5); truck.Data(); System.out.println(); } }
//実行結果
自家用車Aのアクセルが踏まれました! 自家用車Aのブレーキが踏まれました! 車種:自家用車A 価格:2000000円 自家用車Bのアクセルが踏まれました! 自家用車Bのブレーキが踏まれました! 車種:自家用車B 価格:3000000円 トラックのアクセルが踏まれました! トラックのブレーキが踏まれました! トラックに5トンの貨物を積載します! 車種:トラック 価格:5000000円 最大積載量:10トン
上記のサンプルプログラムでは、3種類の自動車のインスタンスを生成しています。しかし、どのクラスも同じような内容であるにもかかわらず、車種ごとに個別のクラスを作成しているため冗長です。また、すべてのアクセス修飾子が「public」なので、インスタンスを通せばどこからでも内容が書き換えられてしまうことも問題点です。
先ほどの例では、「CarAクラス」と「CarBクラス」の中身は、プロパティに格納する内容以外はほとんど同じです。そのため、これらのクラスをまとめて、コンストラクタでプロパティの値を変更するようにしたものが、以下のサンプルコードです。
//サンプルプログラム
class Car { // 車種名 public String name; // 販売価格 public int value; // コンストラクタ public Car(String name, int value) { this.name = name; this.value = value; } // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); } } class Truck { // 車種名 public String name; // 販売価格 public int value; // 積載重量 public int capacity; // コンストラクタ public Truck(String name, int value, int capacity) { this.name = name; this.value = value; this.capacity = capacity; } // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // 荷物を載せる public void Load(int weight) { // 重量をチェックする if (weight > capacity) { System.out.println("最大積載量" + capacity + "トンを超過しています!"); } else { System.out.println(name + "に" + weight + "トンの貨物を積載します!"); } } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); System.out.println("最大積載量:" + capacity + "トン"); } } public class Main { public static void main(String[] args) { // 各クラスをインスタンス化する Car carA = new Car("自家用車A", 2000000); Car carB = new Car("自家用車B", 3000000); Truck truck = new Truck("トラック", 5000000, 10); // 各クラスのメソッドを実行する carA.Accelerator(); carA.Break(); carA.Data(); System.out.println(); carB.Accelerator(); carB.Break(); carB.Data(); System.out.println(); truck.Accelerator(); truck.Break(); truck.Load(5); truck.Data(); System.out.println(); } }
//実行結果
自家用車Aのアクセルが踏まれました! 自家用車Aのブレーキが踏まれました! 車種:自家用車A 価格:2000000円 自家用車Bのアクセルが踏まれました! 自家用車Bのブレーキが踏まれました! 車種:自家用車B 価格:3000000円 トラックのアクセルが踏まれました! トラックのブレーキが踏まれました! トラックに5トンの貨物を積載します! 車種:トラック 価格:5000000円 最大積載量:10トン
自動車をひとつの「Carクラス」にまとめたので、同じコードを繰り返し記述する冗長さは緩和されました。しかし、まだまだ冗長な点が多いので、さらにオブジェクト指向化を進めて、簡潔で扱いやすいプログラムに変えていきましょう。
先ほどのソースコードでは、クラスが十分に細分化されていないため、「Carクラス」と「Truckクラス」で重複している部分が多数ありました。そこで継承を駆使し、ソースコードを大幅に簡潔化したのが、以下のサンプルコードです。
//サンプルプログラム
class Vehicle { // 車種名 public String name; // 販売価格 public int value; // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); } } class CarA extends Vehicle { // コンストラクタ public CarA() { name = "自家用車A"; value = 2000000; } } class CarB extends Vehicle { // コンストラクタ public CarB() { name = "自家用車B"; value = 3000000; } } class Truck extends Vehicle { // 積載重量 public int capacity; // コンストラクタ public Truck() { name = "トラック"; value = 5000000; capacity = 10; } // 荷物を載せる public void Load(int weight) { // 重量をチェックする if (weight > capacity) { System.out.println("最大積載量" + capacity + "トンを超過しています!"); } else { System.out.println(name + "に" + weight + "トンの貨物を積載します!"); } } // データ表示 public void Data() { // まず親クラスの同名メソッドを呼び出す super.Data(); System.out.println("最大積載量:" + capacity + "トン"); } } public class Main { public static void main(String[] args) { // 各クラスをインスタンス化する CarA carA = new CarA(); CarB carB = new CarB(); Truck truck = new Truck(); // 各クラスのメソッドを実行する carA.Accelerator(); carA.Break(); carA.Data(); System.out.println(); carB.Accelerator(); carB.Break(); carB.Data(); System.out.println(); truck.Accelerator(); truck.Break(); truck.Load(5); truck.Data(); System.out.println(); } }
//実行結果
自家用車Aのアクセルが踏まれました! 自家用車Aのブレーキが踏まれました! 車種:自家用車A 価格:2000000円 自家用車Bのアクセルが踏まれました! 自家用車Bのブレーキが踏まれました! 車種:自家用車B 価格:3000000円 トラックのアクセルが踏まれました! トラックのブレーキが踏まれました! トラックに5トンの貨物を積載します! 車種:トラック 価格:5000000円 最大積載量:10トン
まず「Vehicleクラス」を作り、そこにすべてのクラスに共通するプロパティやメソッドを設置します。それを継承した「CarAクラス」と「CarBクラス」は、コンストラクタ内で異なる値を設定するのみ。同じくVehicleクラスを継承した「Truckクラス」には、ほかのクラスにはない積載重量に関する要素を追加しています。
なお、子クラスは親クラスのすべての要素を受け継ぐので、子クラスに記載されていないメソッドも、子クラスのインスタンスを介して呼び出せます。ただし、親クラスと同じメソッドを子クラスで記載した場合、「オーバーライド」となるため、親クラスのメソッドは隠蔽されることに注意が必要です。そのため、Truckクラスの「Dataメソッド」では、明示的に親クラスの同名メソッドを呼び出しています。
継承の導入により、クラス設計自体は簡潔化できましたが、mainメソッド内で各クラスの生成と操作を個別に行っているのが冗長です。そこでポリモーフィズムを活用し、クラスの管理も簡潔化したのが、以下のサンプルコードです。
//サンプルプログラム
class Vehicle { //車種名 public String name; // 販売価格 public int value; // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); } } class CarA extends Vehicle { // コンストラクタ public CarA() { name = "自家用車A"; value = 2000000; } } class CarB extends Vehicle { // コンストラクタ public CarB() { name = "自家用車B"; value = 3000000; } } class Truck extends Vehicle { // 積載重量 public int capacity; // コンストラクタ public Truck() { name = "トラック"; value = 5000000; capacity = 10; } // 荷物を載せる public void Load(int weight) { // 重量をチェックする if (weight > capacity) { System.out.println("最大積載量" + capacity + "トンを超過しています!"); } else { System.out.println(name + "に" + weight + "トンの貨物を積載します!"); } } // データ表示 public void Data() { // まず親クラスの同名メソッドを呼び出す super.Data(); System.out.println("最大積載量:" + capacity + "トン"); } } public class Main { public static void main(String[] args) { // 各クラスをインスタンス化する Vehicle[] vehicles = { new CarA(), new CarB(), new Truck() }; // 各クラスのメソッドを実行する for (Vehicle v : vehicles) { v.Accelerator(); v.Break(); // TruckならLoadメソッドも実行する if (v instanceof Truck) { ((Truck) v).Load(5); // ダウンキャストで子クラスのメソッドにアクセス } v.Data(); System.out.println(); } } }
//実行結果
自家用車Aのアクセルが踏まれました! 自家用車Aのブレーキが踏まれました! 車種:自家用車A 価格:2000000円 自家用車Bのアクセルが踏まれました! 自家用車Bのブレーキが踏まれました! 車種:自家用車B 価格:3000000円 トラックのアクセルが踏まれました! トラックのブレーキが踏まれました! トラックに5トンの貨物を積載します! 車種:トラック 価格:5000000円 最大積載量:10トン
Mainメソッドでは、すべての基底に位置する「Vehicleクラス」の配列を作成し、そこに各クラスのインスタンスを格納しています。子クラスのインスタンスは、親クラスの型に変換して一括管理することが可能です。この型変換を「アップキャスト」と呼びます。
こうすることにより、ひとつの変数で複数のオブジェクトをまとめて管理でき、for文などで一斉に操作できます。オブジェクト指向プログラミングの基本的なテクニックなので、ぜひ覚えておきましょう。
ただし、アップキャストした変数からは、子クラス固有のメソッドにはアクセスできません。今回の例では、TruckクラスのLoadメソッドが該当します。そのため、for文の中で「instanceof演算子」を使い、そのインスタンスがTruckクラス型かを確認しています。該当する場合は、「ダウンキャスト」でTruckクラス型に変換することで、Loadメソッドへのアクセスが可能です。
ポリモーフィズムの魅力は、仕様変更時にソースコードの変更を最小限に済ませられることです。たとえば、新たに「CarCクラス」や「TruckBクラス」などを追加する場合でも、mainメソッドに大きな変更を加える必要はありません。
これまでのソースコードでは、各クラスのプロパティのアクセス修飾子が「public」だったので、どこからでもインスタンスの値を変更できたことが問題です。これを「private」や「protected」に変更することで、外部からのアクセスを制限できます。また、より拡張性が高いように、「抽象クラス」も採用してみましょう。
//サンプルプログラム
abstract class Vehicle { //車種名 protected String name; // 販売価格 protected int value; // アクセル public void Accelerator() { System.out.println(name + "のアクセルが踏まれました!"); } // ブレーキ public void Break() { System.out.println(name + "のブレーキが踏まれました!"); } // データ表示 public void Data() { System.out.println("車種:" + name); System.out.println("価格:" + value + "円"); } } abstract class Car extends Vehicle { } class CarA extends Car { // コンストラクタ public CarA() { name = "自家用車A"; value = 2000000; } } class CarB extends Car { // コンストラクタ public CarB() { name = "自家用車B"; value = 3000000; } } abstract class Truck extends Vehicle { // 積載重量 protected int capacity; // 荷物を載せる public void Load(int weight) { // 重量をチェックする if (weight > capacity) { System.out.println("最大積載量" + capacity + "トンを超過しています!"); } else { System.out.println(name + "に" + weight + "トンの貨物を積載します!"); } } // データ表示 public void Data() { // まず親クラスの同名メソッドを呼び出す super.Data(); System.out.println("最大積載量:" + capacity + "トン"); } } class TruckA extends Truck { // コンストラクタ public TruckA() { name = "トラックA"; value = 5000000; capacity = 10; } } class TruckB extends Truck { // コンストラクタ public TruckB() { name = "トラックB"; value = 10000000; capacity = 100; } } public class Main { public static void main(String[] args) { // 各クラスをインスタンス化する Vehicle[] vehicles = { new CarA(), new CarB(), new TruckA(), new TruckB() }; // 各クラスのメソッドを実行する for (Vehicle v : vehicles) { v.Accelerator(); v.Break(); // TruckならLoadメソッドも実行する if (v instanceof Truck) { ((Truck) v).Load(50); } v.Data(); System.out.println(); } } }
//実行結果
自家用車Aのアクセルが踏まれました! 自家用車Aのブレーキが踏まれました! 車種:自家用車A 価格:2000000円 自家用車Bのアクセルが踏まれました! 自家用車Bのブレーキが踏まれました! 車種:自家用車B 価格:3000000円 トラックAのアクセルが踏まれました! トラックAのブレーキが踏まれました! 最大積載量10トンを超過しています! 車種:トラックA 価格:5000000円 最大積載量:10トン トラックBのアクセルが踏まれました! トラックBのブレーキが踏まれました! トラックBに50トンの貨物を積載します! 車種:トラックB 価格:10000000円 最大積載量:100トン
Truckクラスにも変更を加え、2種類のトラックのインスタンスを生成しています。ポリモーフィズムを意識した設計なので、オブジェクトの種類や数が増えても、mainメソッドはほとんど変更する必要がありません。
最初のソースコードと比べると、はるかに簡潔・わかりやすいコードになっているはずです。このように、オブジェクト指向プログラムは、簡潔で拡張性の高い、極めて魅力的なプログラミングテクニックなのです!
オブジェクト指向プログラミングに関する基本的な知識と、実際のサンプルコードの例を紹介しました。オブジェクト指向は、ソースコードの簡潔化を可能とするため、プログラム開発の効率化につながります。そのため、オブジェクト指向の習得は、現代のプログラマにとって必須といっても過言ではありません。
オブジェクト指向は、「C++」「Java」「C#」「Python」など、さまざまなプログラミング言語で採用されています。基本的な概念さえわかれば、どの言語でもオブジェクト指向の実現は決して難しくありません。この機会にぜひ、オブジェクト指向プログラミングにチャレンジしてみてください。
2024.06.17
子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介
#プログラミングスクール
2022.01.06
【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説
#プログラミングスクール
2024.01.26
【未経験でも転職可】30代におすすめプログラミングスクール8選!
#プログラミングスクール
2024.01.26
初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説
#JAVA
2024.01.26
忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説
#プログラミングスクール
2022.01.06
【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介
#プログラミングスクール
2024.01.26
【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説
#プログラミングスクール
2024.01.26
【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説
#プログラミングスクール