[雑談] 試験の役に立たないJava講座 (11)

ということで引き続きインプレス社の「徹底攻略Java SE 11 Silver問題集」の章立てに沿って、少し雑談していきたいと思います。

以前よりお話ししているとおり問題集の内容に沿ったものではなく抜けているところも多々ありますので、認定資格を取得しようというのであれば問題集を購入して、そちらをしっかり勉強してください。

こちらは章立てに沿って適当に書き散らかしているものですので、試験の役には立ちません。

今回は第7章の続きでクラスにおけるメソッド、フィールドの継承についてと、継承できないクラスについて触れていきたいと思います。

クラスとインタフェースの中間にあたる抽象クラスについて、まず考えていきます。

抽象クラス

インタフェースはメソッドの仕様、引数と戻り値の型を指定することで異なる振る舞いを持ったクラスを統一的に扱える便利な仕組みですが、共通の振る舞いを持ったメソッドもインタフェースを実装するクラスそれぞれが実装しなければならないため、共通メソッドの修正が必要になった場合にインタフェースを実装するクラスすべてを修正しなければならないという問題が発生します。

このような問題を避ける方法としてJavaには抽象クラスという仕組みが用意されています。

抽象クラスは実装を持たず、引数と戻り値型だけを定義した抽象メソッドを定義するクラスで、クラス定義においてabstractを宣言するとともに抽象メソッドにも同じくabstractを宣言します。

抽象クラスを拡張するクラスはすべての抽象メソッドを実装する必要はありませんが、実装していない抽象メソッドが存在している場合は抽象クラスと宣言する必要があります。

そしてすべての抽象メソッドを実装したクラスが具象クラスとしてインスタンスを生成できるようになります。

抽象クラスの定義

抽象クラスの定義は先にも説明した通り、クラス定義にabstractをつけるだけで、抽象メソッドはabstractをつけた実装のないメソッドを定義することで行います。

以下に抽象クラスの例を示します。

public abstract class AbstractClass {
  public void concreteMethod() {
    System.out.println("抽象クラスで定義された具象メソッド");

  }

  public abstract void abstractMethod();

}

抽象メソッドは上の例にある「abstractMethod」のようにメソッドの本体は定義せず、セミコロンだけを記述します。

抽象クラスの拡張は以下のように行います。

public class ConcreteClass extends AbstractClass {
  @Override
  public void abstractMethod() {

    System.out.println("具象クラスで定義された抽象メソッド");

  }

}

この例では抽象メソッドである「abstractMethod」しかオーバーライドしていませんが、もちろんスーパークラスである「AbstractClass」の具象メソッド「concreteMethod」をオーバーライドすることも可能です。

このように実装の継承と実装の強制の両方を実現できるのが抽象クラスの利点となります。

抽象クラスの制約事項と代替案

抽象クラスはクラスとインタフェースの両方の特徴を兼ね備えた便利な機能ですが、クラスであるための制約が存在します。

それは1個のサブクラスが継承できるスーパークラスは1個だけであるという点です。

Javaは2個以上のスーパークラスを継承する多重継承を認めていません。理由を説明すると面倒になりますが、要約すると同じスーパークラスを継承する2個のクラスを同時に継承する、いわゆるダイヤモンド継承が発生した場合に共通の祖先クラスの扱いが面倒になるからです。

それぞれに共通メソッドがあることが望ましい2個以上のインタフェースを実装するクラスを使用する場合、抽象クラス2個を同時に継承することはできないので、代替案として実装の移譲という考え方を使用する場合があります。

これは簡単に言うと共通メソッドを実装するクラスを用意して、共通メソッドの処理は共通メソッドを実装するクラスのインスタンスに実行させるというものです。

これを実際にやる場合にはさらに考慮しなければならない点がありますので、そのあたりは先で説明していきたいと思います。

継承とオーバーライド

クラスを拡張した際にクラスを構成するフィールド、プロパティ、コンストラクタの振る舞いがどうなるかを一度まとめておこうと思います。

メソッドの継承とオーバーライド

クラスの継承において典型的なふるまいを持つのがメソッド (インスタンスメソッド) となります。

スーパークラスで定義されたメソッドはサブクラスでオーバーライドされていなければ、スーパークラスとサブクラスのどちらのインスタンスにおいても同じ振る舞いを行います。

スーパークラスで定義され、サブクラスでオーバーライドされているメソッドは、スーパークラスのインスタンスではスーパークラスで定義されたメソッドとしてふるまい、サブクラスのインスタンスではサブクラスで定義されたメソッドとしてふるまいます。

これは、サブクラスのインスタンスがスーパークラスにアップキャストされている場合でも同じで、スーパークラスのメソッドとして呼ばれたとしてもサブクラスのメソッドがとしてふるまいます。

これがポリモーフィズムといわれるものとなります。

フィールドの継承

フィールドはオーバーライドされないため、期待した振る舞いとは異なる振る舞いをする場合があります。

スーパークラスで定義され、サブクラスでは定義されていないフィールドはスーパークラスのインスタンスとサブクラスのインスタンス両方から参照することができます。

スーパークラスとサブクラスの両方で定義された同じ名前のフィールドはスーパークラスのインスタンスからはスーパークラスのフィールドが、サブクラスのインスタンスからはサブクラスのインスタンスが参照できます。

