Java Programming
目次へ戻る

オブジェクト指向プログラミング(2)

オブジェクト指向プログラミング(2)では,以下の項目に関して学ぶ.

  1. 継承と合成によるクラスの拡張
  2. 多態性と動的束縛
  3. 配列とコレクション

継承と合成

オブジェクト指向で構築されたクラスやインスタンスを拡張する方法には,継承(inheritance)と合成(composition)がある.継承とは,既存クラスのフィールドやメソッドを引き継いで新しいクラスを定義することである.継承を用いると,すでに存在するクラスに機能を追加したり,一部の機能を書き換えたりするだけで新しい機能を持つクラスを作成することができる.

継承を用いてクラスSmartphoneの機能を拡張するソースコードを以下に示す.

Smartphone.java

public class Smartphone {
    private String name;
    int price;
    
    public Smartphone(String name, int p) {
        this.name = name;
        price = p;
    }
    
    public Smartphone(String name) {
        this(name, 30000);
    }
    
    public String getName() {
        return name;
    }
    
    public void setPrice(int p) {
        price = p;
    }
    
    public int getPrice() {
        return price;
    }
    
    private String getPriceInfo() {
        return String.valueOf(getPrice()) + "-yen";
    }
    
    public void print() {
        System.out.println(name + ": " + getPriceInfo());
    }
}

RentalSmartphone.java

public class RentalSmartphone extends Smartphone {  // クラスSmartphoneを継承
    private int period;                             // レンタル期間データを格納するフィールド
    
    public RentalSmartphone(String name, int price, int period) {
        super(name, price);
        this.period = period;
    }
    
    public void setPeriod(int period) {
        this.period = period;
    }
    
    public int getPrice() {
        return price * period;
    }
}

予約語extendsは継承の宣言を表す.つまり,クラスRentalSmartphonはクラスSmartphonを拡張していることを指す.Smartphonを親クラスあるいはスーパークラス(super-class),RentalSmartphonを子クラスあるいはサブクラス(sub-class)と呼ぶ.Javaでは,すべてのクラスはObject (java.lang.Object)の子クラスであり,extendsを省略した場合,Objectの直接の子クラスになる.また,継承においては,直接の親クラスだけでなく,間接的に継承している親クラス(祖先)からも機能が引き継がれる.つまり,RentalSmartphonObjectの機能も引き継ぐ.コンストラクタは継承されない.ここで,superは親クラスを指す.super()はコンストラクタ内部でのみ利用可能で,親クラスのコンストラクタを呼び出す.

RentalSmartphonから生成したインスタンスは,RentalSmartphonで宣言されたフィールドperiodとメソッドsetPeriod()に加えて,Smartphonの2つのフィールドと5つのメソッドを持つ.ただし,nameのアクセスレベルはprivateであるため,RentalSmartphonから直接アクセスすることはできない.また,getPriceInfo()のアクセスレベルはprivateであるため,これもRentalSmartphonからアクセスできない.

RentalSmartphoneではgetPrice()を再定義することで,Smartphoneの機能の一部を修正している.このように,親クラスに存在するメソッドと同じシグニチャを持つメソッドを子クラスで再定義することをメソッドのオーバーライド(override)という.シグニチャとは,メソッドの名前,引数の型と並び,戻り値の型で構成される.

クラスRentalSmartphonのインスタンスを生成し,そのインスタンスを利用するソースコードを以下に示す.

Sample2.java

public class Sample2 {
    
    public static void main(String[] args) {
        RentalSmartphone phone = new RentalSmartphone("ABC", 2980, 12);
        System.out.println("Name = " + phone.getName());
        System.out.println("Price = " + phone.getPrice());
        phone.print();
    }
}

Sample2.javaのプログラムを実行すると,次のようになる.

% java Sample2
Name = ABC
Price = 35760
ABC: 35760-yen

合成とは,集約関係により既存オブジェクトを包含することで,その機能を拡張することである.新規に作成したクラスへのメソッド呼び出しを,既存クラスのメソッドに委譲(delegation)あるいは転送(forwarding)することで,既存クラスの機能の一部を再利用し,さらに独自の機能を追加することができる.

合成によりSmartphonの機能を拡張するソースコードを以下に示す.

LeasedSmartphone.java

public class LeasedSmartphone {
    private Smartphone phone;     // クラスSmartphoneのインスタンスへの参照
    private String company;       // リース会社の名前データを格納するフィールド
    
    public LeasedSmartphone(Smartphone phone, String c) {
        this.phone = phone;
        company = c;
        
        int price = (int)(phone.getPrice() * 0.8);
        phone.setPrice(price);
    }
    
    public String getCompany() {
        return company;
    }
    
