カテゴリー
Java

Java(Step11-1)

抽象クラス

抽象クラス

オブジェクト指向のプログラムの基本から応用的なことについて説明していきます。

抽象クラス

クラスの派生を応用して、図形を表すクラス群を作っていきます。

最初に考える図形は「点」と「長方形」です。両方のクラスに描画のためのメソッドdrawを持たせることにします。2つのクラスは以下のように設計します。

■点クラスPoint

点を表すクラスで、フィールドは持ちません。メソッドdrawは、以下のように実現することで、記号文字’+’を1個だけ表示します。

void draw(){
        System.out.println('+');
}

■長方形クラスRectangle

長方形を表すクラスで、幅と高さを表すint型のフィールドwidthとheightを持たせます。メソッドdrawは以下のように実現します。

void draw(){
        for(int i = 1; i <= height; i++){
            for(int j = 1; j <= width; j++)
                System.out.print('*');
            System.out.println();
        }
    }

個別に定義されたクラスでメソッドdrawを作っても、それらは無関係なものになります。そこで多相性を有効に使い設計しましょう。

「図形」クラスから「点」と「長方形」を派生する

■図形クラスShape

図形を表すクラスです。点や長方形などのクラスは、このクラスから直接的、間接的に派生することにします。クラスShapeは、図形の設計図というよりも図形のという概念を表す抽象的なものになります。

  • インスタンスを生成できない、または生成すべきではない。
  • メソッドの本体が定義できない、サブクラスで具体化すべきである。

といった性質をもつクラスを表すのが抽象クラス(abstract class)です。抽象クラスとして定義すると以下のようになります。

abstract class Shape {
    abstract void draw();
}

3つのクラスの階層図は、以下のようになります。

図形クラス群のクラス階層図

クラスShapeを抽象クラスとしてそこからクラスPointとRectangleを派生するプログラムを以下に示します。

//図形クラス群

//図形
abstract class Shape {
    abstract void draw();
}
//点
class Point extends Shape{
    Point(){}

    void draw(){
        System.out.println('+');
    }
}
//長方形
class Rectangle extends Shape{
    private int width;
    private int height;

    Rectangle(int width, int height){
        this.width = width;
        this.height = height;
    }

    void draw(){
        for(int i = 1; i <= height; i++){
            for(int j = 1; j <= width; j++)
                System.out.print('*');
            System.out.println();
        }
    }
}
抽象メソッド

クラスShapeのメソッドdrawの先頭にabstractが付いています。このように宣言されたメソッドは、抽象メソッド(abstract method)となります。メソッドの前に付けられたabstractは、メソッドの実体をここでは定義できないので、派生したクラスで定義してください。という意味を持ちます。

クラスPointとクラスRectangleでは、メソッドdrawをオーバーライドして本体を定義しています。このように抽象クラスから派生したクラスで、抽象メソッドをオーバーライドして本体を定義することを、メソッドを実装する(implement)といいます。

抽象クラス

クラスShapeのように、抽象メソッドを1個でも有するクラスは、必ず抽象クラスとして宣言しなければならないことになっています。クラスを抽象クラスとして宣言するには、classの前にabstractを置くことです。ただ、抽象メソッドが1つもなくても抽象クラスにできます。

図形クラス群をテストするプログラムを以下に示します。

//ShapeTester.java
class ShapeTester {
    public static void main(String[] args){
        //以下の宣言はエラー:抽象クラスはインスタンス化できない
        //Shape s = new Shape();

        Shape[] a = new Shape[2];
        a[0] = new Point();
        a[1] = new Rectangle(4, 3);

        for(Shape s : a){
            s.draw();
            System.out.println();
        }
    }
}
ShapeTester.java実行結果

■抽象クラスのインスタンスは生成できない。

sの宣言がエラーとなることに注目します(コメントアウト)。抽象クラスは、具体定義のないメソッドを持っているため、new Shape()によってインスタンスを生成できません。

■抽象クラスと多相性

aは、Shape型の配列です。各要素a[0]とa[1]はShape型のクラス型変数であってShapeから派生したクラスのインスタンスを参照しています。

拡張for文では、配列a中の要素に対してメソッドdrawを呼び出します。先頭の要素に対しては、クラスPointのメソッドdrawが呼び出され、2番目の要素に対しては、クラスRectangleのメソッドdrawが呼び出されることが実行結果から確認できます。

抽象性をもつ非推奨メソッドの設計

抽象メソッドと非推奨メソッドとが入り組んだ複雑な構造のメソッドを説明します。

図形クラス群の改良

図形クラス群に対して、以下の変更・追加を行っていきます。

  • toStringメソッドの追加
  • 直線クラスの追加
  • 情報解説付き描画メソッドの追加
toStringメソッドの追加

文字列を返すtoStringメソッドの宣言を示したのが以下の図です。

図形と点と長方形におけるtoStringメソッドの実装

ここでは、非常に重要な点に着目しましょう。

クラスShapeでは、toStringメソッドを抽象メソッドとして宣言している。

具体的な図形ではないクラスShapeは、適切な文字列として表現できないからです。toStringメソッドは、Javaの全クラスの親玉であるObjectクラス内で定義されたメソッドです。また、extendsを与えられずに宣言されたクラスは、暗黙の内にObjectクラスから派生しています。

よって、クラスShapeは、スーパークラスであるObjectの非抽象メソッドを、抽象メソッドとしてオーバーライドしていることになります。toStringメソッドを抽象メソッドと宣言することは、そのクラスの下位クラスに対して、toStringメソッドの実装を強要する役割を持っています。

直線クラスの追加

次に直線クラスの追加について考えてみましょう。水平直線クラスHorzLineと垂直直線クラスVirtLineは、クラスShapeから派生しており次の図のようになります。

