Jave SE17 Sliver

Article Cover Image

背景

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

学習材料

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

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

感想

それほど難しくないかと思います。60分で57問の英語試験ですが、基本的に問題文は長くなく、パッパと答えられるようになっています。私は35分くらいで解き終わって特に見直しもせずに提出しました。提出直後に合格していればPASSと表示されます。

試験環境はオンラインのみ対応です。特につまづくこともなく、チャットにある指示通りに身分証明書や机の上、身の回りを見せてあげれば良いです。

試験用メモ

Basic

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

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

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

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

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コマンドでコンパイルする際にはクラス名とソースファイル名が一致しないとコンパイルエラーが出る
コマンドライン引数とエスケープ
  • \¥)によってエスケープできる。
  • " "でスペースを囲める。
  • 通常のスペースによってのみ区切られる。
java Sample a \" a\" "a "b c
# 引数は以下の5つとして受け取る
1. a
2. "
3. a"
4. a b
5. c

Primitive type and String

整数リテラルの接頭辞
  • 2進数: 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
    ex) int a = 0x10B; (10進数で83 (= 1 * 64 + 2 * 8 + 3 * 1))
    A ~ Fは小文字でも良い
整数リテラル表記の_(アンダースコア)
  • リテラルの先頭と末尾につけることができない。
  • 記号の前後には記述できない
    記号とみなされるものには小数点の. , 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を違反
変数に使用して良い文字
  • $, _ 以外は基本的にダメ
  • 数字は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
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()メソッド

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

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

一方、以下の様にすると実行時に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);
    }
}
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であることに注意

演算子と制御

a += x, a++, ++a の違い
  • a += x, ++aはaの値の更新と反映が同時に行われる。
  • 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
x.equals(null)の様にequalsnull が渡された場合

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

{}なしでelseif間で改行した場合

elseとif間で改行した場合、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
	}
}
switch文の条件式で戻せる型
  • int以下の整数型とそのラッパークラス
  • 文字, 文字列
  • 列挙型(enum)

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

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

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式では値を返す。そして末尾で;が必要であることに注意!

// switch文
switch (a) {
    case 1:
        System.out.println(1);
        break;
    case 2:
        System.out.println(2);
        break;
    default:    
        System.out.println("default");
        break;
}

// switch式
int b = switch (a) {
    case 1 -> 1;
    case 2 -> 2;             
    default -> -1;
}; // 末尾に;が必要

switch式ではyieldを使用して値を返す。またthrowを使用してエラーを投げることも可能。
switch式では矢印ではなく:を使用しても記述可能ただしその場合はyieldを忘れるとフォールスルーが発生する。(矢印と:は1つの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);

// 出力
print 0
print 1
1
while文, do-while文での{}の省略

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

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

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

ただし以下の様にdo-while文でdoとwhileの間は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,
ラベル

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

  • コードブロック({}
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

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

doubleは数字リテラルを受け取るため、以下ではどちらの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;
    }
}
初期化子{}とコンストラクタ

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

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

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);
    }
}

// 出力
0

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);
    }
}

// 出力
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()
⚠️
Recodeクラスから作成されるインスタンスは更新不可(immutable)
クラス(recordenumなども)に使用できる修飾子
  • トップレベルコード(ネストされていない一番外側のクラス): publicデフォルト(修飾子なし)
  • インナーレベルコード: protected, privateも使用可能
  • ローカルレベルコード: そもそも修飾子が使用不可能
レコードで宣言できるもの
  • コンストラクタ
  • staticフィールド
  • staticメソッド
  • static 初期化子
  • インナークラスやインナーインターフェイス
⚠️
staticでないものが宣言できない。(初期化子もstatic初期化子でないと宣言できない。)
レコードのコンストラクタ
  • 標準コンストラクタ: 自動的に生成され、フィールドの初期化を行う。(クラスのデフォルトコンストラクタと異なり、プログラマが代替コンストラクタなどを定義しても必ず生成される。)
  • 代替コンストラクタ:
    • プログラマが定義するコンストラクタで、標準コンストラクタと同じ引数を受け取り、すべてのフィールドの初期化を行う必要がある。
    • アクセス修飾子はレコードそのものの修飾子と同じかよりゆるい必要がある。以下の例だと修飾子は
    
    record Data(String value) {
    	Data(String) {// publicとprotectedでも可(修飾子なしよりもゆるい)
    		this.value = value
    	}
    }
  • コンパクトコンストラクタ:
    • 渡された引数の妥当性を検証する機能を追加するためのコンストラクタ。
    • レコード名だけで宣言する。
    public record Customer(String name, int age){
        public Customer{
            if (age < 0) {
                throw new IllegalArgumentException("Age cannot be negative");
            }
        }
    }
    • コンパクトコンストラクタが実行されてから標準(または代替)コンストラクタが実行される。(コンパクトコンストラクタが標準コンストラクタの先頭に定義されている様にコンパイルされる。)
    • コンパクトコンストラクタは複数定義できない。
    • コンパクトコンストラクタ内で明示的に別のコンストラクタを呼び出してはならない。
    • コンパクトコンストラクタ内でフィールドにアクセスできない。
    • コンパクトコンストラクタ内でreturnを記述してはならない。(フィールドを初期化する標準コンストラクタが呼び出されなくなるため。)
    • コンパクトコンストラクタは1つしか記述できない。

    ex).

    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については以下の様に中身を記述する必要がある。その際に同じシグネチャの関数を重複定義することになる。

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のインスタンスと差分のインスタンスの両方を合わせた構造となる。

継承で引き継がれないもの
  • コンストラクタ
  • privateフィールド、メソッド
interfacedefaultメソッドではjava.lang.Objectクラスにあるメソッドは定義できない(コンパイルエラーとなる)

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

interface Inf {
    default String toString() { // Default method 'toString' overrides a member of 'java.lang.Object'
        return "Interface String";
    };
}
デフォルトメソッドの呼び出し

以下の様に<実行したinterface>.supper.<defaultメソッド名>によって呼び出すことができる。
ただし、直属のinterfaceであるBのみできる。(Aを呼び出すことはできない。)

interface A {
    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のメソッドをデフォルトメソッド内で呼び出すことは可能
        // なぜなら実行する具体クラスでは必ず実装されるから
    }

    void method();
}

abstract class AbsClass {
    void sample() {
        System.out.println("Abstract Class");
        method(); // abstractメソッドでも呼び出すことは可能
        // なぜなら継承する具体クラスでは必ず実装されるから
    }

    abstract void method();
}

abstract class AbsClass2 extends AbsClass {}

class ConcClass extends AbsClass {
    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
継承関係のクラスに同名フィールドが存在するとき
  • 同じフィールド名がある場合、変数の型で宣言された方を使用。
  • 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+
        ((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

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 method or fieldしか使えない
        System.out.println(val);
        privateStaticMethod();
    }

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

    private static void privateStaticMethod() {}
}

sealedpermits

sealedpermitsによって継承できるクラスを指定(制限)できる。(必ず同時に使用する)
また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 {}
⚠️
interface, abstract classはそもそも実行または継承されて中身を実装されることを目的としているのでfinalがつけられない。

Exception

関連記事