新人エンジニア研修で知っておきたい抽象クラスの使い方

なぜ、抽象クラスの理解が重要なのか、その理由

この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。

今回は抽象クラスについて解説します。抽象クラスはその名前が示すようにインスタンス化できないクラスです。一方、インスタンス化できるクラスを具象クラスといいます。

抽象クラスは、他のクラスに共通するメソッドの実装や構造を定義するためのクラスです。抽象クラス自体はインスタンス化できないため、具象クラスを派生させるためのベースとして使用されます。

前回までの解説で継承を使用したポリモーフィズムを解説しました。スーパークラスのメソッドを複数のサブクラスがオーバーライドすることで同じメソッド呼び出しに対して、それぞれのサブクラスに応じた動作をさせることができました。しかし、オーバーライドを強制することまではできませんでした。

抽象クラスは、サブクラスに対してメソッドの実装を強制することができます。抽象クラス内に宣言された抽象メソッドは、サブクラスで必ず実装する必要があります。これにより、サブクラスが必要な機能を提供することが保証されるのです。確実にポリモーフィズムが実現できるようになるのです。例えば、抽象クラスとサブクラスを実装する人が別人でも、別会社でも確実にメソッドを実装させることができるのです。

今回は少し寄り道をしながら抽象クラスの存在意義について解説します。

1.Integerクラス

例によって、Javaの標準APIから、今回はInteger(標準API)というクラスを見てみましょう。

Integerクラスは、プリミティブ型intの値をオブジェクトにラップします。Integer型のオブジェクトには、型がintの単一フィールドが含まれます。

さらにこのクラスは、intStringに、Stringintに変換する各種メソッドや、intの処理時に役立つ定数およびメソッドも提供します。

このような解説があります。

ラップするというのは【wrap】、すなわち、サランラップと同じように包み込むという意味です。このようなクラスをラッパークラスと呼びます。つまり、プリミティブ型をオブジェクト型に(包み込んで)変換するのがラッパークラスの役割です

なぜ、プリミティブ型をオブジェクト型として扱う必要があるのでしょうか?

それは、オブジェクトにはいろいろと便利なメンバがあるからです。

package chap16;

public class Example01 {

    public static void main(String[] args) {
        String num1 = "1024";
        String num2 = "512";
        System.out.println(Integer.parseInt(num1) + Integer.parseInt(num2));
    }
}

<実行結果>

1536

文字列の"1024"と"512"を足し算しています。文字列のままではできない相談ですが、上記では出来ています。

Integer.parseInt(num1)のところでIntegerクラスのparseInt()メソッドを使っていますね。【parse】という英語の意味は”解析する”です。intとして解析する、つまり整数として扱うというメソッドなわけです。

文字列を整数として扱えると何が嬉しいのでしょうか?

一例ですが、皆さんがWebアプリケーションを作るときに必要です。実は、データーがファイルやネットワーク、キーボード入力からプログラムに入ってくる場合、大抵は文字列の形式なのですね。

Webページのフォームに入力した数値は文字列扱いなので、それを数値として扱いたいときに使います。

いまから覚えておいてくださいね。

こういうと、そもそもなぜ、プリミティブ型があるのかという鋭い疑問を持たれる方もいらっしゃいます。プリミティブ型が存在する一番の理由は、パフォーマンスだという説があります。(俗説という説もあります。)Integerクラスなどの参照型はヒープ領域にインスタンスを作るのでパフォーマンスが悪いのです。現代であればそれほど問題にならないかもしれませんが、Javaが企画されたのは1990年代ですので、マシンパワーが非力だったという訳です。

ちなみに、本書で何度か登場したKotlinやPython、近年iOSなどで人気のswiftという言語にはプリミティブ型はなく、すべてがオブジェクトです。

さらにIntegerクラスを使うと次のようなことが可能です。

package chap16;

public class Example02 {

    public static void main(String[] args) {
        System.out.println(Integer.MAX_VALUE);
        System.out.println(Integer.MIN_VALUE);
    }
}

<実行結果>

2147483647
-2147483648

int型の範囲を覚えなくてよい、と嬉しくなりますね。
※ただし、エンジニアのたしなみとして±約21億ということは覚えましょう。

練習問題1

以下のプログラムを実行するとNumberFormatExceptionが出てしまいます。結果が「5.85」になるためにはどうしたらよいでしょうか?

public class Example01 {

    public static void main(String[] args) {
        String num1 = "3.14";
        String num2 = "2.71";
        System.out.println(Integer.parseInt(num1) + Integer.parseInt(num2));
    }
}

なぜ、Integerクラスを抽象クラスの説明で持ち出したかといいますと、理由はそのスーパークラスのNumberクラスにあります。

クラスInteger

APIはこのようになっていますので、java.lang.Numberをクリックしてみてください。

そうすると、public abstract class Numberと定義されているのが見えます。

このabstractというキーワードが抽象クラスを意味しています。

2.Numberクラスは抽象クラス

