Path でファイルパスを扱う

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • PathString ではなく OS 中立 なファイルパス表現 (Java 7 NIO.2)
  • Paths.get("/tmp/foo.txt")Path.of(...) で生成 (Java 11+ は Path.of 推奨)
  • resolve (子パス), getParent (親), getFileName (末尾) など便利メソッド
  • 古い File クラスより安全、Windows / Unix のパス区切り違いを吸収

Path でファイルパスを扱う とは

java.nio.file.Path と Paths.get を使って、OS 非依存にファイルパスを組み立てたり分解したりする基本を学ぶ。

Path でファイルパスを安全に扱う

ファイルを読み書きするコードを書くとき、最初に出てくる悩みは「ファイルの場所をどう表すか」です。Windows なら C:\\Users\\taro\\notes.txtmacOSLinux なら /home/taro/notes.txt のように、OS によって書き方も区切り文字も違います。これを String の連結だけで頑張ろうとすると、バックスラッシュ の数を間違えたり、末尾に / を付け忘れたりして、すぐにバグの温床になります。

Java 7 以降では、こうしたパス操作専用の便利な型として java.nio.file.Path インターフェースが用意されました。Path は単なる文字列ではなく「ファイルパスを表すオブジェクト」で、getFileName getParent resolve といったメソッドを通してパスの各部品を安全に取り出したり組み合わせたりできます。古い java.io.File クラスを置き換える、現代の Java の標準です。

Pathjava.nio.file パッケージにあります。古い java.io.File と名前は似ていますが別物です。新しいコードでは原則 Path を使い、File は古い API との互換のためだけに使う、と覚えておきましょう。

本章では、この Path の基本中の基本、つまり「文字列から Path を作って分解する」ところからスタートします。今回の課題で使う Paths.get(...)getFileName() だけでも、ログファイル名の抽出、アップロードされたファイルの拡張子チェック、コマンドライン引数の検証など、業務で頻出するシーンに対応できます。

Paths.getPath を生成する

Path を作るもっとも基本的な方法が、java.nio.file.Paths クラスの静的メソッド Paths.get(String first, String... more) です。文字列を渡すと、その内容に対応する Path オブジェクトが返ってきます。

Java

