the KodeLab

Java で拡張子と MIME タイプを取得する方法|Files.probeContentType / Path.of 対応

3,659 文字 10 分で読めます
Java で拡張子と MIME タイプを取得する方法|Files.probeContentType / Path.of 対応

ファイル名から拡張子を抜き出したり MIME タイプを判定したりするのに、サードパーティライブラリは必要ありません。Java 7 の NIO(Files.probeContentType)と Java 8 の Optional を組み合わせれば、短く null 安全に書けます。標準ライブラリで済ませると、ランタイムの余計な重量、使わない API、メンテされなくなったライブラリ由来の脆弱性といった面倒を避けられます。

拡張子を取得する

拡張子は、ファイル名で最後の . より後ろの部分です。通常は .jpg / .mp3 / .txt のような英数字の数文字。Apache Commons IO の FilenameUtils でも取得できますが、ここではサードパーティ依存なしで、そのままコピペして使えるコードを目指します。

シンプルな書き方

String の標準メソッドだけで十分です。

// 見つからないときは空文字をデフォルトに
String extension = "";
// 判定するファイル名
String filename = "notes.txt";
// lastIndexOf は該当文字がなければ -1 を返す
int idx = filename.lastIndexOf(".");

if (idx >= 0) {
    // substring で .(ドット)以降を取り出す — ここでは "txt"
    extension = filename.substring(idx + 1);
}

lastIndexOf(".") を使うのがポイントです。report.tar.gz のようなファイル名でも、最後のドットだけ見るので結果は "gz" になり、"tar.gz" にはなりません。

Optional で綺麗に書き直す

ユーティリティクラスにまとめると、1 つの式で null 安全に書けます。

import java.util.Optional;

public class FileTools {
    /**
     * ファイル名から拡張子を返す。見つからなければ orElse を返す。
     *
     * @param filename ファイル名。null 可
     * @param orElse   見つからなかったときの返り値。null 可
     * @return 先頭のドットを含まない拡張子、または orElse
     */
    public static String getExtension(String filename, String orElse) {
        return Optional.ofNullable(filename)
            .filter(s -> s.contains("."))
            .map(s -> s.substring(s.lastIndexOf(".") + 1))
            .orElse(orElse);
    }
}

使い方:

FileTools.getExtension("notes.txt", "");
// → "txt"

FileTools.getExtension("report.tar.gz", "");
// → "gz"

FileTools.getExtension(null, "");
// → ""

MIME タイプを判定する

Files.probeContentType(Java 7 NIO)はファイル名から MIME タイプを推測してくれます。注意点を先に書いておくと、Windows エクスプローラーはデフォルトで拡張子を隠しますし、拡張子は人為的に決まっていて簡単に書き換えられます。拡張子ベースの MIME タイプはあくまで参考値で、ファイルの中身を保証するものではありません。セキュリティが絡む場面では、実際のバイト列を読んで判定する必要があります。

PathFiles

ファイル名から Path を作り、Files.probeContentType に渡すだけです。該当する MIME タイプがなければ null が返り、チェック例外として IOException を宣言しています。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

String filename = "notes.txt";
// Java 7〜10 は Paths.get(filename)、Java 11 以降は Path.of(filename) が使える
Path path = Paths.get(filename);
String mime = Files.probeContentType(path);
// → "text/plain"

Paths.get()Path.of() の違い

この 2 つのメソッドはまったく同じ動作で、文字列を Path に変換するだけです。違いはどのクラスに定義されているか。Paths.get() は Java 7 で NIO と一緒に登場しました。当時の Path はインターフェースで、インターフェースは static なファクトリメソッドを持てなかったため、ユーティリティクラス Paths 側に置かれていました。Java 8 でその制約が外れ(インターフェースでの static メソッドが許可)、Java 11 でようやくファクトリメソッドが Path.of() として Path 本体に移されました。

Java 11 以降、Paths.get() は内部で Path.of() を呼ぶだけの薄いラッパになっていて、2 つは等価です。新しいコードでは Path.of() をおすすめします。Java 9 で導入された List.of() / Set.of() / Map.of() と同じスタイルに揃えられ、Paths クラスの import も減ります。Java 8 の古いプロジェクトをメンテナンスしているなら、選択肢は Paths.get() だけです。

メソッドクラス導入バージョン
Paths.get()java.nio.file.PathsJava 7
Path.of()java.nio.file.PathJava 11

プラットフォーム依存の注意点

Files.probeContentType は純粋な Java の実装ではなく、OS 側に問い合わせます。

  • Linux/etc/mime.types を読みに行く
  • macOS:システム固有のマッピングを持つ
  • Windows:JDK のベンダー/バージョン次第でマッピングが薄く、null が返るケースが多い

プラットフォーム間で一貫した結果が必要なら、自前で拡張子 → MIME タイプのマッピングテーブルを持つのが確実です。自前マップにしておけば、.ts(TypeScript か、video/mp2t の MPEG トランスポートストリームか)のような曖昧なケースで、自分のプロジェクトにとっての「正解」を固定できます。

Optional で綺麗に書き直す(完成版)

IOExceptionnull の両方を呼び出し側から隠したい場合は、次のようにまとめます。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;

public class FileTools {
    /**
     * ファイル名の拡張子から推測した MIME タイプを返す。見つからなければ orElse を返す。
     *
     * @param filename ファイル名。null 可
     * @param orElse   見つからなかったときの返り値。null 可
     * @return MIME タイプ、または orElse
     */
    public static String getContentType(String filename, String orElse) {
        return Optional.ofNullable(filename)
            .map(s -> Paths.get(s))
            .map(p -> {
                try {
                    return Files.probeContentType(p);
                } catch (IOException e) {
                    return null;
                }
            })
            .orElse(orElse);
    }
}

使い方:

FileTools.getContentType("notes.txt", "");
// → "text/plain"

この 2 つの小さなヘルパーがあれば、ファイル名まわりの処理は標準ライブラリに閉じ込められます。余計な依存も、ライブラリの脆弱性を追いかける手間もなく、Gist に貼り付けられる量で済むのが個人的にはいちばん嬉しいポイントです。