Jave SE17 Sliver

Article Cover Image

背景

Java SE17 Silver に一発合格したのでそれについて述べていきます。

学習材料

こちらのUdemyの授業を一周し(Old Contentsはやってないです)、以下の黒本と呼ばれる問題集をやりました。

以下の「試験用メモ」セクションは自分なりに調べてまとめた集大成のようなものです
こちらを活用するのも非常に有効です。

感想

65%で合格なので、合格自体はそれほど難しくないかと思います。
ただ90分で60問解くため、時間はギリギリまで使うかと思います。(足りないことはないですが、余裕があるというわけでもないです。)

個人的に問題が間違っいるものがいくつかあったのは結構イラつきました。(「試験メモ」にもある様に、私は実際にプログラムを実行して確かめたりしたので、間違っていると確信が持てる問題がありました。)
特に酷かったのが一言一句全く同じ選択肢が登場する問題がありました。そういう問題で無駄に時間をロスしたのが悔やまれます。
この様な問題に出会ってもとりあえず答えて次の問題に行くことをおすすめします。

試験用メモ

Basic

無名パッケージ(デフォルトパッケージ)

パッケージに属さないクラスは存在しない。
パッケージ宣言を省略した際は、デフォルトで無名パッケージに属するものとして解釈される。
無名パッケージに属するクラスは、無名パッケージに属するクラスからしかアクセスできない。(パッケージがないのでimportすることもできない)

自動インポートされるパッケージ
  • java.langパッケージに属するクラス
    ex). java.lang.String, java.lang.Integer
  • 同じパッケージに属するクラス

*を使用することによってパッケージに属するクラスを全てimport可能。
ただし、あくまで属するパッケージであってサブパッケージはインポートしない。
例としてimport java.util.*;java.util.loggingパッケージに属するクラスはインポートしない。

staticインポート

指定されたクラス内のstaticメンバーを使用するときに利用する。
ex).

package ex;

public class Sample {
    public static String name = "Sample";
    public static void method() {
        System.out.println("Sample method");
    };
}

import static ex.Sample.method;
import static ex.Sample.name;

public class Main{
    public static void main(String[] args) {
        System.out.print(name);
        method();
    }
}
javacコマンドとjavaコマンド

javacコマンドによってclassファイルを出力する。そのコンパイルされたclassファイルのプログラムをjavaコマンドで実行する。
ex)

public class Sample {
	public static void main(String... args) {
		for (String s: args) {
			System.out.println(s);
		}
	}
}
Sample.java
# Sample.classという実行可能なコンパイルされたファイルを出力
$ javac Sample.java

# Sample.classを実行(.class拡張子はつけない)
$ java Sample a b c


Java SE 11 以降では以下のようにソースファイルモードでの実行が可能。(<クラス名>.javaをソースファイルとしてコマンドの対象にできる。)

# javac -d メモリ空間 Sample.java && java Sample a b c と同じ意味を持つ
# -d: コンパイル後のクラスファイルの出力先を指定する
$ java Sample.java a b c
⚠️
javaコマンドをソースファイルモードで実行する際はクラス名とソースファイル名が一致する必要がない
一方、javacコマンドでコンパイルする際にはクラス名とソースファイル名が一致しないとコンパイルエラーが出る
複数クラスが定義されているソースファイルに対してjavacjavaコマンドを実行
  • publicクラスとソースファイル名は一緒である必要がある。
  • javac <SOURCE_FILE>.javaコマンドで定義されているクラス全部のclassファイルが生成される。
  • javaは生成されたclassファイルを実行できる。(もちろんjava <SOURCE_FILE>.java とソースファイルモードで実行することも可能)

以下のソースファイルではpublicなクラスはMainなのでファイル名もMain.javaとする必要がある。

public class Main {
    public static void main(String[] args) {
        System.out.println("Main");
    }
}

class Main2 {
    public static void main(String[] args) {
        System.out.println("Main2");
    }
}

class Main3 {
    public static void main(String[] args) {
        System.out.println("Main3");
    }
}
Main.java
# ソースファイルモードで実行
$ java Main.java
Main

$ javac Main.java
# 各クラスに対してclassファイルが生成される
$ ls
Main.class	Main.java	Main2.class	Main3.class

$ java Main
Main

$ java Main2
Main2

$ java Main3
Main3
依存関係のファイルに対してjavacjavaコマンドを実行

exパッケージに以下の様にファイルがある。

package ex;

public class Main {
    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.method();
    }
}
ex/Main.java
package ex;

public class Sample {
    public void method() {
        System.out.println("Sample method");
    };
}
ex/Sample.java

このとき、javac ex/Main.javaを実行することでexディレクトリ下にクラスファイルex/Main.classその依存先であるクラスファイルex/Sample.classの両方が生成される。後はjava ex.Main(またはjava ex/Main)コマンドでプログラムを実行できる。
ソースファイルモードでjava ex/Main.javaと直接実行するには依存先であるSample.classファイルが事前に生成されている必要がある

また以下の様にjavac-dオプションでクラスファイルの作成先を指定することが可能。
これに対してjava-cpオプションでクラスファイルのパスを指定して実行が可能。

# -d で生成先のディレクトリを指定(指定したディレクトリがなければ新しくディレクトリが作られる)
$ javac -d build ex/Main.java

# build/exディレクトリ下にMain.classとSample.classが生成される
$ ls build/ex
Main.class	Sample.class

# -cp でディレクトリのパスを指定
$ java -cp build ex.Main # java -cp build ex/Mainでもok
Sample method
# buildはあくまでディレクトリでパッケージではないので、
# java build.ex.Mainやjava build/ex.Mainはできない
コマンドライン引数とエスケープ
  • \¥)によってエスケープできる。
  • " "でスペースを囲める。
  • 通常のスペースによってのみ区切られる。
java Sample a \" a\" "a "b c
# 引数は以下の5つとして受け取る
1. a
2. "
3. a"
4. a b
5. c

Primitive type and String

整数リテラルの接頭辞
  • 2進数: 0b, 0B

    ex) int a = 0b1101; (10進数で13 (= 1 * 8 + 1 * 4 + 0 * 2 + 1 * 1))

  • 8進数: 0 (数字のzero)
    ex) int a = 0123; (10進数で83 (= 1 * 64 + 2 * 8 + 3 * 1))
    当然だが使用できるのは0 ~ 7(それ以外はコンパイルエラー)
  • 16進数: 0x, 0X
    ex) int a = 0x10B; (10進数で83 (= 1 * 64 + 2 * 8 + 3 * 1))
    A ~ Fは小文字でも良い
整数リテラル表記の_(アンダースコア)
  1. リテラルの先頭と末尾につけることができない。
  2. 記号の前後には記述できない
    記号とみなされるものには小数点の. , longfloat の接尾辞L, F そして2進数, 16進数の接頭辞の0b, 0xなどがある。(ただし8進数の接頭辞の0は数字であるので問題ない。)
// ok
int a = 0_52; // 8進数の接頭辞は数字の0なので記号ではない
int b = 0b0_1;

// compile error
int c = _123_456; // 1を違反
int d = 123_456_; // 1を違反
long e = 123_456_L; // 2を違反、longの接尾辞Lは記号とみなされる
double f = 3_.14F; // 2を違反
int g = 0x_52; // 2を違反
ラッパークラスとオートボクシング

ラッパークラスとはboolean, int, float, double …などのリミティブ型に対して、Boolean, Integer, Float, Double…とクラス型(Object)にしたもの。ラッパークラスと元となるプリミティブ間では以下の様にボクシングによって直接記述ができる。

  • オートボクシング

    ラッパークラスに直接数値リテラルを代入するコードの記述が可能
    コンパイル時には数値リテラルの10Integerインスタンスに変換される

    Integar a = 10;
    // Integar a = Integar.valueOf(10); とコンパイル時に解釈される
  • アンボクシング

    以下の様に、ラッパークラスの変数を元のプリミティブに直接代入するコードの記述が可能

    Float a = Float.valueOf(1);
            
    // int b1 = a; // aはFloatインスタンスなのでこのままだとアンボクシングができない
    int b2 = a.intValue(); // この様にintValueメソッドがあるのでそれを使う
    // int b3 = (Integer) a; // この様にキャストはできない
    // int b4 = (int) a; // この様にキャストはできない
    // primitiveとクラス間でのキャストはできない
    int b5 = (int) a.floatValue();
    
    float c = a; // アンボクシング
    // float c = a.floatValue(); とコンパイルされる

ラッパークラスはObjectのサブクラスであることが確認できる。

public class Main {
    Integer a;
    public static void main(String[] args) {
        System.out.println(new Main().a); // null 
        // Integerインスタンスはオブジェクトなので、フィールドで初期化してないとnull
        
        Integer b = Integer.valueOf(10);
        System.out.println(b instanceof Object); // true
        // ラッパークラスのインスタンスはObject
        
        Boolean c = null; // Objectなのでnullで初期化することが可能
    }
}
Numberクラス

数値リテラルのラッパークラス(Byte, Short, Long, Integer, Float, Doulbe)のスーパークラス
以下の様にNumberクラスを引数とした場合、その引数に数値リテラルが渡されたら対応するラッパークラスのインスタンスとして受け取る。

public class Main {
    public static void main(String[] args) {
        test((byte)0b0001);
        test((short)0b0011);
        test(0b0111);
        test(0b1111L);
        test(0.1F);
        test(0.11);
        test(null);
    }

