Ch2. 高度なJavaクラス設計

【復習】共変戻り値

メソッドをオーバーライドした際に、親クラスのメソッドの戻り値と子クラスの戻り値の型は原則同じである必要がある。 ただし、子クラスのメソッドの戻り値の型は親クラスのメソッドの戻り値の型のサブクラスである場合はその限りではない。

隠蔽

クラスメソッドやクラス変数をオーバーライドすることを「隠蔽」というらしい。 当然、クラスメソッドをインスタンスメソッドでオーバーライドしたりすることは出来ない。

package Chapter2.Override;

class Parental{
    static void staticMethod(){
        System.out.println("main method");
    }
    
    Object instanceMethod() {
        System.out.println("main method");
        return new Object();
    }
}

public class Override extends Parental{
    
    static void staticMethod() {
        System.out.println("Childe mein method");
    }
    
    /*static*/ Integer instanceMethod() {
        System.out.println("Child main method");
        return 100; //共変戻り値(Object型→Integer型)
    }
    /*この static メソッドは Parental からのインスタンス・メソッドを隠蔽できません*/

    public static void main(String[] args) {
    }

}

親クラスのインスタンスメソッドを子クラスのクラスメソッドでオーバーライドしようとすると

この static メソッドは Parental からのインスタンス・メソッドを隠蔽できません

というコンパイルエラーが表示される。

可変長引数

プリミティブ型の場合

Silverの学習でも可変長引数は引数の最後尾にしか設定できないことを学んだ。 更に可変長引数の決まり事として「(オーバーロード・オーバーライドされている場合)可変長引数でないメソッドが優先される」という決め事がある。

package Chapter2.Override;

class ParentalClass{

    public void method1(int paramA, int paramB) {
        System.out.println("親クラス-固定引数 : " + paramB);
    }

}

class SampleClass extends ParentalClass{

    public void method1(int paramA, int... paramB) {
        System.out.println("子クラス-可変長引数 : " + paramB);
    }

    public  void method1(int paramA, int paramB) {
        System.out.println("子クラス-固定引数 : " + paramB);
    }
}

public class Variadic {

    public static void main(String[] args) {
        SampleClass object = new SampleClass();
        object.method1(10,20,30,40,50); //Ex1: 子クラス-可変長引数: 可変長引数 : [I@15db9742
        object.method1(10,20);          //Ex2: 子クラス-固定引数 : 20
        object.method1(10);             //Ex3: 子クラス-可変長引数: 可変長引数 : [I@6d06d69c
        //object.method1(null);         //Ex4: コンパイルエラー
        object.method1(10, null);       //Ex5: 子クラス-可変長引数: 可変長引数 : null
        //object.method1(null, 10);     //Ex6: コンパイルエラー
        /* 子クラスの固定引数オーバーロードをコメントアウト*/
        object.method1(10,20);          //Ex7: 親クラス-固定引数 : 20

        int[] array = new int[] {10,20,30};
        object.method1(10, array);      //Ex8: 子クラス-可変長引数 : [I@7852e922

    }
}

Ex2からわかるように、引数の長さが合致するメソッドが存在すればそちらのメソッドが優先される。 また、Ex7からわかるように親クラスのメソッドであっても(固定引数のメソッドが)優先して選択される。

プリミティブ型の引数を取る場合は(変数と同じように)nullが許可されない(Ex4)。 しかし、可変長引数ではnullが許される。nullが引数として渡された場合、受け取り側では「空の配列」とはならない。nullとして受け取られる(Ex5)。

一方で引数が足らないにも関わらずメソッド探索で可変長引数を取るメソッドが選択された場合、受け取り側の可変長引数は「空の配列」と認識される(Ex3)。

プリミティブ型の可変長引数の受け取り方についてまとめると以下のようになる。

呼出元 受取側
null null
無(引数不足) 空の配列
配列 配列

参照型の場合

参照型ではnullを引数として渡すことが許可されている。

package Chapter2.Override;


class SampleClass2 {

    public void method1(int paramA, String... paramB) {
        System.out.println("可変長引数 : " + paramB);
    }
}

public class Variadic2 {

