3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Java】オーバーライドの壁を乗り越える

Last updated at Posted at 2021-04-22

はじめに

Javaを学ぶ上で、理解すべき考え方に継承やインターフェースがあります。
今回は、それらを使いこなすために必要なオーバーライドについて説明したいと思います。

※記事が長いので、簡潔に知りたい方はまとめを参照してください。

ターゲット

  • 継承やインターフェース実装の仕組みは知っている
  • オーバーライドは何となく使えるが、ちゃんと理解したい
  • @Overrideを知らない or 必要性がわからない

構成図

例として図形をテーマに、以下を使用します。

  • Drawableインターフェース (描画のインターフェース)
  • Mainクラス (図形を扱うクラス)
  • Shapeクラス (図形を表すスーパークラス)
  • Triangleクラス (Shapeクラスを継承した三角形を表すサブクラス)
  • Squareクラス (Shapeクラスを継承した四角形を表すサブクラス)
構成図
├─interfaces
│      Drawable.java
│
├─main
│      Main.java
│
└─shapes
       Shape.java
       Square.java
       Triangle.java

オーバーライドの基本と使用例

オーバーライドを直訳すると、「~に優先する」「~を無効にする」などの意味があります。
Javaでのオーバーライド
は、継承したクラス(スーパークラス)や実装したインターフェースのメソッドを再定義することを指します。

インターフェース

Drawableインターフェース

標準出力でOとXを用いて任意の出力をするDrawableインターフェースです。
DrawableインターフェースのdrawSubメソッドは、第三引数のcustomを任意とするためにオーバーロードしています。
今回はオーバーロードについては詳しく触れませんが、オーバーライドと関係が深いので、気になる方は調べてみてください。

Drawable.java
package interfaces;

import java.util.Collections;
import java.util.List;

public interface Drawable {
    String POSITIVE = "O";
    String NEGATIVE = "X";

    String DEFAULT = "\u001B[0m";
    String RED    = "\u001b[00;31m";
    String GREEN  = "\u001b[00;32m";
    String YELLOW = "\u001b[00;33m";
    String PURPLE = "\u001b[00;34m";
    String PINK   = "\u001b[00;35m";
    String CYAN   = "\u001b[00;36m";
    String[] COLORS = new String[]{DEFAULT, RED, GREEN, YELLOW, PURPLE, PINK, CYAN};

    /**
     * 描く
     */
    abstract public void draw(int colorNo, boolean pattern);

    /**
     * 行を生成する
     */
    default public String drawSub(boolean pattern, int elemCount, List<Integer> custom) {
        String line = "";

        if (custom != null) {
            for (int i = 0; i < elemCount; i++) {
                line += custom.contains(i) ? POSITIVE : NEGATIVE;
            }
        } else if (pattern) {
            for (int i = 0; i < elemCount; i++) {
                line += i % 2 == 0 ? POSITIVE : NEGATIVE;
            }
        } else {
            line = String.join("", Collections.nCopies(elemCount, POSITIVE));
        }
        return line;
    };

    default public String drawSub(boolean pattern, int elemCount) { return drawSub(pattern, elemCount, null); };
}

スーパークラス

Shapeクラス

図形のもととなるShapeクラスです。
Drawableインターフェースを実装した抽象クラスであり、具体的な図形を表すクラスはこれを継承します。

Shape.java
package shapes;

import interfaces.Drawable;

public abstract class Shape implements Drawable{
    private int vertices;
    private String name;

    /**
    *  コンストラクタ
    */
    public Shape(int _vertices, String _name) {
        this.vertices = _vertices;
        this.name = _name;
    }

    /**
     * verticesのgetter
     */
    int getVertices() {
        return vertices;
    }

    /**
     * verticesのsetter
     */
    void setVertices(int _vertices) {
        this.vertices = _vertices;
    }

    /**
     * nameのgetter
     */
    String getName() {
        return name;
    }

    /**
     * nameのsetter
     */
    void setName(String _name) {
        this.name = _name;
    }

