ProgateのJava学習コースをやっていた時、オブジェクト指向の解説のわかりやすさに感激したので、こちらに忘備録的に要点をメモしておきます。
オブジェクト指向の基本は分かっているよという方には「釈迦に説法」かもしれません。
浅学ながら概念を整理する、学習の過程をメモする感覚でもあるので、文章の中に間違いがあるかもしれませんし、分かりにくいかもしれません。ぜひご指摘いただければと思います。
~目次~
1:はじめに
自分はPythonやJavaScriptで開発をすることが多いのですが、クラスベースにしろプロトタイプベースにしろ、なかなかオブジェクト指向なるものを理解しきるのが難しく、悩んでおりました。ところが、「レガシーな言語」として避けていたJavaのクラスの書き方を習って初めて、ようやくオブジェクト指向の意図と能力を把握することができた気がします。なるべく歩幅を小さくしてオブジェクト指向を説明できればなと思います。
2:関数の効用と限界
授業や本で始めてプログラミング言語を習う時、データ型の話から始まり、制御構文や反復処理、そして関数(又はメソッド)の定義の仕方まで順番に解説が進んでいくと思います。
関数の定義の仕方は改めて言うまでもないことかもしれませんが、クラスを理解するにあたっては関数からの拡張として解釈することがわかりやすいかもしれません。まずは、関数の書き方から振り返りましょう。
public class Main {
public static void main(String[] args) {
System.out.println(hello("a author"));
//hello a author!が出力されます
}
public static String hello(String name){
return "Hello"+" "+name+"!";
}
}
数学において関数はy=f(x)としてあらわされますが、プログラミング言語における関数も似たようなもので、x:入力値、f(x):処理、y:出力値の三つの要素を含みます。しかし、どの要素を関数に組み込むかは任意です。
1public static String hello(String name){ return name; }
2public static String hello(){ String anonymous="anonymous"; return "Hello" +" "+anonymous+"!"; }
1は処理の段階を飛ばした関数(関数として定義する意味はほとんどありませんが)。2は入力値を省いたものです。
2のように関数は中で独自に変数を定義でき、自分で関数を使えばかなり表現の幅が広がるような気がします。が、関数ではできないことがあるため、クラスが使われます。
まず、関数は変数の状態を保持できません(スコープの外から操作ができない)。そして、関数の中で関数を定義することもできません(*)。
public class Main {
public static int num=0;
public static void main(String[] args) {
System.out.println(increment());
System.out.println(increment());
System.out.println(increment());
System.out.println(increment());
//1,2,3,4と順々に加算されます。しかし呼び出されるたびに加算されるようにするには、関数スコープ外の変数を操作するしかありません。
}
public static int increment(){
num+=1;
return num;
}
//increment関数はint increment(){...}としてMain関数の中で作ろうとしても、エラーになります。
//「整数型ではないのか!?」とJavaのコンパイラーが困惑してのエラーだったと思います。
}
関数関数と言ってきましたが、Javaにおいては必ずクラスを定義しないといけないので、もれなくメソッドとして扱われます。「関数の中で関数を定義できない」とは、Javaにおいては「メソッドの中でメソッドが定義できない」のと同義であります。
これが、値と振る舞いを定義してまとめて保持させることのできるオブジェクトの需要へと直結するのです。
(*):ところが、最近は少々事情が変わってきているようです。
JavaScriptにおいては、関数の中で変数や関数を定義できるクロージャ関数というものがあるらしく、Javaにおいてもlambda構文を用いることでクロージャ関数のような振る舞いを実装することができるようです。関数だけでも表現の幅が広がっているようであります。
3:オブジェクト指向
オブジェクト指向とは、プログラミングの世界で情報と機能を持つ「モノ」に見立てたものを作り、それを軸にプログラムを組み上げていく考え方であります。
3_1:クラスとインスタンス化
Javaにおいてオブジェクトはクラスという「設計図」を実体化させる(new演算子の使用)ことで生み出されます。
凄くシンプルな例をお見せします。
public class Main {
public static void main(String[] args) {
Sample sample=new Sample();
System.out.println(sample);
}
}
class Sample{
}
実行結果はSample@76ed5528
となります。
設計図(class Sample)があって、それをnew演算子を使って実体化(インスタンス化)させることで、sampleオブジェクト(ややこしいことに、オブジェクトはインスタンスとも呼ばれます)がつくられていく。この一連の流れがJavaにおけるオブジェクト指向の基本であります。
今のままでは、情報も機能もありません。次に、オブジェクトが独自の情報を持てるように多少改良を加えようと思います。
class Character{
public String name;
}
キャラクタークラスにおける"name"は「インスタンスフィールド」と呼ばれ、アクセス修飾子がpublicならば、オブジェクトからドット記法で操作することができます(インスタンスフィールドは各オブジェクトごとに設定できます)。
JavaScriptではプロパティと呼ばれることの方が多いと思います。
public class Main {
public static void main(String[] args) {
Character character=new Character();
character.name="クッパJr.";
System.out.println(character.name);
}
}
無事に「クッパJr.」が出力されました。可愛いですね。
ですが、このままでは新しくインスタンス化するたびにオブジェクトに対しドット記法で名前を定義せねばなりません。それはとても面倒です。というわけで、インスタンス化の時点で任意の文字列を渡せるように、改良したいと思います。
ここでは、コンストラクターを使います。コンストラクターとはクラスがインスタンス化された直後に自動で呼び出される特別なメソッドです。コンストラクター(建設者)という名称とは裏腹に、コンストラクターがインスタンス化させる実体ではなく、インスタンス化される際に渡された値を変数として保持するための、いわば初期化処理のようなものです。
class Character{
public String name;
Character(String name){
this.name=name;
}
}
Character(){}
がコンストラクターに当たります。JavaScriptではconstructor(){}
という統一的な記法ですが、Javaではクラス名に合わせないといけないのがやや面倒ですね。
引数として渡された文字をインスタンスフィールドにセットする処理がthis.name=name
にあたります。
public class Main {
public static void main(String[] args) {
Character character=new Character("キングテレサ");
System.out.println(character.name);
}
}
ちゃんと「キングテレサ」が出てきました。可愛いですね。
3_2:クラスフィールドとクラスメソッド
アクセス修飾子の後にstaticとつけると、変数に対しては「クラスフィールド」、メソッドに対しては「クラスメソッド」となります。
class Character{
public String name;
public static int height=20;
Character(String name){
this.name=name;
}
public static void action(){
System.out.println("It goes without saying everyone respires.");
}
}
クラス○○とは、その値やメソッドがオブジェクトではなくクラスに帰属しているということで、平たく言えばインスタンス化しなくても使えるメソッドや値と言うことです。
public class Main {
public static void main(String[] args) {
Character character=new Character("ワドルドゥ");
Character.action(); //これはクラスから
character.action(); //これはオブジェクトから
System.out.println(Character.height);
/*
出力結果は以下の通り
It goes without saying everyone respires.
It goes without saying everyone respires.
20
*/
}
}
正しく結果が表示されました。
インスタンスフィールドやインスタンスメソッドが、インスタンス化されたもの(オブジェクト)のそれぞれが別々に値を持てるようにする一方で、クラス○○はクラス自体がその値やメソッドを持っている、即ちすべてのインスタンスに共通の情報を保持しているため、インスタンス化されたオブジェクトからもアクセスすることができます。
3_3:カプセル化
カプセル化とは、オブジェクト内部の変数を秘匿し、変数の取得や変更をメソッドによって制御しようとする手法のことです。
具体には、アクセス修飾子をprivateにすることでインスタンス化したオブジェクトからドット記法でアクセスできなくさせ、代わりにget○○のメソッドによって変数の取得手段を提供します。
class Character{
private String name;
private double combatPower;
Character(String name,int combatPower){
this.name=name;
this.combatPower=combatPower;
}
public String getName(){
return this.name;
}
public double getCombatPower(){
return this.combatPower;
}
public void setCharacter(String name,double combatPower){
this.name=name;
this.combatPower=combatPower;
}
}
Character character=new Character("yuri",5);
としたとき、character.name
とするとエラーが起こります。代わりに、character.getName()
として、nameインスタンスフィールドを取得するのです。
class Compare{
private Character former;
private Character latter;
Compare(Character former,Character latter){
this.former=former;
this.latter=latter;
}
public String compare(){
if(this.former.getCombatPower()>this.latter.getCombatPower()){
return this.former.getName();
}else{
return this.latter.getName();
}
}
}
Character.javaにおいてset○○メソッドも定めているので、以下のようにキャラクターを変えてもcompareメソッドは新しいキャラクターに対応して結果を返してくれます。
public class Main {
public static void main(String[] args) {
Character challenger=new Character("カービィ",100);
Character defender=new Character("デデデ大王",90);
Compare comparing=new Compare(challenger,defender);
System.out.println(comparing.compare());
challenger.setCharacter("バンダナワドルディ",85);
System.out.println(comparing.compare());
/*
~出力結果~
カービィ
デデデ大王
~~~~~~
*/
}
}
やや面倒かもしれませんが、このように変数を直接操作することを禁止し、その機能をメソッドとして提供することで、内部変数の安定性が担保される訳であります。ただし、JavaScriptやPythonではこの機能が完全には実現できていないため、注意が必要です。
3_4:継承の代わりにポリモーフィズム
継承がなぜいけないとされるのか、自分は実感があるわけではないですが、どことなく継承を避けるのがトレンドのようです。
参考:【Quora】オブジェクト指向の問題点の一つとして継承が挙げられるそうですが、継承の何が問題なのでしょうか?
ポリモーフィズムとは、メソッドの呼び出しを共通化して、さらにオーバーライドさせることで、異なるオブジェクトからあるメソッドを呼び出せばそのオブジェクトに固有の結果を返すような仕組みづくりのことです。この実装にはabstractを用いる場合とinterfaceを用いる場合の二通りあります。
《抽象クラスの場合》
抽象クラスは、他のクラスに継承されることを前提として設計されたクラスで、抽象メソッドを含むことができます。抽象メソッドは、実装がない「未定メソッド」で、継承されたクラスでオーバーライドさせるように強制させます(オーバーライドされなかったらエラーが吐かれるので、プログラマーは継承しないと絶対仕事が終われないという意味です)。
これにより、抽象クラスを継承するクラスは抽象メソッドを実装する必要があり、異なるクラスが同じメソッド名で其々の実装を持つことができ、ポリモーフィズムが実現されます。
( ..)φメモメモ
・抽象クラスはインスタンス化できないそうです。
・オーバーライドの際は親クラスのものと同名のメソッドを定義するだけでよいそうです。
・ちなみに、super()というのは子クラスにおけるインスタンス化の際に親クラスのコンストラクターを呼び出すためのメソッドで、superの引数に渡された値が親クラスの引数にセットされるようです。継承を遡るイメージでしょうか。
以下のコードが抽象クラスによるポリモーフィズムの実装の例です。
abstract class Animal {
public abstract void makeSound();
}
// Animal を継承する Cat クラス
class Cat extends Animal {
public void makeSound() {
System.out.println("にゃー");
}
}
// Animal を継承する Dog クラス
class Dog extends Animal {
public void makeSound() {
System.out.println("わんわん");
}
}
// main メソッドでの利用例
public class Main {
public static void main(String[] args) {
Animal[] animals = new Animal[2];
animals[0] = new Cat();
animals[1] = new Dog();
for (Animal animal : animals) {
animal.makeSound();
//わんわんとニャーがそれぞれ出てきます。
}
}
}
《インターフェースの場合》
インターフェースは、抽象メソッドのみを持ち、その実装は子クラスによって提供されます。ある子クラスがインターフェースを実装する場合、そのクラスはインターフェースが定義する全てのメソッドを実装する必要があります。
これにより、異なるクラスが同じインターフェースを実装することができ、そのインターフェースを利用する子クラスは、実際のオブジェクト型に関係なく、同じインターフェースを利用できるため、ポリモーフィズムが実現されるのであります。
以下のコードがインターフェースによるポリモーフィズムの実装例です。
// インターフェース Animal
interface Animal {
public void makeSound();
}
// Animal を実装する Cat クラス
class Cat implements Animal {
public void makeSound() {
System.out.println("にゃー");
}
}
// Animal を実装する Dog クラス
class Dog implements Animal {
public void makeSound() {
System.out.println("わんわん");
}
}
// main メソッドでの利用例
public class Main {
public static void main(String[] args) {
Animal[] animals = new Animal[2];
animals[0] = new Cat();
animals[1] = new Dog();
for (Animal animal : animals) {
animal.makeSound();
}
}
}
そこはかとなく、TypeScriptによる型定義にも見えてきますね。
正直言って、抽象クラスとインターフェースのどっちを使えばいいのかよくわかりません。
もしかしたら、もうちょっとオブジェクト指向に触れて行ったらどこかで出会うかもしれませんが、今のところは使い分けられる自信はないです。
4:JavaScriptはどうなんだ?
JavaScriptもオブジェクト指向言語です。プロトタイプベースの。
Javaはクラスベースのオブジェクト指向言語でした。
なんのこっちゃいという話かもしれませんが、平たく言えば次のようになるでしょうか。
「JavaScriptにclassという設計図はない。全てはオブジェクト(実体)であって、インスタンス化はコンストラクター関数によって実行される。そしてオブジェクトはプロトタイプチェーンで継承されている」
〖クラスベースからプロトタイプベースへ頭を切り替える際に言いかえるべき語彙〗
クラス→コンストラクター関数
インスタンス(オブジェクト)→オブジェクト
継承→プロトタイプチェーン
var Novels=function(label,price){
this.label=label;
this.price=price;
this.getInfo=function(){
return label+"の値段は"+price+"です";
}
};
var oregairu=new Novels("俺ガイル",660);
console.log(oregairu.getInfo());
コンストラクター関数というだけあって、JavaScriptにおいては関数オブジェクトがそのままclassの役割を果たします。
普通の関数と違うのは、返り値を設定しないことと、慣習的に最初の文字を大文字にすることくらいです。new演算子で呼び出すことによって、コンストラクター関数からオブジェクトが生成されることになります。
これが、「クラスのないオブジェクト指向」というものです。
※ちなみに、ES2015以降class構文が導入されましたが、その内実はコンストラクター関数のシンタックスシュガー(糖衣構文)であって、JavaScriptがクラスベースのオブジェクト指向プログラミング言語に生まれ変わった訳ではありません。
それと同時に、JavaScriptでは「プロトタイプ」なるものがあるのでした。
コンストラクター関数からオブジェクトへ逐一メソッドなどをコピーしていったらメモリーを喰って非効率であります。
そこで、コンストラクター関数(オブジェクト)のprototypeプロパティに格納されているプロトタイプオブジェクトにメソッドをセットすることで、各オブジェクトからコンストラクターのprototypeプロパティへの暗黙的な参照を通じたメソッドの呼び出しが可能となり、メモリを節約できます。即ち、オブジェクトは自前でメソッドを持たなくていいのです。
この、各オブジェクトのプロトタイププロパティへの参照が結びつきあって鎖のようになっている様が「プロトタイプチェーン」と呼ばれています。この点でプロトタイプベースは、オブジェクト同士がつながるためには其々がクラスの継承によって枝木のように間接的に結びつき、メソッドや値を共有するしかないクラスベースとは一線を画しています。
var Novels=function(label,price){
this.label=label;
this.price=price;
};
var oregairu=new Novels("俺ガイル",660);
Novels.prototype.getInfo=function(){
return this.label+"の値段は"+this.price+"です";
}
Novels.prototype.string=function(){
return "string";
}
console.log(oregairu.getInfo());
console.log(oregairu.string());
/*
~~~~出力結果~~~~
俺ガイルの値段は660です
string
~~~~~~~~~~~~
*/
ちなみに、
console.log(oregairu.__proto__);
とconsole.log(Novels.prototype);
の実行結果は等しく{ getInfo: [Function (anonymous)], string: [Function (anonymous)] }
になります。
これが、「コンストラクター関数(オブジェクト)のprototypeプロパティに格納されているプロトタイプオブジェクトに値やメソッドを設定することで、インスタンス化されたオブジェクトから暗黙的な参照(.__proto__
)を通じてメソッドを呼び出す」ということであります。
5:おわりに
これまでこんがらがっていた知識が、個人的にはすっきり整理できた気がして、大変満足でございます。
JavaScriptの基本は全て押さえきれたと思うので、あとはReact.jsとかNext.jsとかのフレームワークに関する理解を相当程度深めることで、応用力の有るプログラミングスキルを培っていきたいです。
フロントエンドJavaScriptのフレームワークとしては最近出てきた、DOMを直接操作するのに実行が速いと噂のSolid.jsなどもあるそうなので、こちらもいつか素振りしてみたいなぁと思ったりして。
やりたいことは尽きないけど、時間が足りないいつものあれですね……。