Javaによるプログラミングに関する世界観2(オブジェクト指向)です。オブジェクト指向も参照のこと。
デザインパターンも参照のこと。
Javaの基本は、特定のインターフェースを介してクラスのデータにアクセスすることです。あるいは、特定のデータを格納し、そのデータを操作するさまざまな機能を提供する、とも言えます。
Javaでは、特定のデータにアクセスするために値型の変数も使えますが、多くの場合クラスのメソッドやインターフェースに隠蔽し、インターフェースを通じてアクセスします。
これは、特定のデータ構造について、さまざまなアクセスを行うためのインターフェースを提供し、ここにさまざまな機能を作成し、提供する、ということであると言えます。
たとえば、ネットワークを介したファイルサーバーを作るのであれば、ファイル名やネットワークアドレスはクラスの中のメンバ変数に保管し、その上でメソッドやインターフェースを通じて、接続やファイルの読み書きなどのネットワークサーバーの機能を提供し、クラスをインスタンス化してメソッドを実行することで、オブジェクト指向のオブジェクトとして、ファイルサーバーとしての機能を使うようにすることができます。
このように、Javaでは特定のデータ構造を使うのであっても、クラスに隠蔽してインターフェースから操作します。このような発想をオブジェクト指向と言います。
クラスを作ってメソッドを実行すれば、何でも作れるのがオブジェクト指向の良さです。ですが、オブジェクト指向に慣れきっていると、逆に分からなくなります。プログラミングは、データ処理と制御であり、計算と変数です。それが分かっていれば、「オブジェクト指向は単にデータをどのように格納し外部から操作するか」という方法論の1つにすぎないと分かるでしょう。
クラスをインスタンス化するのは、アプリケーションの起動や実行と良く似ています。Browserクラスをnew Browser()でインスタンス化するのは、ブラウザのアイコンをクリックしてブラウザを起動するのと同じです。そこが分かると、クラスをどのように設計するのか、ということも見えてきます。Webサービスには、データベース管理用のクラスや、ビュー用のクラスが必要となるでしょう。
オブジェクト指向が流行していることもあって、最近の大規模なプログラムは、多くがクラスとインターフェースによって開発されています。多くのプログラムが、クラスを設計し、そのインターフェースを介して変数にプログラマブルにアクセスすることで、「インターフェースを介してデータを取得する」ことでプログラムを成り立たせています。これはC言語で言うライブラリ関数に近いモデルです。実装の詳細な情報はクラスの中に隠蔽され、プログラマは整理されたクラスのAPI・インターフェースを介して、データ構造とアルゴリズムにアクセスします。Javaのクラスライブラリはとてもよく整理されており、実装の詳細を知らなくても、クラスとメソッドの名前を知っておくだけで、安全に使用することができます。
Javaについて言えるのは、「Javaのプログラム=クラスの設計」ということです。
Javaのプログラムを書く、ということは、クラスの設計をする、ということに他なりません。
たとえば、掲示板を作るのであれば、返信をするメソッドや表示するメソッドが必要ですし、そのために内部的にコメントデータを保管し、それを上手くメソッドの中で利用(アクセス)しなければなりません。
キャッシュ機能を作るのであれば、どのようなクラスにして、どのような配置関係・継承関係にするのかを考えなければなりません。
そう、Javaのプログラムを書くということは、クラスを設計するということなのです。
僕が今思うに、オブジェクト指向の基本とは、「隠蔽」と「再利用」です。
コードの中で、必要に応じて見せるべき部分だけを見せ、それ以外の部分は適切に内部で実行するようにして、外部には見せません。
そして、同じコードを繰り返しコピーするのではなく、構造化プログラムの「関数」という考え方をもっと先に進めて、「クラスの継承」とします。
インターフェースは再利用の技術でもあると同時に、隠蔽の技術でもあります。
可能な限り全てのコードを再利用できるようにした上で、適切に隠蔽し継承すること、これがオブジェクト指向の本質ではないかと思います。
JavaやPHPのようなクラスとオブジェクトを使う言語で言えることとして、初心者にとっては、「継承とインターフェースの実装の使いどころが良く分からない」ということが言えます。
ですが、そうは言うものの、継承とインターフェースは非常に多く使います。
たとえば、ECサイトのカートを作る時にも、イテレータの基本クラスやインターフェースを継承したり実装したりして、メソッドを追加したりオーバーライドしたりするようなことが多くあります。
こういう時には、むしろ、抽象的に考えることです。「プログラマの給料をオーバーライドして二倍にする」という実際のコード例は、どこで使うのかも、それができて何ができるのかも良く分かりませんが、実際はその通り、プログラマのようなデータ構造のクラスをそのまま、給料を二倍にするような用途に使うのだ、と思えば良いのです。
また、機械的な例で言えば、Railsのようなフレームワークでも、継承やインターフェースの実装をばんばん使いますし、Rubyのサンプルコードでも配列を継承して曜日を簡単に計算するデータ構造を作ったりすることができます。特にRailsでは普通のコードを書く場合にもコントローラやレコードのクラスを継承し、継承なくしてRailsは成り立ちません。
そんなこともできるのだ、そういうことにも使えるのだ、という発想と応用が、わけのわからないオブジェクト指向のとっかかりになると思います。使いこなすことは難しいかもしれませんが、そんなものなのだと思うようにしましょう。
僕が思うに、Javaのクラス継承やインターフェースは、関数やライブラリのよりスマートなバージョンだと思います。
Cの関数呼び出しでも、サブルーチンを再利用することはできますが、クラスやインターフェースは、ルーチン(定型処理)だけではなく、プログラムの設計そのものを継承できるようにしたのです。
Javaのオブジェクト指向の基本は、クラスとインターフェースです。
ここで、クラスを「内部の実装のスマートなパッケージ化」、インターフェースは「外部に向けたAPIのスマートな記述」であると言えると思います。
プログラミングとは、APIと実装です。
まず、インターフェース、すなわちAPIがあり、APIに基づいて実装を作り、別の場所からそのAPIでプログラムを実行し、内部の処理は隠蔽されます。
ここで、クラスベースのオブジェクト指向を、「変数空間のパッケージ化」であると言えると思います。
すなわち、メソッドを通じてクラスのメンバ変数を操作し、すべてのメソッドからはメンバ変数にアクセスします。メンバ変数はコンストラクタによって初期化され、インスタンス化された時にパッケージ化されて、それぞれのオブジェクトごとに変数の実体化がなされます。
これを、「変数空間のパッケージ化」と言えると思います。変数空間とは、メンバ変数とメソッドを、グローバル変数に対する関数群として見た時、そのグローバル変数は、クラス内部の変数の名前空間に「隠蔽」されており、それがインスタンス化とともにパッケージ化されるからです。
このように、クラスをインスタンス化して変数空間をパッケージ化し、インターフェースによってアクセスするということが、Javaの基本となります。
実際には、クラスを単にインスタンス化するだけではなく、さまざまなクラスとの関係や構造を「クラス図」にします。クラスをどのように全体で「オーケストラ化するのか」ということが、Javaの開発における基本的な考え方となるでしょう。
Javaにおいて、クラスの作り方を学んでも、「どのようにクラスを使えばいいのか」が分からない人は多いでしょう。
ですが、さまざまなプログラミングの書籍の解説を見ていると、「関連する機能の集まり」であると考えればいいと思います。
ひとつの中核となる「実現したいこと」があったとして、この実現したいことについて、データをメンバ変数に、小さなひとつひとつの操作をメソッドに定義して、そのクラスをオブジェクトとして使う形で使えばいいのです。
中核となる「実現したいこと」は、データ構造の応用だと思えばいいと思います。単なるスタックやキューを実現するだけではなく、「スタックやキューを応用したもっと高度なことがやりたい」と思えば、それがクラスになります。そのスタックやキューの高度版について、必要となるデータをメンバ変数に、必要となるひとつひとつの操作をメソッドに定義して、クラスを実装するのです。
要は、「参照型の変数はコピーしても常に同じ場所を指している」が、「値型の変数はコピーするとひとつひとつ違う場所を指す」ということ。
C言語でいうポインタのようなものだと思えば良い。Javaでは、クラスのオブジェクトは参照型、基本データ型は値型で取り扱われる。
基本データ型の場合、変数を宣言すればそのまま値を代入できる。
int x; x = 100;
だが、配列やクラスなどの参照型の場合、newをしなければ値を代入することはできない。「入れ物」を確保する必要がある。
Hoge obj; obj = new Hoge();
良く使うString(文字列)や配列は参照型であることに注意。
以下の解説が参考になる。Javaでプログラミングする際は、ポインタはないが、基本的に参照型の変数はポインタと同じであると考えれば良い。
クラス。
クラスは、「メンバ変数」と「メソッド(メンバ関数)」で成り立ちます。各メソッドは同じメンバ変数を参照し、new演算子でオブジェクトを作った時に、メンバ変数を生成したクラスのインスタンスが作られます。この時コンストラクタによって変数が初期化されます。このオブジェクトは、メソッドという「メッセージ」によって、「ものを操作する」ように外部から操作されます。ものの中にデータが保持され、ものを扱うようにメソッドでそれ自体が動きます。
public class ClassName { private String text_value; //フィールドの宣言 //メソッドの宣言 public void setText(String arg_str) { text_value = arg_str; } //メソッドの宣言2 public String getText() { return text_value; } }
クラス名は大文字で始めます。
フィールドはメンバ変数のこと。普通、メンバ変数はprivateキーワードをつけて外部からアクセスできないようになっている(不可視)。これをカプセル化と呼ぶ。
たとえば、次のコードはxとyがフィールドである。
public class TwoNumbers { private int x; private int y; ... }
後日注記:フィールドをインスタンスごとに保持するのではなく、クラスの全インスタンスで共有したい場合は、staticをつけることでクラスフィールドにすることができます。
メソッドは、外部からアクセスできるようにpublicキーワードをつける。このメソッドを使って、オブジェクトを外から操作する。メンバ変数を参照・書き換えする場合であっても、メンバ変数を直接触るのではなく、メソッドを介して参照するようにすることが多い。
たとえば、次のコードはadd()がメソッドである。
public class TwoNumbers { private int x; private int y; public int add() { return x + y; } }
後日注記:クラスフィールドと同様、メソッドもstaticをつけることでクラスメソッドにできます。クラスメソッドはインスタンスと関連付けられません。また、インスタンス化しなくても実行できます。
コンストラクタは、オブジェクトの初期化をするメソッド。クラス名と同名のメソッドとして宣言し、newした時に実行される。
自分のインスタンス自身のフィールドにアクセスしたいときは、this.nameのようにする。
super()はスーパークラスのコンストラクタの呼び出しを意味する。また、this()は自分のクラスのコンストラクタの呼び出しを意味する。
たとえば、次のコードはTwoNumbers()がコンストラクタである。
public class TwoNumbers { private int x; private int y; TwoNumbers(int x, int y) { this.x = x; this.y = y; } }
後日注記:スーパークラスのpublicなフィールドやメソッドはサブクラスに継承されますが、コンストラクタは継承されません。ですが、サブクラスのコンストラクタには、スーパークラスの引数なしでのコンストラクタの呼び出しが自動的に挿入されます。引数があるコンストラクタを呼び出したい場合は、super(x, y)のように明示的に記述します。
インスタンスを作ることで、新しい「もの」を作り、新しいデータをオブジェクトに設定・保存することができる。
Javaにはガベージコレクション機能があるため、C++のようにnewしたデータをdeleteする必要はない。
メソッドを使うことで、オブジェクトを外部から操作できる。もちろん内部からメソッドを使うことも可能。
TwoNumbers num = new TwoNumbers(10, 30); System.out.println(num.add());
Javaでは、クラスやメソッド、メンバ変数にアクセス修飾子をつけて、カプセル化を行うことができる。
修飾子 | 説明 |
---|---|
public | すべて公開。 |
protected | 同一パッケージあるいは継承したクラスのみに公開。 |
何もつけない | 同一パッケージのクラスのみに公開。 |
private | 非公開(クラス内部のみで利用)。 |
アクセス修飾子は、たとえば、メンバ変数は非公開(private)にして、アクセサ関数や外部から使用されることを意図したメソッドは公開(public)し、継承された派生クラスが使うメソッドは派生クラス以外からは保護(protected)するという使い方をする。
このように、実装の詳細は隠蔽し、実装の詳細が分からなくても、外部に公開するインターフェースから利用するようにすることを、薬のカプセルになぞらえて、「カプセル化」と呼ぶ。
カプセル化は、クラス継承やインターフェースの実装に並ぶ、オブジェクト指向言語の重要概念である。クラス継承による「機能の拡張」とインターフェースやカプセル化による「公開インターフェースを介した利用」を行うことで、大規模なプログラミングを行う上でも、ひとつの規範に従ってプログラミングを行うことができる。結果、巨大アプリケーションであっても、きちんと動くように作ることができる。
フィールドはprivateにしておき、そのフィールドの内容を得るメソッドと、内容を変更するメソッドを別途宣言しておく方が、拡張性のよいクラスになる。これをアクセスメソッドまたはアクセサと呼ぶ。
たとえば、次のようになる。
public class TwoNumbers { private int x; private int y; public int getX() { return this.x; } public void setX(int x) { this.x = x; } public int getY() { return this.y; } public void setY(int y) { this.y = y; } }
後日注記:JavaにはC#のような代入式を使ってゲッター・セッターを記述するためのプロパティが存在しないため、明示的にゲッターとセッターを記述する必要があります。
以下のページを参照のこと。
以下のページを参照のこと。
Javaのオブジェクト指向は、プログラムをデータとして扱う、ということです。
Javaにおいては、プログラムはデータ構造の中に隠れて存在し、データとプログラムは同じ枠組みの中で同列に扱われます。
この「プログラムをデータとして扱う」という考え方は、LispのS式とよく似ています。
Lispでは、プログラムそのものをS式のリストとして扱うことで、プログラムそのものがデータのように操作できます。
Javaでは、プログラムがデータ構造の中の「ふるまい」として存在し、データを操作するやり方でプログラムを実行します。
このように、JavaとLispの考え方は、まったく異なるやり方でありまったく異なる考え方ではあるものの、「データとプログラムを同一のものであると見なす」という、同じことの努力の結果だと言えます。
だからといって、LispでできることがJavaでできるというわけではありません。逆に、Lispの関数型プログラミングと、Javaのオブジェクト指向プログラミングはよく対立します。Lispでは関数を中心に、副作用のないプログラムコードを書くことを理想としますが、Javaではそれとはまったく逆に、オブジェクトの編集や操作という「副作用」に基づいてプログラミングを行うのです。
なので、どちらかというと、抽象度が高いのはLispであり、実際に実用的かつ生産性も高いのはJavaです。
LispとJavaはまったく異なる言語ではあるものの、データとプログラムの架け橋を築くという意味ではよく似ています。同じことはUNIX、特にFreeBSDやGentoo Linuxのような「プログラムのソースコードとバイナリを扱うOS」にも言えます。
ただし、プログラムとデータを同一のものとして扱うべきでない場合もあります。特に、GUIやWebアプリケーションの場合、ビュー画面とデータとロジックは分割するべきです。ロジックとデータが一緒になっているような設計は避けるべきです。なぜなら、データを変更するだけのためにロジックを書き換える必要があるからです。また、ビューとロジックを分けることで、デザイナーはビューだけを編集し、プログラマはロジックだけを編集できます。商用のシステムを設計する場合にも、フロントエンドとバックエンドは異なる専門のプログラマが担当します。
それでも、データとプログラムが同一のものであると見なせるのは強力です。Emacs LispやMozillaのXULのように、データとプログラムが同一のものであると見なせると、プログラムの拡張性が高まります。プログラムをデータとして扱えるので、データと同じようにプラグインのような拡張機能を後からプログラムに追加できるのです。LispやJavaはそうしたことがとても得意な言語であり、Javaの継承やインターフェースもそのような考え方に基づく拡張性の向上の一種であると言えるでしょう。
2023.09.09
状態を参照のこと。
インターフェースを参照のこと。
保守性も参照のこと。
詳しくは以下の書籍が参考になります。