    /**
    *  面積を計算する
    */
    abstract public Number calcArea();

    /**
    *  図形の性質を表示する
    */
    public void displayProp() {
        System.out.println("名前:" + (this.name == null ? "未設定" : this.name));
        System.out.println("頂点の数:" + this.vertices);
    }
}

サブクラス

Triangleクラス

三角形を表すTriangleクラスです。
図形の抽象クラスであるShapeクラスを継承しています。

Triangle.java
package shapes;

public class Triangle extends Shape {

    private double height;
    private double base;

    public Triangle(double _height, double _base) {
        super(3, "三角形");
        this.height = _height;
        this.base = _base;
    }

    /**
     * verticesのgetter
     */
    protected int getVertices() {
        return super.getVertices();
    }

    /**
     * nameのgetter
     */
    protected String getName() {
        return super.getName();
    }

    /**
     * heightのgetter
     */
    public double getHeight() {
        return height;
    }

    /**
     * heightのsetter
     */
    public void setHeight(double _height) {
        this.height = _height;
    }

    /**
     * baseのgetter
     */
    public double getBase() {
        return base;
    }

    /**
     * baseのsetter
     */
    public void setBase(double _base) {
        this.base = _base;
    }

    /**
     * 三角形の面積を計算する
     */
    public Number calcArea() {
        return 0.5 * this.height * this.base;
    }

    /**
     *  三角形の性質を表示する
     */
    public void displayProp() {
        super.displayProp();
        System.out.println("高さ:" + this.height);
        System.out.println("底辺の長さ:" + this.base);
    }

    /**
     * 三角形を描く
     */
    public void draw(int colorNo, boolean pattern) {
        System.out.print(COLORS[colorNo]);
        for (int i = 0; i < height; i++) {
            System.out.println(drawSub(pattern, i+1));
        }
        System.out.print(DEFAULT);
    }
}

Shapeクラスを継承しているので、抽象メソッドであるcalcAreaメソッドの処理を定義しています。

calcAreaメソッドの差分イメージ
    /**
     * 三角形の面積を計算する
     */
    public Number calcArea() {
+       return 0.5 * this.height * this.base;
    }

displayPropメソッドについては処理をオーバーライドしています。

displayPropメソッドの差分イメージ
    /**
     *  三角形の性質を表示する
     */
    public void displayProp() {
-       System.out.println("名前:" + (this.name == null ? "未設定" : this.name));
-       System.out.println("頂点の数:" + this.vertices);
+       super.displayProp();
+       System.out.println("高さ:" + this.height);
+       System.out.println("底辺の長さ:" + this.base);
    }

また、ShapeクラスがDrawableインターフェースを実装しているので、drawメソッドを定義しています。

drawメソッドの差分イメージ
    /**
     * 三角形を描く
     */
    public void draw(int colorNo, boolean pattern) {
+       System.out.print(COLORS[colorNo]);
+       for (int i = 0; i < height; i++) {
+           System.out.println(drawSub(pattern, i));
+       }
+       System.out.print(DEFAULT);
    }

getter(getVerticesメソッド、getNameメソッド)は、アクセス修飾子で説明します。

このように、スーパークラスやインターフェースのメソッドをサブクラスでオーバーライドします。オーバーライドするときは、メソッド名と引数が同じである必要があります。

抽象クラスを継承した場合は、抽象クラスで定義されている抽象メソッドの処理を決める必要があるため、サブクラスで必ずオーバーライドを用います。インターフェースを実装する場合も同様です。
また、通常の(具象)メソッドについては、任意でオーバーライドできます。

オーバーライドした場合も、スーパークラスのメソッドを呼び出すことは可能です。
今回の例だと、TriangleクラスのdisplayPropメソッド内でsuper.displayProp();と書くことで、ShapeクラスのdisplayPropメソッドを呼び出しています。

Squareクラス

四角形を表すSquareクラスです。
Triangleクラスと同様にShapeクラスを継承しています。

Square.java
package shapes;

public class Square extends Shape{

    private double length;
    private double width;