    public static void main(String[] args) {
        // TODO 自動生成されたメソッド・スタブ
        SampleClass2 object = new SampleClass2();
        object.method1(123, null); //Ex1: 可変長引数 : null
        object.method1(123);       //Ex2: 可変長引数 : [Ljava.lang.String;@15db9742
        object.method1(123, "");   //Ex3: 可変長引数 : [Ljava.lang.String;@6d06d69c
    }
}

Ex2の挙動はプリミティブ型と変わらない。引数が足らないにも関わらず可変長引数を取るメソッドが選択された場合、(nullではなく)空の配列として受け取られる。

Ex1の例のように参照型の可変長引数に対してnullを渡した場合も、受け取り側でもnullとして扱われる(プリミティブ型同じ)。 この際、コンパイラは以下のような警告を表示してる。

メソッド method1(int, String...) の最後の引数 null が、可変引数パラメーターの型と完全に一致しません。可変引数呼び出しを確認するために String[] にキャストするか、可変引数呼び出しに String 型の個々の引数を渡します。

コンパイラは開発者が(nullが入った)からの配列を受け取り側に渡したいなら、nullをString[]型にキャストしなさい、という警告を出す。 警告に従ってキャストを行うと、下記のような出力結果となる(Ex1)。

package Chapter2.Override;

class SampleClass2 {

    public void method1(int paramA, String... paramB) {
        System.out.println("可変長引数 : " + paramB);
    }
}

public class Variadic2 {

    public static void main(String[] args) {
        // TODO 自動生成されたメソッド・スタブ
        SampleClass2 object = new SampleClass2();
        object.method1(123, (String[])null); //Ex1: 可変長引数 : null
        object.method1(123, (String)null);   //Ex2: 可変長引数 : [Ljava.lang.String;@15db9742
    }
}

Ex1のようにコンパイラの指示に従ってキャストを行っても、結果は変わらずnull扱いとなる。 特筆すべきはEx2のようにString型へのキャストを行った場合、受け取り側では空の配列として扱われる。

参照型の可変長引数の受け取り方についてまとめると以下のようになる。

呼出元 受取側
null null
(Object[])null null
(Object)null 空の配列
無(引数不足) 空の配列
配列 配列

引数のオーバーロードが行われていた場合、可変長引数の選択順序は最下位である。

完全一致 > 暗黙の型変換 > AutoBoxing > 可変長引数
package Chapter2.Override;

class SampleClass3{

    //public void method1(Short paramB) {
    // System.out.println("AutoBoxing : " + paramB);
    //}

    //public void method1(short paramB) {
    // System.out.println("完全一致 : " + paramB);
    //}

    //public void method1(int paramB) {
    // System.out.println("暗黙の型変換 : " + paramB);
    //}

    public void method1(Short... paramB) {
        System.out.println("可変長引数 : " + paramB);
    }

}

public class Variadic3 {
    public static void main(String[] args) {
        short object = (short)15;
        new SampleClass3().method1(object);//'可変長引数 : [Ljava.lang.Short;@15db9742'
        
        //完全一致
        //暗黙の型変換
        //AutoBoxing
        //可変長引数 の順番
    }

}

なお、可変長引数をとるメソッドを配列を取るメソッドでオーバーライドすることは出来ない。

@Overrideアノテーション

自分があるメソッドをオーバーライドする意図でメソッドを定義しているという意思表示のために@overrideアノテーションが用いられる。 これにより(パラメータが誤っていたなどの理由で)正しくオーバーライドできていない場合、コンパイラがエラーを出してくれる。

    @java.lang.Override
    public void method1(Short... paramB) {
        System.out.println("可変長引数 : " + paramB);
    }

こうすると、

型 SampleClass3 のメソッド method1(Short...) はスーパータイプ・メソッドをオーバーライドまたは実装する必要があります

というエラーになる。

abstractクラスのクラスメソッド

abstractクラスはそれ単体でインスタンス化できないが、インスタンス化しなくてもクラス変数とクラスメソッドは利用できる。

package Chapter2.Override;

abstract class ParentalC{
    static void method() {
        System.out.println("This is static method");
    }

    static int variable = 100;

}
public class Override2 {

