この記事を読むと分かること
- AIツール(ChatGPT、GitHub Copilotなど)が生成するJavaコードの注意点
- 「リソース解放」って何?なぜ大切なの?
- try-with-resourcesの正しい使い方と落とし穴
- 実際にどんな問題が起きるのか、どう防げばいいのか
対象読者: プログラミングを学び始めた高校生・大学生、Java初心者の方
はじめに:AIでコードを書く時代
最近、ChatGPTやGitHub Copilotを使ってプログラミングする人が増えていますよね。「ファイルを読むコード書いて」とお願いすると、すぐにコードを生成してくれて便利です。
でも実は、AIが生成するコードには「よくある見落とし」 があるんです。
それが今回のテーマ、「リソース解放の忘れ」 です。
リソース解放って何?
日常生活で例えると
図書館で本を借りたら、読み終わったら必ず返却しますよね?
返却しないと:
- 他の人が借りられない
- 延滞料金がかかる
- 図書館が「この本は貸出中」と思い込んだまま
プログラミングでも同じことが起きます!
プログラミングでのリソース
コンピュータの中の「借りたら返さないといけないもの」をリソースと呼びます:
| リソースの種類 | やるべきこと |
|---|---|
| ファイル | 開いたら閉じる |
| ネットワーク接続 | 繋いだら切断する |
| データベース接続 | 使ったら返却する |
| メモリ | 確保したら解放する |
これらをちゃんと返却する(解放する) のが、プログラマーの責任です。
AIが書くコードの問題点
問題例1: ファイルを開きっぱなし
// ❌ AIがよく生成する問題のあるコード
public void readFile(String fileName) throws IOException {
FileReader reader = new FileReader(fileName);
BufferedReader br = new BufferedReader(reader);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
// あれ?ファイルを閉じていない!
}
このコードの問題:
- ❌ ファイルを開いたまま終了している
- ❌ 図書館の本を返さないのと同じ
- ❌ 何度も実行すると、エラーになる
古い書き方:finallyブロックで閉じる
// ⚠️ 古い書き方(Java 7より前)
public void readFileOldWay(String fileName) throws IOException {
FileReader reader = null;
BufferedReader br = null;
try {
reader = new FileReader(fileName);
br = new BufferedReader(reader);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} finally {
if (br != null) {
br.close(); // 必ず閉じる
}
if (reader != null) {
reader.close();
}
}
}
この書き方の問題:
- コードが長くて読みにくい
- close()自体がエラーを起こす可能性がある
- 複数のリソースを管理するのが大変
try-with-resources:現代の正しい書き方
基本の使い方
Java 7(2011年)から、try-with-resourcesという便利な機能が追加されました!
// ✅ 現代の正しいコード
public void readFile(String fileName) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} // ← ここで自動的にファイルが閉じられる!
}
ポイント:
-
try (リソースの作成)の形で書く - tryブロックを抜けると、自動的にリソースが閉じられる
- エラーが起きても、必ず閉じられる
- コードが短くて読みやすい
仕組み:AutoCloseableインターフェース
try-with-resourcesで自動的に閉じられるのは、AutoCloseableというインターフェースを実装しているクラスだけです。
// 自動的に閉じられるクラスの例
public class BufferedReader implements AutoCloseable {
// ...
@Override
public void close() throws IOException {
// ファイルを閉じる処理
}
}
Javaの標準ライブラリで自動的に閉じられる主なクラス:
-
FileReader,FileWriter -
FileInputStream,FileOutputStream -
BufferedReader,BufferedWriter Scanner-
Connection,Statement,ResultSet(データベース) -
Stream(ファイルやコレクション)
複数のリソースを使う場合
// ✅ 複数のリソースも同時に管理できる
public void copyFile(String from, String to) throws IOException {
try (
FileInputStream in = new FileInputStream(from);
FileOutputStream out = new FileOutputStream(to)
) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
} // ← outとinの両方が自動的に閉じられる(逆順で)
}
重要:複数のリソースは逆順で閉じられます
-
outが先に閉じられる - 次に
inが閉じられる
try-with-resourcesの落とし穴
落とし穴1: リソースをtryの外で作る
// ❌ 危険なコード
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
try (reader) { // ← リソース作成とtryが分離している
String line = reader.readLine();
}
何が問題?
// もっと明確な問題例
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
doSomething(); // ← ここでエラーが起きたら?
try (reader) {
// ここまで到達しない = ファイルが閉じられない!
}
✅ 正しい書き方:リソースは必ずtry()の中で作る
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
}
落とし穴2: Streamを閉じ忘れる
Java 8以降、Streamというデータの流れを扱うクラスがあります。
Streamもリソースなので、閉じる必要があります!
// ❌ 危険なコード:Streamが閉じられない
public long countLines(String fileName) throws IOException {
return Files.lines(Paths.get(fileName)).count();
// ← Streamが閉じられていない!
}
// ❌ 危険なコード:フォルダ内のファイル一覧
public void listFiles(String folderName) throws IOException {
Files.walk(Paths.get(folderName))
.forEach(System.out::println);
// ← Streamが閉じられていない!
}
✅ 正しい書き方:Streamもtry-with-resourcesで管理
// ✅ 正しいコード
public long countLines(String fileName) throws IOException {
try (Stream<String> lines = Files.lines(Paths.get(fileName))) {
return lines.count();
}
}
// ✅ 正しいコード
public void listFiles(String folderName) throws IOException {
try (Stream<Path> paths = Files.walk(Paths.get(folderName))) {
paths.forEach(System.out::println);
}
}
覚え方:
-
Files.lines()→ try-with-resourcesで囲む -
Files.walk()→ try-with-resourcesで囲む -
stream()でファイルを扱う → try-with-resourcesで囲む
落とし穴3: 一部のリソースだけtryに入れる
// ❌ 危険なコード
public void copyFile(String from, String to) throws IOException {
FileInputStream in = new FileInputStream(from); // ← これが閉じられない!
try (FileOutputStream out = new FileOutputStream(to)) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
}
// outは閉じられるけど、inは閉じられない!
}
✅ 正しい書き方:全てのリソースをtry()に入れる
public void copyFile(String from, String to) throws IOException {
try (
FileInputStream in = new FileInputStream(from);
FileOutputStream out = new FileOutputStream(to)
) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
}
}
落とし穴4: データベースで一部のリソースを忘れる
データベースを使う時は、3つのリソースを管理する必要があります:
-
Connection(接続) -
StatementまたはPreparedStatement(SQL文) -
ResultSet(結果)
// ❌ 危険なコード:StatementとResultSetが閉じられない
public User getUser(int id) throws SQLException {
try (Connection conn = dataSource.getConnection()) {
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new User(rs.getString("name"));
}
} // ← Connectionは閉じられるが、stmtとrsは閉じられない!
return null;
}
✅ 正しい書き方:全部のリソースを管理する
public User getUser(int id) throws SQLException {
try (
Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")
) {
stmt.setInt(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return new User(rs.getString("name"));
}
}
}
return null;
}
落とし穴5: リソースを返り値として返す
// ❌ これは絶対ダメ!
public BufferedReader openFile(String fileName) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
return reader; // ← tryを抜けた瞬間に閉じられる!
}
}
// 使う側
BufferedReader reader = openFile("file.txt");
String line = reader.readLine(); // ← エラー発生!Stream closed
何が起きた?
-
openFile()の中でファイルを開く - tryブロックを抜ける → 自動的に閉じられる
- 閉じられた
readerが返される - 使おうとする → 既に閉じられているのでエラー
✅ 正しい設計パターン
パターン1: データを読んでから返す
// リソースではなく、読み込んだデータを返す
public List<String> readAllLines(String fileName) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
List<String> lines = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
return lines; // データなのでOK
}
}
パターン2: 処理を受け取る(少し難しい)
// ファイルを開いて、処理を実行して、自動的に閉じる
public void withFile(String fileName, Consumer<BufferedReader> processor) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
processor.accept(reader);
}
}
// 使う側
withFile("file.txt", reader -> {
String line = reader.readLine();
System.out.println(line);
});
自分で作るクラスをAutoCloseableにする
自分でファイルを扱うクラスを作る時は、AutoCloseableを実装しましょう。
悪い例:AutoCloseableを実装していない
// ❌ これではtry-with-resourcesで使えない
public class MyFileReader {
private BufferedReader reader;
public MyFileReader(String fileName) throws IOException {
this.reader = new BufferedReader(new FileReader(fileName));
}
public String readLine() throws IOException {
return reader.readLine();
}
// close()メソッドがない!
}
// 使おうとすると...
try (MyFileReader reader = new MyFileReader("file.txt")) { // ← コンパイルエラー!
// ...
}
良い例:AutoCloseableを実装する
// ✅ 正しい実装
public class MyFileReader implements AutoCloseable {
private BufferedReader reader;
public MyFileReader(String fileName) throws IOException {
this.reader = new BufferedReader(new FileReader(fileName));
}
public String readLine() throws IOException {
return reader.readLine();
}
@Override
public void close() throws IOException {
if (reader != null) {
reader.close(); // 内部のリソースを閉じる
}
}
}
// try-with-resourcesで使える!
try (MyFileReader reader = new MyFileReader("file.txt")) {
String line = reader.readLine();
System.out.println(line);
} // ← 自動的にclose()が呼ばれる
ポイント:
-
implements AutoCloseableを書く -
close()メソッドを実装する -
close()の中で、内部のリソースを閉じる -
nullチェックを忘れずに
なぜAIはこの間違いをするの?
理由1: 古いコードで学習している
AIは過去のプログラムコードを大量に読んで学習しています。
でも、インターネット上には古い書き方のコードがたくさんあります。
- Java 7より前(2011年以前): try-with-resourcesがなかった
- 古いチュートリアル: finallyブロックで閉じる方法を教えている
- GitHubの古いプロジェクト: 昔のやり方で書かれたまま
AIはこれらを「よくあるパターン」として覚えてしまっています。
理由2: 「簡単に見せたい」と思っている
AIにこう聞いたとします:
「ファイルを読むコードを書いて」
AIは「わかりやすく、短く」しようとして:
- ファイルを読む部分だけを書く
- エラー処理は省略
- ファイルを閉じる処理も省略
→ 結果、不完全なコードができてしまいます。
理由3: コードの「つながり」が分かりにくい
FileReader reader = new FileReader("file.txt");
// ...100行の処理...
reader.close();
AIから見ると:
-
new FileReaderを見る → ファイル読み込みパターン! - 100行先の
close()を見る → 閉じる処理パターン! - でも、この2つが対になっていることに気づきにくい
人間なら「開いたら閉じる」とセットで覚えますが、AIはそれぞれ別のパターンとして見てしまうことがあります。
理由4: Streamの特殊性を理解していない
Files.lines(Paths.get("file.txt")) // これもリソース!
一見、ただのメソッド呼び出しに見えますが、実は内部でファイルを開いています。
AIはこの「見えないリソース」に気づきにくいのです。
実際に起きる問題
問題1: プログラムが動かなくなる
// ❌ 危険なコード例
public void processLotsOfFiles() {
for (int i = 0; i < 10000; i++) {
try {
FileInputStream file = new FileInputStream("data" + i + ".txt");
// 処理...
// file.close() を忘れている
} catch (Exception e) {
System.out.println("エラー: " + e);
}
}
}
何が起きるか:
- 最初の数百ファイルは問題なく動く
- だんだん遅くなる
- 突然「Too many open files」というエラーが出る
- プログラムが止まる
原因: OSには「同時に開けるファイル数」に上限がある(例: 1024個)
問題2: メモリが足りなくなる
閉じられていないリソースは、メモリを占有し続けます。
開いているファイル: 1個 → メモリ使用量: 100MB
開いているファイル: 10個 → メモリ使用量: 1GB
開いているファイル: 100個 → メモリ使用量: 10GB ← メモリ不足!
問題3: 他の人が困る(Webアプリの場合)
学園祭のWebサイトを作ったとします。
// ❌ 危険なコード
public Student getStudent(int id) {
try {
Connection db = dataSource.getConnection();
// 処理...
// 接続を閉じていない!
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
何が起きるか:
- アクセス1人目: 動く
- アクセス2人目: 動く
- アクセス10人目: 動く
- ...
- アクセス100人目: 「接続できません」エラー
原因: データベース接続プールの上限(例: 100個)を超えた
AIに正しいコードを書いてもらうコツ
コツ1: 具体的に指示する
❌ 悪い例:
ファイルを読むコードを書いて
✅ 良い例:
ファイルを読むJavaコードを書いてください。
以下の条件を守ってください:
- try-with-resourcesを使う
- エラーハンドリングを含める
- Java 17の書き方で
- リソースリークがないようにする
コツ2: 「最新の書き方で」と伝える
最新のJavaのベストプラクティスに従って、
try-with-resourcesを使ってファイルを読むコードを書いてください
コツ3: Streamを扱う時は特に注意
Files.lines()を使ってファイルを読むコードを書いてください。
Streamは必ずtry-with-resourcesで閉じてください。
コツ4: レビューを依頼する
このコードをレビューして、リソースリークがないか確認してください:
[コードを貼り付け]
自分でチェックする方法
簡単チェックリスト
AIが生成したコードを受け取ったら、以下を確認しましょう:
レベル1: 基本のチェック
- ファイルを開いている?(FileReader、FileInputStreamなど)
- try-with-resourcesを使っている?
- tryブロックの中でリソースを作っている?
レベル2: Stream のチェック
-
Files.lines()を使っている? -
Files.walk()を使っている? - これらをtry-with-resourcesで囲んでいる?
レベル3: データベースのチェック
- データベース接続を使っている?
- Connection、Statement、ResultSetの全てを閉じている?
レベル4: 複雑なケースのチェック
- リソースを返り値として返していない?
- 複数のリソースを全てtry()の中に入れている?
- 自作クラスがAutoCloseableを実装している?
危険なコードパターン
こんなコードを見たら要注意:
// ⚠️ パターン1: tryの外でリソース作成
BufferedReader reader = new BufferedReader(...);
try (reader) { ... }
// ⚠️ パターン2: Streamを閉じていない
Files.lines(Paths.get("file.txt")).forEach(...);
// ⚠️ パターン3: 一部だけtryに入れている
FileInputStream in = new FileInputStream(...); // 閉じられない
try (FileOutputStream out = ...) { ... }
// ⚠️ パターン4: リソースを返している
public BufferedReader getReader() {
try (BufferedReader r = ...) {
return r; // 危険!
}
}
ツールに助けてもらう
IntelliJ IDEAの警告機能
IntelliJ IDEA(人気のJava開発ツール)は、リソースの閉じ忘れを自動的に警告してくれます。
FileReader reader = new FileReader("file.txt"); // ← 黄色い波線が出る
// マウスを乗せると「Resource 'reader' is never closed」と表示される
警告が出たら:
- 電球アイコンをクリック
- 「Surround with try-with-resources」を選択
- 自動的に正しいコードに修正される!
その他の便利ツール(無料)
| ツール名 | 何をしてくれる? |
|---|---|
| SpotBugs | コードの問題を自動検出 |
| SonarLint | リアルタイムでコードを分析 |
| Checkstyle | コーディング規約をチェック |
| Error Prone | Googleが作った静的解析ツール |
これらをインストールすると、コードを書いている最中にリソースの閉じ忘れを教えてくれます。
実践:良いコードと悪いコードの比較
例1: ファイル読み込み
// ❌ 悪いコード
public void readFileBad(String fileName) throws IOException {
FileReader fr = new FileReader(fileName);
BufferedReader br = new BufferedReader(fr);
String line = br.readLine();
System.out.println(line);
// 閉じていない
}
// ✅ 良いコード
public void readFileGood(String fileName) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line = br.readLine();
System.out.println(line);
}
}
例2: ファイルコピー
// ❌ 悪いコード
public void copyFileBad(String from, String to) throws IOException {
FileInputStream in = new FileInputStream(from);
try (FileOutputStream out = new FileOutputStream(to)) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
}
// inが閉じられていない
}
// ✅ 良いコード
public void copyFileGood(String from, String to) throws IOException {
try (
FileInputStream in = new FileInputStream(from);
FileOutputStream out = new FileOutputStream(to)
) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
}
}
例3: ファイルの行数カウント
// ❌ 悪いコード
public long countLinesBad(String fileName) throws IOException {
return Files.lines(Paths.get(fileName)).count();
// Streamが閉じられていない
}
// ✅ 良いコード
public long countLinesGood(String fileName) throws IOException {
try (Stream<String> lines = Files.lines(Paths.get(fileName))) {
return lines.count();
}
}
例4: データベース検索
// ❌ 悪いコード
public User getUserBad(int id) throws SQLException {
try (Connection conn = dataSource.getConnection()) {
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new User(rs.getString("name"));
}
}
return null;
// stmtとrsが閉じられていない
}
// ✅ 良いコード
public User getUserGood(int id) throws SQLException {
try (
Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")
) {
stmt.setInt(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return new User(rs.getString("name"));
}
}
}
return null;
}
まとめ
覚えておきたいポイント
- リソースは「借りたら返す」: ファイル、データベース接続などは必ず閉じる
- try-with-resourcesを使おう: 自動的に閉じてくれて安全
- リソースはtry()の中で作る: 外で作ってはいけない
- Streamも忘れずに: Files.lines()、Files.walk()も閉じる
- 全部のリソースを管理: 一部だけでは不十分
- リソースを返り値にしない: 閉じられてしまう
- AutoCloseableを実装: 自作クラスも自動で閉じられるように
- AIのコードは完璧じゃない: 必ずチェックする
- ツールを活用: IDEの警告機能が助けてくれる
try-with-resources の基本ルール
// ✅ これが基本形!
try (リソースの作成) {
// リソースを使った処理
} // ← ここで自動的に閉じられる
守るべきルール:
- ✅ リソースは必ずtry()の中で作る
- ✅ 複数のリソースは全てtry()に入れる
- ✅ Streamも必ずtry-with-resourcesで囲む
- ✅ データベースはConnection、Statement、ResultSet全てを管理
- ❌ リソースを返り値として返さない
AIとの上手な付き合い方
AIは素晴らしい「アシスタント」です。でも、最終的な責任はプログラマー(あなた)にあります。
- ✅ AIが生成したコードは必ず確認する
- ✅ 分からない部分は調べる・質問する
- ✅ 動いたからOKではなく、正しいか考える
- ✅ 小さなプログラムでも丁寧に書く習慣をつける
- ✅ リソース管理は特に注意深くチェックする
これらを意識すれば、AIを使いこなしながら、良いプログラマーになれます!
もっと学びたい人へ
おすすめの学習リソース
- 📘 Oracle Java Tutorials - try-with-resources
- 📕 書籍『スッキリわかるJava入門』
- 📗 書籍『Effective Java 第3版』(少し難しいけど名著)
- 🎥 YouTube: Java入門講座(検索してみてください)
次のステップ
- 自分の過去のコードを見直してみる
- try-with-resourcesを使って書き直してみる
- 友達のコードをレビューしあう
- GitHubで有名なプロジェクトのコードを読んでみる
- IntelliJ IDEAやEclipseの警告機能を活用する
最後まで読んでくださり、ありがとうございました!
質問や感想があれば、コメント欄で教えてください 😊
いいね👍やストック📚もお願いします!
参考文献
- Oracle公式ドキュメント
- Effective Java, Third Edition
- Java言語仕様書
- JLS 14.20.3 - try-with-resources
#Java #初心者向け #プログラミング #AI #ChatGPT #学習メモ #try-with-resources #リソース管理