    public Square(double _length, double _width) {
        super(4, "四角形");
        this.length = _length;
        this.width = _width;
    }

    /**
     * verticesのgetter
     */
    @Override
    public int getVertices() {
        return super.getVertices();
    }

    /**
     * nameのgetter
     */
    @Override
    public String getName() {
        return super.getName();
    }

    /**
     * lengthのgetter
     */
    public double getLength() {
        return length;
    }

    /**
     * lengthのsetter
     */
    public void setLength(double length) {
        this.length = length;
    }

    /**
     * widthのgetter
     */
    public double getWidth() {
        return width;
    }

    /**
     * widthのsetter
     */
    public void setWidth(double width) {
        this.width = width;
    }

    /*
     * 四角形の面積を計算する
     */
    @Override
    public Double calcArea() {
        return this.length * this.width;
    }

    /*
     * 四角形の性質を表示する
     */
    @Override
    public void displayProp() {
        super.displayProp();
        System.out.println("縦の長さ:" + this.length);
        System.out.println("横の長さ:" + this.width);
    }

    /**
     * 四角形を描く
     */
    @Override
    public void draw(int colorNo, boolean pattern) {
        System.out.print(COLORS[colorNo]);
        for (int i = 0; i < length; i++) {
            System.out.println(drawSub(pattern, (int) this.width));
        }
        System.out.print(DEFAULT);
    }
}

Triangleクラスと同様にShapeクラスを継承していますが、オーバーライドの内容は少し違います。
四角形なので、面積の計算はthis.length * this.widthとなっていたり、表示する性質が違ったり、図形を描く処理も違います。

このように、共通の意味を持つメソッドを抽象クラスやインターフェースで定義して、各サブクラスでそれぞれ違う処理に定義したいときの手段としてオーバーライドを使用します。

オーバーライドはなぜあるのか

Javaでは、同じクラス内に同名かつ同じ引数を持つメソッドを定義することは、基本的にできないルールとなっています。
オーバーライドがないと仮定して、Triangleクラスに性質の表示メソッドを定義してみます。

Triangle.java(オーバーライドがない場合)
    /**
     *  三角形の性質を表示する
     */
-   public void displayProp() {
+   public void displayTriangleProp() {
        super.displayProp();
        System.out.println("高さ:" + this.height);
        System.out.println("底辺の長さ:" + this.base);
    }
}

このように、メソッド名を変えるのが一番無難ですね。
これでも問題ないように思いますが、この場合のデメリットはなんでしょう?

図形が三角形だけならさほど問題ではないでしょう。
でも、その他の図形と一緒に扱うときに手間がかかります。

例えば以下のようにそれぞれの図形の性質を表示する場合です。(Squareクラスにも同様の変更しているとします。)

Main.java(オーバーライドなし)
package main;
import shapes.Square;
import shapes.Triangle;

public class Main {

    public static void main(String[] args) {

        Triangle triangle = new Triangle(30, 40);
        Square square = new Square(50, 60);

        triangle.displayTriangleProp();
        square.displaySquareProp();
}

三角形と四角形それぞれで処理を記述しています。
他の図形が増える度に、図形の性質を表示するメソッドの呼び出しを書かなければいけません。

では、オーバーライドがあるとどうでしょうか?

Main.java(オーバーライドあり)
package main;
import shapes.Shape;
import shapes.Square;
import shapes.Triangle;

public class Main {

    public static void main(String[] args) {

        Triangle triangle = new Triangle(30, 40);
        Square square = new Square(50, 60);

        Shape[] shapes = {triangle, square};

        for (Shape shape : shapes) {
            shape.displayProp();
        }
    }
}

図形として配列にまとめて、for文で同じメソッド名で性質を表示することができます。
これなら他の図形に対しても、性質を表示するメソッドは同じように扱うことができます。

このように、同じ要素に対して色んな振る舞いを持たせることを**ポリモーフィズム(多態性・多相性)**といいます。
今回は、
同じ要素 = メソッド名
振る舞い = メソッドを実行した時の結果
です。

ポリモーフィズムはオブジェクト指向の中で重要な概念です。
ポリモーフィズムを実現する方法の一つとして、オーバーライドがあります。

@Overrideアノテーションで安全に実装

オーバーライドにはいくつか条件があります。その条件を満たすことで初めてオーバーライドされます。
条件を満たさない場合は、以下のどちらかとなります。