ここで気を付けなければいけないことが2点あります。

ひとつめは、サブクラスのインスタンスからアクセスされたとしてもスーパークラスで定義されたメソッドからはスーパークラスで定義されたフィールドしか見えないという点です。

そしてもう一つは、サブクラスのインスタンスであってもスーパークラスにアップキャストされるとスーパークラスで定義されたフィールドにしかアクセスできない点です。

例えば以下のような例を考えてみます。

public class SuperClass {
  public String name = "SuperClass";
  public String getName() {

    return name;

  }

}


public class SubClass extends SuperClass {

  public String name = "SubClass";

}


public static class Main {

  public static void main(String[] args) {

    SubClass subClass = new SubClass();

    System.out.println(subClass.getName());

    SuperClass superClass = subClass;

    System.out.println(superClass.name);

  }

}

この例の最初の出力操作では「SubClass」クラスのインスタンスを生成して「getName」メソッドを呼んでいますが、「getName」メソッドは「SuperClass」で定義されているために「SuperClass」で定義された「name」フィールドの値 "SuperClass" を返します。

2番目の出力操作は「SubClass」のインスタンスを「SuperClass」型にアップキャストしたうえで「name」フィールドの値を参照しています。実際のインスタンスは「SubClass」型であるにも関わらず、参照できる「name」フィールドの値は "SuperClass" となります。

メソッドの場合はオーバーライドされてあたかも1個のメソッドしかないようにふるまいますが、フィールドの場合はスーパークラス、サブクラスそれぞれにフィールドが存在し、どのクラスとしてアクセスするかによってアクセスできるフィールドが決まるという点に注意が必要です。

スタティックフィールドとスタティックメソッド

スタティックフィールド (クラスフィールド) とスタティックメソッド (クラスメソッド) もフィールドと同様の振る舞いを行います。

スーパークラスだけで定義されたスタティックフィールド、スタティックメソッドはサブクラスでも参照することができます。

スーパークラスとサブクラスの両方で定義された同じ名前のスタティックフィールド、スタティックメソッドはどちらのクラスとして呼ばれたかで参照されるものが決まります。

推奨はされませんが、スタティックフィールド、スタティックメソッドにインスタンスを通してアクセスすることができます。

この場合、インスタンスがアップキャストされている場合にはスーパークラスのスタティックフィールド、スタティックメソッドが参照される点に注意してください。

コンストラクタ

コンストラクタは継承されません。

サブクラスのコンストラクタはその最初でsuperキーワードを使用してスーパークラスのコンストラクタを明示的に呼ぶ必要があります。

ただし、スーパークラスの引数なしのコンストラクタを呼ぶ場合に限ってスーパークラスのコンストラクタ呼び出しの記述を省略することができます。

もちろん省略できるのはコンストラクタ呼び出しの記述だけで、スーパークラスのコンストラクタは必ず実行されます。

クラスの継承とセッター・ゲッター

フィールドのところで見てきたように、スーパークラスで定義されたメソッドはスーパークラスで定義されているフィールドにしかアクセスすることができません。

このため、サブクラスで定義されたフィールドを使用したい場合には、スーパークラスで定義されたメソッドをサブクラスでオーバーライドする必要があります。

しかし、このやり方ではたんにフィールドアクセスのためだけにメソッドのコピーを作るという無駄を行ってしまうことになります。

代わりの対策としてフィールドへのアクセスをメソッドとして定義し、フィールドへのアクセスメソッドをサブクラスでオーバーライドすることが考えられます。

このやり方を用いると、スーパークラスのメソッドからサブクラスのフィールドにアクセスできるようになり、スーパークラスのメソッドからサブクラスのフィールドにアクセスできないという問題を解消することができます。

セッター・ゲッターはフィールドアクセスの隠ぺいだけでなく、クラス階層化という面でも有効です。

finalクラスとsealedクラス

クラスの継承に関してfinalクラスとsealedクラスについて触れておきたいと思います。

技術的な理由はどうしても思い浮かばないのですが、クラスの継承をさせたくない、という場合があるそうです。

このような場合に使われるのがfinalクラスとsealedクラスです。

finalクラスは以下の例のようにクラスにfinal修飾子を付けたものです。

public final class FinalClass {
  // クラス定義の本体
}

上の例のようにfinal修飾子を付けたクラスに対してpublic class DerivedClass exptends FinalClass { }のような継承を行おうとするとコンパイルエラーが発生します。

ところで、final修飾子を使用したクラス継承の制限だけでは不便な場面もあるため、最近になってより柔軟な継承の制限ができるsealedクラスが追加されました。

Java 11の時点では仕様化されておらず、Java SE 11の試験には出ませんので詳しくは説明しませんが、以下のような書き方をします。

public sealed class SealedClass permits SomeClass {
  // クラス本体の定義
}

上の例のようにクラスを定義するとSomeClassSealedClassを継承できますが、それ以外のクラスはクラス継承を行うとコンパイルエラーとなるというものです。

ほかにもいくつかの制約事項がありますので、そのうちもう少し調べておきたいと思います。

ということで、今回は抽象クラスと継承できないクラスについてまとめてみました。

0コメント

  • 1000 / 1000