    public static void main(String args[]) {
        ParentalC.method(); //'This is static method'
        System.out.println(ParentalC.variable); //'100'
    }

}

Interface

Silverではinterefaceではpublicなメソッドしか定義できないことを学んだ。

変数

interfaceにはpublic static finalな変数も設定できる。

なにも明示しなければpublic static finalが自動的に宣言され、これに違反する宣言をしているメソッドはコンパイルエラーとなる。 final宣言をしているため、変数宣言時には初期化しておく必要がある。interfaceにはstaticイニシャライザは存在しないので、その場で代入するしかない。

クラスメソッド

SE8からinterfaceにクラスメソッドが持てるようになった。interfaceの普通のメソッドと同じくpublicなメソッドしか宣言できない。 注意すべきは、クラスのstaticメソッドと違ってインスタンスからの参照が出来ない。インターフェースから直接参照しなくてはならない。

defaultメソッド

SE8からInterfaceにも実装を持ったインスタンスメソッドを持てるようになった。 相変わらずpublicなメソッドしか宣言できない。

package Chapter2.Interface;

interface MyInterface{
    //static {
    // this.variable = 100;
    //}
    // Interfaceではstaticイニシャライザを定義できない。
    int variable = 100;

    static void classMethod() {
        System.out.println("This is  Class method on interface.");
    }

    default void defaultMethod() {
        System.out.println("This is Defalt Method.");
    }
    
    //default int hashCode() {
        //A default method cannot override a method from java.lang.Object 
    //}

}

class MyClass implements MyInterface{
}

public class Interface {
    public static void main(String[] args) {
        MyInterface.classMethod();
        System.out.println(MyInterface.variable);
        new MyClass().defaultMethod();
        // new MyClass().classMethod(); メソッド classMethod() は型 MyClass で未定義です
    }
}

hashCode()メソッドをinterfaceでオーバーライドしようとして失敗している。 これはinterfaceではObjectクラスのメソッドをオーバーライドできないことによるもので、toStringとかequalsとかも継承できない。

菱形継承問題

interfaceにはdefaultメソッドを使って実装が持てるようになった。実装クラスは複数のinterfaceを持てるので、当然菱形継承問題が起こり得る。

package Chapter2.Interface;

interface AbstractInterface{
    abstract void method();
}

interface BlueInterface extends AbstractInterface{
    @Override
    default void method() {
        System.out.println("BlueInterface");
    }
}

interface RedInterface extends AbstractInterface{
    @Override
    default void  method() {
        System.out.println("RedInterface");
    }
}

public class Diamond implements BlueInterface, RedInterface{
    //パラメーター () および () を持つ重複した default メソッド method は型 RedInterface および BlueInterface から継承されています
}

上記の現象は共通の親インターフェースを持たなくても(AbstractInterfaceが存在しなくても)、同じように発生する。

この解決方法として、菱形継承問題が発生している根本のクラスで問題が発生しているメソッドをオーバーライドしてしまうという方法がある。

public class Diamond implements BlueInterface, RedInterface{
    public void method() {
        System.out.println("Override!");
    }
}

オーバーライドする際に実装を持たせず、superとしてしまうこともできるが、当然三角継承問題が起こっているので、どちらの継承元を利用するのか明示しなくてはならない。 その際は、クラスメソッドを利用するようにインターフェース名.super.メソッド名と記述する。

package Chapter2.Interface;

interface AbstractInterface{
    abstract void method();
}

interface BlueInterface extends AbstractInterface{
    @Override
    default void method() {
        System.out.println("BlueInterface");
    }
}

interface RedInterface extends AbstractInterface{
    @Override
    default void  method() {
        System.out.println("RedInterface");
    }
}

public class Diamond implements BlueInterface, RedInterface{
    public void method() {
        RedInterface.super.method();
    }

    public static void main(String[] args) {
        Diamond object = new Diamond();
        object.method();//'RedInterface'
    }
}

ここまではinterface-interface間で起こる菱形継承問題についてみてきたが、interface-class間では菱形継承問題は起こらない。常にclassが優先される。

package Chapter2.Interface;

interface AbstractInterface2{
    abstract void method();
}

class BlueInterface2 implements AbstractInterface2{
    @Override
    public void method() {
        System.out.println("BlueInterface");
    }
}

