今まで何となく定数を宣言するときにstatic finalとして付けるくらいで、finalが実際にどのようなものなのかをしっかり理解できていなかったので、仕様を確認してみます。 仕様の確認は主にJava SE 11の言語仕様 を参照しています。
ブログ内容とは無関係な写真。飛行機からうっすら見えた富士山。
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 ) {
}
}
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 ) {
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 bool ) throws IOException , InterruptedException {
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 bool ) throws IOException , InterruptedException {
if (bool) throw new IOException ();
else throw new InterruptedException ();
}
}
catch IOException
実質的なfinal(effectively final)
初期化子のある変数で以下の条件をすべて満たす場合実質的なfinalとみなされます。 ・final修飾子がついていない ・代入式の左側に現れない ・インクリメント/デクリメント演算子 のオペランド にならない
初期化子のない変数で以下の条件をすべて満たす場合実質的なfinalとみなされます。 ・final修飾子がついていない ・確実に未代入であり確実に代入されていないときのみ代入式の左側に現れている*1 ・インクリメント/デクリメント演算子 のオペランド にならない
メソッドやコンストラク タ、ラムダ、例外のパラメータで、初期化子ありで宣言された変数のように扱われている場合、実質的なfinalとみなされます。*2
finalについて詳細に理解するには、代入について 理解しておく必要があるようです。(果てしない。。)