    public int getPrice() {
        return phone.getPrice();
    }
    
    public void print() {
        phone.print();
    }
}

クラスLeasedSmartphoneのコンストラクタは,クラスSmartphoneのインスタンスへの参照を受け取り,それをフィールドphoneに格納している.また,LeasedSmartphoneは,独自に宣言したフィールドcompanyおよびそれにアクセスするメソッドgetCompany()を持つ.さらに,メソッドgetPrice()print()への呼び出しをphonegetPrice()print()にそれぞれ転送することで,その実装を再利用している.

クラスLeasedSmartphoneのインスタンスを生成し,そのインスタンスを利用するソースコードを以下に示す.

Sample3.java

public class Sample3 {
    
    public static void main(String[] args) {
        Smartphone base = new Smartphone("ABC", 35000);
        LeasedSmartphone phone = new LeasedSmartphone(base, "ZZZ");
        System.out.println("Price = " + phone.getPrice());
        phone.print();
    }
}

Sample3.javaのプログラムを実行すると,次のようになる.

% java Sample3
Price = 28000
ABC: 28000-yen

継承による機能拡張は静的(コンパイル時)に決定される.このため,継承を用いて動的(実行時)に機能を拡張することはできない.これに対して,フィールドに格納されたインスタンスへの参照は動的に切り替えることができる.よって,合成による機能拡張は動的に行うことが可能である.

多態性と動的束縛

継承を用いると,類似した機能を持つクラスの共通部分をまとめて親クラスで定義しておき,それぞれ異なる機能だけをその子クラスに定義することができる.その際,親クラスではメソッドの実装を定義せずに,その呼び出し方法(シグニチャ)だけを規定しておきたいことがある.このような要求に対して,抽象クラス(abstract class)が用意されている.実装が定義されていないシグニチャだけのメソッドを抽象メソッド(abstract method)という.

抽象クラスは実装を持たないメソッドを含むため,直接インスタンスを生成することはできない.抽象クラスの子クラスからインスタンスを生成するためには,その子クラスで抽象メソッドをすべて実装する必要がある.実装を持つ(抽象メソッドを持たない)クラスを具象クラスという.

抽象クラスを含むソースコードを以下に示す.

MobileDevice.java

abstract public class MobileDevice {  // 抽象クラスを宣言
    protected String name;            // 名前データを格納するフィールド
    
    protected MobileDevice(String name) {
        this.name = name;
    }
    
    abstract public void feature();   // 抽象メソッドを宣言
}

MobilePhone.java

public class MobilePhone extends MobileDevice {  // クラスMobileDeviceを継承
    
    public MobilePhone(String name) {
        super(name);
    }
    
    public void feature() {                     // 抽象メソッドを実装
        System.out.println(name + " is Cheap but Poor");
    }
}

Tablet.java

public class Tablet extends MobileDevice {  // クラスMobileDeviceを継承
    
    public Tablet(String name) {
        super(name);
    }
    
    public String getName() {
        return name;
    }
    
    public void feature() {                // 抽象メソッドを実装
        System.out.println(name + " is Rich but Expensive");
    }
}

abstractは抽象クラスおよび抽象メソッドを宣言する予約語である.MobileDeviceは抽象クラス,feature()は抽象メソッドを指す.MobileDeviceを継承しているクラスMobilePhoneおよびTabletがそれぞれメソッドfeature()を実装している.よって,これらのクラスからインスタンスを生成することができる.ここで,MobileDeviceのフィールドnameとコンストラクタMobilePhoneの宣言に付与されているprotectedは,そのフィールドやメソッドが子クラス内からアクセス可能なことを指している.詳しい説明は後述する.

クラスMobilePhoneTabletのインスタンスを生成し,それらのインスタンスを利用するプログラムを以下に示す.

Sample4.java

public class Sample4 {
    
    public static void main(String[] args) {
        MobilePhone phone = new MobilePhone("PPP");
        phone.feature();
        
        Tablet tablet = new Tablet("QQQ");
        tablet.feature();
        
        MobileDevice device;
        
        device = phone;
        device.feature();
        
        device = tablet;
        device.feature();
        
        Tablet tablet1 = (Tablet)device;
        System.out.println(tablet1.getName());
    }
}

あるクラスで宣言された型をその親クラス(祖先)の型にアップキャスト(upcast)して扱うことが可能である.つまり,あるクラスのインスタンスをその親クラスのインスタンスとみなすことができる.MobilePhoneおよびTabletから生成したインスタンスを,これらの親クラスであるMobileDeviceにより宣言された(MobileDevice型の)フィールドdeviceに代入している.このように,ひとつのインスタンスが複数の型に属し,それらの型のうちどれかひとつのように振る舞うことを多態性(polymorphism,多相性)という.たとえば,MobilePhoneのインスタンスphoneは,MobilePhone型とMobileDevice型に属しているとみなせる.また,Tabletのインスタンスtabletは,Tablet型とMobileDevice型に属しているとみなせる.