interface RedInterface2 extends AbstractInterface{
    @Override
    default void  method() {
        System.out.println("RedInterface");
    }
}

public  class Interface2 extends BlueInterface2 implements RedInterface2{
    public static void main(String[] args) {
        Interface2 object = new Interface2();
        object.method(); //'BlueInterface'
    }
}

参照型の型変換

Silverの学習をしていた際は、「たとえ変数のキャストを行ったとしても初期化したクラスのオブジェクトが変数に代入されるだけで、実行時は初期化したクラスのオブジェクトに対する操作になる」と判断していた。 これはインスタンスメソッドに限った話であり、インスタンス変数・static変数・staticメソッド等は全て代入した変数のクラスの性質が引き継がれる。

package Chapter2.classCasting;

class ParentalClass{
    static int staticVariable = 100;
    int instanceVariable = 100;

    static void staticMethod() {
        System.out.println("Static Method(Parental)");
    }

    void instanceMethod() {
        System.out.println("instance Method(Parental)");
    }
}

class ChildClass extends ParentalClass{
    static int staticVariable = 90;
    int instanceVariable = 90;

    static void staticMethod() {
        System.out.println("Static Method(Child)");
    }

    void instanceMethod() {
        System.out.println("instance Method(Child)");
    }
}

public class classCasting {
    public static void main(String[] args) {
        ParentalClass object = new ChildClass();
        System.out.println(object.staticVariable);  //'100'
        System.out.println(object.instanceVariable);//'100'
        object.instanceMethod();                    //'instance Method(Child)'
        object.staticMethod();                      //'Static MEthod(Parental)'
    }

}

mainメソッド4行目のインスタンスメソッドの呼び出しだけが、生成したオブジェクトのクラスに紐づいていて残りは全て変数に紐づいているのがわかる。

ネストクラス

クラスの中にクラスを定義することが出来る。 使いどころはよくわからない。この記事に詳しく載っているけど、いまいち理解できていない。

あなたの知らない、4つのマニアックなJava文法:【改訂版】Eclipseではじめるプログラミング(17)(1/3 ページ) - @IT

外側クラスのメンバとしてクラスを定義することになる。 メンバなので、staticなメンバとして宣言してもインスタンスのメンバとして宣言することもできる。特にインスタンスメンバーとして(≒非staticクラスとして)宣言するクラスを【インナークラス】と呼ぶ

package Chapter2.innerClass;

 class ParentalClass{
     private int outSideInstanceVariable = 100;
     static  int outSideStaticVariable   = 200;

     static class NotInnerClass{
         static int notInnerClassStaticVariable = 300;
         int notInnerClassInstanceVariable      = 400;
         void method() {
             //System.out.println(outSideInstanceVariable); '非 static フィールド instanceVariable を static 参照できません'
             System.out.println(outSideStaticVariable);
         }
     }

     class InnerClass{
         // static int innerClassStaticVariable = 500; 'The field innerClassStaticVariable cannot be declared static in a non-static inner type, unless initialized with a constant expression'
         int innerClassInstanceVariable = 600;
         void method() {
             System.out.println(outSideInstanceVariable);
             System.out.println(outSideStaticVariable);
         }
     }

}

インナークラスで無い方(staticメンバとしてのネストクラス)の方は「外側のクラスのインスタンス変数にアクセス出来ていない」という特徴がある。 これは外側のクラスに定義したstaticメソッドが外側のインスタンス変数にアクセスできないのと同じことなので、あまり違和感はない。

インナークラスの方(インスタンス変数としてのネストクラス)を見てみると「staticクラスのメンバを宣言できない」という特徴に気づく。 一方、「外側のクラスで定義したメンバ変数」には(メンバのstatic/非staticにかかわらず)アクセスすることが出来ている

これらのネストクラスの初期化方法は以下の通り。

public class innerClass {

    public static void main(String[] args) {
        //InnerClass
        ParentalClass.InnerClass object = new ParentalClass().new InnerClass();

        //not-InnerClass(NestClass as static member)
        ParentalClass.NotInnerClass object2 = new ParentalClass.NotInnerClass();
    }
}

