第26講・STEP 03

abstractって何?

共通のメソッド名だけを親クラスで決め、具体的な処理を子クラスに任せたい。そんな時に使うのがabstractです。Humanと職業別のクラスを例に、抽象クラスと抽象メソッドを学びましょう。

abstractなメソッド

 abstractというのは、メソッドの修飾子として使われます。

 abstractは、メソッドを定義する際に「処理内容はここでは定義しない」と宣言する修飾子です。

例)abstractでvoidなメソッドを定義しています↓

abstract void method();

 メソッドを定義しているにも関わらず処理内容を定義する為の{ }がなく、;(セミコロン)で終わっているのが分かると思います。
 abstractを付けてこのようなちょっと変わった書き方をすることで、処理内容が未定義のメソッドを定義することができるわけです。

 処理内容が決まっていないメソッドなんて定義しても意味がないように思いますが、具体的な処理内容は継承先の子クラスで定義してもらうことを前提としたメソッドなんです。

abstractなクラス

 abstractの具体的な使い方や、その意味を理解するために、簡単なHumanクラスを作ってみます。各ファイルは同じパッケージに置いてください。

Human.java

public class Human {
    void eat(){
        System.out.println("ご飯を食べました");
    }
}

 
 eatメソッドをメンバとして持つHumanクラスを書いてみました。このHumanクラスをインスタンス化して、食べさせると以下のようなコードになります。

Main.java

public class Main {
    public static void main(String[] args) {
        Human human = new Human();
        human.eat();
    }
}
実行結果
ご飯を食べました

 このHumanクラスにabstractなメソッドを追加してみます。

Human.java

public class Human {
    void eat(){
        System.out.println("ご飯を食べました");
    }
    abstract void work();
}

 work()には名前と引数・戻り値の指定だけがあり、処理内容はありません。ただし、このままではコンパイルエラーになります。

 abstractなメソッドをメンバーとして持つクラスは、クラス自体にもabstractを付ける必要があります。

Human.java

public abstract class Human {
    void eat(){
        System.out.println("ご飯を食べました");
    }
    abstract void work();
}

※abstractなメンバーを持っているのに、classの宣言部分にabstractを付けずにいるとコンパイルエラーになります。

 さて、これでHumanクラスはabstractなクラスになりました。

 さあ、このHumanクラスをインスタンス化してみましょ・・とは出来ません。

 abstractなクラスは、new Human()のように直接インスタンス化できません。未定義のメソッドがあるクラスをインスタンス化するなんてのはNGです。abstractなクラスを継承したクラスで具体的な処理を実装してから、その子クラスをインスタンス化します。

 つまり、abstractなクラスというのはそれ自体をインスタンス化するのではなく、継承元(親クラス)として使う為に書くんです。ちょっと言ってることが分からないかも知れませんが、だんだん分かってくるので、実際にHumanクラスを継承したProgrammerクラスを書いてみましょう。

Programmer.java

public class Programmer extends Human {
    
}

 このProgrammerは、work()の処理を実装していないのでコンパイルエラーです。解決方法は、work()を実装するか、Programmer自体にもabstractを付けるかの二つです。今回はインスタンスを作りたいので、work()を実装しましょう。

 IDEには未実装のメソッドのひな形を生成する機能があります。例えば次のようなコードを用意し、work()の中身を書きます。手で書いても構いません。

Programmer.java

public class Programmer extends Human {
    
    @Override
    void work() {
        // TODO Auto-generated method stub
        
    }
    
}

 ↑こんな風にworkメソッドの中身を定義する為のコードが自動的に挿入されると思うので、定義部分を書いてしまいましょう。

Programmer.java

public class Programmer extends Human {
    @Override
    void work() {
        System.out.println("プログラミングをしました");
    }
}

※コメント部分は消しています。

 さて、これで仕事をするProgrammerクラスが出来ました。実際にインスタンス化して仕事させてみましょう。

Main.java

public class Main {
    public static void main(String[] args) {
        Programmer programmer = new Programmer();
        programmer.eat();
        programmer.work();
    }
}
実行結果
ご飯を食べました
プログラミングをしました

 ちゃんと仕事していますね。

抽象クラスと抽象メソッド

 ・・・で?

 ・・・何をそんなに回りくどいことをしてるの??

 と思う気持ちも分かりますが、もう少し辛抱して読み進めて下さい。

 この一連のabstractによって、今回のプログラムで扱うHumanの具体的な子クラスには「work()の処理を用意する」というルールを作ったわけです。

 上記の例ではProgrammerですが、例えばDoctorクラスを書くなら、以下のように書くことができます。

Doctor.java

public class Doctor extends Human {
    @Override
    void work() {
        System.out.println("患者を診ました");
    }
}

 同様に、Teacherクラスなら以下のように書けます。

