RAT Java勉強会2004 第03回 オフジェクトの構成要素
オブジェクト指向といえば「継承」とか「ポリモーフィズム」と結び付けて説明されることが多々あります。
しかし、オブジェクト指向の利点はすべて「カプセル化」を行うという前提で得られるものです。
今回は「カプセル化」に必要な知識として、オブジェクトの構成要素を掘り下げて紹介します。
クラスとインスタンスの関係
Section titled “クラスとインスタンスの関係”設計図と製造物
Section titled “設計図と製造物”-
クラスはインスタンスの設計図である
-
インスタンスはクラス(設計図)を元に作られる製造物である
-
“設計図” もオブジェクトであると考えると、あとあと楽
- 設計図 is-a Object
インスタンスの生成
Section titled “インスタンスの生成”- インスタンスは、クラスが持つ”コンストラクタ”を使って生成される
- コンストラクタは、製造工場で行う作業
コンストラクタの記述
Section titled “コンストラクタの記述”- 戻り値なしの、クラス名と同じのメソッドを定義する感じ
class CafeShop { public CafeShop() { // ... }}オブジェクトが持つべきもの
Section titled “オブジェクトが持つべきもの”- オブジェクトの持つ特性は、次の2つで表現できる
- 状態
- 振る舞い
オブジェクトに属するメンバ
Section titled “オブジェクトに属するメンバ”- オブジェクトからアクセスできるメンバは2種類ある
- インスタンスフィールド
- インスタンスメソッド
インスタンスフィールド
Section titled “インスタンスフィールド”- オブジェクト自身が持つ変数
- オブジェクトの状態を保持する
class Point { public int x; public int y; ...}インスタンスメソッド
Section titled “インスタンスメソッド”- オブジェクトの振る舞いを表現する
class Point { public double getX() { // ... } ...}- メソッドが呼ばれると、自分自身のオブジェクトを保持するローカル変数 this が自動的にできる
オブジェクトに属するメンバへのアクセス
Section titled “オブジェクトに属するメンバへのアクセス”-
インスタンスフィールド
- (オブジェクト) . (フィールド名) でアクセス
-
インスタンスメソッド
- (オブジェクト) . (メソッド名) でアクセス
-
オブジェクトが this 変数である場合は省略できる
クラスに属するメンバ
Section titled “クラスに属するメンバ”-
Javaではインスタンスだけではなく、クラスにもメンバを持たせることができる。
- クラスフィールド
- クラスメソッド
-
基本的には、これらのメンバにはstatic修飾子がつけられる
- インスタンスではなく、クラスに属する
クラスフィールド
Section titled “クラスフィールド”- クラス自身が持つ変数
- 設計図に何か書き込むというイメージが近い
class Main { public static final String MESSAGE = "Hello, world!"; public static void main(String[] args) { System.out.println(MESSAGE); }}-
特定のパターンでしかクラスフィールドは使用されない
- クラスで使う定数を表現する場合
- シリアルナンバーを持つインスタンスを生成
- シングルトンパターン
- その他、世界に1つしかないものを保持する場合
-
多用するとオブジェクト指向の利点が失われる
クラスメソッド
Section titled “クラスメソッド”- クラス自身の振る舞い
class Main { public static void main(String[] args) { int i = Integer.parseInt(args[0]); }}- 特定のパターンでしかクラスメソッドは使用されない
- mainメソッド
- ユーティリティメソッド (SystemやMathなど)
- ファクトリメソッド (valueOfなどの名前を持つ)
- 世界に働きかけるためのメソッド (Logging系)
クラスに属するメンバへのアクセス
Section titled “クラスに属するメンバへのアクセス”-
クラスフィールド
- (クラス名) . (フィールド名) でアクセス
-
クラスメソッド
- (クラス名) . (メソッド名) でアクセス
-
自クラスに属するメンバである場合は省略できる
クラスとインスタンスメンバ
Section titled “クラスとインスタンスメンバ”- 以下のプログラムはエラーになる
class Hello { public static void main(String[] args) { printHello(); } public void printHello() { System.out.println("Hello, world!"); }}-
printHello()は次の省略形である
- this.printHello();
-
クラスメソッドはthisを持たない
-
次のようなイメージ
- 設計図から製品は特定できない
- 製品から設計図は特定できる
クラスとインスタンスの初期化
Section titled “クラスとインスタンスの初期化”- 初期化を行うための機構がある
- コンストラクタ
- staticイニシャライザ
コンストラクタ
Section titled “コンストラクタ”- クラスを元に、インスタンスを生成する
- インスタンスの初期化処理を記述する
class Coffee { private double sizeInMl; public Coffee() { sizeInMl = 180.0; }}- 一つもコンストラクタを作らないと、引数無しの空コンストラクタが勝手に用意される
staticイニシャライザ
Section titled “staticイニシャライザ”- 始めてこのクラスを使うときに、1度だけ呼び出される
- クラスフィールドの初期化などに使用する
class Vault { private static final Random random; static { random = new Random(123456789L); random.nextLong(); random.nextLong(); random.nextLong(); }}- 設計図を作るためのコンストラクタ、というイメージ
”has-a” 関係
Section titled “”has-a” 関係”複素数の構成要素
Section titled “複素数の構成要素”-
“複素数”というオブジェクトの構成要素を考える
- 複素数は実数部と虚数部からなる
-
これは次のように言い換えられる
- 複素数は実数部を持つ
- 複素数は虚数部を持つ
-
これらは複素数オブジェクトの振る舞いであるか?
“has-a” 関係
Section titled ““has-a” 関係”-
A has-a B であるとき、BはAの”状態”とも考えられる
- 複素数 has-a 実数部
- 複素数 has-a 虚数部
-
オブジェクトの状態を保持するには、インスタンスフィールドを持てばよい
複素数クラス
Section titled “複素数クラス”class Complex { // 複素数 has-a 実数部 public double realPart;
// 複素数 has-a 虚数部 public double imaginaryPart; ...}複素数クラスの仕様変更
Section titled “複素数クラスの仕様変更”-
複素数クラスを次のように変更したい
- 極座標表現にする
-
どこをどのように変えればよいか
内部表現と公開仕様
Section titled “内部表現と公開仕様”-
realPartとimaginaryPartは消せない
- 公開仕様であるから変更できない
- 変更したらComplexを使う全てのクラスを修正する
-
極座標が変わったら、位置座標も連動しなければならない
アクセシビリティ
Section titled “アクセシビリティ”-
アクセス修飾子をメンバに付けることができる
- private (クラス内からのみ)
- [付けない] (同一パッケージ内からのみ)
- protected (同一パッケージや子クラスからのみ)
- public (どこからでも)
-
privateでフィールドを宣言すると、内部表現を公開しないで済む
getterとsetter
Section titled “getterとsetter”- フィールドへのアクセスを「振る舞い」とする方法
class Complex { private double radius; private double angle; public double getRadius() { return radius; } public void setRadius(double r) { this.radius = r; }}- 位置座標値を取得したい場合は、振る舞いの中で極座標値から算出すればよい
-
内部表現を隠蔽し、振る舞いのみを提供することを「カプセル化」という
- 使用者は内部表現を気にすることなく、機能だけを使える
-
内部表現の隠蔽によって、先の例では両座標系が混在
-
個人的なルール
- publicなフィールドはstatic finalのみ
- static finalでもmutableなオブジェクトはprivate
- immutableなオブジェクト内でmutableなオブジェクトのgetterはguarded copy必須
続・オブジェクト指向プログラミング
Section titled “続・オブジェクト指向プログラミング”- 次の文章をプログラムに変換してみましょう “Tanakaは5000円持った状態でタクシーに乗った。タクシーは乗客が乗ると料金メーターをリセットし、1km走るごとに200円ずつ課金する。その車で10km走り、その後に降りた。タクシーは乗客が降りる際に課金された金額を乗客に請求する。“
オブジェクトになるもの
Section titled “オブジェクトになるもの”-
Tanaka
-
5000円 (5000という数値)
-
タクシー
-
乗客
-
料金メーター
-
その車
-
課金された金額 (数値)
-
この世界では Tanaka = 乗客、タクシー = その車
- “Tanaka is-a 乗客” という一般化は、次回以降に説明する
振る舞いになるもの
Section titled “振る舞いになるもの”- 乗る
- リセットする
- 走る
- 課金する
- 降りる
- 請求する
オブジェクトが持つ振る舞い
Section titled “オブジェクトが持つ振る舞い”-
タクシー
- 乗せる (void takeOn(Tanaka tanaka))
- リセットする (→リセットされる)
- 走る (void drive(int km))
- 課金する (→課金される)
- 降ろす (void putDown())
- 乗客に請求 (→支払う)
-
料金メーター
- リセットされる (void reset())
- 課金される (void fare(int yen))
-
Tanaka
- 請求される (void pay(int yen))
状態を表すもの
Section titled “状態を表すもの”- タクシーに乗っている乗客
- 5000円持った状態
- 課金された金額
オブジェクトが持つ状態
Section titled “オブジェクトが持つ状態”- Tanaka has-a 5000円
- タクシー has-a 乗客(Tanaka)
- タクシー has-a 料金メーター
- 料金メーター has-a 課金された金額
状態の取得(getter)
Section titled “状態の取得(getter)”-
タクシーは乗客が降りる際に(料金メーターに)課金された金額を…
-
料金メーター
- 課金情報を取得 (int getFared())
クラスの記述 (Tanaka)
Section titled “クラスの記述 (Tanaka)”class Tanaka { // Tanaka has-a 5000yen private int money = 5000;
// 請求される (支払う) public void pay(int yen) { this.money = this.money - yen; }}クラスの記述 (Taxi)
Section titled “クラスの記述 (Taxi)”class Taxi { // Taxi has-a FareMeter private FareMeter meter = new FareMeter();
// Taxi has-a 乗客 private Tanaka tanaka;
// 乗る public void takeOn(Tanaka tanaka) { ... }
// 走る public void drive(int km) { ... }
// 降りる public void putDown() { ... }}メソッドの記述 (Taxi#takeOn(Tanaka))
Section titled “メソッドの記述 (Taxi#takeOn(Tanaka))”public void takeOn(Tanaka tanaka) { // タクシーは乗客が乗ると… this.tanaka = tanaka;
// …料金メーターをリセット this.meter.reset();}メソッドの記述 (Taxi#drive(int))
Section titled “メソッドの記述 (Taxi#drive(int))”public void drive(int km) { // 1km走るごとに200円ずつ課金する this.meter.fare(km * 200);}メソッドの記述 (Taxi#putDown())
Section titled “メソッドの記述 (Taxi#putDown())”public void putDown() { // 課金された金額を… int fare = this.meter.getFared();
// …(その金額を)乗客に請求する(乗客が支払う) this.tanaka.pay(fare);
// (乗客情報をクリア) this.tanaka = null;}クラスの記述 (FareMeter)
Section titled “クラスの記述 (FareMeter)”class FareMeter { // FareMeter has-a fared private int fared;
// リセットされる public void reset() { this.fared = 0; }
// 課金される public void fare(int yen) { this.fared = this.fared + yen; }
// getter of fared public int getFared() { return this.fared; }}class World { public static void main(String[] args) { // Tanakaとタクシーを創造 Tanaka tanaka = new Tanaka(); Taxi taxi = new Taxi();
// Tanakaは(5000円持った状態で)タクシーに乗った。 taxi.takeOn(tanaka);
// その車で10km走り、 taxi.drive(10);
// その後に降りた taxi.putDown(); }}課題1 - 物体の状態
Section titled “課題1 - 物体の状態”-
第1週のサンプルプログラムに次の機能を追加しなさい
-
コーヒーは残量という状態がある
-
喫茶店が出すコーヒーは、出した直後には200cc入っている
-
Tanakaがコーヒーを飲むと、コーヒーは60cc減る
-
Tanakaが60ccに満たないコーヒーを飲もうとすると、飲めない
課題2 - 設計図の状態
Section titled “課題2 - 設計図の状態”-
課題1にさらに次の機能を追加しなさい
-
クラスCafeShopから作成された喫茶店は、200ccのコーヒーを出す
-
ただし、クラスCafeShopから初めて作成された喫茶店は、サービスで250ccのコーヒーを出す