コンテンツにスキップ

オブジェクト指向言語#

パラダイム

教科書5章

講義2回分

序論#

%%{init: {'flowchart': {'curve': 'linear', 'nodeSpacing': 40, 'rankSpacing': 40, 'padding': 5}}}%%
graph TD
  classDef focus stroke-width:4px
  パラダイム --> 命令型;
  パラダイム --> 宣言型;
  命令型 -->|ほぼ同義| 手続き型;
  命令型 --> オブジェクト指向;
  宣言型 --> 関数型;
  宣言型 --> 論理型;
  オブジェクト指向:::focus;

命令型言語に続いて,オブジェクト指向言語(object-oriented language,OO言語)と呼ばれるプログラミングパラダイムについて解説する.オブジェクト指向は命令型言語から派生したパラダイムであり,共通する要素も多い.命令型とOOは背反するパラダイムではないという点に注意せよ.

OO言語では,命令型言語の不足点を補うために数々の抽象的なアイデアが取り込まれている.つかみ取りにくい部分もあるが,基本的には具体的なソースコードに落とし込むことが可能である.すなわち抽象⇔具象の変換が容易である.理解しにくいと感じたらソースコードと対応させて考えよ.

なお,本ページでは教科書の流れを無視して説明するので注意すること.また説明題材にはOO言語の代表としてJavaを用いるが,その他のOO言語にも適用できる内容である.本ページの主題はJavaではなくOO言語である点に注意せよ.

本ページでは犬 is a 動物車 has a タイヤのような,OO言語の典型例題は用いない.過度に抽象化されており不毛と感じるためである.プログラミングの際に多用する配列やスタックなどのデータ構造を例題に説明を進める.

OO言語のねらい#

OO言語の本質的なねらいについて説明する.説明題材として,文字列中に含まれる小文字を全て大文字に変換するtoUpper()関数を考える("Hello#123""HELLO#123").

なお説明していない構文が多数含まれるがここでの本質ではない.プログラム中の細かな構文ではなく,OO言語が実現したい最も本質的な要素を解説している点に注意せよ.

まず命令型言語の再確認から始める.命令型言語でも述べたとおり,命令型言語は代入文を基本的な機械操作とし,条件分岐や繰り返しによってその実行順序を制御する.命令型言語の一つであるC言語を用いたtoUpper()の実装は次のとおりである.

命令型言語におけるtoUpper()
void main() {
    char s[30] = "Hello#123";
    for (int i = 0; i < sizeof(s); i++) {  // 全文字について
        if (s[i] >= 'a' && s[i] <= 'z') {  // もし小文字なら
            s[i] += 'A' - 'a';             // 大文字に変換する
        }
    }
    printf("%s\n", s);               // HELLO#123
}

手続き型言語では,命令型言語の処理の一部を部品化(手続き化・関数化)する.部品化によってシステム全体を簡単な機能に分解し,その複雑さを軽減する.これは分割統治の考えに従う.

手続き型言語におけるtoUpper()
void toUpper(char s[30]) {         // 大文字化の処理を手続き化
    for (int i = 0; i < sizeof(s); i++) {
        if (s[i] >= 'a' && s[i] <= 'z') {
            s[i] += 'A' - 'a';
        }
    }
}
void main() {
    char s[30] = "Hello#123";
    toUpper(s);                    // 手続きを呼び出す
    printf("%s\n", s);             // HELLO#123
}

手続き型言語における部品化という考えは単純かつ有効だったが,開発システムの複雑化に伴いその限界が見えてきた.そこで,より強力な分割統治を実現する手段としてOO言語が生まれた.

手続きからオブジェクトへ#

OO言語の根本的な考えは,システムのあらゆる対象をオブジェクトとして捉える点にある.オブジェクトは,データとデータに対する操作から構成される概念である.

\[ \textrm{Object} = \textrm{Data} + \textrm{Operation} \]

OO言語では,オブジェクトを中心としてあらゆる計算対象をモデル化する.例えば数値や文字列などの基本的な値だけでなく,ユーザ情報や画面の構成部品など,システム内のあらゆる要素はオブジェクトとして扱われる.

OO言語の一つであるJavaではtoUpper()は次のように実装できる(1).

  1. クラスという語を多用するが厳密な定義は無視せよ.後に説明する.ひとまずオブジェクト=クラスと解釈して問題ない.
OO言語におけるtoUpper()
public class CharSequence {     // データと操作を持つクラス
    char[] s;                   // データ(文字の集合)
    public void toUpper() {     // 操作(大文字化)
        for (int i = 0; i < s.length; i++) {
            if (s[i] >= 'a' && s[i] <= 'z') {
                s[i] += 'A' - 'a';
            }
        }
    }

    public CharSequence(String s) { // コンストラクタ.無視してOK
        this.s = s.toCharArray();
    }
    public String toString() {      // データの確認用.無視してOK
        return new String(s);
    }
}
OO言語におけるtoUpper()
public class Main {
    public static void main(String[] args) {
        CharSequence charseq = new CharSequence("Hello#123");
        charseq.toUpper();
        System.out.println(charseq);  // HELLO#123
    }
}

まずtoUpper()の最も本質的な操作(文字a-zの大文字への変換)の処理は大きく変わっていない.代入を基本操作として,繰り返し文と条件文によって実行順序を制御している.

最も重要な特徴は,文字列データsと操作toUpper()CharSequenceというクラスにまとめられている点である.文字列データと文字列に対する操作を一つの単位にまとめ上げることで,命令型言語ではできなかった強力な分割統治が可能となる.

またmain()側にも大きな違いがある.命令型言語ではtoUpper(s)のように,手続きへの引数としてデータを指定していた.命令型言語ではデータと操作は独立して定義されるため,プログラマがどの操作にどのデータを割り当てるかを指定する必要がある.他方,OO言語ではcharseq.toUpper()のように,オブジェクトcharseqを主体として適用したい操作を指定する.操作とデータが一体化しているため,操作を呼び出す際にはCharSequenceクラスの持つ操作の中から選択すればよい.

別の言い方をすれば,命令型言語は「操作」を中心に問題を分割する一方で,OO言語は「オブジェクト」を中心に問題を分割する.例えばシステムを実現する場合,2つのパラダイムは以下のような問題分割の違いが生まれる.

パラダイム 問題分割の単位 分割されたシステムの要素
命令型言語 操作 ユーザの新規作成,ユーザのログイン,
データの新規作成,データの変更,データの削除,
DBへの登録,DBからの参照
OO言語 オブジェクト ユーザオブジェクト,
データオブジェクト,
DBオブジェクト

OO言語は「データと操作は常に一体化せよ」という指針をプログラマに与えている.この指針は単純でありプログラマの直感にも従っている.例えば,toUpper()は文字データに特化した処理であり,文字データとtoUpper()を一体化するという考えは自然である.

Quiz

文字列クラスが持つべき操作,すなわち文字列データと一体化されるべき操作を列挙せよ.先のtoUpper()はその一つである.

Answer

toUpper() toLower() replace(..) substring(..) length()など.