Teacher.java

public class Teacher extends Human {
    @Override
    void work() {
        System.out.println("生徒に勉強を教えました");
    }
}

 これらのクラスはHumanクラスを継承しているので、人間(Humanオブジェクト)です。もう継承の本質を理解していますよね?

Child is Superです。

 プログラマーも医者も先生も、Human型として扱えます。仕事内容は違っても、どれに対してもwork()を呼べるようにしたいわけです。

 つまり、abstractという仕組みを使って、「Humanオブジェクトである以上はworkメソッドを持つ」という約束事を作ったわけです。

 abstractなクラスを抽象クラス、abstractなメソッドを抽象メソッドと言います。抽象クラスには、eat()のような共通の処理と、work()のように子クラスへ実装を任せる処理をまとめられます。

 実際にインスタンスを作るのは、抽象クラスを継承した子クラスが担います(上記の例で言うProgrammerクラス、Doctorクラス、Teacherクラス)。

 Humanオブジェクトである以上はworkメソッドを実装するべきなのですが、その実行内容(お仕事内容)に関してはプログラマーなのか医者なのか先生なのかによって様々な違いがあるので、Humanクラスでその仕事内容まで定義することはできません。

 そこでwork()を抽象メソッドにして、「具体的にどう働くかは子クラスで決める。ただし、インスタンスを作れるクラスには、その処理が必要」という約束を作っています。

 抽象クラスは、いわば子クラスに共通する設計のひな形です。

 ここまでの抽象クラスの例なら、三つの職業をHuman型の配列にまとめて扱えます。

Main.java

public class Main {
    public static void main(String[] args) {
        Human[] humans = {
            new Programmer(), new Doctor(), new Teacher()
        };
        for (Human human : humans) {
            human.work();
        }
    }
}
実行結果
プログラミングをしました
患者を診ました
生徒に勉強を教えました

 Human型でもwork()が宣言されているので呼び出せます。実行される処理は、それぞれの子クラスで実装したものです。new Human()はできなくても、Humanを変数の型として使うことはできます。

何の為のabstract?

 で、結局、abstractって何の役に立つの?

 と感じる人もいると居ると思います。その感覚はある意味で正しいと思います。

 別にabstractなんて使わなくても、単にHumanクラスを継承するクラスには必ずworkメソッドを書けばいいだけの話ですから。

 以下は比較用の別の例です。abstractを使わず、Programmerだけにwork()を追加しています。親のHumanにはwork()がないので、ここには@Overrideを付けません。

Human.java

public class Human {
    void eat(){
        System.out.println("ご飯を食べました");
    }
}

Programmer.java

public class Programmer extends Human {
    void work() {
        System.out.println("プログラミングをしました");
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Programmer programmer = new Programmer();
        programmer.eat();
        programmer.work();
    }
}
実行結果
ご飯を食べました
プログラミングをしました

 このMainのようにProgrammer型から呼ぶ限り、同じ結果になります。ただし、この比較用のHumanにはwork()がないので、Human型の変数からwork()を呼ぶことはできません。

 abstractを使う利点は、共通の呼び出し方を用意し、必要な処理の実装漏れをコンパイル時に見つけられることです。

 「Humanクラスの子クラスにはwork()メソッドを定義すること!」と紙に書いてディスプレイの横に貼っておく代わりに、未実装のabstractメソッドを抱えたままではインスタンス化できないように強制しているわけです。そんな状態ではコンパイルが通りません。

 abstractの仕組みが保証するのは子クラスでメソッドを実装するという約束事であって、処理内容の正しさではありません。空のwork()でもコンパイルは通るので、あくまで必要な処理内容、理想的なクラス設計を実現する為の補助でしかありません。ある意味で足枷と言えるものなので、自分で使いこなすのは難しいと思います。

 まずは「共通の処理は親にまとめ、子ごとに違う処理は抽象メソッドとして任せる」という使い方を押さえておきましょう。

 初心者としては、とりあえず抽象クラスというのは、それ自身がインスタンス化される為に存在するのではなく、その子クラス達の雛形として存在しているということを理解しておけば良いと思います。

 次回は、もう一つの仕組みである「インターフェース」を学び、抽象クラスとの違いも見ていきます。

目次へ

質問・コメント

読み込み中

この記事についての質問や、学習して気づいたことをお寄せください。

コメントを書く

投稿フォームを準備しています。

コメントと一緒に公開される名前です。
3,000文字以内。パスワードなどの個人情報は書き込まないでください。

コメントは送信後すぐに公開されます。

この記事のコメント

旧サイトに寄せられたコメントも当時の投稿内容のまま掲載しています。現在の記事では変更している内容や、旧環境についてのやり取りも含まれます。

コメントを読み込んでいます。