    private static void test(Number n) {
        if (n instanceof Byte b) {
            System.out.println(b + " is Byte");
        } else if (n instanceof Short s) {
            System.out.println(s + " is Short");
        } else if (n instanceof Integer i) {
            System.out.println(i + " is Integer");
        } else if (n instanceof Long l) {
            System.out.println(l + " is Long");
        } else if (n instanceof Float f) {
            System.out.println(f + " is Float");
        } else if (n instanceof Double d) {
            System.out.println(d + " is Double");
        } else {
            System.out.println(n + " is not Number");
        }
        /* 上のif-else分岐はswitch文で以下の様に書ける
           instanceofの記述の仕方は以下の様にできSE17では基本的には導入されている
        switch (n) {
            case Byte b -> System.out.println(b + " is Byte");
            case Short s -> System.out.println(s + " is Short");
            case Integer i -> System.out.println(i + " is Integer");
            case Long l -> System.out.println(l + " is Long");
            case Float f -> System.out.println(f + " is Float");
            case Double d -> System.out.println(d + " is Double");
            case null, default -> System.out.println(n + " is not Number");
        }
         */
    }
}

// output
1 is Byte
3 is Short
7 is Integer
15 is Long
0.1 is Float
0.11 is Double
null is not Number
変数に使用して良い文字
  • $, _ 以外は基本的にダメ
  • 数字は2文字目以降
varを使用した変数宣言

ローカル変数を宣言する際にデータ型を推論する。つまりコンパイル時にデータ型が推論できる必要がある。またフィールド変数には使用できない
以下の例ではvarが使用不可能(コンパイルエラー)。

  • var a;: aの値が指定されてないのでaの型が推論できない。
  • var a = null;: anullなので、aの型が推論できない。
  • var a = () -> {};: ラムダ式は型が推論できない。
  • var a = {1, 2, 3};: 配列の初期化式({}を使った初期化)は、配列型変数の宣言と同時にのみ使用可能。varでは何型の配列インスタンスを宣言すれば良いかを判断できない。
    例えば int[] a = {1, 2, 3};int型の配列と宣言(int型の配列とわかる)のでコンパイル可能。
    一方 var a = {1, 2, 3};ではint型なのか、double型なのか、さらにはObject型なのかなど、どれかであるかを判断することが不可能。
  • 以下のコードだとコンパイル時にB a = new B();と解釈されるので、a = new C();と異なる型を代入できない。
    class B extends A {}
    class C extends A {}
    // BとCは互換性はない
    
    var a = new B(); // この時点でaの型はBと判断される
    a = new C();

以下の例はコンパイル可能。

  • var a = new ArrayList<>(); ジェネリクスによって型が指定されていないとき、デフォルトで<Object> となるのでvar a = new ArrayList<Object>(); となり型推論が可能。
  • var a = 123; では int a = 123; とコンパイル時に推論される。よって a = (int) 123L; とキャストすることによってコンパイルが通る。(当然だがint型と推論されているので、キャストしないとコンパイルエラーとなる。)
    var a = 123; // intと推論される
    a = (int) 123L;
    
    var b = 123.4; // doubleと推論される
null + ""

文字列の"null" になる。

String a = "" + null;
System.out.println(a + ", " + a.length());
// null, 4
+は順番に評価されていく

以下のコードの様に文字列を足すと、それ以降は文字列として順次扱う

int a = 0;
System.out.println(a++ + a + "," + a++ + ++a); // 1, 13
// 0 :current a = 0, next a = 1
// 1 (= 0 + 1) :a = 1
// "1," (= 1 + ",")
// "1,1" (= "1," + 1) :current a = 1, next a = 2
// "1,13" (= "1,1" + 3) :current a = 3, next a = 3  
Stringimmutable

Stringの変数は宣言された後は変更不可能。Stringクラスのメソッドは新しいStringインスタンスを返す。(元のインスタンスを変更しない。)

String str = "hoge, World.";
System.out.println(str.replaceAll("hoge", "hello")); // Hello, World.
System.out.println(str); // hoge, World.
Stringreplace, replaceAll, replaceFirst メソッド

3つとも(置換するchar or String, 置換されるchar or String)を引数として受け取り置換を行う。(置換するのが文字ならば、置換されるものも文字である必要がある。文字列についても同様)

  • replace, replaceAll順に全て置換するreplaceFirst は最初のマッチした部分のみ置換する。
  • replaceAll, replaceFirst は置換する対象に正規表現が使用可能
StringBuilderクラスのcapacity
  • デフォルトで16文字分のバッファを持つ。
  • 文字列を引数に渡すコンストラクタを使用した場合は「渡した文字列の長さ + 16文字分」のバッファを持つ。
