Java Tips | 基本ユーティリティ:拡張子取得

Java Java
スポンサーリンク

拡張子取得は「ファイルの中身を推測するためのラベル取り」

業務システムでは、「CSV だけ受け付けたい」「画像ファイルだけ処理したい」「ログローテーションで .log だけ対象にしたい」など、
「ファイル名の拡張子を見て処理を分ける」場面が本当に多いです。

ここで雑に文字列を切り出すと、a.b.c.csv.gitignoreno_extension のような“ちょっと変わった名前”で簡単にバグります。
だからこそ、「拡張子取得」をユーティリティとしてきちんと設計しておくと、
判定ロジックが一箇所にまとまり、実務での事故をかなり減らせます。


基本:最後の「.」以降を拡張子とみなす

文字列から拡張子を切り出す最小パターン

まずは、ファイル名(またはパス)から拡張子を取り出す、いちばん素朴な実装を見てみます。

public final class Extensions {

    private Extensions() {}

    public static String getExtension(String filename) {
        if (filename == null || filename.isEmpty()) {
            return "";
        }
        int lastSeparator = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
        String name = filename.substring(lastSeparator + 1); // ディレクトリ部分を除く

        int dot = name.lastIndexOf('.');
        if (dot <= 0 || dot == name.length() - 1) {
            // 先頭がドット(.gitignore)や、ドットなし、末尾がドット("name.")は「拡張子なし」と扱う
            return "";
        }
        return name.substring(dot + 1);
    }
}
Java

使い方はこうです。

System.out.println(Extensions.getExtension("report.csv"));          // csv
System.out.println(Extensions.getExtension("a.b.c.csv"));          // csv
System.out.println(Extensions.getExtension("/var/log/app.log"));   // log
System.out.println(Extensions.getExtension(".gitignore"));         // ""
System.out.println(Extensions.getExtension("no_extension"));       // ""
System.out.println(Extensions.getExtension("trailing."));          // ""
Java

ここで重要なのは、次のような“微妙なケース”をちゃんと扱っていることです。

  • 先頭ドットのファイル(.gitignore)は「隠しファイル」であって、拡張子とはみなさない。** Baeldung**
  • ドットがないファイル名(no_extension)は拡張子なし。
  • 最後がドットで終わる(name.)のも拡張子なし扱いにする。

「最後のドット以降が拡張子」というルール自体はシンプルですが、
「どこまでを“拡張子あり”とみなすか」をきちんと決めておくことが、実務ではとても大事です。


Path と組み合わせて「パスから拡張子」を取る

まずファイル名部分だけを取り出してから拡張子を取る

java.nio.file.Path を使っているコードなら、Path からファイル名部分を取り出してから、先ほどのロジックを使うときれいです。

import java.nio.file.Path;

public final class PathExtensions {

    private PathExtensions() {}

    public static String getExtension(Path path) {
        if (path == null) {
            return "";
        }
        Path fileName = path.getFileName();
        if (fileName == null) {
            return "";
        }
        return Extensions.getExtension(fileName.toString());
    }
}
Java

使い方はこうです。

Path p1 = Path.of("/var/log/app.log");
System.out.println(PathExtensions.getExtension(p1)); // log

Path p2 = Path.of(".gitignore");
System.out.println(PathExtensions.getExtension(p2)); // ""
Java

ここでのポイントは、「ディレクトリ部分は Path に任せて、拡張子判定は“ファイル名だけ”に集中させる」ことです。
区切り文字(/\)を自分で気にしなくてよくなり、拡張子ロジックもシンプルに保てます。


例題:拡張子で処理を振り分ける

CSV と JSON で処理を変える

よくあるパターンとして、「同じディレクトリに CSV と JSON が混在していて、拡張子で処理を変える」というケースを考えます。

import java.nio.file.Files;
import java.nio.file.Path;

public class ImportJob {

    public void run(Path file) throws Exception {
        String ext = PathExtensions.getExtension(file).toLowerCase();

        switch (ext) {
            case "csv" -> importCsv(file);
            case "json" -> importJson(file);
            default -> throw new IllegalArgumentException(
                    "サポートされていない拡張子です: " + ext + " (" + file + ")");
        }
    }

    private void importCsv(Path file) throws Exception {
        System.out.println("CSV として処理: " + file);
        // CSV 読み込み処理…
    }

    private void importJson(Path file) throws Exception {
        System.out.println("JSON として処理: " + file);
        // JSON 読み込み処理…
    }
}
Java

ここで深掘りしたいのは、「拡張子取得ユーティリティのおかげで、“判定ロジック”がこのクラスから消えている」ことです。
lastIndexOf('.') などの細かい処理は Extensions に閉じ込めてしまい、
ここでは「拡張子が何か」にだけ集中して処理を分けています。

