抽象クラス

これまで継承を使ってクラスを拡張していくことを学んできましたが、逆に考えれば共通する項目(インスタンス変数、メソッド)を抽出して行けば効率よくクラスを構成できると考える事ができます。
本・CDなど幾つかの種類の商品を扱うオンライン ストアを構築するとした場合、それぞれのデータ項目に関してクラスを設計していくことになるでしょう。
しかし、本であってもCDであってもオンライン ストアから見た場合は、商品名と価格を持っている「商品」です。
したがって、商品名や価格などを定義した商品クラスを定義し、そのサブクラスとして本クラス、CDクラスと設計していくと効率がよさそうです。

このように様々なクラスの共通する項目を抽出していく作業は抽象化と呼べます。

抽象クラス

オンラインストアの例では、抽象化により商品クラスが生まれました。
この商品クラスを通常のクラスとして定義してサブクラスを作っていくこともできます。
しかし、本クラスやCDクラスのインスタンスを考えた時にはイメージが沸きますが、商品クラスのインスタンスはもやもやとしたイメージしか沸きません。

このような場合、クラスを抽象クラスとして定義することができます。
抽象クラスを宣言するには、classの前にabstractキーワードを置くだけです。

// 抽象商品クラス
public abstract class AbstractItem
{
    // 価格
    private int price;
    // 価格設定
    public void setPrice(int value)
    {
        price = value;
    }
    // 価格取得
    public int getPrice()
    {
        return price;
    }
}
// 本クラス
public class Book extends AbstractItem
{
}
// CDクラス
public class CompactDisc extends AbstractItem
{
}

抽象クラスは解りやすいように名前をAbstractで始めると良いでしょう。

抽象メソッド

抽象クラスの1つ目の特徴は、抽象メソッドを定義できることです。
抽象メソッドとは実装のないメソッドの事で、定義だけがあるとみなせます。

例えば、本とCDではそれぞれデフォルトの異なる値引率が設定できるようにしたいとします。
異なる値引率ですから、それぞれのクラスにメソッドがあるでしょう。

// 抽象商品クラス
public abstract class AbstructItem
{
    // 価格
    private int price;
    // 価格設定
    public void setPrice(int value)
    {
        price = value;
    }
    // 価格取得
    public int getPrice()
    {
        return price;
    }
}
// 本クラス
public class Book extends AbstractItem
{
    // 値引率(%)の取得
    public int getPriceOffRate()
    {
        return 10;
    }
    // 値引価格の取得
    public int getOffPrice()
    {
        return getPrice() * getPriceOffRate() / 100;
    }
}
// CDクラス
public class CompactDisc extends AbstractItem
{
    // 値引率(%)の取得
    public int getPriceOffRate()
    {
        return 15;
    }
    // 値引価格の取得
    public int getOffPrice()
    {
        return getPrice() * getPriceOffRate() / 100;
    }
}

値引率は異なるのですが、値引き価格を取得するメソッドは同じように定義できます。
上手くAbstractItemに移動したいのですが、getPriceOffRateメソッドはそれぞれのサブクラスに定義しなくてはなりません。

このような時、抽象メソッドを使えば次のように書くことができます。

// 抽象商品クラス
public abstract class AbstractItem
{
    // 価格
    private int price;
    // 価格設定
    public void setPrice(int value)
    {
        price = value;
    }
    // 価格取得
    public int getPrice()
    {
        return price;
    }
    // 値引率(%)の取得(抽象メソッド)
    abstract public int getPriceOffRate();
    // 値引価格の取得
    public int getOffPrice()
    {
        return getPrice() * getPriceOffRate() / 100;
    }
}
// 本クラス
public class Book extends AbstractItem
{
    // 値引率(%)の取得
    public int getPriceOffRate()
    {
        return 10;
    }
}
// CDクラス
public class CompactDisc extends AbstractItem
{
    // 値引率(%)の取得
    public int getPriceOffRate()
    {
        return 15;
    }
}

抽象メソッドではabstractキーワードを付与し実装を記述しません。
このように抽象クラスでは抽象メソッドを定義できます。

値引率の取得は抽象メソッドとして抽象クラスに記述されましたが、実装はありません。
ですが、値引き価格の取得メソッドのようにメソッドを使用することができます。
そして、実際にどのような処理が行われるかはサブクラスの実装に依存します。

