tobiuo1990の日記

プログラマになるブログ

final修飾子の仕様確認

今まで何となく定数を宣言するときにstatic finalとして付けるくらいで、finalが実際にどのようなものなのかをしっかり理解できていなかったので、仕様を確認してみます。
仕様の確認は主にJava SE 11の言語仕様を参照しています。

f:id:tobiuo1990:20191001145438j:plain

ブログ内容とは無関係な写真。飛行機からうっすら見えた富士山。

 

finalの種類

finalはクラス、メソッド、変数の3つを修飾することができ、ざっくりと以下のような効果になります。
finalクラス:他のクラスから継承されなくなる。
finalメソッド:オーバーライドされなくなる。
final変数:一度しか値を代入されなくなる。

finalクラスが継承できないことの確認

以下のようなfinalクラスとそれを継承しようとしているクラスを作ります。

final class SampleFinal {
    public void message() {
        System.out.println("This is final class.");
    }
}
class SampleFinalExtends extends SampleFinal{
    @Override
    public void message() {
        System.out.println("This extends final class.");
    }
}

そして以下のようにfinalクラスを継承したクラスをnewしてみます。

class Main {
    public static void main(String args) {
        SampleFinalExtends sfe = new SampleFinalExtends();
        sfe.message();
    }
} 

すると以下のようなエラーになります。

Exception in thread "main" java.lang.Error: Unresolved compilation problems:
The type SampleFinalExtends cannot subclass the final class SampleFinal
The method message() of type SampleFinalExtends must override or implement a supertype method

一方でfinalクラスでない場合はうまくいきます。

class SampleNotFinal {
    public void message() {
        System.out.println("This is not final class.");
    }
}
 
class SampleNotFinalExtends extends SampleNotFinal {
    
    @Override
    public void message() {
        System.out.println("This extends not final class.");
    }
}
 
class Main {
    public static void main(String args) {
        SampleNotFinalExtends sfe = new SampleNotFinalExtends();
        sfe.message();
    }
}

This extends not final class.

 finalメソッドのオーバライド不可を確認
class Main {
    public static void main(String args) {
        Parent p = new Parent();
        p.print1();
        p.print2();
        Parent c = new Child();
        c.print1();
        c.print2();
    }
}

class Parent {
    public void print1() {
        System.out.println("Parent print1");
    }
    public final void print2() {
        System.out.println("Parent print2");
    }
}

class Child extends Parent {
    public void print1() {
        System.out.println("Child print1");
    }
    @Override
    public final void print2() {
        System.out.println("Child print2");
    }
}

 エラー: メイン・クラスsandbox.Mainを初期化できません
原因: java.lang.VerifyError: class sandbox.Child overrides final method sandbox.Parent.print2()V

final変数の中身が参照型の場合は参照先が不変になる(参照先の値は変わりうる)

 変数がプリミティブ型の場合は、最初に代入された値が変わらなくなります。
一方で参照型の場合は、変数に代入された参照先のオブジェクト自体は変わらなくなりますが、そのオブジェクトの中身(フィールド)は変更することが可能です。 
そのため、ラムダ式や内部クラスで外部の変数を参照するにはその変数が実質的にfinalであることが求められますが、その変数が参照型でフィールドにリストなどを持っていれば、そのリストにラムダ式内でオブジェクトをaddしたりすることは可能になっています。

final変数に一度代入すると代入できなくなることの確認
class Main {
    public static void main(String args) {
        final List<Stringlist = new ArrayList<>();
        list = new ArrayList<>();
    }
}

Exception in thread "main" java.lang.Error: Unresolved compilation problem:The final local variable list cannot be assigned. It must be blank and not using a compound assignment

final変数でもその中身はfinalでない(変更可能)であることの確認
class Main {
    public static void main(String args) {
        final List<Stringlist = new ArrayList<>();
        list.add("str1");
        list.add("str2");
        list.add("str3");
        System.out.println(list);
        list.remove(1);
        System.out.println(list);
        final Obj obj = new Obj();
        System.out.println(obj);
        obj.id = 2;
        obj.name = "second";
        System.out.println(obj);
    }
}
 
class Obj {
    int id;
    String name;
    public Obj() {
        id = 1;
        name = "first";
    }
    public String toString() {
        return "id:" + id + ", name:" + name;
    }
}

[str1, str2, str3]
[str1, str3]
id:1, name:first
id:2, name:second

暗黙的なfinal

以下の3つの場合は暗黙的にfinalとして宣言されます。
・インターフェースのフィールド
・try-with-resources文のresource
・multi-catch句のパラメータ

インターフェースのフィールドが暗黙的にfinalであることの確認
class Main {
    public static void main(String args) {
        Field fields = SampleIF.class.getDeclaredFields();
        for (Field f : fields) {
            String s = Modifier.isFinal(f.getModifiers()) ? "final" : "not final";
            System.out.printf("%s is %s\n"f.getName(), s);
        }
    }
}

interface SampleIF{
    int id = 0;
    String name = "zero";
}

id is final
name is final

try-with-resources文のresourceが暗黙的にfinalであることの確認
class Main {
    public static void main(String args) {
        try (BufferedReader br = new BufferedReader(new FileReader(args[0]))) {
            System.out.println(br.readLine());
            br = new BufferedReader(new FileReader(args[0]));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The resource br of a try-with-resources statement cannot be assigned

multi-catch句のパラメータが暗黙的にfinalであることの確認
class Main {
    public static void main(String args) {
        try{
            multiThrow(true);
        } catch (IOException | InterruptedException e) {
            e = null;
        }
    }

    private static void multiThrow(boolean boolthrows IOExceptionInterruptedException {
        if(bool) throw new IOException();
        else throw new InterruptedException();
    }
}

 Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The parameter e of a multi-catch block cannot be assigned

試しに別々にcatchしてみます。

class Main {
    public static void main(String args) {
        try{
            multiThrow(true);
        } catch (IOException e) {
            e = null;
            System.out.println("catch IOException");
        } catch (InterruptedException e) {
            e = null;
            System.out.println("catch InterruptedException");
        }
    }

    private static void multiThrow(boolean boolthrows IOExceptionInterruptedException {
        if(bool) throw new IOException();
        else throw new InterruptedException();
    }
}

catch IOException

実質的なfinal(effectively final)

初期化子のある変数で以下の条件をすべて満たす場合実質的なfinalとみなされます。
・final修飾子がついていない
・代入式の左側に現れない
・インクリメント/デクリメント演算子オペランドにならない

初期化子のない変数で以下の条件をすべて満たす場合実質的なfinalとみなされます。
・final修飾子がついていない
・確実に未代入であり確実に代入されていないときのみ代入式の左側に現れている*1
・インクリメント/デクリメント演算子オペランドにならない

メソッドやコンストラクタ、ラムダ、例外のパラメータで、初期化子ありで宣言された変数のように扱われている場合、実質的なfinalとみなされます。*2

 

finalについて詳細に理解するには、代入について理解しておく必要があるようです。(果てしない。。)

*1:ここは英語がややこしくかなり自信がないです。ここを参考にしました。

*2:ここも自信ないです。。