ここで,変数deviceは,MobileDevice型で宣言されている.このとき,MobileDeviceを静的な型(みかけ上の型)という.これに対して,プログラムの実行中に,deviceに格納されているインスタンスの型を動的な型という.代入文device = phoneの実行直後のdeviceの動的な型はMobilePhoneである(静的な型はMobileDeviceである).さらに,代入文device = tabletの実行直後のdeviceの動的な型はTabletに変わる(静的な型はMobileDeviceのままである).このように,動的な型はプログラムの実行中に変化する.

あるクラスで宣言された型をその子クラス(子孫)の型に変換することを,ダウンキャスト(downcast)と呼ぶ.Sample4.javaでは,(Tablet)deviceのようにキャスト演算子を用いることで,deviceの静的な型をTabletに変換している.

Sample4.javaのプログラムを実行すると,次のようになる.

% java Sample4
PPP is Cheap but Poor
QQQ is Rich but Expensive
PPP is Cheap but Poor
QQQ is Rich but Expensive
QQQ

この実行例をみると,メソッドdevice.feature()の呼び出しに対して,MobilePhoneTabletのインスタンスのどちらかが選択され,そのインスタンスのメソッドfeature()が実行されていることがわかる.このように,変数deviceに格納されているインスタンスの型(deviceの動的な型)に応じて,実行時に呼び出されるメソッドが決定されることを動的束縛(dynamic binding)と呼ぶ.

抽象メソッド(と定数)だけで構成されているクラスをインタフェース(interface)という.インタフェースの定義と利用の例を以下に示す.

interface WiFiConnectable {  // インタフェースの定義
    boolean connect();
}
public class MobilePC extends MobileDevice implements WiFiConnectable {  // インタフェースの利用
    public boolean connect() { /* 実装 */ }
}

インタフェースは抽象メソッドしか持たないため,abstractは不要である.また,インタフェースを実装(継承)する際には,extendsではなく,implementsを用いる.インタフェースは,メソッドの実装を持たずにそのシグニチャだけを規定でき,さらにインタフェースを継承するクラスにメソッドの実装を強要するため,フレームワークの構築において頻繁に使われる.

配列とコレクション

Javaには,複数のインスタンスをまとめて扱うために配列(array)とコレクション(collection )が用意されている.

配列とは,同じ型の複数の変数(要素)を並べたものである.配列は[]を用いて宣言し,i番目の要素にアクセスする場合は[i]のように指定する(添字は0からまじまる).Javaでは,配列もクラスとして定義されているため,new演算子を用いて,要素数に応じた領域を確保しなければならない.配列の使用例を以下に示す.

int[] numbers;                                     // int numbers[]でもよい
numbers = new int[10];                             // 10個分の領域を確保
numbers[5] = 1;                                    // 5番目の要素に値を代入 (要素は0番目〜9番目に格納)
System.out.println("5th = " + numbers[5]);         // 5番目の要素を取得
System.out.println("length = " + numbers.length);  // 配列の長さを取得
この例では,要素数10の配列numbersを生成している.配列の長さを取得したい場合は,配列インスタンスのフィールドlengthを用いる.また,配列の要素数は生成時に変数により指定することができる.
int max = 10;                                      // max個分の領域を確保
int[] numbers2 = new int[max];

さらに,配列の要素の型には開発者の定義したクラスを指定することもできる.たとえば,Circleクラスのインスタンスを配列circlesに格納したい場合は,以下のように記述する.

Smartphone[] phones = new Smartphone[2]; 
phones[0] = new Smartphone("A", 250, 35000);
phones[1] = new Smartphone("B", 200);

コレクションとは,任意のインスタンスをまとめて格納するための入れ物である.ArrayList, LinkedList, HashSet, TreeSet, HashMap, Stackなどのクラスと,ListSet, Mapなどのインタフェースが,java.utilパッケージに含まれている.配列を用いた場合,要素数を一度指定すると,その大きさを変えることは基本的にできない.これに対して,コレクションを用いた場合は,その大きさを必要に応じて変更することができる.コレクションjava.util.ArrayListの利用するソースコードを以下に示す.

Sample5.java

import java.util.ArrayList;
import java.util.List;

public class Sample5 {
    
