オブジェクト指向プログラミング(2)では,以下の項目に関して学ぶ.
オブジェクト指向で構築されたクラスやインスタンスを拡張する方法には,継承(inheritance)と合成(composition)がある.継承とは,既存クラスのフィールドやメソッドを引き継いで新しいクラスを定義することである.継承を用いると,すでに存在するクラスに機能を追加したり,一部の機能を書き換えたりするだけで新しい機能を持つクラスを作成することができる.
継承を用いてクラスSmartphone
の機能を拡張するソースコードを以下に示す.
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()); } }
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
の直接の子クラスになる.また,継承においては,直接の親クラスだけでなく,間接的に継承している親クラス(祖先)からも機能が引き継がれる.つまり,RentalSmartphon
はObject
の機能も引き継ぐ.コンストラクタは継承されない.ここで,super
は親クラスを指す.super()
はコンストラクタ内部でのみ利用可能で,親クラスのコンストラクタを呼び出す.
RentalSmartphon
から生成したインスタンスは,RentalSmartphon
で宣言されたフィールドperiod
とメソッドsetPeriod()
に加えて,Smartphon
の2つのフィールドと5つのメソッドを持つ.ただし,name
のアクセスレベルはprivate
であるため,RentalSmartphon
から直接アクセスすることはできない.また,getPriceInfo()
のアクセスレベルはprivate
であるため,これもRentalSmartphon
からアクセスできない.
RentalSmartphone
ではgetPrice()
を再定義することで,Smartphone
の機能の一部を修正している.このように,親クラスに存在するメソッドと同じシグニチャを持つメソッドを子クラスで再定義することをメソッドのオーバーライド(override)という.シグニチャとは,メソッドの名前,引数の型と並び,戻り値の型で構成される.
クラスRentalSmartphon
のインスタンスを生成し,そのインスタンスを利用するソースコードを以下に示す.
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
の機能を拡張するソースコードを以下に示す.
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()
への呼び出しをphone
のgetPrice()
とprint()
にそれぞれ転送することで,その実装を再利用している.
クラスLeasedSmartphone
のインスタンスを生成し,そのインスタンスを利用するソースコードを以下に示す.
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)という.
抽象クラスは実装を持たないメソッドを含むため,直接インスタンスを生成することはできない.抽象クラスの子クラスからインスタンスを生成するためには,その子クラスで抽象メソッドをすべて実装する必要がある.実装を持つ(抽象メソッドを持たない)クラスを具象クラスという.
抽象クラスを含むソースコードを以下に示す.
abstract public class MobileDevice { // 抽象クラスを宣言 protected String name; // 名前データを格納するフィールド protected MobileDevice(String name) { this.name = name; } abstract public void feature(); // 抽象メソッドを宣言 }
public class MobilePhone extends MobileDevice { // クラスMobileDeviceを継承 public MobilePhone(String name) { super(name); } public void feature() { // 抽象メソッドを実装 System.out.println(name + " is Cheap but Poor"); } }
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
は,そのフィールドやメソッドが子クラス内からアクセス可能なことを指している.詳しい説明は後述する.
クラスMobilePhone
とTablet
のインスタンスを生成し,それらのインスタンスを利用するプログラムを以下に示す.
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()
の呼び出しに対して,MobilePhone
かTablet
のインスタンスのどちらかが選択され,そのインスタンスのメソッド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
などのクラスと,List
やSet
, Map
などのインタフェースが,java.util
パッケージに含まれている.配列を用いた場合,要素数を一度指定すると,その大きさを変えることは基本的にできない.これに対して,コレクションを用いた場合は,その大きさを必要に応じて変更することができる.コレクションjava.util.ArrayList
の利用するソースコードを以下に示す.
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(); }
多態性とコレクションを組み合わせて利用したプログラムを以下に示す.
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>
など)を自分で定義することもできる.
クラス図を記述せよ.
Exercise22 を実行した際の画面出力を求めよ.
NonCompilable1.java と NonCompilable2.java はコンパイルエラーとなる.エラーの箇所となぜエラーとなるのかを説明せよ.
Weather, Sunny, Rainy, Player クラスのクラス図を記述せよ. さらに,Exercise24 を実行した際の画面出力を求めよ.
この設計は,Strategy Design Pattern である.興味のある人は,デザインパターンで調べてみよ.
以下のクラス図に基づき,ソースコード Element.java, File.java, Folder.java を作成せよ. さらに,Exercise25 を実行した際の Element, File, Folder クラスに関するオブジェクト図を作成せよ.
この設計は,Composite Design Pattern である.興味のある人は,デザインパターンで調べてみよ.
回答例はこちら
Copyright 2024 Katsuhisa Maruyama. All rights reserved.