この分離ができていると、「拡張子の扱いを変えたい(例:.tar.gz を特別扱いしたい)」ときも、
ユーティリティ側だけを直せばよくなり、業務ロジックを触らずに済みます。


例題:アップロード時に「許可された拡張子」だけ受け付ける

拡張子ホワイトリストで簡易バリデーション

ファイルアップロード機能では、「画像だけ」「CSV だけ」といった制限をかけることが多いです。
その第一段階として、「拡張子でフィルタする」ユーティリティを用意しておくと便利です。

import java.nio.file.Path;
import java.util.Set;

public final class UploadExtensions {

    private UploadExtensions() {}

    private static final Set<String> ALLOWED_IMAGE_EXT = Set.of("png", "jpg", "jpeg", "gif");

    public static void validateImage(Path uploadedFile) {
        String ext = PathExtensions.getExtension(uploadedFile).toLowerCase();
        if (!ALLOWED_IMAGE_EXT.contains(ext)) {
            throw new IllegalArgumentException(
                    "画像として許可されていない拡張子です: " + ext + " (" + uploadedFile + ")");
        }
    }
}
Java

使い方はこうです。

Path uploaded = Path.of("work/upload/user-icon.PNG");
UploadExtensions.validateImage(uploaded); // 小文字化しているので png として扱われる
Java

ここでの重要ポイントは、「拡張子を小文字にそろえてから判定している」ことです。
PNGpng を別物として扱うと、ユーザーにとっては理不尽な挙動になります。
toLowerCase() で正規化してからホワイトリストと比較するのが、実務ではほぼ必須のパターンです。

もちろん、本気のセキュリティ対策としては「中身の MIME タイプを検査する」などが必要ですが、
「拡張子チェック」はその前段として、安価に弾けるものを弾くフィルタとして有効です。


既存ライブラリ(Apache Commons IO など)を使うかどうか

FilenameUtils.getExtension の存在を知っておく

Apache Commons IO には、FilenameUtils.getExtension(String) という便利メソッドがあります。

import org.apache.commons.io.FilenameUtils;

public class CommonsExample {

    public static void main(String[] args) {
        String ext = FilenameUtils.getExtension("student-records.pdf");
        System.out.println(ext); // pdf
    }
}
Java

既に Commons IO を使っているプロジェクトなら、これをそのまま使うのも十分アリです。
ただし、「ライブラリに依存するか」「自前ユーティリティで完結させるか」はプロジェクト方針次第なので、
チームで方針を決めておくとよいです。

個人的には、「拡張子取得くらいなら自前ユーティリティで十分」「他にも Commons IO を使うなら乗っかる」というバランスが取りやすいと思っています。


拡張子取得の“落とし穴”と注意点

拡張子は「中身の保証」ではない

一番大事な注意点は、「拡張子は“中身の種類”を保証してくれない」ということです。
virus.exevirus.png にリネームすることは簡単にできますし、
.csv と名乗っているけれど実際は全然違う形式、ということもあり得ます。

拡張子でできるのは、あくまで「ざっくりとした振り分け」や「ユーザーへのフィードバック」までです。
本当に安全性や形式を保証したいなら、MIME タイプや中身のパースで検証する必要があります。

「複合拡張子」をどう扱うかを決めておく

backup.tar.gz のように、「ドットが複数ある拡張子」をどう扱うかも設計ポイントです。
今回の実装では「最後のドット以降(gz)だけ」を拡張子とみなしていますが、
業務によっては「tar.gz というセットで扱いたい」こともあります。

その場合は、ユーティリティ側で特別扱いするか、
「まず gz として判定し、必要ならファイル名全体で追加チェックする」といった二段構えにするなど、
プロジェクトとしてのルールを決めておくと、後から迷いません。


まとめ:拡張子取得ユーティリティで身につけたい感覚

拡張子取得は、「ただ文字列を切り出す」ではなく、「現実世界の“ちょっと変なファイル名”も含めて安全に扱う」ための技です。

押さえておきたい感覚はこうです。

  • 先頭ドット(.gitignore)、ドットなし、末尾ドットなどのケースをどう扱うかを決め、ユーティリティに閉じ込める。
  • Path からは getFileName() でファイル名だけを取り出し、拡張子判定ロジックをシンプルに保つ。
  • 拡張子は小文字にそろえてからホワイトリスト判定し、「csv と CSV を同じもの」として扱う。
  • 拡張子はあくまで“ラベル”であり、中身の保証ではないことを理解したうえで、バリデーションの一部として使う。
  • 必要に応じて Commons IO などの既存ライブラリも検討しつつ、「プロジェクトとしてどこまで自前でやるか」を決めておく。

タイトルとURLをコピーしました