実装の強制

抽象クラスに抽象メソッドを定義したならばサブクラスでは抽象メソッドを実装しなくてはなりません。
これは言語仕様レベルで定められており実装がない場合はコンパイルエラーとなります。
つまり、抽象メソッドとすることでサブクラスで実装を強制することができると言えます。

この性質はしばしば処理のテンプレートとして使用されます。
処理の前後に決まった処理を行うことはしばしば発生するものです。
例えば、データベースへ接続してSQLを実行するには、事前処理としてデータベースへの接続と、事後処理としてデータベースからの切断が必要となるでしょう。
これらはすべてのSQL処理に書く必要がありますが全て同じ記述となり、冗長で変更があった時にはすべてのクラス(処理)を書き換える必要があります。
そこで個別の処理を抽象メソッドとして定義し、抽象クラスで事前処理およびに事後処理を挟む形で個別処理のメソッドを呼び出します。
すると、各処理のクラスではSQL処理のみを抽象メソッドに実装するだけで済むでしょう。
また、新しい処理クラスを追加する場合でも、抽象クラスを継承し、必要なメソッドを実装するだけで済みます。
実装しなければコンパイルエラーになる為、実装ミスも減ることになります。

このような実装方法はテンプレート メソッド パターンと呼ばれます。

インスタンス

抽象クラスはインスタンスを作成できません。
インスタンスを作成する為には、実装クラスを作る必要があります。
抽象メソッドが許可される事を考えれば、当然といえば当然ですが重要な特徴です。
尚、抽象メソッドが存在しない抽象クラスでもインスタンスは生成できません。

ですが、参照型変数の型としては有効であり、サブクラスからの暗黙的な型変換を行うことが可能です。
よって、次のようなコードが成立します。

        AbstractItem item1 = new Book();
        AbstractItem item2 = new Book();
        AbstractItem item3 = new CompactDisc();

これは異なるクラスのインスタンスを同一視していると見なせます。
AbstructItem型の変数という時点ではどのクラスのインスタンスであるかは解りません。
ですが、getOffPriceメソッドを使うことで価格を取得できます。

共通の実装を使う

抽象クラスを使うことで本とCDを同一視できるようになりました。
これを利用して計算機クラスを修正してみましょう。
[Calculator.java]

package example04;

public class Calculator {
    // 合計金額
    private int totalAmount = 0;
    
    // コンストラクタ
    public Calculator()
    {
    }
    
    // 本を追加(本・数量)
    public void addItem(Book book, int num)
    {
        int price = book.getOffPrice() * num;
        totalAmount = totalAmount + price;
    }
    
    // CDを追加(CD・数量)
    public void addItem(CompactDisc cd, int num)
    {
        int price = cd.getOffPrice() * num;
        totalAmount = totalAmount + price;
    }
    
    // 本を追加(本・数量1)
    public void addItem(Book book)
    {
        addItem(book, 1);
    }
    
    // CDを追加(CD・数量1)
    public void addItem(CompactDisc cd)
    {
        addItem(cd, 1);
    }
    
    // 合計金額取得
    public int getTotalAmount()
    {
        return totalAmount;
    }
}

現在は2つのaddItemメソッドがオーバーロードされていますが、その実装にはほとんど差がありません。
また、どちらでも使用しているメソッドも抽象クラスで定義されているメソッドです。

つまり、addItemメソッドがAbstructItemを引数に持てば2つの異なるクラスをまとめて処理する事が可能です。
[Calculator.java]

package example04;

public class Calculator {
    // 合計金額
    private int totalAmount = 0;
    
    // コンストラクタ
    public Calculator()
    {
    }
    
    // 商品を追加(商品・数量)
    public void addItem(AbstractItem item, int num)
    {
        int price = item.getOffPrice() * num;
        totalAmount = totalAmount + price;
    }
    
    // 商品を追加(商品・数量1)
    public void addItem(AbstractItem item)
    {
        addItem(item, 1);
    }
    
    // 合計金額取得
    public int getTotalAmount()
    {
        return totalAmount;
    }
}

計算機クラスは本やCDと言った個別の商品に依存せず、抽象化することに成功しました。
メソッドも1本にまとめることができたのでコードの見通しもよくなりました。

そして、Example04のmainメソッドに変更を加えていないのです。