    public static void main(String[] args) {
        List<Smartphone> list = new ArrayList<>();  // コレクションの生成
        Smartphone phone1 = new Smartphone("ABC", 35000);
        Smartphone phone2 = new Smartphone("XYZ");
        list.add(phone1);                                       // 0番目に要素を追加
        list.add(phone2);                                       // 1番目に要素を追加
        
        System.out.println("size = " + list.size());
        for (int i = 0; i < list.size(); i++) {
            Smartphone phone = list.get(i);                     // i番目の要素を取得
            phone.print();
        }
        
        list.remove(1);                                         // 1番目の要素を削除
        
        System.out.println("size = " + list.size());
        for (int i = 0; i < list.size(); i++) {
            Smartphone phone = list.get(i);
            phone.print();
        }
        
        list.add(0, new Smartphone("PQR", 20000));             // 0番目に要素を追加
        
        System.out.println("size = " + list.size());
        for (int i = 0; i < list.size(); i++) {
            Smartphone phone = list.get(i);
            phone.print();
        }
    }
}

総称(generics)により,クラス,インタフェース,メソッドなどの型をパラメータとしてとして定義することができる.Sample5.javaでは,ArrayListの後ろに<Smartphone>と記述する(ArrayListのパラメータにSmartphoneを割り当てる)ことで,listインスタンスに追加できるインスタンスをSmartphone型に制限している.このように,総称を用いることで,listに追加されるインスタンスの型を保証することが可能となる.

Sample5.javaのソースコードをコンパイルして実行すると,以下のようになる.

% java Sample5
size = 2
ABC: 35000-yen
XYZ: 30000-yen
size = 1
ABC: 35000-yen
size = 2
PQR: 20000-yen
ABC: 35000-yen

ここで,総称に関する型チェックは,実行時ではなくコンパイル時に行われるため,以下のコードは,実行時エラーでなく,コンパイルエラーとなる.

List<Smartphone> list = new ArrayList<>();
String str = new String("ABC");
list.add(str);

コレクションクラスのインスタンスに格納されている要素に順番にアクセスするために,反復子(インタフェースjava.util.Iterator)が用意されている.Iteratorを用いた各要素への反復アクセスの例を以下に示す.

Iterator<Smartphone> it = list.iterator();       // listに対する反復子を取得
while (it.hasNext()) {                                     // 要素が存在する場合は繰返し
    Smartphone phone = it.next();                 // 要素を順番に取り出し
    phone.print();
}

要素のアクセスには,反復子のほかに拡張forを利用することもできる.

for (Smartphone phone: list) {    // 要素の順番に取り出し
    phone.print();
}

多態性とコレクションを組み合わせて利用したプログラムを以下に示す.

Sample6.java

import java.util.ArrayList;
import java.util.List;

public class Sample6 {
    
    public static void main(String[] args) {
        List<MobileDevice> list = new ArrayList<>();
        list.add(new MobilePhone("PPP"));
        list.add(new Tablet("QQQ"));
        list.add(new Tablet("RRR"));
        list.add(new MobilePhone("SSS"));
        
        for (MobileDevice device : list) {
            device.feature();
        }
    }
}

Sample6.javaのソースコードをコンパイルして実行すると,以下のようになる.

% java Sample6
PPP is Cheap but Poor
QQQ is Rich but Expensive
RRR is Rich but Expensive
SSS is Cheap but Poor

反復子や拡張forを用いることで,コレクションクラスの型にとらわれずに,各要素にアクセス可能となる.たとえば,クラスArrayListをクラスTreeSet(要素の格納に木構造を用いる)に変更した場合でも,インスタンスの生成以外のコードを変更する必要はない.また,ここでは説明しないが総称を持つクラス(MyList<E>など)を自分で定義することもできる.


演習課題

課題(2-1)

クラス図を記述せよ.

課題(2-2)

Exercise22 を実行した際の画面出力を求めよ.

課題(2-3)

NonCompilable1.java と NonCompilable2.java はコンパイルエラーとなる.エラーの箇所となぜエラーとなるのかを説明せよ.

課題(2-4)

Weather, Sunny, Rainy, Player クラスのクラス図を記述せよ. さらに,Exercise24 を実行した際の画面出力を求めよ.

この設計は,Strategy Design Pattern である.興味のある人は,デザインパターンで調べてみよ.

課題(2-5)

以下のクラス図に基づき,ソースコード Element.java, File.java, Folder.java を作成せよ. さらに,Exercise25 を実行した際の Element, File, Folder クラスに関するオブジェクト図を作成せよ.

この設計は,Composite Design Pattern である.興味のある人は,デザインパターンで調べてみよ.

回答例

回答例はこちら


目次へ戻る ページのトップへ戻る

Copyright 2024 Katsuhisa Maruyama. All rights reserved.