StringBuilder sb = new StringBuilder("abcde");
System.out.println(sb.capacity()); // 21
テキストブロック
  • 最初の1行目は""" の後に改行する必要がある。(末尾の""" に関してはどちらでも良い。)
  • 特殊文字(" など)や改行文字(\n)のエスケープが不要。
  • インデントは一番左側にある文字列に揃えられる。(末尾の""" も含まれることに注意。)
コンスタントプール

文字列リテラルは定数値としてヒープとは異なる定数用のメモリ空間にインスタンスが作られ、そこへの参照がString型変数に代入される。
同じ文字列リテラルがプログラム内に再登場すれば、定数用のメモリ空間にある文字列インスタンスへの参照が「使い回し」にされる。

String a = "sample";
String b = "sample";
System.out.print(a == b); // true コンスタントプールにある同じStringインスタンスを指すため

コンスタントプールが有効になるのは文字列リテラルまたはString.valueOf()を使用したとき。
new演算子で作成した場合はその都度インスタンスが作られ、異なる参照を持つ

String a = "sample";
String b = String.valueOf("sample");
String c = new String("sample");
System.out.println(a == b); // true コンスタントプールにある同じStringインスタンスを指す
System.out.println(a == c); // false
Stringインスタンのintern()メソッド

既にメモリ内にある文字列への参照を返す。
以下の様に各変数のinternは全て同じ参照を返す。

String a = "sample";
String b = String.valueOf("sample");
String c = new String("sample");
System.out.println(a == a.intern()); // true
System.out.println(a == b.intern()); // true
System.out.println(a == c.intern()); // true
配列クラスのprint(出力)

配列クラスはObjectクラスを引き継ぐため、出力する際は一般的にObjectクラスのtoString()メソッドに定義されている文字列を出力する。
よって配列をprintすると以下の様に「クラス名 + @ + ハッシュコード」が出力される。

int a = new int[0];
System.out.println(a); // [I@f6f4d33
System.out.println(Arrays.toString(a)); // []

double[] b = new double[3];
System.out.println(b); // [D@f6f4d33
System.out.println(Arrays.toString(b)); // [0.0, 0.0, 0.0]

String[] c = new String[3];
System.out.println(c); // [Ljava.lang.String;@23fc625e
System.out.println(Arrays.toString(c)); // [null, null, null]

一方、char型配列はtoString()を使用しないと文字列を出力する

// charのデフォルト\u0000は表示されないので以下の様に値を入れておく
char[] d = {'a', 'b', 'c'};
System.out.println(d); // abc
System.out.println(d.toString()); // [C@f6f4d33
System.out.println(Arrays.toString(d)); // [a, b, c]

これはprint()char[]を引数に受け取る場合、文字列として出力するようPrintStream.javaに定義されているため。

public void print(char[] s) {
    write(s);
}

private void write(char[] buf) {
    try {
        if (lock != null) {
            lock.lock();
            try {
                implWrite(buf);
            } finally {
                lock.unlock();
            }
        } else {
            synchronized (this) {
                implWrite(buf);
            }
        }
    } catch (InterruptedIOException x) {
        Thread.currentThread().interrupt();
    } catch (IOException x) {
        trouble = true;
    }
}
PrintStream.java
配列の初期化子{}の使用条件
  • 変数宣言と同時にのみ使用可能
  • 初期化子を使用した場合は初期化子内の要素数でサイズを指定するので、サイズを指定してはいけない
// ok
int[] a = {1, 2, 3};
var b = new int[]{1, 2, 3};
int[] c;
c = new int[]{2, 3};
int[][] d = {};
int [][] e = new int[][]{};

// compile error
int[] f = new int[3]{1, 2, 3};
int[] g;
g = {1, 2, 3};
配列のclone()メソッド

同じ要素を持つ別のインスタンスを作成。ただし、各要素は参照で渡されているので、クローン先も同じ要素を参照する。

StringBuilder a0 = new StringBuilder("a0");
StringBuilder a1 = new StringBuilder("a1");
StringBuilder a2 = new StringBuilder("a2");
StringBuilder[] a = {a0, a1, a2};

var b = a.clone();
b[0].append("b0"); // b[0]とa[0]は同じインスタンスを参照
a1.append("x1"); // a[0]とb[0]は共にa1を参照
b[2] = new StringBuilder("b2"); // b[2]は新しく作成したStringBuilderインスタンを参照
System.out.println(Arrays.toString(a)); // [a0b0, a1x1, a2]
System.out.println(Arrays.toString(b)); // [a0b0, a1x1, b2]
ArrayListはスレッドセーフでない

スレッドセーフなリストを扱いたい場合はjava.util.Vectorを使用する。

ArrayListremove()メソッド
  • intを引数とするとき、indexとして扱い、その位置にある要素を削除。
  • Objectを引数とするとき、最初の同値(equalstrue)の要素を削除。
ArrayList<Integer> a = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 2, 3, 2, 5));
a.remove(1); // indexとして受け取る
System.out.println(a); // [1, 3, 4, 2, 3, 2, 5]
a.remove(Integer.valueOf(2)); // Objectとして受け取る
System.out.println(a); // [1, 3, 4, 3, 2, 5]
for文内での配列remove()

カーソルは変わらずに繰り上がりが起きる。
次のカーソルに移ったとき終了する場合のみ実行時エラーが出ない。(これは削除後に配列の読み出しをしていないため)

ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String str : list) {
    if ("b".equals(str)) {
		    // この時点でカーソルは1
        list.remove("b");
        list.remove("c");
        // "b", "c"を削除したので、"d"がカーソル1に来る
        // 次の処理に入る時にカーソルは2となり、listの終わりなのでエラーなく実行が終了する
    } else {
        System.out.println(str);
    }
}
// 出力
a


for (String str : list) {
    System.out.println(str);
}
// 出力
a
d

一方、削除後にも呼び出しをするということは、削除の処理とfor文内での要素呼び出しの処理を同時に配列に対して行うこととなる。
以下のコードではjava.util.ConcurrentModificationExceptionが実行時にスローされる。

ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String str : list) {
    if ("b".equals(str)) {
		    // この時点でカーソルは1
        list.remove("b");
        // "b"を削除したので、"c", "d"がそれぞれ繰り上がる
        // まだ終了しない(listの終わりではない)ので実行時エラー
    } else {
        System.out.println(str);
    }
}

ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String str : list) {
    if ("d".equals(str)) {
		    // この時点でカーソルは3
        list.remove("d");
        // "d"を削除したので、次のカーソルがlistの範囲を超えていることになり実行時エラー
    } else {
        System.out.println(str);
    }
}

要素の追加も同様にfor文内での要素呼び出しの処理と同時に行うこととなるためjava.util.ConcurrentModificationExceptionが実行時にスローされる。

ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String str : list) {
    if ("b".equals(str)) {
        list.add("B");
    } else {
        System.out.println(str);
    }
}

for (String str : list) {
    System.out.println(str);
}


以下の様に配列の要素に変更を加える分には問題ない。(for文を回して配列に対して操作しているだけ)

String[] strings = {"a", "b", "c"};
int i = 0;
for (String s : strings) {
    System.out.println(s);
    strings[(i+1)%3] += strings[(i+2)%3];
    i += 1;
}

System.out.println(Arrays.toString(strings));
// output
a
bc
ca
[abc, bc, ca]
List.of(), Arrays.asList(), ArrayList()の違い
  • List.of()で作成されたリストは不変長値も変更不可能
    これは以下の様にImmutableCollectionsを返すため。
    @SafeVarargs
    @SuppressWarnings("varargs")
    static <E> List<E> of(E... elements) {
        switch (elements.length) { // implicit null check of elements
            case 0:
                @SuppressWarnings("unchecked")
                var list = (List<E>) ImmutableCollections.EMPTY_LIST;
                return list;
            case 1:
                return new ImmutableCollections.List12<>(elements[0]);
            case 2:
                return new ImmutableCollections.List12<>(elements[0], elements[1]);
            default:
                return ImmutableCollections.listFromArray(elements);
        }
    }
    List.java

    ListインタフェースはSequencedCollectionインタフェースを継承し、SequencedCollectionインタフェースはCollectionインタフェースを継承するためset()add()メソッドはあるが、実行するとUnsupportedOperationException実行時エラーが起きる。(コンパイルは通る。)

  • Arrays.asList() で作成されたリストは不変長変更可能
    Arrays.asList()では以下の様にArrays.javaに定義されているprivate classArrayListを返す。(次に紹介するArrayList.javaに定義されているものとは異なることに注意。)
    以下の様にsetメソッドが定義されているので要素の変更が可能。
    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        @java.io.Serial
        private static final long serialVersionUID = -2764017481108945198L;
        @SuppressWarnings("serial") // Conditionally serializable
        private final E[] a;
    
        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
        
        ...
        
        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }
        
        ...
    }
    Arrays.java

    一方addメソッドは定義されていいなく、そのため継承しているAbstractListaddメソッドをそのまま使用する。しかし、これは以下の様にUnsupportedOperationExceptionthrowするため実行時エラーとなる。(コンパイルは通る。)

    ...
    
    public boolean add(E e) {
        add(size(), e);
        return true;
    }
    
    ...
    
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    AbstractList.java
  • ArrayList() は可変長で値の変更も可能。以下の様にList.ofまたはArrays.asListで作成したリストを渡すことでArrayListが作成できる。
    var list1 = List.of(1, 2, 3);
    var list2 = Arrays.asList(4, 5, 6);
    
    ArrayList<Integer> a = new ArrayList<>(list1);
    ArrayList<Integer> b = new ArrayList<>(list2);
    ArrayList<Integer> c = new ArrayList<>(3);
    // intを渡す場合、これはキャパシティを指定することになる
    // あくまでキャパシティでサイズではないので、c.isEmpty() = trueかつc.size() = 0であることに注意

演算子と制御

複数変数に対して同じ値を同時に代入
public class Main{
    int a, b, c;
    void setAll(int x) {
        a = b = c = x;
    }

    public static void main(String[] args) {
        Main m = new Main();
        m.setAll(5);
        System.out.println(m.a + ", " + m.b + ", " + m.c);

        int d, e, f;
        d = e = f = 10;
        System.out.println(d + ", " + e + ", " + f);
    }
}

// output
5, 5, 5
10, 10, 10
a += x, a++, ++a の違い
  • a += x, ++aaの値の更新と反映が同時に行われる。
  • a++aの値の更新が反映されるのは次に参照されるとき。
int a = 1;
int b = a += 1 + 5; // b = 2 + 5; となっている
System.out.println(a); // 2
System.out.println(b); // 7

a = 1;
b = a++ + 5; // b = 1 + 5;
System.out.println(a); // 2
System.out.println(b); // 6

a = 1;
b = ++a + 5; // b = 2 + 5;
System.out.println(a); // 2
System.out.println(b); // 7

a = 1;
b = a += 1 + (a += 2); // a += 1 + a += 2だとコンパイルエラー、()をつける必要がある
System.out.println(a); // 4
System.out.println(b); // 6
byte型の範囲

-128 ~ 127 であることに注意。


byte a = 0b1000_0000; // 左は128の整数リテラルなのでコンパイルエラー
byte b = -0b1000_0000; // 左は-128の整数リテラルなのでコンパイルが通る
Objectequals関数にnull が渡された場合

オーバーライドしてない限り必ずfalseを返す。

public boolean equals(Object obj) {
    return (this == obj);
}
Object.java
{}なしでelseif間で改行した場合

elseif間で改行した場合、else内にあるifブロックとみなされる

if (A) 
	// Do 1
else
if (B)
  // Do 2
else
  // Do 3
  
// 以下の様に解釈される  
if (A) {
	// Do 1
} else {
	if (B) {
		// Do 2
	} else {
		// Do 3
	}
}
変数の初期化

変数は必ず初期化してから使用する必要がある。
以下はコンパイルエラーとなる例

ex1).

int a;
System.out.println(a); // Variable 'a' might not have been initialized

ex2).

int a; // 変数aを初期化せずにswtich文で使用するとコンパイルエラー
switch(a) { // Variable 'b' might not have been initialized
   case 0 -> System.out.println("zero");
}

ただしフィールドの宣言は初期化を記述しなくてもデフォルトの値が使用される。

public class Main{
    int a;
    String str;
    Number num;

    void print() {
        System.out.println("a: " + a + ", str: " + str + ", num: " + num);
    }

    public static void main(String[] args) {
        new Main().print();
    }
}

// ouput
a: 0, str: null, num: null
switch文の条件式で戻せる型
  • int以下の整数型とそのラッパークラス
  • 文字, 文字列
  • 列挙型(enum)

float, double== による比較がブレるので使用不可能。
long型を使用するほど多岐に渡る分岐はあり得ないので使用不可能。

boolean型は二値なのでswitch文を使うまでもないので使用不可能。

swtich文で評価するString変数の中身がnullだった場合

switch文では評価するString変数の中身がnullだった場合、実行時にNullPointerExceptionが発生する。
これはswitch文の分岐ではStringに対してハッシュコード(hashCodeメソッド)が使われるため。

public class Main{
    int a;
    String str;
    public void method1() {
        switch(a) {
            case 0 -> System.out.println("zero");
            case 1 -> System.out.println("one");
        }
    }

    public void method2() {
        switch(str) {
            case "A" -> System.out.println("A");
            case "B" -> System.out.println("B");
        }
    }

    public static void main(String[] args) {
        Main m = new Main();
        m.method1(); // zero
        // int型は初期化してない場合デフォルトで0
        
        m.method2(); // NullPointerException: Cannot invoke "String.hashCode()" because "<local1>" is null
        // String型は初期化してない場合デフォルトでnullなので実行時エラーが発生       
    }
}
breakがないswtich文の挙動

当てはまるものがなければdefault文に戻り、default文にbreakがなければbreakがあるまで以降順番に実行する。(default文はどこに記述してもよく、記述した位置の順序で実行される。)

int a = -1;
switch (a) {
    case 0: System.out.println("0");
    case 1: System.out.println("1");
    default: System.out.println("Default");
    case 2: System.out.println("2");
    case 3:
        System.out.println("3");
        break;
    case 4: System.out.println("4");
}

// 出力
Default
2
3
switch文とswitch式の違い

どちらも:または->で記述可能。
switch式では値を返し、またdefaultと末尾の;が必要

  • switch
    • :を使用する場合
      int a = 1;
      switch (a) {
          case 1:
              System.out.println(1);
              break;
          case 2:
              System.out.println(2);
              break;
          default:    
              System.out.println("default");
              break;
      }
      
      // ouput
      1
    • ->を使用する場合
      int b = 3;
      int c = 0;
      switch (c)
          case 1 -> System.out.println("1");
          case 2 -> System.out.println("2");
          case 3 -> c += 1;
          case 4 -> System.out.println("4");
          default -> System.out.println("default");
      }
      System.out.println(c);
      
      // output
      1
  • switch
    • switch式ではyieldを使用して値を返す。またthrowを使用してエラーを投げることも可能
    • ->:は1つのswitch式で同時に使用できない
    • :を使用したswitch文ではyieldを忘れるとフォールスルーが発生
    • defaultと末尾の;が必要
    • :を使用したswitch式のdefaultでは必ず値を返すかスローする必要がある。これは何も値を返さないパターンを避けるため。(->を使用したswitch文ではそもそも値を返さないとコンパイルエラー)
    • :を使用する場合
      // :を使用する場合
      int a = 0;
      String b = switch (a) {
          case 0: System.out.println("print 0"); // yieldがないのでフォールスルーが起きる
          case 1: {
              System.out.println("print 1");
              yield "1";
          }
          case 2: {
              System.out.println("2");
              yield "2";
          }
          default: throw new RuntimeException("Unexpected");
      }; // ;を忘れずに!
      System.out.println(b);
      
      // output
      print 0
      print 1
      1
    • ->を使用する場合
      a = 1;
      String c = switch (a) {
          case 0 -> "0"; // case 0 -> System.out.println("print 0"); の様に値を返さないのはコンパイルエラーとなる
          case 1 -> {
              System.out.println("print 1");
              yield "1";
          }
          case 2 -> "2";
          default -> throw new RuntimeException("Unexpected");
      }; // ;を忘れずに!
      System.out.println(c);
      
      // output
      print
swtichの評価する値を更新

switchの評価する値をcase内で更新することは可能。

ex1).

int a = 0;
switch (a) {
    case 0 -> a += 1;
    case 1 -> a += 1;
    case 2 -> a += 1;
}
System.out.println(a);

// output
1

ex2).

int a = 1;
switch (a) {
    case 0:
        System.out.println(a + ", case 0");
        a += 1;
    case 1:
        System.out.println(a + ", case 1");
        a += 2;
    case 2:
        System.out.println(a + ", case 2");
        a += 3;
    case 3:
        System.out.println(a + ", case 3");
        a += 4;
}
System.out.println(a);

// output
1, case 1
3, case 2
6, case 3
10

ex3).


int a = 1;
switch(++a) {
    case 1 -> System.out.println("print 1");
    case 2 -> System.out.println("print 2");
}

// output
print 2
while文、 do-while文での{}の省略

if-else構文同様に最初の一文までが実行される。

int cnt = 0;
while (cnt++ < 5)
    System.out.println(cnt);
    cnt += 5; // while文内の処理ではなく、while文の次の処理となっている
System.out.println(cnt);
// output
1
2
3
4
5
11

int cnt = 0;
while (cnt < 3)	
    do System.out.println(cnt++);
    while (cnt < 5);
// output
0
1
2
3
4

ただし以下の様に{}なしのdo-while文ではdowhileの間は2文以上記述するとコンパイルエラー

int cnt = 0;
do
    System.out.println(cnt++);
    System.out.println("-"); // 2文目があるのでコンパイルエラー
while (cnt < 5);
for文の更新文部分では複数処理が記述可能
for (int i = 0; i < 3; i++, System.out.print(",")) {
    System.out.print(i);
}
// 出力
0,1,2,
ラベル

breakcontinueのときに制御を移す箇所を自由に指定できる。以下がラベルがつけられる対象。(ほとんどにつけることができる)

  • コードブロック({}
a: {
	int i = 10;
}
  • ループ文と分岐(if-else)
b: for (int i = 0; i < 5; i++) {
	System.out.println(i);
}

c: if (true) {
	// do something
}
  • 代入と式
int x = 0; // これは宣言なのでラベルをつけることはできないことに注意!
d: x = 2; // これは代入なのでラベルをつけることが可能
e: System.out.println(x); // 式なのでラベルをつけることが可能
  • return
private static int sample() { // これは関数宣言なのでラベルをつけることはできないことに注意!
	f: return 0; // return文
}
  • try, throw
g: try {
	System.out.println("try something");
} finally {
	h: throw new RuntimeException();
}

Class and Instance

クラスのフィールドとローカルの変数名が同じとき

ローカルとフィールドで同じ名前の変数がある場合はローカルが優先される。

public class Main{
    private int num = 10;

    public void method() {
        // int num = num; // コンパイルエラー Variable 'num' might not have been initialized
        // this.numを参照することはない

        // ローカルのnumは定義されてないのでフィールドのnumを参照
        int num2 = num;

        int num = 100;
        num = num; // ローカルで定義した後に代入なのでコンパイルされる(自己代入)
        System.out.println(num); // ローカルのnum
        System.out.println(this.num); // フィールドのnum
        System.out.println(num2); 
    }

    public static void main(String[] args) {
        new Main().method();
    }
}

// output
100
10
10
int, float, doubleを引数にする際のオーバーロード

doubleint同様に整数リテラルを受け取るため、以下ではどちらのcalcを使えば良いかわからなくなりコンパイルエラーとなる。(floatも同様)

public class Main {
    public static void main(String[] args) {
        Main m = new Main();
        m.calc(2, 3); // Ambiguous method call compile error となる
    }

    private double calc(int a, double b) {
        return (a + b) / 2;
    }

    private double calc(double a, int b) {
        return (a + b) / 2;
    }
}

ただし、以下の様にどちらも引数がintのオーバーロードが存在すれば、そちらが優先されてコンパイルエラーとはならない

public class Main {
    public static void main(String[] args) {
        Main m = new Main();
        m.calc(2, 3); // (3)のcalcが優先される
    }

    private double calc(int a, double b) { // (1)
        return (a + b) / 2;
    }

    private double calc(double a, int b) { // (2)
        return (a + b) / 2;
    }

    private double calc(int a, int b) { // (3)
        return (a + b) / 2;
    }
}
premitive配列とObject配列には互換性がない

以下のコードの様にpremitive配列はラッパークラスの配列とは解釈されず、Objectとして解釈される。
(もちろんtest(int[] args)があればこっちの方がより厳密なのでこっちが使われる)

public class Main {
    public static void main(String[] args) {
        test(new int[]{1, 2, 3});
    }

    public static void test(Object[] args) {
        System.out.println("Object[]");
    }

    public static void test(Object arg) {
        System.out.println("Object");
    }

    public static void test(Integer[] args) {
        System.out.println("Integer[]");
    }
}

// ouput
Object
instanceof

そのクラス、またはサブクラスであるときtrueを返す。

class A {}
class B extends A {}

public class Main{
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new B();
        B b = new B();
        System.out.println((a1 instanceof A) + ", " + (a1 instanceof B));
        System.out.println((a2 instanceof A) + ", " + (a2 instanceof B)) ;
        System.out.println((b instanceof A) + ", " + (b instanceof B));
    }
}

// output
true, false
true, true
true, true
instanceofのパターン変数のスコープ

パターン変数はif-else文内で評価がtrueとなるスコープ内で使用可能

public class Main {
    String str = "field str";

    public static void main(String[] args) {
        Main m = new Main();

        m.test1("string 1"); // string 1
        m.test1(null); // field str

        m.test2("string 2"); // string 2
        m.test2(10); // obj:{10} is not String

        m.test3("string 3"); // String:{string 3} is String and not Number, so here is always true
        m.test3(10); // obj:{10} is not String

    }

    public void test1(Object obj) {
        if (obj instanceof String str) {
             System.out.println(str); // パターン変数のstr
        } else {
            System.out.println(str); // フィールド変数のstr
        }
    }

    public void test2(Object obj) {
        if (!(obj instanceof String s)) {
            // System.out.println(s); // 評価がtrueでないので使用できない
            System.out.println("obj:{" + obj + "} is not String");
        } else {
            System.out.println(s); // こちらは評価がtrueなので使用可能
        }
    }

    public void test3(Object obj) {
        if (!(obj instanceof String s)) {
            // System.out.println(s); // 評価がtrueでないので使用できない
            // System.out.println(n); // パターン変数nはここより後のelse if に定義されているので使用できない
            System.out.println("obj:{" + obj + "} is not String");
        } else if (!(obj instanceof Number n)) { // 実はここではobjがStringであるためNumberでないことが確定(常にtrue)
            System.out.println("String:{" + s + "} is String and not Number, so here is always true");
            // System.out.println(b); // パターン変数bはここより後のelse if に定義されているので使用できない
        } else if (!(obj instanceof Boolean b)) {
            System.out.println(s);
            System.out.println(n);
        } else {
            System.out.println(s);
            System.out.println(n);
            System.out.println(b);
        }
    }
}
初期化子{}とコンストラクタ

初期化子はコンストラクタ関数が実行される前に実行される。
これは複数のコンストラクタに対して共通の事前処理を記述するのに便利。

⚠️
初期化子もコンストラクタもインスタンスが生成されるタイミングで初めて実行される。
また初期化子は複数定義できる

ex).

class Sample {
    Sample() {
        System.out.println("constructor");
    }
    {
        System.out.println("initializer {}");
    }
}

public class Main {
    public static void main(String[] args) {
        Sample sample = new Sample();
    }
}

// initializerの方が先に出力される
initializer {}
constructor
static初期化子

初期化子もコンストラクタもインスタンスが生成されるタイミングに初めて実行されるため、以下の様なコードだと初期化子もコンストラクタも呼び出されない。

class Sample {
    static int num = 0;
    Sample() {
        num = 10;
    }
    {
        num = 20;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Sample.num);
    }
}

// output
0

static初期化子は、staticフィールド、staticメソッド呼び出しのタイミングでも実行される。これによって以下の様にstaticフィールドの定義が可能となる。(static初期化子なので扱うフィールドとメソッドもstaticである必要がある)

class Sample {
    static int num = 0;
    Sample() {
        num = 10;
    }
    static {
        num = 20;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Sample.num);
    }
}

// output
20
デフォルトコンストラクタ

プログラマがコンストラクタを定義してないときのみ自動で追加される。

protectedの継承関係

インスタンスは1つのクラスから作られるのではなく、複数のクラス(親のクラスを遡る)によって定義される。そのため、以下の様な例ではコンパイルエラーとなる。

package other;

public class OtherSample {
    private String str;
    public void setStr(String str) {
        this.str = str;
    }
    protected void printInfo() {
        System.out.println(str);
    }
}

package ex;

import other.OtherSample;

public class ChildSample extends OtherSample {}
package ex;

public class Main {
    public static void main(String[] args) {
        ChildSample sample = new ChildSample();
        sample.setStr("abc");
        // sample.printInfo(); // コンパイルエラー
    }
}

printInfo関数はprotectedであるため継承したChildSampleクラス内では使用可能。しかしMainクラス内で作成されたChildSampleインスタンスは、作成時に以下の様に継承関係を遡っていく。

MainChildSampleOtherSample

MainクラスはOtherSampleを継承してなく、また同じパッケージにないためprotected関数であるprintInfoは使用できない。

Recordクラスの構造
public record Customer(String name, int age) {}

このクラスを逆コンパイル

javac Customer.java && javap -c Customer
Compiled from "Customer.java"
public final class Customer extends java.lang.Record {
  public Customer(java.lang.String, int);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Record."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #7                  // Field name:Ljava/lang/String;
       9: aload_0
      10: iload_2
      11: putfield      #13                 // Field age:I
      14: return

  public final java.lang.String toString();
    Code:
       0: aload_0
       1: invokedynamic #17,  0             // InvokeDynamic #0:toString:(LCustomer;)Ljava/lang/String;
       6: areturn

  public final int hashCode();
    Code:
       0: aload_0
       1: invokedynamic #21,  0             // InvokeDynamic #0:hashCode:(LCustomer;)I
       6: ireturn

  public final boolean equals(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: invokedynamic #25,  0             // InvokeDynamic #0:equals:(LCustomer;Ljava/lang/Object;)Z
       7: ireturn

  public java.lang.String name();
    Code:
       0: aload_0
       1: getfield      #7                  // Field name:Ljava/lang/String;
       4: areturn

  public int age();
    Code:
       0: aload_0
       1: getfield      #13                 // Field age:I
       4: ireturn
}
  • java.lang.Recordを継承したfinalクラスが作られる(finalなのでレコードの継承はできない)
  • コンストラクタが自動で生成されている
  • toString, hashCode, equals関数が自動で生成されている
  • フィールドと同じ名前のgetter関数が生成されている(この例だとname(), age()
⚠️
Recordクラスから作成されるインスタンスは更新不可(immutable)
クラス(recordenumなども)に使用できる修飾子
  • トップレベルコード(ネストされていない一番外側のクラス): publicデフォルト(修飾子なし)
  • インナーレベルコード: protected, privateも使用可能。またstaticクラスとして宣言することも可能。
  • ローカルレベルコード: そもそも修飾子が使用不可能(無修飾子のデフォルトのみ)
// トップレベルはpublicか無修飾子のみ(static不可)
public class Sample {
    
    // インナーレベルはさらにprotected, privateが可(static可)
    public class PublicInner {}
    protected class ProtectedInner {}
    private class PrivateInner {}
    public static class StaticInner {}
    
    void method() {
        new PublicInner();
        new ProtectedInner();
        new PrivateInner();
        new StaticInner();
        
        // ローカルレベルでは無修飾子のみ(static不可)
        class Local {}
    }
    
    static void staticMethod() {
        // new PublicInner(); // staticでないインナークラスは不可
        new Sample(); // トップレベルクラスは可
        new Sample2();
        new StaticInner(); // staticなインナークラスは可
    }
}

class Sample2 {}
レコードで宣言できるもの
  • コンストラクタ
  • staticフィールド
  • static初期化子(通常の初期化子は記述できない
  • インナークラス、ローカルクラス、インナーインタフェース
  • メソッド(public, protected, 無修飾子, private全部できる。またstaticであってもなくても良い。つまり通常のクラスと同様にメソッドの宣言ができる)
record Person(String name, int age) {
    // フィールドと初期化子はstaticである必要がある
    static int staticValue;
    static int staticValue2;
    static {
        staticValue = 10;
    }
    static {
		    staticValue2 = 20;
    }
    
    // クラス同様にインナークラスが宣言可能
    public class PublicClass {}
    class DefaultClass {}
    protected class ProtectedClass {}
    private class PrivateClass {}

    private void print() {
        // クラス同様にローカルクラスが宣言可能
        class LocalClass {}
        System.out.println("private");
        System.out.println(this.name + ", " + age);
    }
    public void print2() {
        System.out.println("private");
        System.out.println(this.name + ", " + age);
    }

    protected void print3() {
        System.out.println("protected");
        System.out.println(this.name + ", " + age);
    }

    void print4() {
        System.out.println("default");
        System.out.println(this.name + ", " + age);
    }

    public static void print5() {
        System.out.println("public static");
        System.out.println(staticValue + staticValue2);
        // System.out.println(this.name); // staticでないフィールドにアクセスできない
    }
};
レコードのコンストラクタ
  • 標準コンストラクタ: 自動的に生成され、フィールドの初期化を行う。(クラスのデフォルトコンストラクタと異なり、プログラマが代替コンストラクタなどを定義しても必ず生成される。)
  • 代替コンストラクタ:
    • プログラマが定義するコンストラクタで、標準コンストラクタと同じ引数を受け取り、すべてのフィールドの初期化を行う必要がある。(受け取る引数が標準コンストラクタと同じため、実質標準コンストラクタのオーバーライド)
    • 代替コンストラクのアクセス修飾子はレコードそのものの修飾子と同じかよりゆるい必要がある。
    
    record Data(String val1, String val2) {
    		// 代替コンストラクタ
        Data(String val1, String val2) {// publicとprotectedでも可(無修飾子よりもゆるいか同じ)        
            // 標準コンストラクタと同じ引数を受け取り、全フィールドの初期化を行っている
            this.val1 = val1 + "+val1";
            this.val2 = val2 + "+val2";
        }
        
        // これはあくまで標準コンストラクタを拡張(オーバーロード)したもので代替コンストラクタではない    
        Data(String val1) {
            this(val1, "value2"); // 上の代替コンストラクタを使用する
        }
    }
    
    public class Main{
        public static void main(String[] args) {
            Data a = new Data("a1","a2");
            Data b = new Data("b1");
            System.out.println(a); // Data[val1=a1+val1, val2=a2+val2]
            System.out.println(b); // Data[val1=b1+val1, val2=value2+val2]
        }
    }
  • コンパクトコンストラクタ:
    • 渡された引数の妥当性を検証する機能を追加するためのコンストラクタ。ただし代替コンストラクタを記述した場合は記述できない
      (代替コンストラクタを記述すると同じシグネチャのコンストラクタが2つあると判断されコンパイルエラー。代替コンストラクタでの検証は代替コンストラクタ内で記述すれば良い。)
    • レコード名だけで宣言する。
    • コンパクトコンストラクのアクセス修飾子はレコードそのものの修飾子と同じかよりゆるい必要がある。
    • コンパクトコンストラクタが実行されてから標準コンストラクタが実行される。(コンパクトコンストラクタが標準コンストラクタの先頭に定義されている様コンパイルされる。)
    • コンパクトコンストラクタは複数定義できない。(1つまで)
    • コンパクトコンストラクタ内で明示的に別のコンストラクタを呼び出してはならない。
    • コンパクトコンストラクタ内でフィールドにアクセスできない。以下の例だとthis.val1とフィールドにアクセスすることはできない。
    • コンパクトコンストラクタ内でreturnを記述してはならない。(フィールドを初期化する標準コンストラクタが呼び出されなくなるため。)

    ex1).

    record Data(String val1, String val2) {
        Data{ // publicとprotectedでも可(修飾子なしよりもゆるいか同じ)  
            if (val1.isEmpty()) {
                throw new IllegalArgumentException("val1 cannot be empty");
            }
            if (val2.isEmpty()) {
                throw new IllegalArgumentException("val2 cannot be empty");
            }
        }
    
        Data(String val1) {
            this(val1, "value2");
        }
    }

    ex2).

    public record Customer(String name, int age){
        static {
            System.out.println("Initializing Customer 1");
        }
        static {
            System.out.println("Initializing Customer 2");
        }
        public Customer {
            if (age < 0) { // this.ageはアクセスできない
                throw new IllegalArgumentException("Age cannot be negative");
            }
            System.out.println("age is valid");
        }
    
        public static void main(String[] args) {
            Customer customer = new Customer("John", 10);
        }
    }
    
    // 出力
    Initializing Customer 1
    Initializing Customer 2
    age is valid
レコードとineterface

レコードはjava.lang.Recordを継承したクラスとしてクラスファイルが出力されるため他のクラスの継承はできないがinterfaceの実装は可能。
その際にinterfacedefaultメソッドの名前と同じフィールドが存在する場合、オーバーライドする。
ex).

interface Inf {
    default String name() {
        return "interface";
    }
}

public record Customer(String name, int age) implements Inf {
    public static void main(String[] args) {
        Customer customer = new Customer("John", 10);
        System.out.println(customer.name());
    }
}

// 出力
John

ただし、以下の様に戻り値型が違うと、レコードのコンパイル時にエラーが発生する。

interface Inf {
    default void name() {
        System.out.println("interface");
    }
}

public record Customer(String name, int age) implements Inf { // 'name()' in 'Customer' clashes with 'name()' in 'Inf'; attempting to use incompatible return type
    public static void main(String[] args) {
        Customer customer = new Customer("John", 10);
        System.out.println(customer.name());
    }
}
⚠️
あるクラスがあるinterfaceを実装する際、interfaceにある関数と同じシグネチャで違う戻り値の関数を定義することはできない。以下の様なコードはコンパイルエラーとなる。
interface Inf {
    void name();
    default void age() {
        System.out.println("default method");
    }
}

public class Customer implements Inf {
    public String name() {
        return "name method";
    }
    
    public int age() {
        return 10;
    }
}

クラスではinterfaceの抽象関数を実装する必要があり、nameについては以下の様に中身を記述する必要がある。その際に同じシグネチャの関数を重複定義することになる。defaultメソッドについても同じ様な理論で衝突を起こす。

interface Inf {
    void name();
    default void age() {
        System.out.println("default method");
    }
}

public class Customer implements Inf {
    public void name() {
        System.out.println("Customer name");
    }
    
    public String name() {
        return "name method";
    }

    public int age() {
        return 10;
    }
}

Interface and inheritance

継承の構造

スーバークラスAを継承したサブクラスをBを定義する。そしてサブクラスBのインスタンスを作成する。
このとき、作成したインスタンスの構造としては、クラスAのインスタンスと差分インスタンスの両方を合わせた構造となる。

継承の際のメソッドの挙動

JVMは、まず宣言したクラスのメソッドに対応するメソッドを探す。そしてインスタンスに対してオーバーライドしたメソッドがあればオーバーライドしたメソッドを使用する。(なければスーパークラスのメソッドを使用)

以下の例のA a2 = new B(); については、宣言はクラスAAとして扱う)でインスタンスはクラスBAインスタンスと差分インスタンス)となっている。
このとき、a2.methodはまずAmethodを探す。実際のa2の中身はBインスタンスで、methodはオーバーライドされているのでBmethodを呼び出す。

CollctionListのスーパーインタフェース。(正確にはListSequencedCollectionを継承し、SequencedCollectionCollectionを継承している。)よってList型を引数に渡したときは2つのオーバーロードのメソッド間で互換性がある。このときJVMはより厳密な方を選択する。

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

class A {
    public void method(Collection args) {
        System.out.println("A");
    }
}

class B extends A {
    public void method(Collection args) {
        System.out.println("B");
    }

    public void method(List args) {
        System.out.println("C");
    }
}

public class Main{
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new B();
        B b1 = new B();

        Collection collection = new ArrayList<>();
        List<String> list = new ArrayList<>();

        a1.method(collection);
        a1.method(list);
        // output
        // A
        // A
        
        a2.method(collection);
        a2.method(list); // クラスAのメソッドをオーバーライドしたクラスBのメソッドを使用
        // output
        // B
        // B

        b1.method(collection);
        b1.method(list); // より厳密な型であるListを引数として受け取るメソッドを使用
        // output
        // B
        // C
    }
}
継承元のメンバーをsuperで取得
  • superによって親クラスのメンバーを取得する。(親クラスに定義されてない場合はさらに親を遡っていく)
  • superで指定しない場合はそのクラスで定義したものを使用する。(そのクラスで定義していない場合は親クラスを遡っていく)
class A {
    int a1 = 10;
    int a2 = 10;

    public void method() {
        System.out.println("        == A method ==");
        System.out.println("        a1 = " + a1 + ", a2 = " + a2);
        System.out.println("        == A method end ======");
    }
}

class B extends A {
    int a2 = 20;
    int b1 = 20;
    int b2 = 20;

    public void method() {
        System.out.println("    == B method ==");
        super.method();
        System.out.println("    a1 = " + a1 + ", a2 = " + a2
                + ", super.a1 = " + super.a1 + ", super.a2 = " + super.a2);
        System.out.println("    b1 = " + b1 + ", b2 = " + b2);
        System.out.println("    == B method end ==");
    }
}

class C extends B {
    int b2 = 30;
    public void method() {
        System.out.println("== C method ==");
        super.method();
        System.out.println("a1: " + a1 + ", a2: " + a2
                + ", super.a1: " + super.a1 + ", super.a2: " + super.a2);
        System.out.println("b1: " + b1 + ", b2: " + b2
                + ", super.b1: " + super.b1 + ", super.b2: " + super.b2);
        System.out.println("== C method end ==");
    }
}

public class Main{
    public static void main(String[] args) {
        A a = new C();
        B b = new C();
        C c = new C();
        a.method();

        // b.method(); c.method();も同じ結果
        // メソッドについてあくまで実装に依存
    }
}

// output
== C method ==
    == B method ==
        == A method ==
        a1 = 10, a2 = 10
        == A method end ======
    a1 = 10, a2 = 20, super.a1 = 10, super.a2 = 10
    b1 = 20, b2 = 20
    == B method end ==
a1: 10, a2: 20, super.a1: 10, super.a2: 20
b1: 20, b2: 30, super.b1: 20, super.b2: 20
== C method end ==
継承で引き継がれないもの
  • コンストラクタ
    class A {
        public A(String str) {
            System.out.println(str);
        } 
    }
    
    // class B extends A {}
    // class A継承してもコンストラクタは継承されないためコンパイルエラー
    // There is no parameterless constructor available in 'A'
    // class Aではstrを引数に持つコンストラクタを定義したため以下の様にこれを呼び出す必要がある
    class B extends A {
        public B(String str) {
            super(str);
        }
    }
    
    class C {}
    class D extends C {} // defaultコンストラクタが使われる
  • privateフィールド、メソッド
interfacedefaultメソッドではjava.lang.Objectクラスにあるメソッドは定義できない(コンパイルエラーとなる)

以下のコードはコンパイルエラーとなる。

interface Inf {
    default String toString() { // Default method 'toString' overrides a member of 'java.lang.Object'
        return "Interface String";
    };
}

// 以下のように抽象メソッドとしては定義できる
interface Inf2 {
    String toString();
}
デフォルトメソッドの呼び出し

以下の様に<継承先interface>.supper.<defaultメソッド名>によって呼び出すことができる。
ただし、直属のinterfaceであるBに対してのみできる。(Aで呼び出すことはできない。)
またデフォルトメソッドをstaticメソッド内で呼び出すことはできない。

interface A { // interfaceは暗黙的にabstractが付けられている(abstractを前に付けれるが、冗長)
    default void sample() {
        System.out.println("A");
    };
}

interface B extends A {}

class C implements B {
    @Override
    public void sample() {
        // A.super.sample();
        // Aを直接呼び出せないのでコンパイルエラーとなる

        B.super.sample(); // Bは直属のinterfaceなので呼び出せる
        System.out.println("C");
    }

    public void sample2() {
        B.super.sample();
        System.out.println("C2");
    }
}

public class Main{
    public static void main(String[] args) {
        C c1 = new C();
        c1.sample(); // A, Cを出力
        c1.sample2(); // A, C2を出力

        A c2 = new C();
        c1.sample(); // A, Cを出力
        // c2.sample2();
        // c2の中身はCインスタンスだが、Aの型として認識されているのでsample2がなくコンパイルエラー

        ((C) c2).sample2(); // Cとキャストすることでsample2が実行できる
        // A, C2を出力
    }
}
多重継承で同じシグネチャのdefaultメソッドがある場合

以下の様にオーバーライドしないとどちらを使えば良いか分からなくなりコンパイルエラーとなる。
同じシグネチャで戻り値が異なる場合は、そもそも同じ関数が複数あるとみなされてコンパイルエラーとなる。

interface A {
    default void sample() {
        System.out.println("A");
    }
}

interface B {
    default void sample() {
        System.out.println("B");
    }
}

class C implements A, B {
    // Overrideしないとどちらのsampleかわからずコンパイルエラーとなる
    // どちらも必ずしも呼び出す必要はない、オーバーライドして中身を明示的に実装しておけば良い
    public void sample() {
        A.super.sample();
        B.super.sample();
        System.out.println("C");
    }
}
abstractメソッドは具体サブクラスで必ず実装される必要がある。(抽象サブクラスであれば実装する必要はない)

abstractメソッドやinterfaceメソッドは必ず具象クラスで実装される必要がある。
実装されることが前提なため、以下の様にabstarctクラスのメソッド、interfacedefaultメソッド内で使用できる。

interface Inf  {
    default void sample() {
        System.out.println("Interface");
        method(); // interfaceのメソッドをデフォルトメソッド内で呼び出すことは可能
        // なぜなら実装する具体クラスでは必ずmethodの中身が実装されるため
    }

    void method();
}

abstract class AbsClass {
    void sample() {
        System.out.println("Abstract Class");
        method(); // abstractメソッドを呼び出すことは可能
        // なぜなら継承する具体クラスでは必ず実装されるから
    }
		
		// abstractメソッドにはpublic, protected, 無修飾子が使用可能
		// privateだと継承先で定義ができないため使用不可能
		// 抽象メソッドの場合は必ずabstractをつける(interfaceの様に省略できない)
    abstract void method();
}

abstract class AbsClass2 extends AbsClass {}

class ConcClass extends AbsClass {
		// ポリモーフィズムより、abstractメソッドの実装でも修飾子は継承元と同じかより緩い
		// ここでは無修飾子、protected、publicが可能
    void method() { // abstractメソッドを実装しないとコンパイルエラー
        System.out.println("Conc Class Method");
    }
}

public class Main{
    public static void main(String[] args) {
        ConcClass concClass = new ConcClass();
        concClass.sample();
    }
}

// 出力
Abstract Class
Conc Class Method
オーバーライドと共変戻り値

オーバーライドの戻り値は、同じ型かそのサブクラスであれば指定できます。これを共変戻り値と呼ぶ。
以下の様にNumberクラスのサブクラスであるIntegarクラスを戻り値とするメソッドにオーバーライド可能

public Number method() {
	// any code
}

public Integar method() {
	// any code
}	
⚠️
オーバーライドはスーバークラスの定義を上書きするものではない。スーバークラスの定義に加えて、サブクラスに新しい定義を追加する。
そのためサブクラスのインスタンスにはスーパークラスに定義したメソッドと、サブクラスにオーバーライドされたメソッドの両方が当時に存在する。
オーバーライドした際の修飾子

オーバーライドした際のアクセス修飾子は元と同じか、より緩いものである必要がある。
これは以下の様にポリモーフィズムを使った時に正しく動作させるため。

class A {
    protected void method() {
        System.out.println("A method");
    }
}

class B extends A {
    public void method() { // protectedか、それより緩いアクセス修飾子にする
        System.out.println("B method");
    }
}

public class Main{
    public static void main(String[] args) {
        A obj = new B(); // Bのインスタンスとして作成される
        obj.method(); // objはクラスAとしてコンパイラは判断する
        // そのためprotectedの制限下でmethodにアクセスする
        // しかしBでprivateにオーバーライドされた場合、protectedよりも厳しい制限となり、ここでのアクセスが不可能となる
        // これは矛盾してしまう
    }
}

// 出力
B method
オーバーライドとジェネリクス

前提として、ジェネリクスに対してコンパイル時にはtype erasure型消去)という挙動が起こる。これはジェネリクス内はObjectとして解釈されるため。
以下の様に2つのtest関数のジェネリクス内は<Number>, <String>と完全に異なる。これはどちらもSet<Object>として解釈される。よって衝突が起こりコンパイルエラーとなる。(オーバーロードとはならない)

class A {
    public ArrayList<Integer> test(Set<String> s) { // 'test(Set<String>)' clashes with 'test(Set<Number>)'; both methods have same erasure
        return new ArrayList<>();
    }

    public List<Integer> test(Set<Number> s) {
        return new ArrayList<>();
    }
}

一方、オーバーライドに関しては、シグネチャはジェネリクス内も含めて完全に同じものが対象となる。

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

class A {
    public List<Number> test(Set<CharSequence> s) {
        return new ArrayList<>();
    }
}

// type erasureより、
// 事実上クラスB内で定義できるtest(set<T> s)の形をとる関数はtest(set<CharSequence> s)のみで、
// オーバーライドを行なっている
class B extends A {
    // ArrayListはListを継承したものなのでオーバーライドが成立する
    // ただしList<Integer>の様にジェネリクス内が継承されているのはコンパイルエラー
    @Override
    public ArrayList<Number> test(Set<CharSequence> s) { // シグネチャはジェネリクス内まで完全に一致する必要がある
        return new ArrayList<>();
    }
    
    // overload
    public List<Number> test(TreeSet<CharSequence> s) {
        return new ArrayList<>();
    }
}
継承関係のクラスに同名フィールドが存在するとき
  • 同じフィールド名がある場合、変数の型で宣言された方を使用。
  • thisについても同様で、スーバークラス内のthisはスーパークラスを指し、差分クラスのthisは差分クラスを指す。
  • メソッドに関しては、メソッド内の指示に従う(オーバーライドがあればオーバーライドされたものを使用)。
class A {
    String val = "A";
    String val2 = "A+";
    void method() {
        System.out.println("A method: " + val + ", " + this.val);
    }

    void method2() {
        System.out.println("A method2: " + val + ", " + this.val);
    }
}

class B extends A {
    String val = "B";
    void method() {
        System.out.println("B method: " + val + ", " + this.val);
    }

    void method3() {
        System.out.println("B method3: " + val2);
    }
}

public class Main{
    public static void main(String[] args) {
        A b = new B(); // Bのインスタンスとして作成される
        System.out.println(b.val); // A
        System.out.println(((B) b).val); // B

        b.method(); // B method: B, B
        // methodはクラスBを実装する時にoverrideしている
        // インスタンス生成時ではnew B()としているのでそれが中身となっている
        ((B) b).method(); // B method: B, B

        b.method2(); // A method2: A, A
        ((B) b).method2(); // A method2: A, A
        // クラスBではmethod2は定義されてないので、Aインスタンスの部分からとって来て使用
        // インスタンスb = クラスAのインスタンス + 差分インスタンス
        
        b.val += "2";
        System.out.println(b.val + ", " + ((B) b).val); // A2, B
        ((B) b).val += "2";
        System.out.println(b.val + ", " + ((B) b).val); // A2, B2

        System.out.println(b.val2); // A+
        System.out.println(((B) b).val2); // A+
        ((B) b).method3(); // B method3: A+
    }
}
コンストラクタの実行順序

サブクラスのインスタンスはスーバークラスのインスタンスと差分インスタンスから構成される。
この順序は以下の通りとなる。

  1. スーパークラスのインスタンスを作るコンストラクタが呼ばれてスーパークラスのインスタンスが作成される
  2. サブクラスのコンストラクタが呼ばれて差分インスタンスが作成される。
class Parent {
    public Parent() {
        System.out.println("Parent Constructor");
    }
}

class Child extends Parent {
    public Child() {
        System.out.println("Child Constructor");
    }
}

public class Main{
    public static void main(String[] args) {
        new Child();
    }
}

// 出力
Parent Constructor
Child Constructor
継承されたときのデフォルトコンストラクタ

サブクラスのコンストラクタでsuperクラスのコンストラクを呼び出さないとき、デフォルトでコンパイル時にsuper(); がコンストラクタの先頭に追加される。
これはクラスが継承関係にある時、まずはスーパークラスからインスタンスを生成する必要があるため。

class Sample {
    String name;
    int num;
    public Sample(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

class SubSample extends Sample {
    int price;
    public SubSample(int price) {
        // 以下の様に、明示的にスーパークラスのコンストラクタを呼び出さないと、
        //「super();」がコンパイルのタイミングで追加される
        // しかし、スーパークラスであるSampleではコンストラクタを定義しているため、
        // 引数なしのコンストラクタがない(デフォルトコンストラクタがない)のでコンパイルエラーとなる
        super("SubSample", price);
        
        this.price = price;
    }
    
    public SubSample(String name, int num, int price) {
        super(name, num);
        
        // this(price);
        // this(price)コンストラクタは上記で定義されている様に、
        // すでにスーパークラスのコンストラクタをsuperで呼び出している
        // そのためここで再度呼び出すのはsuperが重複することとなりコンパイルエラー
        // よって以下の様にフィールドを初期化すべき
        this.price = price;
    }
}

class Sample2 {
    String name;
    int num;
    
    public Sample2() {
        
    }
    
    public Sample2(String name, int num) {
        this.name = name;
        this.num = num;
    }
}
class SubSample2 extends Sample2 {
    public SubSample2(String name) {
        // ここにsuper();がコンパイルのタイミングで追加される
        // スーパークラスのSample2では引数なしのコンストラクタが定義されているので、
        // 問題なくコンパイルされる
        this.name = name;
    }    
}
interfaceで定義できる具体メソッド
  • defaultメソッド(publicがデフォルトで、privateにできない)
  • privateメソッド(defaultメソッドや、他のprivateメソッドで使用することを想定)
  • staticメソッド(publicがデフォルトで、privateできる
⚠️
interfaceではprotectedとデフォルト(無修飾子)が存在しない。
無修飾子の場合は自動でpublic扱いされる。
(フィールド、メソッド共にこの制約がある。)
interface Inf {
   String val = "val"; // public final staticがデフォルト
    // private fieldは定義できない

    default void defaultMethod() { // publicがデフォルト(privateにできない)
        privateMethod();
    } //;

    static void staticMethod() { // publicがデフォルト(privateにもできる)
        // staticメソッドと(public final static)フィールドしか使えない
        System.out.println(val);
        privateStaticMethod();
    }

    // defaultメソッドまたは他のprivate具体メソッド内で使用することを想定
    // private具体メソッドはこの目的のためにあるので、publicにはそもそもできない(明示的にprivateをつける必要あり)
    // pubicは具体メソッドで使えない、逆にprivateは抽象メソッドに使用できない
    private void privateMethod() {}
    private void privateMethod2() {
        privateMethod();
        staticMethod();
    }

    private static void privateStaticMethod() {}
}
sealedpermits

sealedpermitsによって継承できるクラスを指定(制限)できる。(同時に使用することが前提だが、同じソースファイル内にあるサブクラスに対してのみpermitsを省略して指定しなくても可
またinterfaceにも使用可能。ただしクラス同様継承先は必ずfinal, sealed (とpermits), non-sealedのいずれかで修飾する必要がある。

sealed interface ParInf permits ChildInf, AbstClass {}

// interface, abstract classにはfinalがつけられない
// よって必ず以下の様にnon-sealedかsealedのどちらかをつける必要がある
non-sealed interface ChildInf extends ParInf {}
sealed abstract class AbstClass implements ParInf permits A {}

final class A extends AbstClass {}
class B implements ChildInf {}

sealed class C {} // permitsを省略することが可能
// ただし必ずこのソースファイル内にCを継承するサブクラスを定義する必要がある
final class D extends C {}
⚠️
interface, abstract classはそもそも実装または継承されて中身を実装されることを前提としているのでfinalがつけられない。

Exception

try, catch, finallyの順序
  • trycatchfinallyの順序で記述する必要がある。そうでないとコンパイルエラー。
  • finallyブロックは1つまで。
catchブロックでのreturnfinallyブロックでのreturn

戻り値を格納するための専用の変数が用意されている。そこに最終的に何の値が入るかを考える。以下3つのパターンを例記。

  1. catchreturnしてもfinallyは実行される。
    catchでのreturnはその時点での値を戻り値用変数に格納する。
    public class Main{
        public static void main(String[] args) {
            System.out.println(test(null));
        }
    
        private static String test(Object obj) {
            try {
                System.out.println(obj.toString());
            } catch (NullPointerException e) {
                return "A";
            } finally {
                System.out.println("B");
            }
            return "C";
        }
    }
    
    // 出力
    B
    A
  2. catchreturnfinallyでもreturnする場合。
    最終的にfinallyが実行され、finallyreturnした値が戻り値用の変数に格納される。
    public class Main{
        public static void main(String[] args) {
            System.out.println(test(null));
        }
    
        private static String test(Object obj) {
            try {
                System.out.println(obj.toString());
            } catch (NullPointerException e) {
                System.out.println("Catch NullPointerException");
                return "A";
            } finally {
                return "B";
            }
            // finallyでreturnしているのでこれ以降は到達できない
            // 処理を記述するとコンパイルエラー
        }
    }
    
    // 出力
    Catch NullPointerException
    B
  3. finallyで値の更新を行なっても、それは戻り値用変数には影響しない。
    public class Main{
        public static void main(String[] args) {
            System.out.println(test(null));
        }
    
        private static int test(Object obj) {
            int val = 0;
            try {
                System.out.println(obj.toString());
            } catch (NullPointerException e) {
                val = 10;
                return val;
            } finally {
                val += 100;
            }
            return val;
        }
    }
    
    // 出力
    10
Exception(例外)とError(エラー)

java.lang.Exceptionクラスとjava.lang.Errorクラスはどちらもjava.lang.Throwableクラスのサブクラスで、スローできる性質がある。

  • Exception: プログラムが対処できるトラブル。
  • Error: プログラムが対処できないトラブル。たとえばネットワークへ接続できない、ディスクに対する読み取り書き込み権限がない、メモリ不足など実行環境に関するトラブル。
    Errorはプログラムで対処することを求められていない。そのためtry-catchしたり、throwsで宣言する必要がない。(必要ないだけでtry-catchでキャッチすることは可能。throwsに宣言することも可能)
mainメソッドの引数で参照しているString配列はmainメソッドの実行前にJVMが作成したもの

よって以下の(1)コードを引数なしで実行したときのエラーはArrayIndexOutOfBoundsException となる(NullPointerExceptionではない)

import java.util.Arrays;

public class Main{
    public static void main(String[] args) {
        // System.out.println(args[0]); - (1)
        System.out.println(Arrays.toString(args)); // []
    }
}
IndexOutOfBoundsException, ArrayIndexOutOfBoundsException, StringIndexOutOfBoundsException
  • IndexOutOfBoundsException
    配列や文字列、コレクションの範囲外であることを示す例外クラス。以下の様にRuntimeExceptionを継承している。
    
    package java.lang;
    
    /**
     * Thrown to indicate that an index of some sort (such as to an array, to a
     * string, or to a vector) is out of range.
     * <p>
     * Applications can subclass this class to indicate similar exceptions.
     *
     * @author Frank Yellin
     * @since 1.0
     */
    public class IndexOutOfBoundsException extends RuntimeException {
    		...
    }
    IndexOutOfBoundsException.java
  • ArrayIndexOutOfBoundsException
    配列の範囲外アクセスを示す例外クラス。以下の様にIndexOutOfBoundsExceptionのサブクラス。ArrayListなどのクラスではこれがスローされる。
    package java.lang;
    
    /**
     * Thrown to indicate that an array has been accessed with an illegal index. The
     * index is either negative or greater than or equal to the size of the array.
     *
     * @since 1.0
     */
    public class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {
        ...
    }
    ArrayIndexOutOfBoundsException.java
  • StringIndexOutOfBoundsException
    文字列の範囲外アクセスを示すクラス。以下の様にIndexOutOfBoundsExceptionのサブクラス。StringcharAtメソッドなどではこれがスローされる。
    package java.lang;
    
    /**
     * Thrown by {@code String} methods to indicate that an index is either negative
     * or greater than the size of the string.  For some methods such as the
     * {@link String#charAt charAt} method, this exception also is thrown when the
     * index is equal to the size of the string.
     *
     * @see java.lang.String#charAt(int)
     * @since 1.0
     */
    public class StringIndexOutOfBoundsException extends IndexOutOfBoundsException {
    		...
    }
    StringIndexOutOfBoundsException.java
チェック例外非チェック例外
  • チェック例外: 非チェック例外以外のExceptionで、try-catchしてない場合throwsに宣言してコンパイラに伝える必要がある。
  • 非チェック例外: java.lang.Exceptionを継承したRuntimeExceptionとそのサブクラスは非チェック例外となる。try-catchしてなくてもthrowsを宣言する必要がない。(必要ないだけで宣言することも可能)
継承関係にあるクラスをマルチキャッチに記述できない

そもそもスーパークラスのみでキャッチされる。よって以下のようなコードはコンパイルエラーとなる。

class ParentException extends Exception {}
class ChildException extends ParentException {}

void test() {
    try {
      throw new ChildException();
    } catch (ParentException | ChildException e) { // Types in multi-catch must be disjoint: 'ChildException' is a subclass of 'ParentException'
        System.out.println(e);
    }
}
try-with-resourses文の目的

try-with-resourcesはプログラム中で扱うリソースを自動的に閉じるためにある。
データが書き込まれるファイル、そしてファイルにアクセスするためのFileクラスやFileReader クラスのインスタンスなどがこの扱うリソースに当てはまる。(例外やエラーを処理するのが目的ではない。)
よってcatchブロックもfinallyブロックも必ずしも必要ではない。
(一般のtry構文では必ずどちらか必要。try内でチェック例外をスローする場合は必ずcatchを書くかthrows宣言をする。)

try-with-resourses文で扱えるクラス

java.lang.AutoCloseableインタフェースまたはjava.io.Closeableインタフェースを実装したクラス。

  • java.lang.AUtoCloseable: try-with-resourses文が導入されたときに、それに対応する形で導入された。
    package java.lang;
    
    public interface AutoCloseable
        void close() throws Exception;
    }
    AutoCloseable.java
  • java.io.Closeable: try-with-resourses文が導入される以前から存在する。java.io.BufferedReaderの様なストリームを扱うクラスがそれぞれ実装していたCloseメソッドが共通の型として扱えるように作られた。今では以下の様にCloseableインターフェースはAUtoCloseableインターフェースのサブインタフェースとして定義されている。
    package java.io;
    
    import java.io.IOException;
    
    public interface Closeable extends AutoCloseable {
        public void close() throws IOException;
    }
    Closeable.java

    Closealbejava.ioパッケージにあり、スローする例外もIOExceptionであることから、Closealbeインタフェースはあくまで入出力ストリームを扱うことを前提としたインタフェースであることがわかる。なので独自でtry-with-resourses文で扱うリソースを定義する時は一般的にAutoCloseableインタフェースで実装する。

try-with-resourses文の記述ルールとリソースのcloseメソッドの呼び出される順序
  • try()内に;で区切ることで複数のリソースを対象にできる。(最後のリソースの;は省略可)
  • リソースはfinalまたは実質的にfinalである必要がある。よってtryブロック内、外での再代入はできない。
  • リソースはnullでも良い。事前にnullチェックを行なってからcloseを実行するのでNullPointerExceptionも起こらない。(実際にはリソースはnullなので、closeメソッドはなく、実行もされない
  • closeメソッドはリソースの定義された逆の順に実行される。

以下の3つクラスに対して具体例を見ていく。

class A implements AutoCloseable {
    @Override
    public void close() {
        System.out.println("Closing A");
    }
}

class B implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("Closing B");
    }
}

class C implements AutoCloseable {
    @Override
    public void close() {
        System.out.println("Closing C");
    }
}

実行可能な例

try (A a = new A()) {
    System.out.println("try a");
}
// 出力
try a
Closing A

A a = new A();
try (a) {
    System.out.println("try a");
}
// 出力
try a
Closing A

try (B b = new B()) {
    System.out.println("try b");
} catch (Exception e) { // Bではチェック例外であるExceptionをthrowsに宣言したのでcatchする必要がある
    System.out.println("catch b");
}
// 出力
try b
Closing B

try (A a = null) {
    System.out.println("try null a");
}
// 出力
try null a
// 正常にtryブロック内のコードが実行される
// ただし、リソースはnullなのでcloseメソッドは無いのでcloseメソッドは実行されない

A a = new A();
try (a;
B b = new B();
C c = new C()) {
    System.out.println("try a b c");
} catch (Exception e) { // Bではチェック例外であるExceptionをthrowsに宣言したのでcatchする必要がある
    System.out.println("catch");
}
// 出力
try a b c
Closing C
Closing B
Closing A

コンパイルエラーの例

A a;
try (a) { // Variable 'a' might not have been initialized
    System.out.println("try a");
}
// (nullでも良いから)リソースの初期化をする必要がある


A a = new A();
try (a) {
		a = new A(); // aは実質finalなのでここでの再代入はコンパイルエラー
    System.out.println("try a");
}

A a = new A();
try (a) {
    System.out.println("try a");
}
a = null; // aは実質finalなのでここでの再代入はコンパイルエラー
try-with-resourses文のtryブロックでの例外とclose内の抑制された例外

try-with-resourses文のtryブロック内で例外が発生したとき、closecatchfianllyの順序で処理が起こる。(catchfinallyが定義されていた場合)
その際にcloseメソッドでスローされた例外は抑制された例外として隠されてしまう(無視される)。

class TroubleResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        // 処理は実行されるが例外は抑制されてしまう
        System.out.println("TroubleResource closed");
        throw new RuntimeException("TroubleResource Runtime Exception");
    }
}

public class Main{
    public static void main(String[] args) {
        try (TroubleResource a = new TroubleResource()) {
            throw new Exception();
        } catch (RuntimeException e) {
            System.out.println("catch RuntimeException from TroubleResource");
        } catch (Exception e) {
            System.out.println("catch Exception from try block");
        } finally {
            System.out.println("finally block");
        }
    }
}
// 出力
TroubleResource closed
catch Exception from try block
finally block

抑制された例外を扱いたい場合はjava.lang.ThrowableクラスのgetSuppressedメソッドを使用する。

class TroubleResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        // 処理は実行されるが例外は抑制されてしまう
        System.out.println("TroubleResource closed");
        throw new RuntimeException("TroubleResource Runtime Exception");
    }
}

public class Main{
    public static void main(String[] args) {
        try (TroubleResource a = new TroubleResource()) {
            throw new Exception();
        } catch (RuntimeException e) {
            System.out.println("catch RuntimeException from TroubleResource");
        } catch (Exception e) {
            System.out.println("catch Exception from try block");
            for (Throwable t : e.getSuppressed()) {
                System.out.println(t);
            }
        } finally {
            System.out.println("finally block");
        }
    }
}
// 出力
TroubleResource closed
catch Exception from try block
java.lang.RuntimeException: TroubleResource Runtime Exception
finally block

試験用テクニック

  • packageがあったら、正しくimportしているかどうかや継承関係に注意する。
  • エラーの選択肢があるかを事前に把握する。
  • switch式のdefault;を見逃さない。

関連記事