1. Jak Java nauczyła się pracować z plikami
Java i pliki: od czego się zaczęło
Dawno temu, w odległym 1996 roku, twórcy języka zaproponowali pierwszy sposób pracy z plikami — pakiet java.io. Pojawiły się w nim takie klasy jak File, FileInputStream, FileOutputStream, Reader, Writer i inne. Klasy te pozwalały wykonywać różne operacje na plikach: sprawdzać, czy plik istnieje, jego rozmiar, datę modyfikacji oraz pracować z katalogami.
Jednak, jak to często bywa z pierwszymi wersjami, nie było idealnie — wiele rzeczy można było zrobić wygodniej i bezpieczniej.
Ograniczenia starego API (java.io)
- File — to nie plik, a „skrót/uchwyt”. Klasa File nie potrafi czytać ani zapisywać zawartości. Opisuje ścieżkę i metadane. Do odczytu/zapisu potrzebne są osobne strumienie: FileInputStream/FileOutputStream lub Reader/Writer.
- Praca ze ścieżkami — udręka. Ręczne sklejanie ścieżek w stylu "C:\\Users\\" + user + "\\Documents" często psuło się przy przenoszeniu między Windows i Linux.
- Brak obsługi dowiązań symbolicznych. Stare API nie rozumiało symlinków i mogło pracować z nimi niepoprawnie.
- Słabe wsparcie atrybutów. Trudno było uzyskać uprawnienia, właściciela, flagi „ukryty” itp.
- Niewydajne dla dużych plików. Klasyczne strumienie nie oferowały wygodnych i szybkich scenariuszy dla dużych ilości danych, brakowało asynchronicznego wejścia/wyjścia.
Pojawienie się java.nio.file (NIO.2, Java 7)
W Javie 7 pojawił się nowy pakiet: java.nio.file (często — NIO.2). Wniósł:
- Path — nowoczesną abstrakcję ścieżki (w tym ścieżki wewnątrz ZIP, chmurowych i sieciowych systemów plików).
- Files — statyczne narzędzia do odczytu, zapisu, kopiowania, usuwania.
- FileSystem — pracę nie tylko z dyskiem lokalnym, lecz także z innymi systemami plików.
- Obsługę dowiązań symbolicznych, rozszerzonych atrybutów, uprawnień.
- Asynchroniczne I/O i lepszą wydajność dla dużych danych.
- Wygodną pracę z katalogami i API strumieniowymi.
NIO to skrót od „New Input/Output” i choć ma już ponad dziesięć lat, dla Javy to wciąż współczesny standard.
2. Porównanie podejść: java.io vs java.nio.file
Klasa File (java.io)
- Reprezentuje ścieżkę do pliku lub katalogu i ich metadane.
- Nie potrafi czytać/zapisywać zawartości pliku.
- Umożliwia sprawdzenie istnienia, rozmiaru, typu (plik/katalog), ścieżki bezwzględnej itp.
import java.io.File;
public class ExampleIO {
public static void main(String[] args) {
File file = new File("example.txt");
if (file.exists()) {
System.out.println("Plik istnieje!");
System.out.println("Rozmiar: " + file.length() + " bajtów");
}
}
}
Klasa Path (java.nio.file)
- Nowoczesna abstrakcja ścieżki, potrafi naturalnie się łączyć i normalizować.
- Może reprezentować ścieżki lokalne, archiwalne i sieciowe.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ExampleNIOPath {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
if (Files.exists(path)) {
System.out.println("Plik znaleziony!");
System.out.println("Rozmiar: " + Files.size(path) + " bajtów");
}
}
}
Klasa Files (java.nio.file)
- Zestaw statycznych metod do odczytu/zapisu w całości i częściami.
- Kopiowanie, usuwanie, przenoszenie plików.
- Tworzenie/usuwanie katalogów.
- Dostęp do rozszerzonych atrybutów.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class ExampleNIOFiles {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
}
}
Tabela porównawcza
| Funkcja | |
|
|---|---|---|
| Reprezentacja ścieżki | Tak | Tak |
| Odczyt/zapis zawartości | Nie | Tak (przez Files) |
| Praca ze ścieżkami | Niewygodnie | Wygodnie (resolve, normalize) |
| Dowiązania symboliczne | Nie | Tak |
| Atrybuty plików | Ograniczone | Rozszerzone |
| Asynchroniczny IO | Nie | Tak (NIO.2) |
| Wieloplatformowość | Częściowa | Doskonała |
3. Kiedy czego używać?
Stare API (java.io): kiedy jest potrzebne?
- Do wsparcia kodu legacy. Jeśli projekt rozpoczął się przed Javą 7 i wszędzie używa File, FileInputStream, FileOutputStream, trzeba zachować kompatybilność.
- W nowych projektach — niezalecane. Korzystanie ze „staruszka” File dziś to jak oglądanie wideo z kaset VHS.
Nowe API (java.nio.file): współczesny standard
- Zawsze preferuj Path i Files w nowych projektach.
- Prostsze, bezpieczniejsze, potężniejsze i lepiej integruje się ze strumieniami, lambdami i try-with-resources.
Krótka ściągawka
- Odczyt, zapis, kopiowanie, usuwanie? — przez Files.
- Praca ze ścieżkami (łączenie, normalizacja)? — przez Path.
- Rozmiar, data, uprawnienia? — Files.getAttribute() i metody pokrewne.
- Przechodzenie po katalogach, rekurencyjnie? — Files.walk, Files.list.
4. Przykłady: jak wyglądałby kod w starym i nowym API
Przykład 1: Sprawdzenie istnienia pliku
Stary sposób:
import java.io.File;
File file = new File("data.txt");
if (file.exists()) {
System.out.println("Plik znaleziony!");
}
Nowy sposób:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("data.txt");
if (Files.exists(path)) {
System.out.println("Plik znaleziony!");
}
Przykład 2: Odczyt wszystkich linii pliku
Stary sposób:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
File file = new File("data.txt");
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
Nowy sposób:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
Path path = Paths.get("data.txt");
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
Komentarz: Nowy sposób jest krótszy i bezpieczniejszy; z try-with-resources zarządzanie zasobami jest jeszcze prostsze.
Przykład 3: Zapis ciągu do pliku
Stary sposób:
import java.io.FileWriter;
FileWriter writer = new FileWriter("output.txt");
writer.write("Cześć, pliku!");
writer.close();
Nowy sposób:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("output.txt");
Files.write(path, "Cześć, pliku!".getBytes());
5. Przydatne niuanse
Dlaczego Java postanowiła wszystko przebudować?
- Bezpieczeństwo i niezawodność. Mniej błędów z zamykaniem zasobów i ścieżkami.
- Wieloplatformowość. Te same klasy dla Windows, Linux, a nawet archiwów ZIP.
- Łatwość rozszerzania. Prościej dodawać wsparcie chmurowych i sieciowych systemów plików.
- Wydajność. Lepsze zachowanie na dużych plikach i operacje asynchroniczne.
- Zgodność ze współczesnym stosem Javy. Lambdy, strumienie, try-with-resources.
Jak przejść ze starego API na nowe?
Prawie zawsze można przekształcić File w Path i odwrotnie:
File file = new File("data.txt");
Path path = file.toPath();
File file2 = path.toFile();
Co zrobić, gdy trafisz na stary kod?
- Bez paniki: wszystko da się zrobić prościej i nowocześniej.
- Jeśli jest możliwość — przepisuj na java.nio.file.
- Jeśli nie — używaj mostów (toPath(), toFile()) i migruj stopniowo.
6. Końcowy przykład: miniaplikacja
Przykład: Sprawdzamy, czy plik istnieje, i wypisujemy jego rozmiar.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileInfo {
public static void main(String[] args) {
Path path = Paths.get("test.txt");
if (Files.exists(path)) {
try {
long size = Files.size(path);
System.out.println("Plik znaleziony. Rozmiar: " + size + " bajtów");
} catch (Exception e) {
System.out.println("Błąd podczas pobierania rozmiaru pliku: " + e.getMessage());
}
} else {
System.out.println("Nie znaleziono pliku!");
}
}
}
Czego tutaj użyliśmy:
- Path — reprezentacja ścieżki.
- Files.exists() — sprawdzenie istnienia.
- Files.size() — pobranie rozmiaru.
7. Typowe błędy podczas pracy z API plików
Błąd nr 1: Oczekiwanie, że File potrafi czytać lub zapisywać zawartość. W rzeczywistości File to tylko „skrót/uchwyt”. Do odczytu/zapisu używaj FileInputStream/FileOutputStream (stare API) lub narzędzi Files (nowe API).
Błąd nr 2: Ręczne sklejanie ścieżek przez + lub ukośnik. Prowadzi to do błędów na różnych systemach operacyjnych. Używaj Path.resolve() albo Paths.get(...) z wieloma argumentami.
Błąd nr 3: Zapomnienie o zamknięciu strumienia. W starym API to częsta przyczyna wycieków zasobów. W nowym API wiele operacji przez Files nie tworzy strumieni, a tam, gdzie są potrzebne — stosuj try-with-resources.
Błąd nr 4: Używanie starego API w nowych projektach. Jeśli startujesz nowy projekt — wybieraj java.nio.file. Do java.io warto sięgać jedynie dla kompatybilności z kodem legacy.
GO TO FULL VERSION