bookmark_border[解説] Scannerのclose()は必要?

Javaの入門書などで学習中、 java.util.Scanner を使うと次のように表示されることがあります。

リソース・リーク: 'scanner' が閉じられることはありません

これは何

Javaのコンパイラが出す警告で、エラーではありません(無視しても動くといえば動く)。

Scannerは使い終わった後 closeメソッドの呼び出しを期待されています。でも呼び出していないので警告を発しているという状況です。以下のようにcloseメソッドを呼び出すコードを書くと警告は消えます。

Scanner scanner = new Scanner(System.in);
// いろんな処理...
scanner.close();

scanner.close(); は必要?

様々な意見があるかと思いますが、以下の理由でJavaの練習で書くnew Scanner(System.in); に関してはclose()の必要はありません

理由1: close()するとSystem.in ごと閉じてしまう

一度closeしてから再度new Scanner(System.in); してSystem.inを使おうとするとエラーになります。

エラーになるコード

Scanner scanner = new Scanner(System.in);
scanner.close();
// もう一度Scanner使いたい
scanner = new Scanner(System.in);
scanner.nextLine(); // ここで落ちる

実行結果

Exception in thread "main" java.util.NoSuchElementException: No line found
	at java.util.Scanner.nextLine(Scanner.java:1540)
	at Hogehoge.main(Hogehoge.java:15)

System.inは標準入力と言い、Javaのプログラムが入力を受け取るためのデータの通り道のようなものです。eclipseで言うところの「コンソール」とJavaの橋渡し的存在です。

scanner.close(); すると、その橋(System.in)自体の使用をやめる処理が走ります。そしてそのSystem.inは、一度closeするとそのプログラムの実行中はもう一度開くことができません

It is not possible to reopen System.in, System.out or System.err. … it is not possible to reopen them.

Stack Overflow – In Java is it possible to re-open System.in after closing it

プログラムの最後に「作法」としてclose();を書くのは良いことですが(後述)、Javaの練習で書くnew Scanner(System.in); に関してはプログラムの途中でcloseするメリットは無いように思います。

理由2: Javaのプログラム自体の実行終了はclose()を呼んだのと大体同じ

Javaのプログラム自体が終了すると、橋渡しするそのSystem.in含め全てがメモリから破棄されます。練習で書くプログラムは長時間動き続けるものではなく、System.inも自分でcloseしなくても他のプログラムに悪影響はありません。


じゃあclose()書かなくていい?

いいえ、個人的な意見ですが一言で言えば、「new Scanner(System.in); に関しては事実上必要ないが、今後必須の考え方なので書いて練習の機会に」です。

Javaの練習で書くnew Scanner(System.in); に関しては 書かなくても動くといえば動きますが、closeはScannerだけでなくプログラミング全般に通じる考え方, 作法の一つです。そのマインド作り, 練習としてぜひ書いていただきたいと考えます。

Scannerはcloseするべきか について調べると「書くようにしましょう」という文献を見かけますが、この重要な概念を覚えてもらいたいという願いからと思います。


close()の意義

closeは広義には資源を開放して他のプログラムでも使えるようにすることです。

借りたものは返すという作法

「(図書館の本のような)公共のものを借りたら早めに返して他の人も使えるようにする」一般常識はプログラミングの世界でもあり、「借りる」がopen (new Scanner(…);など)、「返す」が close に相当します。

例えばファイルの書き込みで言えば以下のような処理が一般に行われています。

  1. 「自分がこのファイルに書き込むので、終わるまで他のプログラムが書き込めないようロックする」(open)
  2. 「書き込み終わったから他のプログラムも書き込めるようにファイルを開放する」(close)

closeしないとどうなるのか

自分がファイルを使う権利を握り続けるので、ほかのプログラムがいつまで経ってもそのファイルを操作できません。

ファイルに限らず例えばデータベースに接続するプログラムがあるとして、closeしないで放置すると他のプログラムがデータベースに接続できなくなったりシステムの性能が落ちたりします。

Scannerの場合は?

Scannerはキーボード入力を受け付けるSystem.inだけでなく、テキストファイルなど他の入力も扱うことができます。その場合、scannerを使い終わった時点でcloseをすぐ実行するのは作法として適切と言えます。

Scanner scanner = null;
// 必要なtry-catch文は省略しています。
scanner = new Scanner(new File("hoge.txt"));
System.out.println(scanner.nextLine());
scanner.close(); // 使い終わったのでclose
// いろんな処理
// System.inと違い、一度closeしても再度openできる。
// ファイルに読み込みのロックが掛かっていなければエラーにはならない
scanner = new Scanner(new File("hoge.txt"));
System.out.println(scanner.nextLine());
scanner.close(); // 使い終わったのでclose
/*
hogehoge
hogehoge
と出力されます。(hoge.txtには hogehoge と書いてある)
*/

bookmark_border【eclipse】謎の不具合に遭遇したときに試すclean【Java】

次のような謎の不具合に遭遇したときまず試す操作のメモです。

  • コードの文法は合っているのに文法エラーの指摘がある
  • パッケージ・エクスプローラーなどで表示されるディレクトリ構造が変
  • サーブレットでサーバーが起動できない

まず試す:プロジェクトのクリーン

[プロジェクト]→[クリーン]→[すべてのプロジェクトをクリーン]→[Enter]

これは何

生成済のclassファイル(プロジェクトのビルド時に出力されるファイル)などを全部消します。

eclipseはビルド時間の短縮を目的に、コードに変更があったとき変更箇所だけをビルドし直しています。この操作でその生成済みの成果物を一度全て消すことで、eclipseはサーブレットやJavaプロジェクト全体を次回 一からビルドし直してくれます

奥の手:eclipseのclean起動

