1. Obsługa błędów: nie ignoruj wyjątków!
Praca z plikami — to zawsze interakcja ze światem zewnętrznym, który może być bardzo nieprzewidywalny. Dyski mogą się zapełniać, pliki — znikać, uprawnienia — zmieniać, a użytkownicy — robić rzeczy nie do pomyślenia (na przykład uruchamiać twój program z folderu z odstępami w nazwie albo z pendrive’a, który za chwilę zostanie odłączony). Jeśli twój kod nie jest na to gotowy, ryzykuje nie tylko „się wysypać”, ale też zostawić użytkownika bez potrzebnych danych.
Dobre praktyki — to nie tylko „modne porady”, lecz zestaw sprawdzonych w czasie technik, które pomagają uniknąć najgorszych scenariuszy: utraty danych, wycieków pamięci, ujawnienia poufnych informacji i po prostu głupich bugów, przez które potem wstyd spojrzeć w oczy kolegom (a zwłaszcza — użytkownikom).
Dlaczego nie wolno pisać pustego catch?
W Javie (i nie tylko) bardzo kusi, by napisać coś w tym stylu:
try {
// praca z plikiem
} catch (IOException e) {
// cóż, nie wyszło — trudno!
}
To — najgorsze, co można zrobić. Taki kod nie tylko „połyka” błąd: sprawia, że staje się on niewidoczny dla użytkownika i dla was samych. W rezultacie, jeśli coś pójdzie nie tak, nigdy się nie dowiecie, co dokładnie i kiedy.
Jak robić to dobrze?
- Loguj błędy: przynajmniej wypisuj komunikat na konsolę lub zapisuj do pliku dziennika.
- Informuj użytkownika: jeśli błąd jest krytyczny, pokaż przyjazny komunikat.
- Nie ujawniaj zbyt wielu szczegółów: nie pokazuj użytkownikowi wewnętrznych detali systemu (na przykład pełny stack trace — to raczej dla dewelopera).
Przykład:
try {
List<String> lines = Files.readAllLines(Path.of("data.txt"));
// przetwarzanie danych
} catch (IOException e) {
System.err.println("Błąd podczas odczytu pliku: " + e.getMessage());
// Można zapisać szczegóły do pliku logów
e.printStackTrace(System.err);
}
Dlaczego warto łapać konkretne wyjątki?
Bo różne błędy wymagają różnej reakcji. Na przykład, jeśli plik nie został znaleziony — można zaproponować użytkownikowi wybór innego pliku (NoSuchFileException lub FileNotFoundException). Jeśli brakuje uprawnień — poprosić o uruchomienie programu z odpowiednimi prawami (AccessDeniedException). Jeśli dysk jest pełny — zasugerować zwolnienie miejsca (IOException przy zapisie).
2. Uprawnienia i bezpieczeństwo
Sprawdzaj uprawnienia przed operacjami
Zanim zaczniesz czytać lub pisać plik, warto upewnić się, że masz do tego uprawnienia. W Javie są metody:
- File.canRead()
- File.canWrite()
Nawet jeśli te metody zwracają true, nie gwarantuje to powodzenia — uprawnienia mogą zmienić się w dowolnej chwili (na przykład inny proces je zmieni). Dlatego zawsze bądź gotów na wyjątki.
Przykład:
File file = new File("config.properties");
if (!file.canRead()) {
System.err.println("Brak uprawnień do odczytu pliku!");
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
// odczyt pliku
} catch (IOException e) {
System.err.println("Błąd podczas odczytu: " + e.getMessage());
}
Nie ujawniaj wewnętrznych szczegółów
Jeśli twój program pracuje z plikami poufnymi (na przykład z hasłami), nie wypisuj ścieżek do tych plików ani ich zawartości w błędach widocznych dla użytkownika.
3. Nie używaj ścieżek względnych do operacji krytycznych
Ścieżka względna (new File("data.txt")) — to ścieżka względem bieżącego katalogu roboczego, który może być różny w zależności od sposobu uruchomienia programu (na przykład z IDE lub z linii poleceń). To może prowadzić do zamieszania i błędów.
Dobra praktyka: dla ważnych plików używaj ścieżek bezwzględnych albo jawnie określ katalog roboczy.
Przykład:
String userHome = System.getProperty("user.home");
Path configPath = Path.of(userHome, "myapp", "config.properties");
4. Praca z plikami i katalogami tymczasowymi
Po co są pliki tymczasowe?
Pliki tymczasowe są potrzebne do różnych zadań. Czasem używa się ich do operacji pośrednich: na przykład najpierw dane zapisuje się do pliku tymczasowego, a potem ten plik zastępuje plik główny. Inny wariant — pliki tymczasowe pomagają przechowywać informacje, która nie jest potrzebna po zakończeniu programu i może zostać bezpiecznie usunięta.
Jak bezpiecznie tworzyć pliki tymczasowe?
Używaj metod z java.nio.file.Files:
Path tempFile = Files.createTempFile("myapp_", ".tmp");
// ... praca z plikiem
Files.deleteIfExists(tempFile);
Katalogi tymczasowe
Path tempDir = Files.createTempDirectory("myapp_");
5. Niezawodność: kopie zapasowe i kontrola integralności
Używaj kopii zapasowych przy modyfikacji ważnych plików
Zanim nadpiszesz ważny plik (na przykład ustawienia), zrób jego kopię:
Path config = Path.of("config.properties");
Path backup = Path.of("config.properties.bak");
if (Files.exists(config)) {
Files.copy(config, backup, StandardCopyOption.REPLACE_EXISTING);
}
Jeśli podczas zapisu coś pójdzie nie tak — zawsze można odtworzyć dane z kopii zapasowej.
Sprawdzaj integralność danych
Dla szczególnie ważnych danych można używać sum kontrolnych (na przykład MD5 lub SHA-256). Po zapisie pliku — oblicz checksum i zapisz ją obok. Przy odczycie — sprawdź, czy plik nie uległ zmianie.
Przykład obliczenia SHA-256 (dla miłośników kryptografii):
import java.security.MessageDigest;
import java.nio.file.Files;
import java.nio.file.Path;
byte[] data = Files.readAllBytes(Path.of("important.dat"));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
// Zapisujemy hash w osobnym pliku albo porównujemy przy odczycie
6. Minimalizuj okno między sprawdzeniem a użyciem pliku
To — klasyczny problem TOCTOU (Time Of Check To Time Of Use): między momentem, gdy sprawdzisz, że plik istnieje, a chwilą, gdy zaczniesz go czytać, plik może zniknąć lub się zmienić.
Dlatego zawsze staraj się wykonywać sprawdzenie i użycie w jednym bloku try. I koniecznie obsługuj wyjątki, nawet jeśli przed chwilą sprawdziłeś plik.
Przykład:
Path filePath = Path.of("data.txt");
if (Files.exists(filePath)) {
try (BufferedReader reader = Files.newBufferedReader(filePath)) {
// odczyt pliku
} catch (IOException e) {
System.err.println("Błąd podczas odczytu pliku (możliwe, że plik zniknął): " + e.getMessage());
}
}
7. Jeszcze kilka przydatnych wskazówek
Używaj try-with-resources dla wszystkich zasobów
Wszystkie klasy implementujące interfejs AutoCloseable (a to prawie wszystkie strumienie Java IO/NIO) można używać w try-with-resources. Chroni to przed wyciekami zasobów.
try (BufferedReader reader = Files.newBufferedReader(Path.of("data.txt"))) {
// odczyt
}
Pamiętaj o usuwaniu plików tymczasowych
Files.deleteIfExists(tempFile);
Nie zamykaj zasobu podwójnie
Jeśli używasz try-with-resources, nie wywołuj close() ręcznie — może to prowadzić do błędów i podwójnych prób zamknięcia.
8. Typowe błędy przy pracy z plikami
Błąd nr 1: ignorowanie wyjątków.
Pisać pusty catch — to jak łapać muchy rękami i wypuszczać je z powrotem. Zawsze loguj albo przynajmniej informuj użytkownika, co poszło nie tak.
Błąd nr 2: niezamykanie strumieni.
Jeśli zapomnisz zamknąć strumień, plik może pozostać zablokowany, a system — bez wolnych deskryptorów. Używaj try-with-resources.
Błąd nr 3: używanie ścieżek względnych dla ważnych plików.
Nie zakładaj, że katalog roboczy zawsze jest tym, którego oczekujesz. Lepiej jawnie wskazywać ścieżkę lub używać specjalnych katalogów (user.home, java.io.tmpdir).
Błąd nr 4: nadpisywanie ważnych plików bez kopii zapasowej.
Zanim nadpiszesz coś ważnego, zrób kopię zapasową. To uratuje twoje nerwy i dane użytkownika.
Błąd nr 5: brak sprawdzania uprawnień dostępu.
Sprawdzaj, czy użytkownik ma uprawnienia do odczytu/zapisu potrzebnych plików lub katalogów — w przeciwnym razie otrzymasz nieoczekiwane AccessDeniedException.
Błąd nr 6: okno TOCTOU.
Między sprawdzeniem a użyciem pliku ktoś inny może go zmienić lub usunąć. Zawsze obsługuj wyjątki, nawet po wykonaniu sprawdzenia.
Błąd nr 7: zostawianie plików tymczasowych i śmieci.
Po awaryjnych zakończeniach programu lub błędach pliki tymczasowe mogą pozostać. Nie zapominaj ich usuwać, zwłaszcza jeśli to dane wrażliwe.
GO TO FULL VERSION