Javaには文字列を扱うStringクラスが標準で用意されている.Stringクラスが持つ操作はAPIドキュメントを確認せよ.なお先のCharSequenceクラスはStringクラスの劣化版であり,自作する必要は一切ない.

Note

オブジェクト指向の各種機能はC言語の構造体や関数ポインタによって再現可能である.OO言語はこの機能を言語仕様として定義しているといえる.

なお,OO言語を使えば自然とオブジェクト指向を実現できるわけではない点に注意せよ.特にJavaはC言語を源流としており,命令型言語にオブジェクト指向の機能を追加した言語である.そのため,Javaを使ってオブジェクト指向の考えに則らないプログラムを記述することもできてしまう.以下のtoUpper()実装はJavaを使った非オブジェクト指向のプログラムである.

OO言語で実装されたオブジェクト指向ではないtoUpper() 👎
public class MainNotOOP {
    public static void toUpper(char s[]) { // ただの手続き
        for (int i = 0; i < s.length; i++) {
            if (s[i] >= 'a' && s[i] <= 'z') {
                s[i] += 'A' - 'a';
            }
        }
    }

    public static void main(String[] args) {
        char s[] = "Hello#123".toCharArray();
        toUpper(s);
        System.out.println(s);  // HELLO#123
    }
}

OO言語にはオブジェクト指向を実践する強制力があるわけではない.オブジェクト指向とは制約ではなく,プログラミングにおける分割統治の指針であると認識せよ.

Note

C言語を使えば自然と構造化プログラミングを実現できるわけではないのと同じである.

  • 構造化プログラミングのためには,意識的にgoto文を排除する必要がある
  • OOプログラミングのためには,意識的にオブジェクトを設計する必要がある

データ構造の隠蔽化#

続いてOO言語の数ある利点の一つであるデータ構造の隠蔽化について取り上げる.説明題材としてスタックの実装を考える.このスタックはint型の要素のみを格納する.

Javaによるスタック実装
public class Main {
    public static void main(String[] args) {
        Stack stack = new ArrayStack();  // オブジェクト生成(1)

        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack);  // [1, 2, 3]

        stack.pop();
        System.out.println(stack);  // [1, 2]

        stack.push(5);
        System.out.println(stack);  // [1, 2, 5]
    }
}
  1. stackの型はArrayStackよりもStackが適切である.理由は後述するので今は気にしなくて良い.
Javaによるスタック実装
public interface Stack {     // Stackのインタフェースの定義
    void push(int element);  // スタックにデータを入れる
    int pop();               // スタックからデータを取り出す
}
Javaによるスタック実装
// 配列を使ったスタック実装
public class ArrayStack implements Stack {
    private int[] elements;
    private int sp;                // スタックポインタ
    public void push(int element) {
        elements[sp++] = element;
    }
    public int pop() {
        return elements[--sp];
    }

    public ArrayStack() {          // コンストラクタ.無視してOK
        sp = 0;
        elements = new int[10];
    }
    public String toString() {     // データの確認用.無視してOK
        int[] buffer = new int[sp];
        System.arraycopy(elements, 0, buffer, 0, sp);
        return Arrays.toString(buffer);
    }
}
Javaによるスタック実装
// Listを使ったスタック実装
public class ListStack implements Stack {
    private LinkedList<Integer> elements;
    public void push(int element) {
        elements.add(element);
    }
    public int pop() {
        return elements.removeLast();
    }

    public ListStack() {           // コンストラクタ.無視してOK
        elements = new LinkedList<>();
    }
    public String toString() {     // データの確認用.無視してOK
        return elements.toString();
    }
}

Stackはスタックの仕様(インタフェース)であり,スタックというクラスが満たすべき以下の条件を定義している.

  • 必ず2つの操作push() pop()を持つ
  • push()のシグネチャ(メソッド名・返り値・引数)はvoid push(int)である
  • pop()のシグネチャ(メソッド名・返り値・引数)はint pop()である

このインタフェースを実装したクラスがArrayStackListStackである.この2つはスタックを保持する内部データ構造が異なる.

ここで最も重要な点は,ArrayStackListStackのどちらを使っても,Mainクラスでの呼び出し方法や結果が一切変わらない点である.スタック内部のデータ構造がクラス内に隠蔽化され,操作が抽象化されているといえる.よってスタックを利用するプログラマが知るべき情報はStackインタフェースのみであり,具体的な実装の中身(この場合ArrayStackListStackの中身)は意識する必要がない.

このようなデータ構造と操作が一体化された型は抽象データ型(abstract data type)とも呼ばれ,オブジェクト指向の登場前から存在していた.OO言語は抽象データ型をオブジェクトという概念に発展させ,言語仕様として抽象データ型の優れた性質(データの隠蔽化等)を実現している.

Note

Stringクラスを利用する際に,Stringクラスのソースコードを確認しなくてよいのと同じである.利用者が知るべき情報は,そのクラスの持つ公開メソッドのシグネチャのみである.よってAPIドキュメント等を調べれば十分である.

Quiz

ArrayStackクラスにはバグがある.どのようなバグでどうすれば発現するか?

Answer

push()pop()時にスタックサイズの上限・下限チェックを行っていない.よって,例えばpop()しすぎると配列外参照を起こす.