ところで、"Number"つまりは、”数”のインスタンスとはどのようなものでしょうか?

"1"も”3.14”も数といえば数ですが、それぞれ、IntegerDoubleという、よりふさわしいクラスがJavaには存在します。

Integerは一種のNumberである。

Doubleは一種のNumberである。

これは、どこかで聞いたようなお話ですね。サブクラスは一種のスーパークラスである 。ということで、NumberクラスはIntegerクラスやDoubleクラスのスーパクラスなのでした。

抽象クラスとは、スーパークラスであるということだけが存在意義でインスタンス化できないクラスです。(図16.1参照)

ナンバークラスは実在するか?ということを新人エンジニア研修で説明している
図16.1 ナンバークラスは実在するか?

※abstractという単語には抽象絵画といった意味もあります。リンク先の絵をご覧いただくと、そのイメージが掴めるかと思います。

ですから、以下のサンプルプログラムにはエラーがあります。

package chap16;

public class Example03 {

    public static void main(String[] args) {
        Number n = new Number();
        System.out.println(n);
    }
}

では、抽象クラスにはどのような意義があるのでしょうか?

抽象クラスには抽象メソッドを1つ以上定義しなければなりません。そして抽象クラスを継承したサブクラスは抽象メソッドをオーバーライドしなければならないという決まりがあります。

つまり、ある抽象クラスのサブクラスには必ず、スーパクラスの抽象メソッドの実装があることが保証されるのです。

例えば、クラスNumberには以下の抽象メソッドintValue()があります。

public abstract int intValue();

整数値を返すメソッドです。

つまり、IntegerクラスにもDoubleクラスにも(Byte, Float, Shortにも)このintValue()メソッドがオーバーライドされていることが保証されているのです。

以下のサンプルプログラムを見てください。

package chap16;

public class Example04 {

    public static void main(String[] args) {
        Integer i = 128;
        Double d = 3.14;
        System.out.println(i.intValue());
        System.out.println(d.intValue());
    }
}

<実行結果>

128
3

このように確かにintValue()メソッドがあります。

サブクラスにスーパークラスの抽象メソッドのオーバーライドを義務付けるというのが抽象クラスの役割です。

では、もしも、抽象クラスという存在がなかったら同じことは可能でしょうか?

もちろん気を付けてオーバーライドすれば可能かもしれません。しかし、人はうっかりしやすく、忘れやすいものです。そのため、抽象クラスの仕組みを採用したのです。

この抽象クラスの仕組みを皆さんも使うことができます。

3.オリジナルの抽象クラスを作る

ここでは、あくまでサンプルとして簡単な抽象クラスを作成してみます。

例えば、ゲームを例にとって考えます。オンラインカジノを作るとします。カジノには通常のプレイヤー(NormalPlayer1)とVIPプレイヤー(VipPlayer1)がいるとします。

どちらも人間ですので、抽象クラスAbstractHuman1を作ることにします。

package chap16;

public abstract class AbstractHuman1 {
    abstract void play();
}

クラス名の先頭にAbstractをつけて分かりやすくしています。

プレイする(play)というメソッドを抽象メソッドとしてAbstractHuman1クラスに定義するとサブクラスではオーバーライドしなくてはなりません。

以下にAbstractHuman1クラスを継承したNormalPlayer1クラスを作成してみます。

package chap16;

public class NormalPlayer1 extends AbstractHuman1 {

    @Override
    void play() {
        System.out.println("通常会員用の画面が表示される");
    }
}

IDEで保存をすると5行目の「@Override」が追加されたのではないでしょうか。

@Overrideは正しくオーバーライドされていない時に、コンパイラがエラーを出してくれるという便利なannotation(注釈)です。playのスペルをわざと間違えるなどしてその効果を確かめてみて下さい。

package chap16;

public class VipPlayer1 extends AbstractHuman1 {

    @Override
    void play() {
        System.out.println("VIP会員用の画面が表示される");
    }
}

以下のテストコードで検証してみます。

package chap16;

public class Example05 {

    public static void main(String[] args) {
        AbstractHuman1[] ah = {new NormalPlayer1(), new VipPlayer1() };
        
        for (AbstractHuman1 ah1 : ah) {
            ah1.play();
        }
    }
}

<実行結果>

通常会員用の画面が表示される
VIP会員用の画面が表示される

抽象クラスを使うことで、このようにオーバーライドを義務付けることができます。

4.スーパークラスのメンバ呼び出し

抽象クラスに限ったことではないのですが、superキーワードは、サブクラスがスーパークラスのメンバやコンストラクタにアクセスするために使用されます。この研修ではスーパークラスのコンストラクタにアクセスする際によく使用しますので、そのことを解説します。

先程のゲームの例で考えます。

通常のプレイヤー(NormalPlayer2)とVIPプレイヤー(VipPlayer2)がいて、共通のスーパークラスに人間(AbstractHuman2)クラスがあるものとします。 また、3つのクラス全てに名前というフィールドがあるものとします。