インナークラスを初期化する場合は、外側のクラスのインスタンス化→インナークラスのインスタンス化の順で行わなくてはならない。(new 外側クラス(). new インナークラス()) 一方、非インナークラスの初期化の場合は外側クラスのインスタンス化をしてしまえば、後はメンバ変数のように呼び出すことが出来る。呼び出すネストクラス自体の初期化不要である。

更にネストクラスを定義したクラス自体でインナークラスと非インナークラスを初期化する方法もある。 その場合、ネストクラスを定義したクラス自身のstaticメソッドで初期化するのかインスタンスメソッドで初期化するのかに注意する。

package Chapter2.innerClass;

 class ParentalClass{
     private int outSideInstanceVariable = 100;
     static  int outSideStaticVariable   = 200;

     static class NotInnerClass{
         static int notInnerClassStaticVariable = 300;
         int notInnerClassInstanceVariable      = 400;
         void method() {
             //System.out.println(outSideInstanceVariable); '非 static フィールド instanceVariable を static 参照できません'
             System.out.println(outSideStaticVariable);
         }
     }

     class InnerClass{
         // static int innerClassStaticVariable = 500; 'The field innerClassStaticVariable cannot be declared static in a non-static inner type, unless initialized with a constant expression'
         int innerClassInstanceVariable = 600;
         void method() {
             System.out.println(outSideInstanceVariable);
             System.out.println(outSideStaticVariable);
         }
     }
     
     static void myStaticMethod() {
         new ParentalClass().new InnerClass().method();
         new NotInnerClass().method();
         int number = NotInnerClass.notInnerClassStaticVariable;
     }
     
     void myInstanceMethod() {
         new InnerClass().method();
         new NotInnerClass().method();
         int number2 = NotInnerClass.notInnerClassStaticVariable;
     }

}

自クラス内で宣言したInnerClassをstaticなメソッド内で初期化するときは、(InnerClassはインスタンスメンバなのだから)外側のクラスから初期化する必要がある。 一方、staticなネストクラスを初期化するときは、そのまま初期化が出来る。staticなネストクラスにstaticなメソッドや変数が宣言されている場合、何の初期化も経ることなくそのままアクセスが可能になる。

一方、自クラスで宣言したInnerClassをインスタンスメソッドで初期化するときは、初期化したい対象であるInnerClassをそのまま初期化することが出来る。

最後にインナークラス内の変数スコープについて確認する。 インナークラスは外側のクラスで宣言されたインスタンス変数にアクセス可能、という特徴があった。

class ParentalClass{
     private int outSideInstanceVariable = 100;
     static  int outSideStaticVariable   = 200;
     private int variable = 789;

     static class NotInnerClass{
         static int notInnerClassStaticVariable = 300;
         int notInnerClassInstanceVariable      = 400;
         void method() {
             //System.out.println(outSideInstanceVariable); '非 static フィールド instanceVariable を static 参照できません'
             System.out.println(outSideStaticVariable);
         }
     }

     class InnerClass{
         // static int innerClassStaticVariable = 500; 'The field innerClassStaticVariable cannot be declared static in a non-static inner type, unless initialized with a constant expression'
         int innerClassInstanceVariable = 600;
         int variable = 456;
         void method() {
             int variable = 123;
             System.out.println(outSideInstanceVariable);
             System.out.println(outSideStaticVariable);

             System.out.println(variable);                   //'123'
             System.out.println(this.variable);              //'456'
             System.out.println(ParentalClass.this.variable);//'789'
         }
     }
}

メソッド内から外側のクラスで定義されたインスタンスメソッドにアクセスするには、外側クラス.this.インスタンス変数名と明示的にスコープを指定する。 スコープの明示的な指定をしなくても、Javaは勝手にアクセスできる範囲から値を探しに行ってくれるので、OutSideInstanceVariableのように名前さえ被っていなければ直接指定もできる。

InnerClass NestClass as static memeber
外側staticメンバへのアクセス
外側インスタンスメンバへのアクセス ×

ローカルクラス