抽象クラスから水平直線クラスと垂直直線クラスの派生

これらのクラスに対して、長さを表すフィールドlengthのアクセッサを追加していくことを考えてみましょう。値を取得するgetLengthとセットするsetLengthは、両クラスとも全く同じものになります。水平直線と垂直直線の共通部を直線クラスとして独立させ、そのクラスから水平直線クラスと垂直直線クラスを派生した方がよいです。

情報解説付きメソッドの追加

メソッドprintでは、以下の2つを実装します。

  • toStringメソッドが返す文字列を表示する。
  • メソッドdrawによる描画を行う。

点クラスPoint2と長方形クラスRectamgle2を例にとると、以下のようになります。

点と長方形におけるメソッドprint

ここで注意するのは以下の点です。

両クラスのメソッドprintの定義が、まったく同じである。

このように無駄な部分は、スーパークラスでくくりだすべきです。図形クラスShapeの中でメソッドprintを定義するように書きだすと良いです。

改良した図形クラス

ここまでの設計を元に作成した図形クラス群のクラス階層図は以下になります。

図形クラス群のクラス階層図

さらに各クラスのプログラムを以下に示します。

//Shape2.java
//クラスShapeは、図形の概念を表す抽象クラス
//具体的な図形クラスはこのクラスから派生します。

public abstract class Shape2 {
    
    /*図形情報を表す文字列を返却するメソッド
    クラスShapeから派生し、このメソッド本体を実装*/
    public abstract String toString();

    /*メソッドdrawは、図形を描画するためのメソッド
    クラスShapeから派生し、このメソッド本体を実装*/
    public abstract void draw();

    //メソッドprintは、図形情報の表示と図形の表示を行う
    public void print(){
        System.out.println(toString());
        draw();
    }
}
//Point2.java
//クラスPointは、点を表すクラス
//Shapeから派生しています
public class Point2 extends Shape2 {
    
    /*点を生成するコンストラクタ
    受け取る引き数はなし*/
    public Point2(){
        //何も行わない
    }

    /*メソッドtoStringは点に関する図形情報を表す文字列を返却
    返却する文字列はPointです*/
    public String toString(){
        return "Point";
    }

    /*メソッドdrawは、点を描画するメソッド
    プラス記号を1つ表示して改行*/
    public void draw(){
        System.out.println('+');
    }
}
//AbstLine.java
//クラスAbstLineは直線を表す抽象クラス
//Shapeクラスから派生したクラス
public abstract class AbstLine extends Shape2{
    //直線の長さを表すint型のフィールド
    private int length;

    /*直線を生成するコンストラクタ
    長さを引数として受け取る*/
    public AbstLine(int length){
        setLength(length);
    }
    /*直線の長さを取得*/
    public int getLength(){
        return length;
    }
    /*直線の長さを設定*/
    public void setLength(int length){
        this.length = length;
    }
    /*メソッドtoStringは、直線に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "AbstLine(length:" + length + ")";
    }
}
//HorzLine.java
//クラスHorzLineは水平直線を表すクラス
//AbstLineから派生したクラス
public class HorzLine extends AbstLine {

    /*水平直線を生成するコンストラクタ
    長さを引数として受け取る*/
    public HorzLine(int length){super(length);}

    /*メソッドtoStringは、水平直線に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "HorzLine(length:" + getLength() + ")";
    }
    /*メソッドdrawは、水平直線を描画
    マイナス記号を横に並べます*/
    public void draw(){
        for(int i = 1; i <= getLength(); i++)
            System.out.print('-');
        System.out.println();
    }
}
//VirtLine.java
//クラスVirtLineは垂直直線を表すクラス
//AbstLineから派生したクラス
public class VirtLine extends AbstLine {

    /*垂直直線を生成するコンストラクタ
    長さを引数として受け取る*/
    public VirtLine(int length){super(length);}

    /*メソッドtoStringは、垂直直線に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "VirtLine(length:" + getLength() + ")";
    }
    /*メソッドdrawは、垂直直線を描画
    縦線記号を横に並べます*/
    public void draw(){
        for(int i = 1; i <= getLength(); i++)
            System.out.println('|');
    }
}
//Rectangle2.java
//クラスRectangle2は、長方形を表すクラスです。
public class Rectangle2 extends Shape2{
    //長方形の幅を表すint型のフィールド
    private int width;
    //長方形の高さを表すint型のフィールド
    private int height;

    /*長方形を生成するコンストラクタ
    幅と高さを引数として受け取る*/
    public Rectangle2(int width, int height){
        this.width = width;
        this.height = height;
    }
    /*メソッドtoStringは、長方形に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "Rectangle(width:" + width + ", height:" + height + ")";
    }
    /*メソッドdrawは、長方形を描画
    描画はアスタリスクを並べます*/
    public void draw(){
        for(int i = 1; i <= height; i++){
            for(int j = 1; j <= width; j++)
                System.out.print('*');
            System.out.println();
        }
    }
}

図形クラス群を利用するプログラムを例を以下に示します。このプログラムでは、多相性の効果を確認するためにクラスShape型の配列を利用しています。拡張for文では、全ての要素に対してメソッドprintを起動しています。正しく実行されていることが実行結果から分かります。

//ShapeTester2.java
class ShapeTester2 {
    public static void main(String[] args){
        Shape2[] p = new Shape2[4];

        p[0] = new Point2();
        p[1] = new HorzLine(5);
        p[2] = new VirtLine(3);
        p[3] = new Rectangle2(4, 4);

        for(Shape2 s : p){
            s.print();
            System.out.println();
        }
    }    
}
ShapeTester2.java実行結果