ただし、前回のカプセル化の考え方にのっとり、名前フィールドはプライベート宣言されており、コンストラクタで初期化されるものとしました。

package chap16;

public abstract class AbstractHuman2{

    private String name;

    public AbstractHuman2(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

この場合でもサブクラスのコンストラクタ内でスーパークラスのコンストラクタを呼んでフィールドを初期化できます。

super(実引数列);

という書き方をします。

以下のNormalPlayer2クラスを見てください。

package chap16;

public class NormalPlayer2 extends AbstractHuman2 {

    public NormalPlayer2(String name) {
        super(name);
        System.out.println("Hey! I'm " + getName());
    }
}

コンストラクタでは、名前フィールドをセットして、その名前を使って挨拶をするだけです。

VIPプレイヤーも同様ですが、挨拶表現が洗練されています。

package chap16;

public class VipPlayer2 extends AbstractHuman2 {

    public VipPlayer2(String name) {
        super(name);
        System.out.println("How do you do, I'm " + getName());
    }
}

以下のサンプルコードでテストしてみます。

package chap16;
public class Example06 {

    public static void main(String[] args) {
        new NormalPlayer2("yamazaki");
        new VipPlayer2("kokubun");
    }
}

<実行結果>

Hey! I'm yamazaki
How do you do, I'm kokubun

このことは何が嬉しいのでしょうか?

この短いコードではメリットが感じられないかもしれません。しかし、スーパークラスのコンストラクタで複雑な処理をしているとしたら大変便利です。

もし、コンストラクタを使った同じ初期化処理が、色々なクラスに書かれていたとしたら、その初期化処理を書き換えるのは大変です。しかし、スーパークラスと複数のサブクラスで共用していれば変更は楽になります。

5.オートボクシングとオートアンボクシング

今回、ラッパークラスのお話が出てきましたので、ついでにオートボクシングとオートアンボクシングのお話もしておきます。

どういうお話かといいますと、プリミティブ型とラッパーの間の変換に手間はいらないというお話です。

下図16.2はイメージ図です。

オートボクシングとオートアンボクシングのイメージ
図16.2 オートボクシングとオートアンボクシングのイメージ

次のサンプルコードを見てください。

package chap16;

public class Example07 {

    public static void main(String[] args) {
        double e = 2.72;
        Double num = e;
        System.out.println(num);
        double num2 = num;
        System.out.println(num2);
    }
}

<実行結果>

2.72
2.72

上記※1のようにプリミティブ型から対応するラッパーに自動変換する(代入できる)ことをオートボクシング【Autoboxing】といいます

逆に、上記※2のようにラッパーから対応するプリミティブ型に自動変換する(代入できる)できることをオートアンボクシング【Auto-Unboxing:箱から出す】といいますちょうど、購入した電化製品を箱から出すことも英語で【Unboxing】といいますね。

実は、ラッパーは後ほど学ぶArrayListクラスなどのコレクションフレームワークに関連して重要なのですが、詳細は後ほど。したがって新人エンジニアのみなさんとしては、まず、覚えましょう。

6.NumberFormatException

抽象クラスとは直接関係がありませんが、今回、Integer.parseIntのお話が出てきましたので、ついでにそれにまつわる数値として解釈できない場合の例外の話もしておきたいと思います。

NumberFormatExceptionといいます。

【Number:数値】の【format:形式】の【Exception:例外】ということで文字通りですね。

以下のサンプルプログラムを見てください。

package chap16;

public class Example08 {

    public static void main(String[] args) {
        System.out.println(Integer.parseInt("256"));
        System.out.println(Integer.parseInt("1byte"));
    }
}

<実行結果>

256
Exception in thread "main" java.lang.NumberFormatException: For input string: "1byte"
(以下略)

”256”は数値に変換されましたが"1byte"は例外が発生しています。数値ではないものを引数に取れないのですね。これもWebアプリケーション作成時によく見る例外です。

<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□ プリミティブ型を参照型に(包み込んで)変換するのがラッパークラスの役割
 
□ サブクラスにスーパークラスの抽象メソッドのオーバーライドを義務付けるというのが抽象クラスの役割
 
□ @Overrideは正しくオーバーライドされていない時に、コンパイラがエラーを出してくれるという便利なannotationである
 
□ プリミティブ型から対応するラッパーに自動変換することをオートボクシングと言い、その逆をオートアンボクシングという

まとめができたら、アウトプットとして演習問題にチャレンジしましょう。

【今回の復習Youtube】

041-抽象クラス-抽象クラスの定義

042-抽象クラス-オーバーライドとポリモーフィズム

043-抽象クラス-抽象クラスの配列

以上、今回は「抽象クラスで継承の利用シーンを広げる」方法について見てきました。

抽象クラスを使うことで、サブクラスにスーパークラスの抽象メソッドのオーバーライドを義務付けることができました。より、堅牢なクラス体系を作り出すことができましたね。

抽象クラスで継承の利用シーンを広げる 最後までお読みいただきありがとうございます。