メソッド内だけで利用できるクラスをローカルクラスと呼ぶ。 ローカルクラスの外側のローカル変数またはメソッドの引数にアクセスするにはfinalである事が必要だが、Java 8ではローカルクラスから参照されている変数を勝手にfinal宣言にしてくれるので、ユーザーはあまり意識することなく利用できる。

package Chapter2.localClass;

 class ParentalClass{

     private static String staticVariable = "staticVariable";
     private String instanceVariable      = "instanceVariable";

     public void method(String param) {
         String localVariable = "LocalVariable";
         staticVariable = "editStaticVariable";
         instanceVariable = "editInstanceVariable";
       class Local{
             Local(){
                 System.out.println(staticVariable);
                 System.out.println(instanceVariable);
                 System.out.println(localVariable);
                 System.out.println(param);
                 staticVariable = "editStaticVariable2";
                 instanceVariable = "editInstanceVariable2";
                 //localVariable    = "editLocalVariable"; //Local variable localVariable defined in an enclosing scope must be final or effectively final
                 //param            = "editMethodParam";   //Local variable param defined in an enclosing scope must be final or effectively final
             }
         }
         new Local();
     }

}

public class LocalClass {
    public static void main(String[] args) {
        new ParentalClass().method("params");
    }
}

ローカルクラスの外側の(メソッド内)ローカル変数またはメソッドの引数以外はfinal宣言がされていなくても、そのまま使うことが出来る。

匿名クラス

ローカス変数に近い仕組みとして「匿名クラス」が存在する。 ラムダ式が導入される前はこれを使っていたらしい。

package Chapter2.anonymous;


interface OriginalInterface{ void method(); }
public class anonymous {

    public void outerMethod() {
        new OriginalInterface(){
            @Override
            public void method() {
                // TODO 自動生成されたメソッド・スタブ
                System.out.println("implement method() method.");
            }
        }.method();
    }
}

メソッドの中でインターフェースをインスタンス化するような記述を行っている。 クラス宣言の中では、インターフェースで指定されたメソッドの実装を行って、最後にそのメソッドを実行している。 クラス宣言をしているので、最後には;(セミコロン)が必要。

「匿名クラス」という名前がついているだけあり、インターフェース名をクラス名の代わりに設定して、実装クラス自体には名前がついていない。

関数型インターフェース

関数型インターフェースとは【定義されている抽象メソッドが一つだけのインタフェース】なので、例えば先の匿名クラスの例のOriginalInerfaceインターフェースは【関数型インターフェース】と呼ぶことが出来る。 このような【関数型インターフェース】はSE8から標準で様々なものが提供されている。

例えば下記はFunctionインターフェースの実装。

jdk/Function.java at jdk8-b111 · openjdk/jdk · GitHub

恐らく下記のようなインターフェースを実装している。

public R apply(T ???);

T及びRに入る型はユーザー自身で決定されるようになっているのだとしたら、

public String apply(Integer param);

のように解釈できる。 これを匿名クラスで実装すると次のようになる。

package Chapter2.anonymous;

interface Function{
    public String apply(Integer param);
}

public class FunctionTest {
    public static void main(String[] args) {
        String result = new Function(){
            @Override
            public String apply(Integer param) {
                return "casted :" + param.toString();
            }
        }.apply(100);
        System.out.println(result);//'casted: 100'
    }
}

関数インターフェースは自分で定義することもできる。 このとき@FunctionalInterfaceというアノテーションを付与することでコンパイラが関数インターフェースとしての要件(≒抽象メソッドが1つだけ宣言されているインターフェース)の要件を満たすかどうか確かめてくれる。 なお、抽象メソッドが1つだけ宣言されている必要があるのが関数インターフェースだが、Objectクラスのメソッド(hashcode()とかequals()とか)は抽象メソッドとして存在して定義しても良い(意味あるのかな)。 また、通常のインタフェースと同じくstaticメソッドとかデフォルトメソッドはいくつでも宣言できる。

package Chapter2.anonymous;

@FunctionalInterface
interface MyInterface<T>{
     void greeting(T param);
}

public class FunctionTest2 {
    public static void main(String[] args) {
        String result = new MyInterface(){
            @Override
            String greeting( param) {
                String result = "Hello " + param;
                System.out.println(result);
            }
        }.greeting(100);
    }
}