メモ置き場

メモ置き場です.開発したものや調べたことについて書きます.

[tex: ]

Javaの勉強(3):ポリモフィズム

Javaポリモフィズムについてまとめておく

ポリモフィズムとは

いろんなメソッドやクラスを一まとめにして扱えるようにしましょう,といった具合.実装を見るのが早いと思う.
ポリモフィズムを実現する方法として,abstractとinterfaceがある.

どちらも戻り値や引数などだけを決めて,具体的な処理はオーバーライドを使って(abstractを継承したりinterfaceを実装したりした)クラスごとに実装してから使う.メソッドを箱のようなものと考えた場合,箱の外側だけ同じであるが中身は別物といった具合になる.しかし箱=メソッドは外側から見る分には全く同じに見えるので,統一的にプログラム上で扱うことができるようになるわけである.

abstract class(抽象クラス)

abstract class(抽象クラス)とは,

  • abstract method(抽象メソッド)をメンバに持つ
  • 抽象クラスを親に持ち,親クラスの抽象メソッドをオーバーライドしていない
  • インターフェースを実装して,その抽象メソッドをオーバーライドしていない(インターフェースについては後ほどまとめる)

のいずれかを満たすクラスのこと.ようするに,クラスの中に実装されていないメソッドが含まれていればそのクラスは抽象クラスになる.
抽象クラスはそのままではインスタンス化できない.抽象クラスは継承されてから使われる.

abstract method(抽象メソッド)

スーパークラスでメソッドを定義しても,どうせオーバーライドされるからあんまり意味がないし実装するのめんどくさいなあ… でもこのクラスを継承したらこういうメソッドは定義しておいて欲しいよなあ… という考えを解消するための方法と思えばいい.
継承先のクラスで,オーバーライドを使って具体的なメソッドを実装する必要がある.

抽象クラスの具体的な書き方を示す.

// abstract class
// MyAbstract.java

public abstract class MyAbstract { // abstractをつける
    protected String name;

    public MyAbstract(String name){
        System.out.println("This is MyAbstract class!");
        System.out.println(name);
        this.name = name;
    }

    abstract void printName(); //抽象メソッドにはabstractをつける
}
// sub class
// MySub.java
public class MySub extends MyAbstract { // extendsで継承するクラスの指定
    private String subName;

    public MySub(String name) {
        super("Abstract class of " + name);
        this.name = name;
    }

    //抽象メソッドをオーバーライドする.オーバーライドしないでインスタンス化しようとするとコンパイルエラー
    @Override
    public void printName() {
        System.out.println("MySub :" + name);
    }
}

>|java|
// Main.java
public class Main{
    public static void main(String[] args){
        MySub mySub = new MySub("Pochi");
        mySub.printName();
    }
}
実行結果
This is MyAbstract class!
Abstract class of Pochi
MySub :Pochi

抽象クラスを実装するには,クラス定義時にabstractをつける.また抽象メソッドにもabstractをつける.
MySubクラスではextendsをつけて抽象クラスを継承し,その中で抽象メソッドをオーバーライドする.

インターフェース

