はじめに
現在、私が進行しているJavaの勉強会でまとめた内容です。
内容に不正確な点があれば、ご指摘いただけるとありがたいです。
- 韓国人として、日本語とコンピュータの勉強を同時に行うために、ここに文章を書いています
- 翻訳ツールの助けを借りて書いた文章なので、誤りがあるかもしれません
try-catch
-
try
文はJavaで例外を処理するために使用されます -
try
文を使用すると、例外が発生する可能性のあるコードを囲み、例外が発生した際に適切に処理することができます
try {
// 例外が発生する可能性のあるコード
} catch (例外タイプ1 変数名) {
// 例外タイプ1を処理するコード
} catch (例外タイプ2 変数名) {
// 例外タイプ2を処理するコード
} finally {
// 例外の発生に関係なく必ず実行されるコード(オプション)
}
try
- 例外が発生する可能性のあるコードを含みます
- 例外が発生しない場合、
try
の後のコードが実行されます
catch
- 特定の例外を処理するためのブロックです
- 複数の
catch
を持つことができ、順番に検査されます
finally
- 例外の発生に関係なく実行されます
単一キャッチ句 (Uni-catch Clause)
- 特定の単一エラーのみを処理します
public class Main{
public static void main(String[] args) {
try {
int result = 1 / 0;
} catch (ArithmeticException e) {
System.out.println("0で割ることはできません: " + e.getMessage());
}
}
}
マルチキャッチ句 (Multi-catch Clause)
- Java 7で導入された文法
- 複数の例外を同時に処理
public class Main {
public static void main(String[] args) {
try {
// 主なロジック
} catch (NullPointerException | ArithmeticException e) {
System.out.println("例外発生: " + e.getClass().getSimpleName() + " - " + e.getMessage());
}
}
}
- 複数の共通エラー処理ロジックを1つにまとめることで、繰り返しコードを削減できます
try-with-resources
- close()はAutoCloseableインターフェースを実装したオブジェクトで使用することができます
- ファイルやソケットハンドルのようなリソースを保持し、
close()
を使用してリソースを解放することで、リソース解放時に発生する例外やエラーを防ぐためのインターフェースです
close()
- このリソースを閉じるために使用されるメソッドです
- try-with-resources文で記述された場合、自動的に呼び出されます
-
close()
の実装ではInterruptedException
をスローしないことを強く推奨します- なぜなら、スレッドの中断状態と相互作用し、ランタイムの誤動作が発生する可能性が高くなるためです
-
Closeable
とは異なり、冪等性は必須ではありません - ただし、複数回呼び出されると副作用が発生する可能性があるため、冪等性を持たせることを推奨します
なぜ使用するのか
上記のコードには致命的な問題があります。なぜなら
finally {
br.close();
fis.close();
}
finally
内でclose()
メソッドを呼び出すと、NullPointerException
が発生する可能性があります。
このようにnull
チェックを行っても、完全に安全とは言えません。
finally {
if (br != null) {
br.close();
}
if (fis != null) {
fis.close();
}
}
もしbr = new BufferedReader(new InputStreamReader(fis));
のbr.close()
で例外が発生した場合、
fis = new FileInputStream("test.txt");
のリソースの解放は保証されません。
この問題を解決するにはtry-catch
文で囲む必要があります
コードが非常に煩雑になってしまいます。
リソース管理が必要なリソースが増え、close()
を呼び出すオブジェクトが多くなるほど、管理が難しくなります。
try-with-resources文を使いましょう
try-with-resources
を使えば、開発者が手動で管理しなくても自動的にclose()
が呼び出されます。
その結果、はるかに簡潔で安全なコードが実現できます。
try-catchとtry-with-resourcesのバイトコード比較
try-catch
以前に記述したtry-catch
文のコードは
package org.example;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
BufferedReader br = null;
try {
fis = new FileInputStream("test.txt");
br = new BufferedReader(new InputStreamReader(fis));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
次のようなコードであり、バイトコードは以下の通りです
~/Documents/github/study-gof/build/classes/java/main/org/example on main !1 ?4 ❯ javap -c Main at 12:10:53 AM
Warning: File ./Main.class does not contain class Main
Compiled from "Main.java"
public class org.example.Main {
public org.example.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.io.IOException;
Code:
0: aconst_null
1: astore_1
2: aconst_null
3: astore_2
4: new #7 // class java/io/FileInputStream
7: dup
8: ldc #9 // String test.txt
10: invokespecial #11 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
13: astore_1
14: new #14 // class java/io/BufferedReader
17: dup
18: new #16 // class java/io/InputStreamReader
21: dup
22: aload_1
23: invokespecial #18 // Method java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V
26: invokespecial #21 // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
29: astore_2
30: aload_2
31: invokevirtual #24 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
34: dup
35: astore_3
36: ifnull 49
39: getstatic #28 // Field java/lang/System.out:Ljava/io/PrintStream;
42: aload_3
43: invokevirtual #34 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: goto 30
49: aload_2
50: ifnull 65
53: aload_2
54: invokevirtual #39 // Method java/io/BufferedReader.close:()V
57: goto 65
60: astore_3
61: aload_3
62: invokevirtual #44 // Method java/io/IOException.printStackTrace:()V
65: aload_1
66: ifnull 81
69: aload_1
70: invokevirtual #47 // Method java/io/FileInputStream.close:()V
73: goto 81
76: astore_3
77: aload_3
78: invokevirtual #44 // Method java/io/IOException.printStackTrace:()V
81: goto 191
84: astore_3
85: aload_3
86: invokevirtual #50 // Method java/lang/Exception.printStackTrace:()V
89: goto 191
92: astore_3
93: aload_3
94: invokevirtual #44 // Method java/io/IOException.printStackTrace:()V
97: aload_2
98: ifnull 113
101: aload_2
102: invokevirtual #39 // Method java/io/BufferedReader.close:()V
105: goto 113
108: astore_3
109: aload_3
110: invokevirtual #44 // Method java/io/IOException.printStackTrace:()V
113: aload_1
114: ifnull 129
117: aload_1
118: invokevirtual #47 // Method java/io/FileInputStream.close:()V
121: goto 129
124: astore_3
125: aload_3
126: invokevirtual #44 // Method java/io/IOException.printStackTrace:()V
129: goto 191
132: astore_3
133: aload_3
134: invokevirtual #50 // Method java/lang/Exception.printStackTrace:()V
137: goto 191
140: astore 4
142: aload_2
143: ifnull 160
146: aload_2
147: invokevirtual #39 // Method java/io/BufferedReader.close:()V
150: goto 160
153: astore 5
155: aload 5
157: invokevirtual #44 // Method java/io/IOException.printStackTrace:()V
160: aload_1
161: ifnull 178
164: aload_1
165: invokevirtual #47 // Method java/io/FileInputStream.close:()V
168: goto 178
171: astore 5
173: aload 5
175: invokevirtual #44 // Method java/io/IOException.printStackTrace:()V
178: goto 188
181: astore 5
183: aload 5
185: invokevirtual #50 // Method java/lang/Exception.printStackTrace:()V
188: aload 4
190: athrow
191: return
Exception table:
from to target type
53 57 60 Class java/io/IOException
69 73 76 Class java/io/IOException
49 81 84 Class java/lang/Exception
4 49 92 Class java/io/IOException
101 105 108 Class java/io/IOException
117 121 124 Class java/io/IOException
97 129 132 Class java/lang/Exception
4 49 140 any
92 97 140 any
146 150 153 Class java/io/IOException
164 168 171 Class java/io/IOException
142 178 181 Class java/lang/Exception
140 142 140 any
}
もう少し読みやすくするために、IntelliJを使用してクラスファイルを開いてみると、
このような構造になっています。
try-with-resuorse
では、try-with-resources
を使用した場合のバイトコードはどうなるのでしょうか?
このように記述されたコードは
~/Documents/github/study-gof/build/classes/java/main/org/example on main !1 ?4 ❯ javap -c Main at 12:10:57 AM
Warning: File ./Main.class does not contain class Main
Compiled from "Main.java"
public class org.example.Main {
public org.example.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.io.IOException;
Code:
0: new #7 // class java/io/FileInputStream
3: dup
4: ldc #9 // String test.txt
6: invokespecial #11 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #14 // class java/io/BufferedReader
13: dup
14: new #16 // class java/io/InputStreamReader
17: dup
18: aload_1
19: invokespecial #18 // Method java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V
22: invokespecial #21 // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
25: astore_2
26: aload_2
27: invokevirtual #24 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
30: dup
31: astore_3
32: ifnull 45
35: getstatic #28 // Field java/lang/System.out:Ljava/io/PrintStream;
38: aload_3
39: invokevirtual #34 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: goto 26
45: aload_2
46: invokevirtual #39 // Method java/io/BufferedReader.close:()V
49: goto 70
52: astore_3
53: aload_2
54: invokevirtual #39 // Method java/io/BufferedReader.close:()V
57: goto 68
60: astore 4
62: aload_3
63: aload 4
65: invokevirtual #44 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
68: aload_3
69: athrow
70: aload_1
71: invokevirtual #48 // Method java/io/FileInputStream.close:()V
74: goto 93
77: astore_2
78: aload_1
79: invokevirtual #48 // Method java/io/FileInputStream.close:()V
82: goto 91
85: astore_3
86: aload_2
87: aload_3
88: invokevirtual #44 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
91: aload_2
92: athrow
93: goto 101
96: astore_1
97: aload_1
98: invokevirtual #51 // Method java/io/IOException.printStackTrace:()V
101: return
Exception table:
from to target type
26 45 52 Class java/lang/Throwable
53 57 60 Class java/lang/Throwable
10 70 77 Class java/lang/Throwable
78 82 85 Class java/lang/Throwable
0 93 96 Class java/io/IOException
}
次のような形になっており、
IntelliJを通じて確認すると、
このように、自分が記述していなかったtry-catch
文で囲まれた後に、close()
メソッドが呼び出されていることが確認できます。
これは、javac
コンパイラがコンパイルの過程でtry-with-resources
文のリソース管理ロジックを自動的に追加するためです。
結論
-
try-with-resources
文は、コンパイル後に自動でtry-catch
文で囲まれたclose()
文が生成されます - 結果的に、開発者が手動で定義する場合と
try-with-resources
を使用する場合のパフォーマンスや機能の違いは全くありません - したがって、
try-with-resources
を使用することをお勧めします
throw
- 例外を直接発生させる際に使用します
- 例外オブジェクト(
Throwable
参照)を生成してスローします
public void isUser(User user){
if(!isValidUser){
throw new IllegalArgumentException("認証されたユーザーではありません");
}
}
throws
- メソッド内で発生する可能性のある例外を宣言する際に使用するキーワードです
- メソッドを呼び出す箇所(上位メソッド)で例外処理を行うように伝達します
public void readFile(String filename) throws FileNotFoundException {
FileReader file = new FileReader(filename);
// ファイル処理ロジック
}
- また、複数の例外を宣言することもできます
public void someMethod() throws FileNotFoundException, SQLException {
// メソッドの内容
}
Throwableの階層構造
-
Throwable
はすべてのエラーと例外の最上位クラスであり、Object
を継承しています
Throwable
├── Error
│ ├── VirtualMachineError
│ │ ├── StackOverflowError
│ │ ├── OutOfMemoryError
│ │ ├── InternalError
│ │ └── UnknownError
│ ├── LinkageError
│ │ ├── NoClassDefFoundError
│ │ ├── ClassFormatError
│ │ ├── UnsupportedClassVersionError
│ │ ├── VerifyError
│ │ └── UnsatisfiedLinkError
│ ├── AssertionError
│ ├── ThreadDeath
│ ├── ExceptionInInitializerError
│ └── NoSuchFieldError
└── Exception
├── RuntimeException
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ │ └── NumberFormatException
│ ├── IllegalStateException
│ ├── ClassCastException
│ ├── IndexOutOfBoundsException
│ │ ├── StringIndexOutOfBoundsException
│ │ └── ArrayIndexOutOfBoundsException
│ ├── UnsupportedOperationException
│ └── ArithmeticException
├── IOException
│ ├── FileNotFoundException
│ ├── EOFException
│ ├── InterruptedIOException
│ ├── UnsupportedEncodingException
│ ├── CharConversionException
│ ├── FileSystemException
│ ├── SocketException
│ │ ├── BindException
│ │ ├── ConnectException
│ │ ├── NoRouteToHostException
│ │ ├── PortUnreachableException
│ │ └── SocketTimeoutException
│ └── UnknownHostException
├── SQLException
├── ClassNotFoundException
├── CloneNotSupportedException
├── InstantiationException
├── IllegalAccessException
├── InterruptedException
├── NoSuchMethodException
├── NoSuchFieldException
├── ReflectiveOperationException
├── TimeoutException
├── ParseException
├── MalformedURLException
└── URISyntaxException
- "GPTを通じて少し作ってもらいました(多すぎて...)"
Throwableの主なメソッド(エラーメッセージの出力方法)
Throwable
で定義されているメソッドの中から、出力方法を調べてみました。
getMessage()
toString()
printStackTrace()
Javaのエラーと例外
Javaで発生する問題は、大きく2つに分けることができます。
Error
- エラーはアプリケーションが復旧できない深刻な問題です
- 開発者がこれを解決する方法は(ほぼ)ありません
注意点
Error
クラスは以下のように定義されています。
この内容を簡単に要約すると、
Error
は本質的にシステムに深刻な問題が発生(復旧不可能)したため、復旧することはできません。
そのため、catch
するべきではありません。
また、後に登場するunchecked exception
として分類されるため、throws
で宣言する必要もありません。
Exception
JavaのException
には、大きく2つのケースがあります。
Checked Exceptions
- コンパイル時に例外処理を強制される
Exception
を指します -
throws
でスローするか、try-catch
文を使用する必要があります
以下のように記述した場合、
error: unreported exception IOException; must be caught or declared to be thrown
throw new IOException();
^
次のようなメッセージが表示されます。
以下のようにthrows
やtry-catch
を使用することで解決できます。
注意点
- ループ内でChecked Exceptionを処理するのは避けるべきです
for (String filePath : filePaths) {
try {
readFile(filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
なぜなら、例外処理にはコストがかかるため、パフォーマンスが低下する可能性があるからです。
Unchecked Exceptions (RuntimeException)
-
RuntimeException
およびそのサブクラスは、実行時に発生する例外を定義します -
主にロジックのエラーや不適切なAPIの使用によって発生します
-
コンパイラによるチェックが行われないため、例外処理が強制されません
-
try-catch
で処理するか、throws
で宣言しなくてもコンパイル可能です
カスタム例外 (Custom Exception)
-
コードを書いていると、ドメインルールに基づく例外を発生させる必要性を感じる場合があります
-
これはドメインルールとして特別な管理が必要なため、通常以下のようなネーミングでカスタム例外を作成します
{ドメインルール}Exception
このようなネーミングでカスタム例外を作成することができます。
たとえば、「ユーザーではない」という状況は、Javaコードそのものには問題がありません。
しかし、私たちがエラーとして定義し、統合的に管理する必要があると感じた場合は、カスタム例外を作成することができます。
public class NotUserException extends RuntimeException {
public NotUserException() {
super("ユーザーではありません。");
}
public NotUserException(String message) {
super(message);
}
public NotUserException(String message, Throwable cause) {
super(message, cause);
}
}
注意点
- カスタム例外を無分別に使用すると、不要なクラスが増え、管理が困難になります
- 「Effective Java」という本でも、標準例外が状況に適している場合は再利用することを推奨しています
- Javaの標準例外で十分に定義できるものをカスタム例外として作成すると、他の開発者が見たときにそのコードが何をするものか理解できなくなります
以下のような場合にカスタム例外を作成することをお勧めします:
- コアドメインやビジネスロジックを含む例外(文書化が必要な例外)
- 例外に追加情報や機能が必要な場合
協業やビジネスの観点から例外を考えることで、どのような場合に使用すべきかをより明確に理解できるようになるでしょう。
リファレンス
https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.20
https://sujl95.tistory.com/62
https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html
https://www.amazon.co.jp/-/en/Joshua-Bloch/dp/0134685997