背景
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.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
コマンドでコンパイルする際にはクラス名とソースファイル名が一致しないとコンパイルエラーが出る。 複数クラスが定義されているソースファイルに対してjavac
とjava
コマンドを実行
-
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");
}
}
# ソースファイルモードで実行
$ 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
依存関係のファイルに対してjavac
とjava
コマンドを実行
ex
パッケージに以下の様にファイルがある。
package ex;
public class Main {
public static void main(String[] args) {
Sample sample = new Sample();
sample.method();
}
}
package ex;
public class Sample {
public void method() {
System.out.println("Sample method");
};
}
このとき、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
は小文字でも良い
整数リテラル表記の_
(アンダースコア)
- リテラルの先頭と末尾につけることができない。
- 記号の前後には記述できない
記号とみなされるものには小数点の.
,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を違反
ラッパークラスとオートボクシング
ラッパークラスとはboolean
, int
, float
, double
…などのリミティブ型に対して、Boolean
, Integer
, Float
, Double
…とクラス型(Object
)にしたもの。ラッパークラスと元となるプリミティブ間では以下の様にボクシングによって直接記述ができる。
- オートボクシング
ラッパークラスに直接数値リテラルを代入するコードの記述が可能
コンパイル時には数値リテラルの10
がInteger
インスタンスに変換される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;
: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
+
は順番に評価されていく
以下のコードの様に文字列を足すと、それ以降は文字列として順次扱う
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
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()
メソッド
既にメモリ内にある文字列への参照を返す。
以下の様に各変数の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
一方、削除後にも呼び出しをするということは、削除の処理と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.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であることに注意
演算子と制御
複数変数に対して同じ値を同時に代入
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
,++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
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の整数リテラルなのでコンパイルが通る
Object
のequals
関数にnull
が渡された場合
オーバーライドしてない限り必ずfalse
を返す。
public boolean equals(Object obj) {
return (this == obj);
}
{}
なしで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
}
}
変数の初期化
変数は必ず初期化してから使用する必要がある。
以下はコンパイルエラーとなる例
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文では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
クラスのフィールドとローカルの変数名が同じとき
ローカルとフィールドで同じ名前の変数がある場合はローカルが優先される。
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
を引数にする際のオーバーロード
double
はint
同様に整数リテラルを受け取るため、以下ではどちらの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
インスタンスは、作成時に以下の様に継承関係を遡っていく。
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
クラスとして宣言することも可能。 - ローカルレベルコード: そもそも修飾子が使用不可能(無修飾子のデフォルトのみ)
// トップレベルは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
の実装は可能。
その際に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
を実装する際、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();
については、宣言はクラスA
(A
として扱う)でインスタンスはクラスB
(A
インスタンスと差分インスタンス)となっている。
このとき、a2.method
はまずA
のmethod
を探す。実際のa2
の中身はBインスタンスで、method
はオーバーライドされているのでB
のmethod
を呼び出す。
Collction
はList
のスーパーインタフェース。(正確にはList
はSequencedCollection
を継承し、SequencedCollection
がCollection
を継承している。)よって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
フィールド、メソッド
interface
のdefault
メソッドでは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
クラスのメソッド、interface
のdefault
メソッド内で使用できる。
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+
}
}
コンストラクタの実行順序
サブクラスのインスタンスはスーバークラスのインスタンスと差分インスタンスから構成される。
この順序は以下の通りとなる。
- スーパークラスのインスタンスを作るコンストラクタが呼ばれてスーパークラスのインスタンスが作成される
- サブクラスのコンストラクタが呼ばれて差分インスタンスが作成される。
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() {}
}
sealed
とpermits
sealed
とpermits
によって継承できるクラスを指定(制限)できる。(同時に使用することが前提だが、同じソースファイル内にあるサブクラスに対してのみ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
の順序
-
try
→catch
→finally
の順序で記述する必要がある。そうでないとコンパイルエラー。 -
finally
ブロックは1つまで。
catch
ブロックでのreturn
とfinally
ブロックでのreturn
戻り値を格納するための専用の変数が用意されている。そこに最終的に何の値が入るかを考える。以下3つのパターンを例記。
-
catch
でreturn
しても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
-
catch
でreturn
、finally
でもreturn
する場合。
最終的にfinally
が実行され、finally
で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) { System.out.println("Catch NullPointerException"); return "A"; } finally { return "B"; } // finallyでreturnしているのでこれ以降は到達できない // 処理を記述するとコンパイルエラー } } // 出力 Catch NullPointerException B
-
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
のサブクラス。String
のcharAt
メソッドなどではこれがスローされる。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.javaClosealbe
がjava.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ブロック内で例外が発生したとき、close
→ catch
→ fianlly
の順序で処理が起こる。(catch
とfinally
が定義されていた場合)
その際に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
、;
を見逃さない。