Programowanie średnio-zaawansowane #25: pakiet NIO (new input/output)

Oprócz używania standardowych strumieni wyjścia i wejścia, w Javie został zaimplementowany specjalny pakiet o nazwie NIO, który również odpowiada za pracę z sygnałami przychodzącymi i wychodzącymi z Twojego programu. Specyfiką tego rozwiązania jest jednak to, że nie korzysta on ze strumieni, tylko z tzw. buforów i kanałów (ang. channel)*.

Pierwszą metodę, którą Ci pokażę, będzie funkcja tworząca plik w kreślonej ścieżce na dysku przy pomocy klasy Files. Zawiera ona metody pomocnicze, przydatne do pracy na plikach.

private static void createFile(String directory, String file) throws IOException {
	Path directoryPath = Paths.get(directory);
	if(!Files.exists(directoryPath))	{
		Files.createDirectories(directoryPath);
	} else {
		Path filePath = Paths.get(directory + file);
		Files.deleteIfExists(filePath);
		Files.createFile(filePath);
		writeFile(filePath);
		
		if (Files.isReadable(filePath)) {
			List<String> allLines = Files.readAllLines(filePath);
			for (String line : allLines) {
				System.out.println(line);
			}
		}
	}
}

Dla ćwiczenia wydzieliłem w funkcji dwa argumenty, pierwszy to ścieżka do katalogu z plikiem do wczytania, natomiast drugi to nazwa tego pliku. Każdy z tych argumentów używasz wpierw do stworzenia odpowiedniego obiektu klasy Path. Następnie używam metody exist, aby sprawdzić, czy dana ścieżka już istnieje. Jeśli nie, to tworzę potrzebne katalogi. Kolejne operacje, to usunięcie pliku, jeśli istnieje oraz stworzenie nowego, pustego o nazwie wskazanej w parametrze file. Teraz mogę zapisać odpowiednie dane do niego, używając metody writeFile. W ostatniej klauzuli if, sprawdzam dodatkowo, czy plik jest dostępny do odczytu. Jeśli tak wczytuję wszystkie linie do listy za pomocą metody readAllLines.

Dodatkowo wydzieliłem metodę pomocniczą, służącą tylko do zapisu. Zawiera ona jedynie, znany Ci już, try – with – resources, z tą jednak różnicą, że tutaj używam newBufferedWriter z pakietu NIO (a nie, tak jak w poprzednim wpisie, zwykłego BufferWritera). Klasa Path, nie tylko tworzy plik w danej ścieżce, ale zawiera całe spektrum użytecznych metod pomocniczych, jak np. toFile, za pomocą którego wyciągam aktualną nazwę pliku i ścieżkę, na którym pracuję.

private static void writeFile(Path filePath) throws IOException {
	try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) {
		bufferedWriter.write(filePath.toFile().getPath() + "\n");
		bufferedWriter.write(filePath.toFile().getName() + "\n");
	}
}

Jak widzisz w przykładzie powyżej, nie musisz zawsze używać sekcji catch. Dzieje się tak dlatego, że zamiast łapać wyjątek, możesz go po prostu, wyrzucić dalej (throws IOException).

Efekt na konsoli:

c:\data\myfile.txt
myfile.txt

Kolejną metodą będzie funkcja wczytująca plik z określonej ścieżki. Zakładam tutaj optymistyczne, że plik i ścieżka zawsze istnieją.

private static void readFile(Path filePath) throws IOException {
	try (BufferedReader bufferedReader = Files.newBufferedReader(filePath, UTF_8)) {
		String readLine;
		while ((readLine = bufferedReader.readLine()) != null) {
			System.out.println(readLine);
		}
	}
}

Ostatnim tematem tej notki, będzie wspomniany wcześniej channel. Jak już wspomniałem podejście NIO różni się znacznie od biblioteki IO. Dane tutaj są wczytywane do bufora, z którego są dalej przetwarzane za pomocą kanału. Istotną zmianą w stosunku do strumieni, jest fakt, że dostęp do np. plików nie jest blokowany podczas pracy na nich. Dlatego myślę, że warto poznać alternatywny sposób dostępu do plików.

private static void readFixedSizedBuffer(Path path) throws IOException {
	try (RandomAccessFile accessFile = new RandomAccessFile(path.toFile().getAbsolutePath(), "r")) {
		FileChannel inChannel = accessFile.getChannel();
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		while (inChannel.read(buffer) > 0) {
			buffer.flip();
			for (int i = 0; i < buffer.limit(); i++) {
				System.out.print((char) buffer.get());
			}
			buffer.clear();
		}
	}
}

Pierwszą rzeczą, która rzuca się w oczy, jest klas RandomAccessFile, która w konstruktorze używa drugiego parametru. Jest to tryb pracy z plikiem. Dostępne są cztery opcje:

  • r ” – tylko odczyt. Użycie metody służącej do zapisu, wyrzuci wyjątek IOException.
  • rw” – read/write. Odczyt lub (i) zapis.
  • rwd” – zapis i odczyt synchroniczny
  • rws” – to samo co wyżej, ale zapisuje też meta dane.

Nie przejmuj się, trybami trzecim i czwartym. W tym przykładzie będę używał tylko trybu „read”. Po otwarciu pliku korzystając ze strumienia, przesyłam go do kanału getChannel(). Utworzony przed chwilą FileChannel jest rodzajem kanału blokującego, dzięki czemu wiele wątków (programów) może korzystać z tego samego zasobu bez niebezpieczeństwa, że praca jednego będzie miała negatywny skutek dla innego. Następnym krokiem jest utworzenie obiektu klasy ByteBuffer i przypisanie do niego odpowiedniej wielkości pamięci (w tym przypadku 1024 bajtów). W końcu dane zapisane w pliku zostają wczytane do bufora za pomocą funkcji read. Wczytywanie będzie kontynuowane aż ostatni z bajtów nie zostanie przeprocesowany. We wnętrzu pętli while, metodą flip przestawiam kursor na początkową pozycję (podobnie jak w starych maszynach do pisania), oraz wyświetlam dane, pobierając je wpierw z bufora za pomocą operacji get. Na koniec każdej iteracji czyszczę bufor, po to aby nieużywane dane nie zalegały niepotrzebnie w pamięci.

Brawo! Znasz już podstawowe operacje jakie możesz wykonać za pomocą pakietu NIO. Na koniec przedstawiam Ci moją metodę main.

public static void main(String[] args) throws IOException {
	String directory = "c:\\data\\";
	String file = "myfile.txt";
	createFile(directory, file);
	Path path = Paths.get(directory + file);
	readFile(path);
	readFixedSizedBuffer(path);
}

*Więcej o kanałach i buforach: https://www.javatpoint.com/java-nio-vs-input-output

Dodaj komentarz