コンテンツにスキップ

RAT Java勉強会2004 第03回 オフジェクトの構成要素

オブジェクト指向といえば「継承」とか「ポリモーフィズム」と結び付けて説明されることが多々あります。
しかし、オブジェクト指向の利点はすべて「カプセル化」を行うという前提で得られるものです。
今回は「カプセル化」に必要な知識として、オブジェクトの構成要素を掘り下げて紹介します。

  • クラスはインスタンスの設計図である

  • インスタンスはクラス(設計図)を元に作られる製造物である

  • “設計図” もオブジェクトであると考えると、あとあと楽

    • 設計図 is-a Object
  • インスタンスは、クラスが持つ”コンストラクタ”を使って生成される
    • コンストラクタは、製造工場で行う作業
  • 戻り値なしの、クラス名と同じのメソッドを定義する感じ
class CafeShop {
public CafeShop() {
// ...
}
}
  • オブジェクトの持つ特性は、次の2つで表現できる
    • 状態
    • 振る舞い
  • オブジェクトからアクセスできるメンバは2種類ある
    • インスタンスフィールド
    • インスタンスメソッド
  • オブジェクト自身が持つ変数
    • オブジェクトの状態を保持する
class Point {
public int x;
public int y;
...
}
  • オブジェクトの振る舞いを表現する
class Point {
public double getX() {
// ...
}
...
}
  • メソッドが呼ばれると、自分自身のオブジェクトを保持するローカル変数 this が自動的にできる

オブジェクトに属するメンバへのアクセス

Section titled “オブジェクトに属するメンバへのアクセス”
  • インスタンスフィールド

    • (オブジェクト) . (フィールド名) でアクセス
  • インスタンスメソッド

    • (オブジェクト) . (メソッド名) でアクセス
  • オブジェクトが this 変数である場合は省略できる

  • Javaではインスタンスだけではなく、クラスにもメンバを持たせることができる。

    • クラスフィールド
    • クラスメソッド
  • 基本的には、これらのメンバにはstatic修飾子がつけられる

    • インスタンスではなく、クラスに属する
  • クラス自身が持つ変数
    • 設計図に何か書き込むというイメージが近い
class Main {
public static final String MESSAGE = "Hello, world!";
public static void main(String[] args) {
System.out.println(MESSAGE);
}
}
  • 特定のパターンでしかクラスフィールドは使用されない

    • クラスで使う定数を表現する場合
    • シリアルナンバーを持つインスタンスを生成
    • シングルトンパターン
    • その他、世界に1つしかないものを保持する場合
  • 多用するとオブジェクト指向の利点が失われる

  • クラス自身の振る舞い
class Main {
public static void main(String[] args) {
int i = Integer.parseInt(args[0]);
}
}
  • 特定のパターンでしかクラスメソッドは使用されない
    • mainメソッド
    • ユーティリティメソッド (SystemやMathなど)
    • ファクトリメソッド (valueOfなどの名前を持つ)
    • 世界に働きかけるためのメソッド (Logging系)

クラスに属するメンバへのアクセス

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イニシャライザ
  • クラスを元に、インスタンスを生成する
    • インスタンスの初期化処理を記述する
class Coffee {
private double sizeInMl;
public Coffee() {
sizeInMl = 180.0;
}
}
  • 一つもコンストラクタを作らないと、引数無しの空コンストラクタが勝手に用意される
  • 始めてこのクラスを使うときに、1度だけ呼び出される
    • クラスフィールドの初期化などに使用する
class Vault {
private static final Random random;
static {
random = new Random(123456789L);
random.nextLong();
random.nextLong();
random.nextLong();
}
}
  • 設計図を作るためのコンストラクタ、というイメージ
  • “複素数”というオブジェクトの構成要素を考える

    • 複素数は実数部と虚数部からなる
  • これは次のように言い換えられる

    • 複素数は実数部を持つ
    • 複素数は虚数部を持つ
  • これらは複素数オブジェクトの振る舞いであるか?

  • A has-a B であるとき、BはAの”状態”とも考えられる

    • 複素数 has-a 実数部
    • 複素数 has-a 虚数部
  • オブジェクトの状態を保持するには、インスタンスフィールドを持てばよい

class Complex {
// 複素数 has-a 実数部
public double realPart;
// 複素数 has-a 虚数部
public double imaginaryPart;
...
}
  • 複素数クラスを次のように変更したい

    • 極座標表現にする
  • どこをどのように変えればよいか

  • realPartとimaginaryPartは消せない

    • 公開仕様であるから変更できない
    • 変更したらComplexを使う全てのクラスを修正する
  • 極座標が変わったら、位置座標も連動しなければならない

  • アクセス修飾子をメンバに付けることができる

    • private (クラス内からのみ)
    • [付けない] (同一パッケージ内からのみ)
    • protected (同一パッケージや子クラスからのみ)
    • public (どこからでも)
  • privateでフィールドを宣言すると、内部表現を公開しないで済む

  • フィールドへのアクセスを「振る舞い」とする方法
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走り、その後に降りた。タクシーは乗客が降りる際に課金された金額を乗客に請求する。“
  • Tanaka

  • 5000円 (5000という数値)

  • タクシー

  • 乗客

  • 料金メーター

  • その車

  • 課金された金額 (数値)

  • この世界では Tanaka = 乗客、タクシー = その車

    • “Tanaka is-a 乗客” という一般化は、次回以降に説明する
  • 乗る
  • リセットする
  • 走る
  • 課金する
  • 降りる
  • 請求する
  • タクシー

    • 乗せる (void takeOn(Tanaka tanaka))
    • リセットする (→リセットされる)
    • 走る (void drive(int km))
    • 課金する (→課金される)
    • 降ろす (void putDown())
    • 乗客に請求 (→支払う)
  • 料金メーター

    • リセットされる (void reset())
    • 課金される (void fare(int yen))
  • Tanaka

    • 請求される (void pay(int yen))
  • タクシーに乗っている乗客
  • 5000円持った状態
  • 課金された金額
  • Tanaka has-a 5000円
  • タクシー has-a 乗客(Tanaka)
  • タクシー has-a 料金メーター
  • 料金メーター has-a 課金された金額
  • タクシーは乗客が降りる際に(料金メーターに)課金された金額を…

  • 料金メーター

    • 課金情報を取得 (int getFared())
class Tanaka {
// Tanaka has-a 5000yen
private int money = 5000;
// 請求される (支払う)
public void pay(int yen) {
this.money = this.money - yen;
}
}
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();
}
public void drive(int km) {
// 1km走るごとに200円ずつ課金する
this.meter.fare(km * 200);
}
public void putDown() {
// 課金された金額を…
int fare = this.meter.getFared();
// …(その金額を)乗客に請求する(乗客が支払う)
this.tanaka.pay(fare);
// (乗客情報をクリア)
this.tanaka = null;
}
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週のサンプルプログラムに次の機能を追加しなさい

  • コーヒーは残量という状態がある

  • 喫茶店が出すコーヒーは、出した直後には200cc入っている

  • Tanakaがコーヒーを飲むと、コーヒーは60cc減る

  • Tanakaが60ccに満たないコーヒーを飲もうとすると、飲めない

  • 課題1にさらに次の機能を追加しなさい

  • クラスCafeShopから作成された喫茶店は、200ccのコーヒーを出す

  • ただし、クラスCafeShopから初めて作成された喫茶店は、サービスで250ccのコーヒーを出す