import java.nio.file.Path; import java.nio.file.Paths; public class Demo { public static void main(String[] args) { Path p1 = Paths.get("/tmp/foo/bar.txt"); Path p2 = Paths.get("/tmp", "foo", "bar.txt"); Path p3 = Paths.get("notes.txt"); System.out.println(p1); // /tmp/foo/bar.txt System.out.println(p2); // /tmp/foo/bar.txt (同じ結果) System.out.println(p3); // notes.txt } }

注目してほしいのは p2 の作り方です。区切り文字 / を自分で書かなくても、複数の文字列を渡せば Java が OS に合わせて適切なセパレータでつないでくれます。Windows で同じコードを動かせば \ で連結されますし、Linux なら / で連結されます。これだけで、String+ で連結するより圧倒的に安全です。

Java 11 以降では Path.of("/tmp/foo/bar.txt") という書き方もあり、こちらが推奨されています。中身は Paths.get とまったく同じなので、どちらを覚えても問題ありません。今回の課題では Paths.get を使います。

getFileName getParent getRoot でパスを分解する

生成した Path は、メソッド呼び出しで部品ごとに取り出せます。よく使うのは次の 3 つです。

  • getFileName() — パスの一番末尾、つまりファイル名 (またはディレクトリ名) を Path として返す
  • getParent() — 一番末尾を除いた親ディレクトリ部分を Path として返す
  • getRoot()Linux なら /Windows なら C:\ のような根っこの部分を返す

Java

Path p = Paths.get("/tmp/foo/bar.txt"); System.out.println(p.getFileName()); // bar.txt System.out.println(p.getParent()); // /tmp/foo System.out.println(p.getRoot()); // /

ここで覚えておきたい挙動が 2 つあります。1 つ目は、戻り値が String ではなく Path であること。文字列として欲しい場合は p.getFileName().toString() のように toString() を呼びます。2 つ目は、/tmp/foo/bar/ のように末尾が区切り文字で終わっているパスを Paths.get に渡すと、末尾の / は無視されて bar がファイル名として返ってくることです。今回の課題でもこの挙動を使います。

diagram (will load when visible)

resolve でパスを安全に連結する

パスを「組み立てる」ときの主役は resolve メソッドです。base.resolve("child") と書くと、base の下に child をつなげた新しい Path を返してくれます。

Java

Path dir = Paths.get("/var/log"); Path full = dir.resolve("app.log"); System.out.println(full); // /var/log/app.log

Stringdir + "/" + name のように書くと、dir の末尾に / が付いていたかどうかで // になったり、Windows/\ が混ざったりします。resolve はこの面倒事をすべて吸収してくれます。さらに、引数に絶対パス (/etc/hosts など) を渡したときは「絶対パスをそのまま返す」という賢い動きもします。

もう 1 つ、組み立て後のパスを綺麗にしたいときは normalize() が便利です。/a/b/../c のような .. を含むパスを /a/c に整理してくれます。resolvenormalize をセットで使うのが定番の組み合わせです。

Files.exists(path) Files.readString(path) のように、ファイルシステムへの実操作は java.nio.file.Files クラス側にまとまっています。Path 自体はあくまで「パスの値」を表すオブジェクトで、ディスクは触りません。PathFiles の役割分担を意識すると、API 全体がスッと頭に入ります。

PathFile、相対と絶対

古くからの Java を書いてきた人は java.io.File というクラスにも馴染みがあるはずです。File も同じくパスを表しますが、ミュータブル (内部状態を書き換えるメソッドがある) で、ファイル操作系メソッドも一緒に持っているため、責務がやや散らかっていました。Path は対照的に イミュータブル (不変) で、getFileName() などのメソッドは元の Path を変更せず、新しい Path を返します。

Java

Path p = Paths.get("/tmp/foo/bar.txt"); p.getFileName(); // 戻り値は新しい Path、p 自体は変わらない

また、Paths.get("notes.txt") のような 相対パス と、Paths.get("/home/taro/notes.txt") のような 絶対パス の違いも押さえておきましょう。相対パスは「カレントディレクトリからの場所」を、絶対パスは「ルートからのフルパス」を表します。p.isAbsolute() で判定でき、p.toAbsolutePath() を呼ぶと現在のカレントディレクトリを補って絶対パスに変換できます。

比較表にすると以下のとおりです。

観点java.io.Filejava.nio.file.Path
登場時期Java 1.0Java 7
イミュータブルいいえはい
OS 区切り自分で意識自動で吸収
ファイル操作File 自身に内包Files クラスへ分離
推奨度レガシー現代の標準

よくある間違い

ここで、Path を使い始めたばかりの人がハマりやすい落とし穴を 3 つ紹介します。

  • String+ でパスを連結するdir + "/" + name のように書くと Windows では /\ が混ざり、name 側に余計な / が含まれていると // ができます。必ず dir.resolve(name) を使いましょう
  • OS 依存の区切り文字を直書きする"C:\\Users\\taro" のような書き方は Linux で動きません。マルチプラットフォームで動かしたいコードでは Paths.get("C:", "Users", "taro") のように分割して渡すか、設定ファイルから読み込みます
  • resolve+ を同じ感覚で使うresolve に絶対パスを渡すと「左辺を捨てて絶対パスを返す」という挙動になります。これは仕様であり、+ の単純連結とは違うので注意してください。期待しない上書きを避けたい場合は、事前に child.isAbsolute() を確認します

もう 1 つ、Paths.get("/a/b/c/").getFileName() の結果が c/ ではなく c になる点も最初は戸惑います。Path は末尾の区切り文字を意味のあるものとして扱わないので、/a/b/c//a/b/c も同じパスとして解釈されます。今回の課題ではこの挙動をそのまま使うことになります。

エラーメッセージで NoSuchFileException InvalidPathException などが出たら、まずは path.toAbsolutePath() をログに出して、自分が思っているパスを本当に指しているか確認しましょう。デバッグの第一歩は「現実のパスを見る」ことです。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • 設定ファイルの探索Paths.get(System.getProperty("user.home"), ".myapp", "config.json")
  • ログディレクトリの作成Files.createDirectories(logDir) で深いパスを一発作成
  • S3 オブジェクトキーの組み立て ─ ローカル Path の代わりに同じ感覚で組み立てるユーティリティを作る
  • バックアップファイル名生成path.resolveSibling("backup.bak") で同じディレクトリに別ファイル

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • String でパスを連結しているdir + "/" + name ではなく dir.resolve(name) を使う
  • new File(...) を使っている ─ 新規コードでは Path / Files を使う
  • 絶対パスと相対パスを混同path.toAbsolutePath() で明示的に正規化
  • シンボリックリンクを無視Files.isSymbolicLink(path) のチェックが要件にあるか確認

パフォーマンス考慮事項

  • 生成は軽量 ─ パスは内部的に文字列分解されるが、ナノ秒オーダー
  • Files.walk は再帰 ─ 巨大ディレクトリで OOM の可能性、Files.walkFileTree で訪問制御
  • Files.exists は OS 呼び出し ─ 連続で呼ぶと遅い。Files.readAttributes で一括取得
  • Path.toString は OS 依存の区切り文字 ─ クロスプラットフォーム時は要注意
この章のポイント

ここまでの要点 Path.of(...) で OS 中立パス、resolve で結合、getParent で親。File よりも Path + Files ユーティリティが現代風。

やってみよう

今回の課題は、与えられた文字列パスから一番末尾の名前 (ファイル名やディレクトリ名) を取り出すだけのシンプルなメソッド lastName を完成させることです。手順は次のとおりです。

  1. ファイル先頭で import java.nio.file.Paths; を書く (Path 自体は使わないなら不要)
  2. Paths.get(pathStr)Path を作る
  3. .getFileName() で末尾の Path を取り出す
  4. .toString()String に変換して return する

戻り値の例は次のとおりです。

  • lastName("/tmp/foo/bar.txt")"bar.txt"
  • lastName("simple.txt")"simple.txt"
  • lastName("/a/b/c/")"c" (末尾の / は無視される)

コード全体は 1 行で return Paths.get(pathStr).getFileName().toString(); と書けます。短いですが、この 1 行に「Paths.get で生成」「getFileName で分解」「toString で文字列化」という Path の基本操作 3 つが詰まっています。慣れてきたら、getParent()getRoot() に置き換えて結果がどう変わるかも試してみてください。Path の感覚を身につけておくと、次のレッスンで扱う Files.readString Files.write のようなファイル操作がぐっと自然に書けるようになります。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は BufferedReader と Stream API で、java を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. Path の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. Path とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. ファイル先頭で import java.nio.file.Paths; を書くこと
  2. Paths.get(pathStr) を使って Path を生成すること (自前で String 操作しない)
  3. getFileName() で末尾を取得し、toString()String に変換して return すること

入出力例

test-cases.txt

lastName("/tmp/foo/bar.txt")"bar.txt" lastName("simple.txt")"simple.txt" lastName("/a/b/c/")"c" lastName("/var/log/app/2026/05/19/server.log")"server.log" lastName("/etc/hosts")"hosts"

ヒント

main.java
main.java
学習モード

メモ

Path でファイルパスを扱う

⌘S で保存