Windows

  1. eclipseを開いているなら終了する
  2. eclipseが入っているフォルダを開く
  3. eclipse.exe -clean.cmd をダブルクリック (eclipse.exeではなく)

Mac

  1. eclipseを開いているなら終了する。(command + Q などで完全に)
  2. 「ターミナル」を起動する
  3. 次のコマンドを実行する
    • open eclipse.appのフルパス --args -clean
コマンドの例:
open /Applications/Eclipse_2019-09.app --args -clean

eclipse.appのフルパスを入力する楽な方法:

  1. eclipse.appが入っているフォルダをFinderで開く
  2. eclipse.appをFinderからターミナルにドラッグ&ドロップ

これは何

eclipseが保持しているキャッシュを削除+再作成してeclipseを起動します。

キャッシュにはプロジェクト内のファイルやディレクトリ構造の情報が含まれており、何かの拍子にそれが壊れると実際の構造とeclipse上での表示がずれたり、他の問題を引き起こしたりします。

パッケージ・エクスプローラーでF5キーを押してファイルシステム側の構造を再読み込みする方法もありますが、キャッシュを一旦作り直してもらったほうが起こりうる他の何らかの問題も回避できると考えて何かがおかしいときはこれを実行しています。



bookmark_border「サーバーで実行」時にサーバー再起動ダイアログを再表示させる【eclipse】

eclipseでサーブレットを「サーバーで実行」するとき、サーバーを再起動するか尋ねるダイアログが出て欲しいのに出てきてくれないときの対処をメモ。

[Windows: ウインドウ/ Mac: Eclipse] → [設定] → [サーバー] → [起動] → [必要な場合はサーバーの再起動]

ダイアログが出てきてくれないときはここが「なし」になっている可能性。

bookmark_border【Mac】サーバーで必要なポート8080はすでに使用中 の一発解決コマンド

下のコマンドを実行する

8080ポートを使用しているPIDをそのままkillに渡しているだけです。

lsof -t -i:8080 | xargs kill -9 

これは何

lsof -i:8080 → 8080を使用しているプロセスが誰か調べる
-t → そのPIDだけもらう

| xargs kill -9 パイプで受け取ったPIDをkill (-9:強制終了)

bookmark_border【Tomcat, Windows】「サーバーで必要なポート8080はすでに使用中です」を一発解決するスクリプト【eclipse】

Macだとアクティビティモニタで簡単にプロセスをkillできますが(自分はそうしてた)、Windowsはnetstatしたりサービス一覧を調べたりする方法が出てきて、なんか大変そうだったので。

このスクリプトを実行する

PowerShellでこのスクリプトを実行します。

$processes = Get-Process -Id (Get-NetTCPConnection -LocalPort 8080,8005,8009).OwningProcess
foreach($process in $processes) {
  Stop-Process $process.Id
}

実行方法

  1. Windows PowerShellを立ち上げる
    スタートメニュー(Cortana)の検索で powershell とか打つと候補に出てくる
  2. 上記のスクリプトをコピペしてEnterで実行

エラーっぽい赤い文字が出るかもしれませんが、8005やか8009番ポートを使用しているアプリケーションが存在していないだけで特に問題ありません。

これは何

8080, 8005, 8009番ポートを使用しているプロセスを一括で終了しています。

「ローカルホストのtomcat vXX サーバーで必要なポート8080はすでに使用中です」 is 何

8080番ポートを使うサーバーを起動したいけど、8080番ポートを使用している先客(プロセス)がすでに実行中なので、起動できない。
(すでに実行中のそれは、大抵はeclipseで自分が起動したプロセスの残骸だったりする)

ポート番号の変更は根本解決ではない

eclipseでtomcatのポート番号を変更する方法もありますが、このエラーは前述の通りプロセスの残骸が原因なので、そのプロセスを終了させるほうが正攻法と思われます。(最終手段のPC再起動も事実上はその残骸プロセスを終了させる方法)

bookmark_borderTomcat+Log4j2でコンソール出力 & 任意のファイルに書き込み

Log4j2 + サーブレット(Tomcat)でとりあえず任意のファイルとコンソールへログ出力する上で最低限すべきことをメモ。ログレベルの制御といったことまでは書いていません。

1. Log4j2のダウンロード

Apache ソフトウェア財団から最新版をダウンロード

2. Log4j2のjarをビルドパスに追加

ダウンロードしたzipにいっぱいjarが入っています。そのうち以下のファイルをWEB-INF/libにコピー。
大抵はコピーした段階でeclipseが自動でビルドパスに追加してくれるはずです。

  • log4j-XXX-api-XXX.jar
  • log4j-api-XXX.jar
  • log4j-core-XXX.jar
  • log4j-web-XXX.jar

※XXXの部分にはバージョン番号が入ります。

3. log4j2.xmlの編集

WEB-INF直下に「log4j2.xml」を以下の内容で記述しつつ、ハイライトされている部分を適宜書き換えます。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF">
  <Appenders>
        <RollingFile name="file"
                fileName="C:\XXX\YYY\hogeapp.log" <!-- 書き込んでほしいログファイルの場所 -->
                filePattern="C:\XXX\YYY\hogeapp-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="[%date] %-5p %location %m%n"/>
            <Policies>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="256 KB" />
            </Policies>
            <DefaultRolloverStrategy max="100"/>
        </RollingFile>
    </Appenders>
    <Loggers>
        <Logger name="自分のアプリケーションのパッケージ名" level="trace">
            <AppenderRef ref="file" />
        </Logger>
    </Loggers>
</Configuration>

4. ログ出力のコードを書く

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@WebServlet("/Main")
public class Main extends HttpServlet {
    private static Logger logger = LogManager.getLogger();
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        logger.warn("まずい");
        logger.error("かなりまずい");
    }
}