  1. コンパイルエラーとなる
  2. 別のメソッドとして定義される

例えば、引数を間違えた場合などはオーバーロードとなり、気づかないまま別のメソッドとして定義されてしまいます。

そのようなミスを防止するため、オーバーライドしたメソッドをコンパイラが分かるように、アノテーションが用意されています。それが**@Override**です。

Squareクラスを例にして、アノテーションの使用方法を見てみましょう。

Square.java(オーバーライドアノテーション使用例1)
    /*
     * 四角形の面積を計算する
     */
    @Override
    public Double calcArea() {
        return this.length * this.width;
    }

    /*
     * 四角形の性質を表示する
     */
    @Override
    public void displayProp() {
        super.displayProp();
        System.out.println("縦の長さ:" + this.length);
        System.out.println("横の長さ:" + this.width);
    }

オーバーライドアノテーションは、オーバーライドしたメソッドの上に@Overrideと記述します。
使用することで、オーバーライドの条件を満たしていない場合にコンパイルエラーとなるため、安全に実装することができます。

例として、displayPropメソッドの引数を変更してみます。

Square.java(オーバーライドアノテーション使用例2-1)
    /*
     * 四角形の性質を表示する
     */
    @Override
    public void displayProp(int n) {
        super.displayProp();
        System.out.println("縦の長さ:" + this.length);
        System.out.println("横の長さ:" + this.width);
    }

引数が不一致だと通常はオーバーロードとなり、別のメソッドとして定義されますが、オーバーライドアノテーションがあるとオーバーロードされずコンパイルエラーが起こります。

コンパイル結果(オーバーライドアノテーション使用例2-2)
Square.java:54: エラー: メソッドはスーパータイプのメソッドをオーバーライドまたは実装しません
    @Override
    ^
エラー1個

このようなミスを防ぐことに加えて、可読性の向上も見込めます。

記述しなくても問題は起こりませんが、得られる恩恵は大きいと思います。
そのため、オーバーライドしたメソッドを@Overrideで明示することをお勧めします。

オーバーライドの条件

いくつかは既に触れていますが、オーバーライドをするときには守らなければいけない条件があります。
これを理解することで、抽象クラスやインターフェースの設計にも役立つと思いますので、確認していきましょう。

メソッド名

メソッド名は、オーバーライドしたいメソッドと同じ名前で定義する必要があります。

例として、ShapeクラスのcalcAreaメソッドのオーバーライドをサブクラスで行う場合は、メソッド名が「calcArea」となっていることが分かります。

Shape.java-calcAreaメソッド
    /**
    *  面積を計算する
    */
    abstract public Number calcArea();
Triangle.java-calcAreaメソッド
    /**
     * 三角形の面積を計算する
     */
    @Override
    public Number calcArea() {
        return 0.5 * this.height * this.base;
    }
Square.java-calcAreaメソッド
    /*
     * 四角形の面積を計算する
     */
    @Override
    public Double calcArea() {
        return this.length * this.width;
    }

引数

メソッドの引数は、型と順序の両方を一致させる必要があります。

例として、Drawableインターフェースのdrawメソッドに対するオーバーライドを見てみましょう。

Drawable.java-drawメソッド
    /**
     * 描く
     */
    abstract public void draw(int colorNo, boolean pattern);
Triangle.java-drawメソッド
    /**
     * 三角形を描く
     */
    public void draw(int colorNo, boolean pattern) {
// ...省略
Square.java-drawメソッド
    /**
     * 四角形を描く
     */
    @Override
    public void draw(int colorNo, boolean pattern) {
// ...省略

drawメソッドをオーバーライドするので、第一引数がint型、第二引数がboolean型の必要があります。
例え引数の型が同じでも、順序を変えた場合はオーバーロードとなるので気を付けてください。(@Overrideがあればコンパイルエラーです。)

型と違って、引数の命名は変更しても問題ありません。

アクセス修飾子

オーバーライドでは、アクセス修飾子を変更することができます。
しかし、条件としてオーバーライドするメソッドより可視性を下げることはできません。

アクセス修飾子の可視性は以下です。

アクセス修飾子 同一クラス 同一パッケージ サブクラス 全体
public O O O O
protected O O O X
指定なし(packege private) O O X X
private O X X X

参考:【解決Java】アクセス修飾子(protected、privateなど)

つまり、
private < 指定なし < protected < public
の順で可視性が上がります。

もとのメソッドがprotectedの場合は、protectedpublicを指定できますが、指定なしprivateはできません。

例として、getterのオーバーライドを見てみましょう。

Shape.java
// ...省略
    /**
     * verticesのgetter
     */
    int getVertices() {
        return vertices;
    }
// ...省略
    /**
     * nameのgetter
     */
    String getName() {
        return name;
    }
// ...省略
Triangle.java
// ...省略
    /**
     * verticesのgetter
     */
    protected int getVertices() {
        return super.getVertices();
    }

    /**
     * nameのgetter
     */
    protected String getName() {
        return super.getName();
    }
// ...省略
Square.java
// ...省略
    /**
     * verticesのgetter
     */
    @Override
    public int getVertices() {
        return super.getVertices();
    }

    /**
     * nameのgetter
     */
    @Override
    public String getName() {
        return super.getName();
    }
// ...省略

Shapeクラスでは、指定なしです。
Triangleクラスではprotectedに、Squareクラスではpublicにオーバーライドされています。
どちらの場合も指定なしより可視性が上ですので、オーバーライドできます。

戻り値

戻り値については、型をオーバーライドすることができます。
条件として、オーバーライドするメソッドで定義されている型のサブクラスである必要があります。

例としてcalcAreaメソッドを見てみます。

Shape.java-calcAreaメソッド
    /**
    *  面積を計算する
    */
    abstract public Number calcArea();
Triangle.java-calcAreaメソッド
    /**
     * 三角形の面積を計算する
     */
    public Number calcArea() {
        return 0.5 * this.height * this.base;
    }
Square.java-calcAreaメソッド
    /*
     * 四角形の面積を計算する
     */
    @Override
    public Double calcArea() {
        return this.length * this.width;
    }

Shapeクラスは戻り値の型がNumberで定義されており、Triangleクラスも同じくNumberです。
しかし、SquareクラスはDoubleでオーバーライドされています。
これが実現できるのは、DoubleがNumberのサブクラスだからです。

このように、オーバーライドするメソッドで定義された戻り値に対して、そのサブクラスの型でオーバーライドした戻り値を共変戻り値といいます。

今回の例だと、スーパークラスのcalcAreaメソッドの戻り値の型はNumberなので、Double以外にもByte, Float, Integer, Long, Shortなどにオーバーライドできます。

まとめ

オーバーライドは、継承したクラスや実装したインターフェースのメソッドを再定義することをいいます。

条件については、最初の定義を守らなければいけないということです。
メソッド名変更不可です。

引数の順序や型を変えるとオーバーロードになってしまいますので、同じく変更不可です。

アクセス修飾子は、可視性が同一か上の必要があります。最初の定義で許可した部分を、後から制限してはいけないと考えると理解しやすいです。

戻り値についても、アクセス修飾子と同様に考えるとわかりやすいです。同一の型もしくは共変戻り値としてサブクラスの型でオーバーライドすれば、最初の定義を守ることができます。
Number型をDouble型でオーバーライドしても、Double型はNumber型であるとも考えられるため、最初の定義を守っていると考えられます。

参考

共変戻り値型(covariant return type)
君は合成メソッドを知っているか!(Java)
【解決Java】アクセス修飾子(protected、privateなど)

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?