Path でファイルパスを扱う
このレッスンで分かること
PathはStringではなく OS 中立 なファイルパス表現 (Java 7NIO.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.txt、macOS や Linux なら /home/taro/notes.txt のように、OS によって書き方も区切り文字も違います。これを String の連結だけで頑張ろうとすると、バックスラッシュ の数を間違えたり、末尾に / を付け忘れたりして、すぐにバグの温床になります。
Java 7 以降では、こうしたパス操作専用の便利な型として java.nio.file.Path インターフェースが用意されました。Path は単なる文字列ではなく「ファイルパスを表すオブジェクト」で、getFileName getParent resolve といったメソッドを通してパスの各部品を安全に取り出したり組み合わせたりできます。古い java.io.File クラスを置き換える、現代の Java の標準です。
Pathはjava.nio.fileパッケージにあります。古いjava.io.Fileと名前は似ていますが別物です。新しいコードでは原則Pathを使い、Fileは古い API との互換のためだけに使う、と覚えておきましょう。
本章では、この Path の基本中の基本、つまり「文字列から Path を作って分解する」ところからスタートします。今回の課題で使う Paths.get(...) と getFileName() だけでも、ログファイル名の抽出、アップロードされたファイルの拡張子チェック、コマンドライン引数の検証など、業務で頻出するシーンに対応できます。
Paths.get で Path を生成する
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 がファイル名として返ってくることです。今回の課題でもこの挙動を使います。
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.logString で dir + "/" + name のように書くと、dir の末尾に / が付いていたかどうかで // になったり、Windows で / と \ が混ざったりします。resolve はこの面倒事をすべて吸収してくれます。さらに、引数に絶対パス (/etc/hosts など) を渡したときは「絶対パスをそのまま返す」という賢い動きもします。
もう 1 つ、組み立て後のパスを綺麗にしたいときは normalize() が便利です。/a/b/../c のような .. を含むパスを /a/c に整理してくれます。resolve と normalize をセットで使うのが定番の組み合わせです。
Files.exists(path)Files.readString(path)のように、ファイルシステムへの実操作はjava.nio.file.Filesクラス側にまとまっています。Path自体はあくまで「パスの値」を表すオブジェクトで、ディスクは触りません。PathとFilesの役割分担を意識すると、API 全体がスッと頭に入ります。
Path と File、相対と絶対
古くからの 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.File | java.nio.file.Path |
|---|---|---|
| 登場時期 | Java 1.0 | Java 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 も同じパスとして解釈されます。今回の課題ではこの挙動をそのまま使うことになります。
エラーメッセージで
NoSuchFileExceptionInvalidPathExceptionなどが出たら、まずは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 を完成させることです。手順は次のとおりです。
- ファイル先頭で
import java.nio.file.Paths;を書く (Path自体は使わないなら不要) Paths.get(pathStr)でPathを作る.getFileName()で末尾のPathを取り出す.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 つができることを確認しましょう。
- Path の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Path とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.nio.file.Paths;を書くこと Paths.get(pathStr)を使ってPathを生成すること (自前でString操作しない)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"