public static void main(String[] args) {
    Stack stack = new ArrayStack();

    stack.push(1);
    stack.push(2);
    stack.pop();
    stack.pop();
    stack.pop();  // !!!
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 10
    at stack.ArrayStack.pop(ArrayStack.java:12)
    at stack.Main.main(Main.java:13)

以下のようなチェックが必要.

public int pop() {
    if (sp <= 0) {
        // エラー処理
    }
    return elements[--sp];
}

代表的なOO言語#

C言語やPascal,Fortranなど様々な命令型言語が存在するのと同様に,OO言語も多種多様である.OO言語は純粋型とハイブリッド型の2種類に大別できる.前者は当初からOO言語として設計されたものであり,後者は非OO言語にオブジェクト指向の機能を拡張したものである.

タイプ 言語
純粋なOO言語 Smalltalk,Eiffel,Self,Rubyなど
ハイブリッド型OO言語 Java,C++,Simula,Objective-Cなど
再掲:プログラミング言語の歴史(プログラミング言語

©1

JavaはC言語から派生したハイブリッド言語であり,構文的な類似点はかなり多い.C言語の既習者はJavaに移行しやすいともいえるが,純粋なOO言語ではないため混乱を招く要因もある.特にプリミティブ型と参照型の違いには要注意である.後者はオブジェクトであるが前者はオブジェクトではない.全てをオブジェクトとせよという指針を掲げながら,オブジェクトでない要素を持っている言語である.この点は後述する.

オブジェクトの実現方法としてはJavaが採用するクラスベースだけでなく,プロトタイプベースと呼ばれる方法も存在している(1).現在はクラスベースが主流なため,このページではクラスベースを前提に説明を進める.

  1. JavaScriptがプロトタイプベースである.

OO言語の機能:構築#

データと操作の一体化を実現させるために,OO言語には様々な言語機能が搭載されている.以降ではこの言語機能を2パートに分けて紹介する.

まずはオブジェクトの構築について説明する.数多くの要素が含まれるのでよく学習すること.説明題材には先のArrayStackクラスを用いる.ArrayStackのクラス図は次のとおりである.適宜参照せよ.

classDiagram
    class Stack
    class ArrayStack
    class ListStack
    Stack <|.. ArrayStack : implements
    Stack <|.. ListStack  : implements
    class Stack {
        <<interface>>
        +push(int) void
        +pop() int
    }

クラスとインスタンス#

クラスとはインスタンスを作るための型や設計図のことであり,インスタンスはクラスから生成された実体である.

クラスとインスタンス
                    クラス設計図
                      
Stack stack = new ArrayStack();
        
    インスタンス実体
クラスとインスタンス
                  クラスの定義
public class ArrayStack implements Stack {
    private int[] elements;
    private int sp;
    ...

インスタンスはnew演算子によって生成される.この生成操作をインスタンス化(instantiation,実体化)と呼ぶ.

%%{init: {'flowchart': {'curve': 'linear', 'nodeSpacing': 10, 'rankSpacing': 30, 'padding': 5}}}%%
graph TD
  classDef tr fill:#f6f6f6,stroke:#101010,stroke-dasharray:3;
  classDef tx fill:#f6f6f6,stroke:#101010;
  e1[クラス]:::tr;
  e2[インスタンス]:::tx;
  e1 --->|インスタンス化| e2;

単一のクラスから複数のインスタンスを作ることもできる.

複数のインスタンス生成
Stack stack1 = new ArrayStack();
Stack stack2 = new ArrayStack();  // stack1とは独立なインスタンス

Note

「クラス」と「インスタンス」は明確だが,「オブジェクト」という言葉は多義的なので注意せよ.Javaにおいてはオブジェクトはインスタンスと同じ意味で用いられるが,OO言語の文脈ではクラスとして表現しようとしている概念そのものをオブジェクトと呼ぶこともある.

  • Java:オブジェクト ≒ インスタンス
  • OO:オブジェクト ≒ クラス

混乱回避のため,以降ではオブジェクトという語を極力用いずに,クラスとインスタンスという語を用いて説明を進める.

フィールドとメソッド#

クラス内のデータをフィールド,クラス内の操作をメソッドと呼ぶ.C言語における変数・関数とほぼ同義かつ同じ宣言の記法である.クラス内部で宣言された変数がフィールド,クラス内部で宣言された関数がメソッドと解釈して良い.

フィールドとメソッド
public class ArrayStack implements Stack {
    private int[] elements;              // フィールド
    private int sp;                      // フィールド
    public void push(int element) {...}  // メソッド
    public int pop() {...}               // メソッド
    ...

Note

クラスやフィールドの命名規則には共通認識が存在する.Javaにおいては,クラス名は先頭と区切りを大文字(SomeClass),フィールド名とメソッド名は区切りのみ大文字(someField)である.言語仕様ではないので従わなくてもコンパイルはできるが,極力従うこと.

一種のマナーである.守らなくても機能的には問題ないが無作法で美しくはない.

これらのルールはコーディングスタイルと呼ばれる.GoogleがJavaのコーディングスタイルガイド和訳版)を公開しているので,一度目を通すと良い.

クラスを利用する際には,クラスからインスタンス化(実体化)を行い,生成されたインスタンスに対してメソッド呼び出し等の操作を行う.

インスタンス化とインスタンスに対する操作
Stack stack = new ArrayStack();  // インスタンス化
stack.push(1);  // インスタンスに対する操作(メソッド呼び出し)
stack.push(2);  // インスタンスに対する操作(メソッド呼び出し)
stack.pop();    // インスタンスに対する操作(メソッド呼び出し)

インスタンス化の際には,インスタンスの初期化作業としてクラス内の特別なメソッドが呼び出される.この初期化用メソッドをコンストラクタと呼ぶ.コンストラクタはクラス名と同じ名前,かつ返り値の型宣言がないという特殊な形式で宣言される.

コンストラクタ
public class ArrayStack implements Stack {
    ...
    public ArrayStack() {        // コンストラクタ
        sp = 0;                  // フィールドの初期化
        elements = new int[10];  // フィールドの初期化
    }

コンストラクタの宣言に引数を加えることで,クラス利用者によるインスタンス初期値の指定も可能である.

コンストラクタに引数を追加
public class ArrayStack implements Stack {
    ...
    public ArrayStack(int capacity) {  // 引数にcapacityを追加
        sp = 0;
        elements = new int[capacity];  // 初期化時に配列最大サイズを指定
    }
この場合,クラス利用者は以下のようにインスタンス化を行う.

引数ありインスタンス化
Stack stack = new ArrayStack(50);

単一のクラスに複数のコンストラクタを定義しても良い.複数の初期化方法を宣言できると考えると良い.

複数コンストラクタの利用
Stack stack1 = new ArrayStack();    // デフォルトの最大10個のスタック
Stack stack2 = new ArrayStack(50);  // 最大50個のスタック

フィールド宣言にstatic修飾子を付与すると,staticフィールド(静的フィールド)という特殊なフィールドになる.staticではない一般的なフィールド(非静的フィールド)はインスタンスごとに保持されるデータであるが,staticフィールドはクラス共通で保持される.

これまでのArrayStackクラスの全てのフィールドは,インスタンスごとに保持される非静的フィールドであった.非静的なフィールドは単にフィールドと呼ばれることが多い.

非静的フィールド
public class ArrayStack implements Stack {
    private int[] elements;  // 非静的フィールド
    private int sp;          // 非静的フィールド

もし,ArrayStackクラスでスタックのデフォルト最大個数を定義する場合は,staticフィールドが適切である.

非静的フィールド
public class ArrayStack implements Stack {
    private int[] elements;
    private int sp;
    private static final int DEFAULT_CAPACITY = 10;  // 最大個数

最大個数は定数なためフィールド名を大文字とし,finalによって不変としている.

Quiz

Javaでよく用いられる可変長配列クラスArrayListの初期サイズは何個か?

Answer

10個.

Javaの整数値を格納するIntegerクラスの場合,MAX_VALUEMIN_VALUEなどがstaticフィールドとして保持されている.

Integerクラスの静的フィールドの確認
System.out.println(Integer.MAX_VALUE);  // 2147483647 (2^31-1)

Integer.MAX_VALUEIntegerクラスで全体で共通する不動の値であり,インスタンスごとに保持する必要がないためstaticフィールドが適している.staticフィールドはクラス名.フィールド名のように,インスタンスではなくクラスに対して直接フィールドを呼び出す.

フィールドと同様,メソッドにもstatic修飾を付与できる.個々のインスタンスのフィールドには依存せず,クラスで共通するメソッドはstaticメソッドとするのが適切である.

Note

OO言語に慣れないうちはstatic化は最小限にすることを推奨する.特にstaticフィールドはクラス共通のグローバル変数のように扱えるため,オブジェクト指向の原則(隠蔽化やカプセル化)を容易に破壊してしまう.

どうしてもstaticフィールドを利用したいならfinal修飾子をつけて不変にすると良い.可変なstaticフィールドを作りたくなったら,設計そのものの悪さを疑うべきである.

👎👎👎
public class SomeClass {
    static int someField;  // これは避けるべき(可変 & staticはNG)
}

まとめると以下の通りとなる.

宣言方法 OK/NG 理由
int OK 一般的なフィールドの利用方法
final int OK ↑よりもgood.再書き換え禁止が望ましい
static int NG クラス共通かつ書き換え可なので予期できない
static final int OK クラス共通だが書き換え不可なのでOK

プリミティブ型と参照型#

代表的なOO言語で述べたようにJavaは純粋なOO言語ではなく,全てをオブジェクトとせよというOO言語の原則に則っていない部分がある.その一つはプリミティブ型(値型,基本データ型,primitive type)の存在である.プリミティブ型は値を直接格納する原始的なデータ型であり,booleanint longなどの8種類(1)が該当する.他方,参照型(reference type)は値ではなく参照を格納する型であり,プリミティブ型8種を除く全てのインスタンスが参照型に該当する.

  1. boolean byte char short int float long doubleの8種類.
プリミティブ型と参照型
int i = 0;                            // iはプリミティブ型
boolean b = true;                     // bはプリミティブ型
long l = 1000 * 1000;                 // lはプリミティブ型

Stack stack = new ArrayStack();       // stackは参照型
String str = new String("hi");        // strは参照型
int[] nums = new int[]{1, 2};         // numsは参照型(配列も参照型)

参照型はインスタンスのことであり,データと操作が一体となったオブジェクトと呼べる.一方,プリミティブ型は単なるデータでありオブジェクトではない.当然ながら操作を持たない.

プリミティブ型はオブジェクトではない
int i = 0;
i.method();  // メソッド呼び出しのドットの時点でコンパイルエラー

Javaはオブジェクトではないプリミティブ型が存在するため,純粋なOO言語ではない.

なお,全てのプリミティブ型には対応する参照型が存在する.これをラッパークラスと呼ぶ.例えば,プリミティブ型intには参照型のラッパークラスIntegerクラスが存在している.

ラッパークラスの利用
Integer i = 5;
i.toString();    // iは参照型なのでメソッドを呼び出せる

メモリの側面で考えると,参照型は命令型言語におけるポインタ型(間接参照)と同義である.C言語ではアドレス演算子&や参照外し演算子*を用いて明示的にポインタ操作を指定していたが,Javaではそのような演算子を指定しない.プリミティブ型なら常に値が,参照型なら常に参照が渡される.

プリミティブ型と参照型を実引数に渡すときの挙動
int i = 0;     // 適当なプリミティブ型要素
int j = i;     // 値のコピーが渡される
x.method1(i);  // 値のコピーが渡される
               // method1()の中で仮引数を書き換えても実引数iには影響がない

Stack stack1 = new ArrayStack();  // 適当な参照型要素
Stack stack2 = stack1;            // 参照のコピーが渡される
x.method2(stack1);                // 参照のコピーが渡される
                                  // よってmethod2()の中で仮引数を書き換えると
                                  // 実引数stack1の中身も書き換わる

Javaの引数の結合方法はC言語と同様である(命令型言語#引数の結合方法).すなわち原則としては値渡しのみが利用可能だが,参照を値渡しすることで参照渡しを実現している.

プリミティブ型が存在する理由は実行効率にある.参照型はヒープ領域にその実体データが格納され,それに対する参照をスタック領域に格納する.よって実体データにアクセスするには,一度参照を経由する必要がある(スタックを見てヒープを見る).対し,プリミティブ型はスタック領域やレジスタに値そのものを直接格納する.よって参照を経由することなく値を直接取り出すことができる.intなどの小さく多用される原始的なデータに対しては,参照ではなく値を直接格納することで実行効率の改善を狙っている.

再掲:ヒープとスタック(命令型言語

C言語におけるメモリ領域はいくつかの領域から構成されている.

メモリ領域の構成
┏━━━━━━━━━━━━━━━┓
┃コード,大域変数,静的変数 /* (1)! */
┣━━━━━━━━━━━━━━━┫
┃ヒープ(動的データ)          
┣━━━━━━━━━━━━━━━┫
                            
                              
                            
┣━━━━━━━━━━━━━━━┫
┃スタック(ローカル変数)      
┗━━━━━━━━━━━━━━━┛

  1. コード領域にはコンパイルされた機械語命令列が設置される.まさにプログラムをメモリに設置するノイマン型コンピュータである.グローバル変数はコード領域の下側に設置される.

...

基本的にスタックの方がアクセスが高速である.1変数に割り当てるメモリサイズが固定であり,その操作はプッシュとポップのみで実現できるためである.

より詳細なJavaのメモリ領域についてはJava言語で説明する.

《宿題1》#

Homework 約30分 答え

Java標準のStringクラスのソースコードを確認し,String内に含まれる以下4種類の要素を探せ.それぞれ4個以上列挙せよ.

  • フィールド(非staticな一般的なフィールド)
  • staticフィールド
  • メソッド(非staticな一般的なフィールド)
  • staticメソッド

以下の書式をコピーして回答せよ.

# フィールドprivate final byte[] value;
???

# staticフィールド???

# メソッド???

# staticメソッド???

回答時にはソースコード内の宣言文1行のみをコピペすること.つまりフィールドの場合は宣言文,メソッドの場合はシグネチャ文が答えである.例えば,String.javaのこの一文は通常のフィールドであり,次のように答えれば良い.

# フィールドprivate final byte[] value;

回答フォーム

カプセル化と情報隠蔽#

オブジェクト指向では互いに関連するデータと操作を一つの部品・モジュールとしてまとめ,一部のインタフェースを通じてのみ外部と通信させる.この概念をカプセル化(encapsulation)と呼ぶ.カプセル化はオブジェクト指向の最も基本的な指針であるといえる.

カプセル化のためには適切な可視性(visibility)の制御が必要である.可視性とはメソッドやフィールドをどの範囲まで公開するかを表す性質であり,アクセス修飾子(access modifier)であるpublicprivate等の付与によって制御される.Javaには合計4種類(1)のアクセス修飾子が存在している.

  1. 可視性の広さ順に,public protected なし privateの4つ.なしはpackage-privateとも呼ばれる.

クラス内部のデータ構造は,カプセル化によってクラスの利用者から隠蔽されるべきである.この考えを特に情報隠蔽(information hiding)と呼ぶ.

情報隠蔽
public class ArrayStack implements Stack {
    private int[] elements;              // 隠蔽される
    private int sp;                      // 隠蔽される
    public void push(int element) {...}  // 公開される
    public int pop() {...}               // 公開される

この例では,スタックに対する操作(void push(int)int pop())のみを公開し,データ構造(int[] elementsint sp)を情報隠蔽している.スタックを利用するうえでは,スタック内部がどのようなデータ構造によって実現されているかは一切気にする必要はない.

カプセル化と情報隠蔽の結果,以下のような分割統治が可能となる.

  • クラスの利用者:クラスの公開部分(インタフェース)のみに着目すればよい
  • クラスの提供者:クラスの非公開部分を自由に変更できる
public class ArrayStack implements Stack {


    public void push(int element)
    public int pop()
public class ArrayStack implements Stack {
    private int[] elements;
    private int sp;
                                  {...}
                     {...}

Quiz

Java標準の文字列クラスStringの内部データはbyte[]で保持されている.実際に一文字は何バイトで管理されているか考えよ.

String s = new String("Hello日本語abc");
System.out.println(s);                // Hello日本語abc
System.out.println(s.toUpperCase());  // HELLO日本語ABC
System.out.println(s.length());       // 11
Answer

一文字は1バイト or 2バイト or 4バイト.

  • "A":1バイト
  • "あ":2バイト
  • "👍":4バイト

https://docs.oracle.com/javase/jp/25/docs/api/java.base/java/lang/String.html

Stringは、補助文字をサロゲート・ペアで表現するUTF-16形式の文字列を表します(詳細は、Characterクラスの「Unicode文字表現」セクションを参照)。 charコード単位を参照するインデックス値です。したがって、補助文字はStringの2つの位置を使用します。

文字列の内部構造はかなり複雑である.

ここで重要なことは,クラスの内部構造を全く知らなくても利用は可能という点である.データ構造や手段(メソッドの内部)を利用者から隠蔽化する.利用者は使い方だけ分かれば良い.

極端なケースとしては全てのメソッドとフィールドをpublicにすれば,誰でもどこからでもアクセス可能になる.この場合,カプセル化や情報隠蔽の利点(1)が損なわれてしまう.オブジェクト指向を実践する,すなわちカプセル化を実現するためには可視性を可能な限り狭く設定すべきである.

  1. 「隠されている=知らなくて良い」という利点.

Note

狭い可視性を選ぶ理由は,変数のスコープの限定により考慮すべき事項を削減していたのと同じ理屈である.

再掲:スコープと存続範囲(命令型言語
変数名の有効範囲(スコープ)
int global1;         //  global1
int f(int param) {   //    |     param
    ...              //    |       ↓
}                    //    |
int global2;         //    |    global2
int main() {         //    |       |
    int local1;      //    |       |    local1
    if (...) {       //    |       |      |
        ...          //    |       |      |
        int local2;  //    |       |      |    local2
        ...          //    |       |      |      ↓
    }                //    |       |      |
    ...              //    ↓       ↓      ↓
}                    //

ジェネリクス#

次のクイズから考えよ.

Quiz

これまでのArrayStackは配列要素としてint型のみが利用可能であった.int以外の様々な型を格納できるスタックを作るにはどうすればよいか?

ArrayStackの宣言
public class ArrayStack implements Stack {
    private int[] elements;        // int型の配列しか扱えない
    public void push(int e) {...}  // 当然push()できるのはint型のみ
    public int pop() {...}         // 当然pop()の結果もint型のみ
Answer

答え1(非現実的):#

型ごとにスタッククラスを用意する.IntArrayStackDoubleArrayStackStringArrayStackなど.ただし無限の可能性がある.

答え2(理想):#

型をパラメタ化する.

public class ArrayStack implements Stack {
    private E[] elements;        // E型の配列
    public void push(E e) {...}  // E型をpush
    public E pop() {...}         // E型をpop

    public ArrayStack(E) { // コンストラクタで型指定
        ...                // ※構文的には正しくないので注意

答え3(番外):#

Object型で受け付ける.型付け言語の利点を損なう方法である.詳細は略.

public class ArrayStack implements Stack {
    private Object[] elements;  // Object型で受け付ける
    public void push(Object e) {...}
    public Object pop() {...}

Pythonのような動的型付け言語では,型を明示しないため上記クイズのような問題は発生しないが,Javaのような静的型付け言語では問題となる.このような問題への対策として,型を限定せずに型を抽象化・パラメタ化するジェネリクス(総称型)と呼ばれる言語機能がある.ジェネリクスによりインスタンス化の際に具体的な型を指定できる.

ジェネリクスを取り入れたArrayStack
public class Main {
    public static void main(String[] args) {
             // ↓ここで配列内部の型がIntegerであることを指定する
        Stack<Integer> stack1 = new ArrayStack<>();
        stack1.push(1);                    // ↑ここは省略可
        stack1.push(2);
        stack1.push(3);
        System.out.println(stack1);  // [1, 2, 3]

             // ↓ここで配列内部の型がStringであることを指定する
        Stack<String> stack2 = new ArrayStack<>();
        stack2.push("hello");             // ↑ここは省略可
        stack2.push("world");
        System.out.println(stack2);  // [hello, world]
    }
}
ジェネリクスを取り入れたArrayStack
public interface Stack<E> { // 内部データの型をパラメタ化 パラメタ名はE
    void push(E element);   // push時はE型を受け付ける
    E pop();                // pop時はE型を返す
}
ジェネリクスを取り入れたArrayStack
public class ArrayStack<E> implements Stack<E> {
    private E[] elements;         // intをEに書き換え(パラメタ化)
    private int sp;
    public void push(E element) { // intをEに書き換え
        elements[sp++] = element;
    }
    public E pop() {              // intをEに書き換え
        return elements[--sp];
    }

パラメタ化された型(上記の場合E)を型パラメタと呼ぶ.型パラメタとして指定可能な型は参照型のみでありプリミティブ型は指定できない.よって上記の例ではプリミティブ型intではなく,ラッパークラスであるInteger型を指定している.

OO言語の機能:再利用#

ここまで説明した言語機能によって,オブジェクト指向の基本単位であるオブジェクトの構築が可能となった.

オブジェクト指向ではデータと操作の一体化による分割統治だけでなく,既存オブジェクトの再利用(reuse)による効率的なプログラミングの実現も重要な鍵である.本節では既存オブジェクトを再利用するための言語機能を紹介する.

説明題材にはJavaに標準搭載されたArrayListクラスを用いる.ArrayListは可変長配列を表すクラスであり,Javaの集合系クラスの中では最も汎用的でポピュラーである.これまでのArrayStackとは全く別のクラスである点に注意せよ.スタックではなくアレイ(配列)である.

ArrayListクラスの利用例
public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(2);
        list.add(3);
        list.add(1);
        list.add(1);
        list.add(2);
        System.out.println(list);         // [2, 3, 1, 1, 2]
        System.out.println(list.size());  // 5
        System.out.println(list.get(0));  // 2
    }
}

ArrayListクラスでは以下のようなpublicメソッドを利用できる.

  • 要素の追加 add(value)
  • 要素の取得 get(i)
  • 要素の更新 set(i, value)
  • 配列サイズの取得 size()
  • 配列が空かどうかの確認 isEmpty()
  • ...

継承#

既存オブジェクトを再利用するための重要な仕組みの一つが継承(inheritance)である.継承を用いることで親クラス(parent class,スーパークラス)の持つ全てのフィールドとメソッドが,子クラス(child class,サブクラス)に引き継がれる.

一例として,ArrayListを継承したShuffleArrayListクラスを考える.ShuffleArrayListでは配列内の要素をランダムに並び替えることができる.

ShuffleArrayListの実装
public class Main {
    public static void main(String[] args) {
        ShuffleArrayList<Integer> list = new ShuffleArrayList<>();
        list.add(2);
        list.add(3);
        list.add(1);
        list.add(1);
        list.add(2);
        list.shuffle();                  // 中身をシャッフル
        System.out.println(list);        // [1, 2, 2, 3, 1]
    }
}
ShuffleArrayListの実装
public class ShuffleArrayList<E> extends ArrayList<E> {  // 継承
    private Random rand = new Random();     // シャッフル用乱数

    public void shuffle() {                 // 配列内のシャッフル
        int n = size();
        while(n-- > 0) {                    // 配列の個数回繰り返す
            int i1 = rand.nextInt(size());  // 入れ替え先1
            int i2 = rand.nextInt(size());  // 入れ替え先2
            swap(i1, i2);                   // 入れ替え
        }
    }
    private void swap(int i1, int i2) {  // 2要素のswap
        E e1 = get(i1);                  // 親クラスのget()が使える
        E e2 = get(i2);
        set(i1, e2);                     // 親クラスのset()も使える
        set(i2, e1);
    }
}

ShuffleArrayListクラスはextendsキーワードによりArrayListクラスの継承を宣言している.これにより親クラスArrayListの全フィールドと全メソッドが,子クラスShuffleArrayListで利用可能となる(1).よってget()set()などの配列として必要なメソッドを記述する必要はなく,追加したいshuffle()メソッドのみを記述すればよい.当然get()set()などの親クラスのメソッドは子クラス内部でも利用可能であり,swap()メソッド内で利用している.単純に考えると,親クラスの全メソッド・全フィールドが,子クラスにコピーされているとみなしても良い.

  1. ただし可視性privateのフィールドとメソッドは子クラスからも見えない(使えない).

さらに,可変長配列の本質である集合要素がどのようなデータ構造で管理されているかも意識する必要がない.親クラスが集合要素を適切に管理している(情報隠蔽している)ため,子クラスはその責任から解放されている.

フィールドrandとメソッドswap()の可視性は最も狭いprivateとしている.これらはshuffle()メソッドのための内部要素であり,外部に公開する必要がなく情報隠蔽すべき対象である.

継承関係はクラス図を用いて以下のように表現できる.

classDiagram
    class ArrayList
    class ShuffleArrayList
    ArrayList <|-- ShuffleArrayList : extends
    class ArrayList {
        +add()
        +get()
        +...()
    }
    class ShuffleArrayList {
        +shuffle()
    }

さらにShuffleArrayListの拡張を考える.現状のShuffleArrayListでは要素追加add()の後にshuffle()を呼び出すことで配列内部がランダムに入れ替えられる.もし,配列内要素の並びが常にランダムである状態を保持したいのであれば,ShuffleArrayListクラスに以下のような追記を行えば良い.

ShuffleArrayListで常にshuffle()
public class ShuffleArrayList<E> extends ArrayList<E> {  // 継承
    ...
    @Override  // この講義では無視してOK
    public boolean add(E e) {      // 親クラスのadd()を上書き
        boolean f = super.add(e);  // まず親クラスのadd()を直接呼び出す
        shuffle();                 // その後で並び替える
        return f;                  // 親add()時の要素追加の成否を返す
    }
}

add()のたびに常にshuffle()が呼び出されるため,常に内部要素の並びはランダムとなる.このような親クラスのメソッドの上書きをオーバーライドと呼ぶ.

継承はis-a関係とも呼ばれる.すなわち「子クラスは親クラスである」とみなせる.上記の例では「ShuffleArrayListArrayListである」という関係が成り立つ.ShuffleArrayListは機能的にはshuffle()が追加されているものの,ArrayListとしての特性や性質は一切損なわれていない.

Note

大阪大学 is a 大学は成り立つ.阪大は様々な特徴を持つが大学であることに変わりはない.

多段階の継承も可能である.例えば,ShuffleArrayListを継承したAmazingShuffleArrayListも可能である.この場合「AmazingShuffleArrayListArrayListである」という関係も成り立つ(1).

  1. 継承は推移律を満たす.大阪大学 is a 大学かつ大学 is a 学校ならば,大阪大学 is a 学校も成り立つ.
classDiagram
    class ArrayList
    class ShuffleArrayList
    class AmazingShuffleArrayList
    ArrayList <|-- ShuffleArrayList : extends
    ShuffleArrayList <|-- AmazingShuffleArrayList : extends

継承を用いたプログラミングでは,親クラスに対する差分を子クラスに記述する.これにより同一機能を実装する際に記述すべきコード量が減少する.結果的に生産性の向上やバグの軽減などの恩恵が生まれる.

インタフェースと抽象クラス#

継承されることを前提とした特殊なクラスが2つ存在する.インタフェースと抽象クラスである.

インタフェースはメソッドのシグネチャ(メソッド名・返り値・引数の組)のみを定義する仕組みであり,具体的な実装や手段を分離する.例えば,Java標準のArrayListクラスはCollectionインタフェースを継承している.Collectionインタフェースは,集合を表すクラスが持つべき共通のメソッドシグネチャを定義している(1).

  1. 集合クラスには様々な具体実装がありうる.配列を用いたArrayListやリンク構造を用いたLinkedList,木構造を用いたTreeSetなど様々である.Stackも集合クラスの一種である.
Collectionインタフェース
public interface Collection<E> {//(1)!
    boolean add(E e);  // シグネチャのみでありメソッド内部は記述できない
    int size();
    boolean isEmpty();
    ...
}
  1. ここでは実際の実装を単純化しているので注意せよ.実際にはかなり複雑な継承関係を持っている.
Collectionインタフェース
public class ArrayList<E> implements Collection<E> {
    public boolean add(E e) {
        ... // 継承した先で具体的な手段を記述する
    }
    public int size() { ... }
    public boolean isEmpty() { ... }
    ...
}

クラス図では以下のように表現される.

classDiagram
    class Collection
    class ArrayList
    <<interface>> Collection
    Collection <|-- ArrayList : implements

インタフェースを継承する際はextends(拡張)ではなくimplements(実装)というキーワードを用いる.両者に本質的な差はない.extendsimplementsもどちらも継承である.

  • 実クラスから継承:extends
  • インタフェースから継承:implements

インタフェースを用いることでメソッド内部に記述される具体的な手段の分離が可能になる.データ構造の隠蔽化でも述べたとおり,クラスの利用者はインタフェースのみを知っていれば良く,その内部の手段を意識する必要はない.

続いて抽象クラス(abstract class)を考える.インタフェースにはメソッドのシグネチャのみを記載するが,それだけでは利便性が低い状況がある.例えば,集合クラスにおいて集合が空かを判定するisEmpty()メソッドは以下のように実装できる.

isEmpty()の実装方法
boolean isEmpty() {
    return size() == 0;  // size()がゼロなら空
}

集合の要素数size()の取得方法自体は子クラスのデータ構造に依存するが,isEmpty()の処理内容はあらゆる集合系クラスに共通する.よって,isEmpty()は子クラスではなく親クラス側で定義したいが,親クラスであるCollectionはインタフェースなためメソッド内部を記述することはできない.

このような場合に抽象クラスを用いる.JavaではAbstractCollectionという抽象クラスが設けられており,実際にisEmpty()メソッドの中身はこのクラスで定義されている(1).

  1. AbstractCollection.javaのソースコード
AbstractCollectionクラス
public abstract class AbstractCollection<E> implements Collection<E> {
    ...
    public boolean isEmpty() {
        return size() == 0;
    }
    ...
}

抽象クラスの宣言時はabstractキーワードを付与する.この例では抽象クラスがインタフェースを継承しているが,インタフェースの継承は抽象クラスの必須要件ではない.何も継承しない抽象クラスもあり得る.

まとめるとArrayListのような集合クラスは以下のような継承関係を持っている(1).

  1. これでも実際より単純化しているので注意.
classDiagram
    class Collection
    class AbstractCollection
    class ArrayList
    Collection <|-- AbstractCollection : implements
    AbstractCollection <|-- ArrayList : extends
    AbstractCollection <|-- LinkedList : extends
    AbstractCollection <|-- HashSet : extends
    AbstractCollection <|-- Stack : extends
    AbstractCollection <|-- Queue : extends
    class Collection {
        <<interface>>
    }
    class AbstractCollection {
        <<abstract>>
    }

抽象クラスとインタフェースは継承を前提としており,それ単体をインスタンス化することはできない.またインタフェースは仕様であるという性質上,可視性を記述する必要はない.全メソッドがpublicであることが自明なためである.クラスと抽象クラス,インタフェースの特性の違いは次の表のとおりである.

クラス 抽象クラス インタフェース
具体例 ArrayList* AbstractCollection* Collection*
宣言時の予約語 class abstract class interface
継承時の宣言 extends extends implements
インスタンス生成 可能 不可能 不可能
フィールド宣言 可能 可能 不可能
コンストラクタ宣言 可能 可能 不可能
抽象度 最低 最高

Note

多くの概念や要素を説明してきたが「暗記」は本質ではない.個々の概念を「理解」できるよう努力せよ.理解の伴わない暗記には意味がない.

本節ではOOにおける良い設計の一例として,ArrayListクラス周辺のクラス階層について説明した.集合クラスが様々な観点で抽象化されている点を認識せよ.

ポリモーフィズム#

継承を用いることでポリモーフィズム(Polymorphism,多態性,多様性,多相性)と呼ばれる概念を実現することができる.ポリモーフィズムとは,異なる性質を持つインスタンスを全く同一の操作で扱うことができる性質のことである.

Java標準の集合クラスを用いてポリモーフィズムの振る舞いを説明する.集合クラスとしては,単純な配列を表すArrayList以外にも,重複を許さないLinkedHashSet,重複を許さずさらに要素を自動ソートするHashSetなどがある.

public static void main(String[] args) {
    Collection<Integer> list = new ArrayList<>();
    list.add(2);
    list.add(3);
    list.add(1);
    list.add(1);
    list.add(2);
    System.out.println(list);  // [2, 3, 1, 1, 2]
}
public static void main(String[] args) {
    Collection<Integer> list = new LinkedHashSet<>();
    list.add(2);
    list.add(3);
    list.add(1);
    list.add(1);
    list.add(2);
    System.out.println(list);  // [2, 3, 1]
}
public static void main(String[] args) {
    Collection<Integer> list = new HashSet<>();
    list.add(2);
    list.add(3);
    list.add(1);
    list.add(1);
    list.add(2);
    System.out.println(list);  // [1, 2, 3]
}

インスタンス化以外の操作は全く同一であるにも関わらず,その出力結果はクラスによって異なっている.共通のインタフェースadd()の呼び出し時に,各集合クラスの特性(ソートする・重複を許す等)に応じて適切な要素追加が行われる.

Note

RPGのようなゲームにおいて,全敵キャラクターに攻撃をさせる場合は以下のように記述できる.

ポリモーフィズムの利点
for (Enemy enemy : enemies) {  // 全敵について 🐉 🐛 🕷️...
    enemy.attack();            // 攻撃をさせる 🔥 💥 🕸...
}

共通の性質を持つクラスのインタフェースが統一化されており,その呼び出しが容易で可読性も高い.また,追加で新たな敵種類を実装しても呼び出し側の変更は不要であり,拡張が容易である.

ポリモーフィズムが使えない場合,以下のような呼び出しが必要となる.

ポリモーフィズムが使えない場合
for (Enemy enemy : enemies) {  // 全敵について 🐉 🐛 🕷️...
    if (enemy instanceof Dragon) {
        enemy.breath();        // 🔥
    } else if (enemy instanceof Caterpillar) {
        enemy.rolling();       // 💥
    } else if (enemy instanceof Spider) {
        enemy.spin();          // 🕸...
    } else if ...

コンポジションと委譲#

継承とは異なる再利用の仕組みとしてコンポジションと委譲がある.

委譲(delegation)とは,処理を自分自身で行わず別のオブジェクトに任せるという概念である.例として,カードゲームにおけるデッキ(山札)のクラス設計を考える.単純化のために,各カードは数字のみ(スートなし)であり,1~8までの8枚のみとする.

%%{init: {'flowchart': {'curve': 'linear', 'nodeSpacing': 10, 'rankSpacing': 30, 'padding': 3}}}%%
graph TD
  classDef tx fill:#f6f6f6,stroke:#101010;
  e1[1]:::tx;
  e2[2]:::tx;
  e3[3]:::tx;
  e4[4]:::tx;
  e5[5]:::tx;
  e6[6]:::tx;
  e7[7]:::tx;
  e8[8]:::tx;
デッキクラスの中身
[1, 2, 3, 4, 5, 6, 7, 8]  // 初期状態
[2, 5, 3, 6, 8, 4, 1, 7]  // シャッフル後

デッキクラスの操作は次の3つを考える.

  • int size():デッキの残り枚数を確認する
  • int deal():デッキから1枚カードを抜く
  • void shuffle():デッキをかき混ぜる

デッキクラスのデータ(カード群)はint型配列として表現できるため,既存のArrayListクラスをうまく再利用したい.すなわち配列に関する基本操作をArrayListクラスに委譲したい(任せたい).

ArrayListへ委譲するデッキクラス
public static void main(String[] args) {
    Deck deck = new Deck();
    System.out.println(deck);        // [1, 2, 3, 4, 5, 6, 7, 8]
    System.out.println(deck.size()); // 8

    deck.shuffle();
    System.out.println(deck);        // [2, 5, 3, 6, 8, 4, 1, 7]
    System.out.println(deck.deal()); // 7
    System.out.println(deck.deal()); // 1
    System.out.println(deck);        // [2, 5, 3, 6, 8, 4]
    System.out.println(deck.size()); // 6
}
ArrayListへ委譲するデッキクラス
public class Deck {
    private List<Integer> deck;
    public Deck() {                      // コンストラクタ
        deck = new ArrayList<>();
        for (int i = 0; i < 8; i++) {
            deck.add(i + 1);             // 1~8のカードを格納(委譲)
        }
    }
    public int size() {                  // デッキの残り枚数を確認
        return deck.size();              // deckにサイズを聞く(委譲)
    }
    public int deal() {                  // 1枚カードを抜く
        int lastIndex = size() - 1;
        int card = deck.get(lastIndex);  // 配列から1枚取り出す(委譲)
        deck.remove(lastIndex);          // 配列から削除(委譲)
        return card;
    }
    public void shuffle() {              // デッキをかき混ぜる
        ...
    }

DeckクラスはフィールドとしてArrayListのインスタンスを保持しており,デッキデータの格納先として利用している.デッキ内の残り枚数を取得するsize()メソッドでは,deck.size()を呼び出して処理を委譲している.Deckの開発者は配列の基本操作を全てArrayListクラスへ委譲させ,デッキクラスとして必要なメソッド,つまりdeal()shuffle()等のみを実装すれば良い.このように委譲を用いることで,既存クラスの再利用が可能となる.

あるオブジェクトAが異なる別のオブジェクトXに分解できるとき,AとXの関係をコンポジションという.これをhas-a関係と呼ぶ.A has a Xが成り立つ.

Deckクラスは内部にArrayListを抱えている.すなわちDeck has an ArrayListである.

コンポジションの例
public class Deck {
    private List<Integer> deck;
    ...
classDiagram
    direction TD
    Deck o-- ArrayList

コンポジション(has-a)はオブジェクト間の「関係」のことであり,委譲は処理を別オブジェクトに依頼する「行為」である.委譲はコンポジション関係を作るための手段である.

継承 vs コンポジション#

最後に,継承(is-a)とコンポジション(has-a)の違いについて考える.いずれもオブジェクト間の関係を表す概念である.

関係 英語表記 関係の作り方・手段
継承 is-a 継承
(自分自身が引き継ぐ,extends宣言する)
コンポジション has-a 委譲
(別クラスに任せる,内部に別クラスを持つ)

Quiz

次の2項が継承(is-a)かコンポジション(has-a)かを考えよ.

  • 犬と動物
  • 車とタイヤ
  • リンゴとフルーツ
  • 学生と学籍番号
Answer
  • 犬 is a 動物
  • 車 has a タイヤ
  • リンゴ is a フルーツ
  • 学生 has a 学籍番号

オブジェクトを設計する際は,物事間の関係をよく考えて設計すること.

継承とコンポジションはいずれもオブジェクト再利用の概念あり,共通点が多数ある.継承はコンポジションで代替が可能であり,その逆も可能である.例えば,先のDeckクラスはArrayListの委譲(コンポジション)ではなく継承でも実現できる.この場合,Main側の呼び出しやその結果も変わらない.

ArrayListへ委譲するデッキクラス
public class Deck {
    private List<Integer> deck;
    public Deck() {
        deck = new ArrayList<>();
        for (int i = 0; i < 8; i++) {
            deck.add(i + 1);
        }
    }
    public int size() {
        return deck.size();
    }
    public int deal() {
        int lastIndex = size() - 1;
        int card = deck.get(lastIndex);
        deck.remove(lastIndex);
        return card;
    }
    public void shuffle() {
        ...
    }
ArrayListを継承するDeckクラス
public class Deck extends ArrayList<Integer> {

    public Deck() {

        for (int i = 0; i < 8; i++) {
            add(i + 1);
        }
    }



    public int deal() {
        int lastIndex = size() - 1;
        int card = get(lastIndex);
        remove(lastIndex);
        return card;
    }
    public void shuffle() {
        ...
    }

privateフィールドとして保持していたdeckは継承によって自分自身となる.よって,継承ではdeckフィールドの宣言が不要となり,またdeckインスタンスに対するメソッド呼び出しdeck.も不要となる.

継承に適した問題とコンポジションに適した問題があるので,適切に使い分ける必要がある.基本的にはis-a関係が成立している特殊な場合のみ継承とし,そうでない場合はコンポジションとすると良い.Deckクラスの場合,DeckArrayListを利用しているだけであり,Deck is an ArrayListという構造は適切ではない.よってコンポジションが適すると判断できる.

継承の問題点は親クラスの全メソッドを子クラスが引き継ぐ点にある.もしDeckArrayListを継承してしまうと,デッキが持つべきではないメソッドまで引き継いでしまう.例えば,void ensureCapacity(int)* は可変長配列の容量を調整するメソッドである.これはデッキクラスが持つべきメソッドではない.

Note

近年のプログラミングでは,"composition over inheritance"(継承ではなくコンポジションを優先せよ) という考えが広まっている2

オブジェクト指向が登場して50年以上経つが,現在でも上記のような議論がなされている.この事実から,継承とコンポジションの適切な使い分けは容易ではないことがうかがえる.適切なオブジェクト設計には,理論の理解だけではなく実践と経験が必須である.

継承とコンポジションを考える際は,まずはその性質の理解から始めると良い.適切な使い分けや効果的な利用方法は,後に身につければよい.

Quiz

本ページでは以下2つの宣言を使い分けていた.

List<Integer>      list = new ArrayList<>();  // インスタンスはList型である
ArrayList<Integer> list = new ArrayList<>();  // インスタンスはArrayList型である
どちらの宣言が良いだろうか?2つをどう使い分けるべきだろうか?

まとめ#

オブジェクト指向言語のねらいを解説した.オブジェクトとはデータと操作で構成される概念であり,強力な分割統治を実現する.またオブジェクトの構築再利用を実現する言語機能について紹介した.

《宿題2》#

Homework 約30分 答え

まずは適当なJavaの実行環境を用意せよ.その環境下で次のHomeworkクラスのmain()メソッドを実行し,その出力結果を確認せよ.123という文字が6行得られるはずである.

public class Homework {
    public static void main(String[] args) {
        A aaa = new A();
        B bbb = new B();
        C ccc = new C();
        System.out.println(aaa.x());  // 123
        System.out.println(aaa.y());  // 123
        System.out.println(bbb.x());  // 123
        System.out.println(bbb.y());  // 123
        System.out.println(ccc.x());  // 123
        System.out.println(ccc.y());  // 123
    }
}
class A {
    public String x() { return "123"; }
    public String y() { return "123"; }
}
class B extends A {
    public String x() { return "123"; }
}
class C {
    private A a = new A();
    public String x() { return "123"; }
    public String y() { return a.y(); }
}

Q1.#

出力結果とともに,その出力文字列が最終的にどのクラスのどのメソッドから得られたかを答えよ.例えば,a()b()のような呼び出し系列に対してはb()が答えである.また委譲によって得られた出力結果には「委譲」と一言を添えよ.

Q2.#

次の2項がis-aかhas-aか答えよ.

  • クラスBとクラスA
  • クラスCとクラスA

以下の書式をコピーして回答せよ.

# Q1
123  // クラスAのメソッドx()
123  // クラス?のメソッド?()
123  // クラス?のメソッド?() 委譲
???  // ???

# Q2
B ??? A
C ??? A

回答フォーム