背景
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.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
は小文字でも良い
整数リテラル表記の_
(アンダースコア)
- リテラルの先頭と末尾につけることができない。
- 記号の前後には記述できない
記号とみなされるものには小数点の.
,long
やfloat
の接尾辞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;
:a
がnull
なので、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
String
のimmutable性
String
の変数は宣言された後は変更不可能。String
クラスのメソッドは新しいString
インスタンスを返す。(元のインスタンスを変更しない。)
String str = "hoge, World.";
System.out.println(str.replaceAll("hoge", "hello")); // Hello, World.
System.out.println(str); // hoge, World.
String
のreplace
, 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;
}
}
配列の初期化子{}
の使用条件
- 変数宣言と同時にのみ使用可能
- 初期化子を使用した場合は初期化子内の要素数でサイズを指定するので、サイズを指定してはいけない
// 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
を使用する。
ArrayList
のremove()
メソッド
-
int
を引数とするとき、index
として扱い、その位置にある要素を削除。 -
Object
を引数とするとき、最初の同値(equals
でtrue
)の要削除。
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.javaListインターフェイスは
SequencedCollection
インターフェイスを継承し、SequencedCollection
インターフェイスはCollection
インターフェイスを継承するためset()
やadd()
メソッドはあるが、実行するとUnsupportedOperationException
が起きる。(コンパイルエラーは起きない。) -
Arrays.asList()
で作成されたリストは不変長で変更可能。
Arrays.asList()
では以下の様にArrays.javaに定義されているprivate class
のArrayList
を返す。(次に紹介する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
メソッドは定義されていいなく、そのため継承しているAbstractList
のadd
メソッドをそのまま使用する。しかし、これは以下の様にUnsupportedOperationException
をthrow
するため実行時エラーとなる。(コンパイルは通る。)... 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)
の様にequals
にnull
が渡された場合
オーバーライドしてない限り必ずfalse
を返す。
{}
なしでelse
とif
間で改行した場合
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
インスタンスは、作成時に以下の様に継承関係を遡っていく。
Main
→ ChildSample
→ OtherSample
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
、enum
なども)に使用できる修飾子
- トップレベルコード(ネストされていない一番外側のクラス):
public
とデフォルト(修飾子なし) - インナーレベルコード:
protected
,private
も使用可能 - ローカルレベルコード: そもそも修飾子が使用不可能
レコードで宣言できるもの
- コンストラクタ
- 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
の実行は可能。
その際にinterface
のdefault
メソッドの名前と同じフィールドが存在する場合、オーバーライドされる。
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 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
フィールド、メソッド
interface
のdefault
メソッドでは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
クラスのメソッド、interface
のdefault
メソッド内で利用可能となっている。
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+
}
}
コンストラクタの実行順序
サブクラスのインスタンスはスーバークラスのインスタンスと差分インスタンスから構成される。
この順序は以下の通りとなる。
- スーパークラスのインスタンスを作るコンストラクタが呼ばれてスーパークラスのインスタンスが作成される
- サブクラスのコンストラクタが呼ばれて差分インスタンスが作成される。
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() {}
}
sealed
とpermits
sealed
と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 {}
interface
, abstract class
はそもそも実行または継承されて中身を実装されることを目的としているのでfinal
がつけられない。 Exception