ポリモフィズムを実現するための方法として,抽象クラスの他にインターフェースというものがある.
抽象クラスをたくさん作り,それを継承したいと思うことがあるかもしれない.しかし残念ながらJavaでは多重継承はできないので,継承できる抽象クラスは一つだけしかない.それでは不便!なので「多重に継承することの出来るクラスのようなもの」があるとよい.それがインターフェースという機能である.インターフェースは抽象クラスよりもさらに抽象化されていて,メンバ変数やメソッドに関して制限が設けられている.特徴として

  • interface インターフェース名{... で定義
  • 既存のインターフェースを継承できる.
  • インターフェースの持つメソッドは抽象メソッドかデフォルトメソッドのみ.どちらも複数持つことができる.
  • メンバ変数はstatic finalのみでインターフェース固有の値しか持てない
  • 使うときは他のクラスから実装する(継承するのと似ているが言い方が違うだけと思っていい).実装する時のキーワードはimplements
  • 複数のインターフェースを同時に実装可能
  • インターフェースを実装したクラスで,抽象メソッドをオーバーライドしない場合,そのクラスは抽象クラスになる.

インターフェースは抽象メソッドをもっているのでそのままではインスタンス化できない.インターフェースは「クラスで実装する」ことで使えるようになる.
以下にインターフェースを使ったコード例を示す.インターフェースの名前の付け方として,-ableという名前がつくことが多い.

ここでは,Writableというインターフェースを作り,それを実装するクラスMyClass1を定義する.

// Writable.java
public interface Writable {  // Writable interfaceをinterfaceキーワードで宣言する
    String message = "This is Writable"; // インターフェースは変数を持てる.修飾子をつけなくても自動的にstatic finalになる

    void writeMessage(); // 抽象メソッド.abstractをつけなくても自動的にabstractになる.Overrideしてから使う.

    default void writeHello(){ 
    // デフォルトメソッド.defaultをつける.インターフェースに固有のメソッドになる.実装したクラスから呼び出すことができる.
    // staticメソッドではないので,Writable.writeHello() の形では使えない.
        System.out.println("Hello from Writable");
    }
}
||<
>|java|
//MyClass1.java
public class MyClass1 implements Writable{
// MyClass1でWritableインターフェースを実装する.implementsを使う.
    private String name;

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

    // オーバーライドする
    @Override
    public void writeMessage() {
        System.out.println("Hi, this is MyClass1" + name);
    }
}
// Main.java

public class Main{
    public static void main(String[] args){
        MyClass1 myClass1 = new MyClass1("class1");
        myClass1.writeMessage(); // MyClass1でオーバーライドしたメソッドを呼ぶ
        myClass1.writeHello(); //インターフェースを実装したクラスのインスタンスからデフォルトメソッドを呼ぶ.
    }
}
実行結果
Hi, this is MyClass1.  name: class1
Hello from Writable

これだけだと,抽象クラスとの違いがわかりづらい.Writableインターフェースを実装したクラスMyClass2を新たに追加する.

// MyClass2.java
public class MyClass2 implements Writable{
    private String name;

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

    @Override
    public void writeMessage(){
        System.out.println("Wow, this is MyClass2.  name: " + name);
    }
}

インターフェースの面白いところは,インターフェースを実装したクラスのインスタンスは,インターフェースの型で参照できることである.上の例だと,Writableインターフェースを実装したMyClass1とMyClass2のインスタンスはWritableとして参照できるということだ.
インターフェースのこの使い方を見るため,Main.javaも変更する.MyClass1とMyClass2のインスタンスを作り,それをWritable型の配列に入れる.

// Main.java

public class Main{
    public static void main(String[] args){
        MyClass1 myClass1 = new MyClass1("class1");
        MyClass2 myClass2 = new MyClass2("class2");

        // Writable配列を作り,インスタンスを入れる
        Writable[] list = new Writable[2];
        list[0] = myClass1;
        list[1] = myClass2;

        //拡張for文で,配列の中身をWritable型として読み出す.
        for(Writable wtb : list){
            wtb.writeMessage();
            wtb.writeHello();
        }
    }
}
実行結果
Hi, this is MyClass1.  name: class1
Hello from Writable
Wow, this is MyClass2.  name: class2
Hello from Writable

繰り返し処理に注目して欲しい.配列listの中に入っているのはMyClass1とMyClass2のインスタンスであるが,拡張for文ではその中身をWritable型として読みだしている.for文の中ではWritableインターフェースの抽象メソッドとデフォルトメソッドであるwriteMessageとwriteHelloを呼び出している.実行結果をみるとわかるように,Writable型でインスタンスを読みだしているにも関わらず,呼び出されたメソッドはMyClass1とMyClass2でオーバーライドされたものになっている.MyClass1とMyClass2といった別のクラスが存在しているのに,それらを統一的にWritableとして扱うが,その中身は別々のクラスになっている,という動作ができたわけだ.プログラムを書く際は,細かいクラスの違いを機にする必要はなく,その大元にあるインターフェースやクラスとして統一的に扱うことができ,非常に強力な手法となる.

キャスト

型変換のこと.charをintとして扱ったりするという具合.キャスト演算子( )を使って(int) 3.14みたいにする.キャストの種類によってはキャスト演算子を使わない暗示的なキャストもOKな場合がある.めんどくさいのでキャスト演算子を付けておけば間違いないだろう.
ただし,1をtrueとみなしたりfalseを0とみなしたりといった,boolean型は他のプリミティブ型にキャストすることはできないので注意.

参照型のキャスト

Javaのキャストで押さえておくべきは参照型に対するキャストである.参照型のキャストには継承やインターフェースの実装が関係してくる.

まず,参照型に対する暗黙のキャストは「インスタンスを,そのクラスが継承したスーパークラスあるいはインターフェースの型変数で参照する」ことである.

サブクラスのインスタンススーパークラスの情報を含んでいるため,暗黙のキャストとして扱われる.一方でスーパークラスはサブクラスの情報を含んでいない(サブクラスで新たに定義されたメソッドなど)ため,スーパークラスでキャストされたサブクラスインスタンスは,サブクラスで追加されたメソッドにはアクセスできない.
サブクラスでオーバーライドされたメソッドは,スーパークラスインスタンスをキャストしていても,サブクラスでオーバーライドされたメソッドが呼び出される.

クラスAをスーパークラスとするクラスBがある.BのインスタンスbをAでキャストした変数aを用意する.

B b = new B();
A a = (A) b;
a.methodB(); // これはダメ

aを再びクラスBにキャストすることは可能.

B b_cast = (B) a;
b_cast.methodB();  //これはOK

クラスBで定義されたメソッドを使うには,再びクラスBにキャストし直す必要がある.

Object型

Javaのクラスは全てObject型をスーパークラスとして持っている.新たにクラスを定義する場合に,継承するクラスを明示しない場合は,デフォルトのスーパークラスとしてObjectクラスが指定される.したがって,あらゆるクラスのインスタンスはObject型でキャストでき,Objectクラスのメソッドを呼び出すことができる.
Objectクラスのメソッドとしてよく使われるものの1つに,toStringメソッドがあり,インスタンスの情報をString型に変換して返すメソッドである.新しくクラスを定義したときにこのtoStringメソッドをオーバーライドしておくと便利である.

ポリモフィズムの例

標準出力に用いるSystem.out.printlnメソッドは,引数にプリミティブ型・String型・Object型を取ることができる.
なんらかのクラスのインスタンスが引数として渡された場合を考えてみる.ここではクラスAのインスタンスaが渡された場合を考えてみよう.

A a = new A();
System.out.println(a);

Javaの全てのクラスのインスタンスはObject型でキャストできる.したがって,上のコードではprintlnメソッドにObject型のインスタンスが渡されたものとして実行される.printlnメソッドがObject型で参照できるインスタンスを受け取った場合,そのインスタンスのtoStringメソッドの実行結果を表示するようになっている.もしクラスAでtoStringメソッドをオーバーライドしていない場合は,Objectクラスで定義されたtoStringメソッドが呼び出され,オーバーライドしている場合はクラスAでのtoStringメソッドの結果が表示される.

このように,メソッドの引数をスーパークラスにしておき,実際にはサブクラスのインスタンスを渡して,メソッド内部では共通メソッドを呼び出してインスタンスごとにクラスに依存した動作をさせる,といった方法はポリモフィズムの利用法としてよく使われる.