Защо Java IO е толкова лош?
IO (Input & Output) API е Java API, който улеснява разработчиците да работят с потоци. Да речем, че получаваме няHowви данни (например име, бащино име, фамorя) и трябва да ги запишем във файл — дойде времето да използваме java.io.

Структура на библиотеката java.io
Но Java IO има своите недостатъци, така че нека поговорим за всеки от тях на свой ред:
- Блокиране на достъпа за вход/изход. Проблемът е, че когато програмист се опита да прочете or запише нещо във файл с помощта на Java IO , той заключва file и блокира достъпа до него, докато работата не бъде свършена.
- Няма поддръжка за виртуални файлови системи.
- Няма поддръжка за връзки.
- Много, много проверени изключения.
Работата с файлове винаги включва работа с изключения: например опитът за създаване на нов файл, който вече съществува, ще предизвика IOException . В този случай приложението трябва да продължи да работи и потребителят трябва да бъде уведомен защо файлът не може да бъде създаден.
try {
File.createTempFile("prefix", "");
} catch (IOException e) {
// Handle the IOException
}
/**
* Creates an empty file in the default temporary-file directory
* any exceptions will be ignored. This is typically used in finally blocks.
* @param prefix
* @param suffix
* @throws IOException - If a file could not be created
*/
public static File createTempFile(String prefix, String suffix)
throws IOException {
...
}
Тук виждаме, че методът createTempFile хвърля IOException , когато файлът не може да бъде създаден. Това изключение трябва да се третира по подходящ начин. Ако се опитаме да извикаме този метод извън блок try-catch , компилаторът ще генерира грешка и ще предложи две опции за коригирането й: обвийте метода в блок try-catch or накарайте метода, който извиква File.createTempFile, да хвърля IOException ( така че може да се обработва на по-високо ниво).
Пристигане в Java NIO и How се сравнява с Java IO
Java NIO or Java Non-Blocking I/O (or понякога Java New I/O) е предназначен за високопроизводителни I/O операции.
Нека сравним Java IO методите и тези, които ги заместват.
Първо, нека поговорим за работата с Java IO :
Клас InputStream
try(FileInputStream fin = new FileInputStream("C:/codegym/file.txt")){
System.out.printf("File size: %d bytes \n", fin.available());
int i=-1;
while((i=fin.read())!=-1) {
System.out.print((char)i);
}
} catch(IOException ex) {
System.out.println(ex.getMessage());
}
Класът FileInputStream е за четене на данни от файл. Той наследява класа InputStream и следователно имплементира всички негови методи. Ако файлът не може да бъде отворен, се хвърля FileNotFoundException .
Клас OutputStream
String text = "Hello world!"; // String to write
try(FileOutputStream fos = new FileOutputStream("C:/codegym/file.txt")){
// Convert our string into bytes
byte[] buffer = text.getBytes();
fos.write(buffer, 0, buffer.length);
System.out.println("The file has been written");
} catch(IOException ex) {
System.out.println(ex.getMessage());
}
Класът FileOutputStream за запис на byteове във файл. Произлиза от класа OutputStream .
Часове по четец и писател
Класът FileReader ни позволява да четем данни от символи от потоци, а класът FileWriter се използва за запис на потоци от символи. Следният code показва How да пишете и четете от файл:
String fileName = "c:/codegym/Example.txt";
// Create a FileWriter object
try (FileWriter writer = new FileWriter(fileName)) {
// Write content to file
writer.write("This is a simple example\nin which we\nwrite to a file\nand read from a file\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
// Create a FileReader object
try (FileReader fr = new FileReader(fileName)) {
char[] a = new char[200]; // Number of characters to read
fr.read(a); // Read content into an array
for (char c : a) {
System.out.print(c); // Display characters one by one
}
} catch (IOException e) {
e.printStackTrace();
}
Сега нека поговорим за Java NIO :
Канал
За разлика от потоците, използвани в Java IO , Channel е двупосочен интерфейс, тоест може Howто да чете, така и да пише. Java NIO каналът поддържа асинхронен поток от данни Howто в блокиращ, така и в неблокиращ режим.
RandomAccessFile aFile = new RandomAccessFile("C:/codegym/file.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(100);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read: " + bytesRead);
buf.flip();
while(buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Тук използвахме FileChannel . Използваме файлов канал за четене на данни от файл. Обект на файлов канал може да бъде създаден само чрез извикване на метода getChannel() на файлов обект — няма начин директно да се създаде обект на файлов канал.
В допълнение към FileChannel имаме други реализации на канали:
-
FileChannel — за работа с файлове
-
DatagramChannel — канал за работа по UDP връзка
-
SocketChannel — канал за работа през TCP връзка
-
ServerSocketChannel съдържа SocketChannel и е подобен на това How работи уеб сървър

Моля, обърнете внимание: FileChannel не може да бъде превключен на неблокиращ режим. Неблокиращият режим на Java NIO ви позволява да поискате данни за четене от канал и да получите само това, което е налично в момента (or нищо, ако все още няма налични данни) . Въпреки това SelectableChannel и неговите реализации могат да бъдат поставени в неблокиращ режим с помощта на метода connect() .
Селектор
Java NIO въведе възможността за създаване на нишка, която знае кой канал е готов да пише и чете данни и може да обработва този конкретен канал. Тази възможност се реализира с помощта на класа Selector .

Свързване на канали към селектор
Selector selector = Selector.open();
channel.configureBlocking(false); // Non-blocking mode
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Затова създаваме нашия селектор и го свързваме към SelectableChannel .
За да се използва със селектор, каналът трябва да е в неблокиращ режим. Това означава, че не можете да използвате FileChannel със селектор, тъй като FileChannel не може да бъде поставен в неблокиращ режим. Но каналите на гнездата ще работят добре.
Тук нека споменем, че в нашия пример SelectionKey е набор от операции, които могат да бъдат извършени на канал. Клавишът за избор ни позволява да разберем състоянието на даден канал.
Видове SelectionKey
-
SelectionKey.OP_CONNECT означава канал, който е готов да се свърже със сървъра.
-
SelectionKey.OP_ACCEPT е канал, който е готов да приема входящи връзки.
-
SelectionKey.OP_READ означава канал, който е готов да чете данни.
-
SelectionKey.OP_WRITE означава канал, който е готов за запис на данни.
Буфер
Данните се четат в буфер за по-нататъшна обработка. Разработчикът може да се движи напред и назад в буфера, което ни дава малко повече гъвкавост при обработката на данни. В същото време трябва да проверим дали буферът съдържа необходимото количество данни за правилна обработка. Освен това, когато четете данни в буфер, уверете се, че не унищожавате съществуващите данни, които все още не са обработени.
ByteBuffer buf = ByteBuffer.allocate (2048);
int bytesRead = channel.read(buf);
buf.flip(); // Change to read mode
while (buf.hasRemaining()) {
byte data = buf.get(); // There are methods for primitives
}
buf.clear(); // Clear the buffer - now it can be reused
Основни свойства на буфера:
|
|
---|---|
капацитет | Размерът на буфера, който е дължината на масива. |
позиция | Началната позиция за работа с данни. |
лимит | Работният лимит. За операциите за четене ограничението е количеството данни, които могат да бъдат прочетени, но за операциите за запис това е капацитетът or квотата, налична за запис. |
марка | Индексът на стойността, на която параметърът за позиция ще бъде нулиран, когато се извика методът reset() . |
Сега нека поговорим малко за новостите в Java NIO.2 .
Пътека
Пътят представлява път във файловата система. Той съдържа името на файл и списък с директории, които определят пътя до него.
Path relative = Paths.get("Main.java");
System.out.println("File: " + relative);
// Get the file system
System.out.println(relative.getFileSystem());
Paths е много прост клас с един статичен метод: get() . Създаден е единствено за получаване на обект Path от предадения низ or URI.
Path path = Paths.get("c:\\data\\file.txt");
файлове
Files е помощен клас, който ни позволява директно да получаваме размера на файл, да копираме файлове и др.
Path path = Paths.get("files/file.txt");
boolean pathExists = Files.exists(path);
Файлова система
FileSystem предоставя интерфейс към файловата система. FileSystem работи като фабрика за създаване на различни обекти (Пътека,PathMatcher,файлове). Помага ни за достъп до файлове и други обекти във файловата система.
try {
FileSystem filesystem = FileSystems.getDefault();
for (Path rootdir : filesystem.getRootDirectories()) {
System.out.println(rootdir.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
Тест за представяне
За този тест нека вземем два file. Първият е малък текстов файл, а вторият е голям видеоклип.
Ще създадем файл и ще добавим няколко думи и знаци:

Нашият файл заема общо 42 byteа в паметта:

Сега нека напишем code, който ще копира нашия файл от една папка в друга. Нека го тестваме върху малки и големи файлове, за да сравним скоростта на IO и NIO и NIO.2 .
Код за копиране, написан с помощта на Java IO :
public static void main(String[] args) {
long currentMills = System.currentTimeMillis();
long startMills = currentMills;
File src = new File("/Users/IdeaProjects/testFolder/text.txt");
File dst = new File("/Users/IdeaProjects/testFolder/text1.txt");
copyFileByIO(src, dst);
currentMills = System.currentTimeMillis();
System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}
public static void copyFileByIO(File src, File dst) {
try(InputStream inputStream = new FileInputStream(src);
OutputStream outputStream = new FileOutputStream(dst)){
byte[] buffer = new byte[1024];
int length;
// Read data into a byte array and then output to an OutputStream
while((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
И ето codeа за Java NIO :
public static void main(String[] args) {
long currentMills = System.currentTimeMillis();
long startMills = currentMills;
File src = new File("/Users/IdeaProjects/testFolder/text.txt");
File dst = new File("/Users/IdeaProjects/testFolder/text2.txt");
// Code for copying using NIO
copyFileByChannel(src, dst);
currentMills = System.currentTimeMillis();
System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}
public static void copyFileByChannel(File src, File dst) {
// 1. Get a FileChannel for the source file and the target file
try(FileChannel srcFileChannel = new FileInputStream(src).getChannel();
FileChannel dstFileChannel = new FileOutputStream(dst).getChannel()){
// 2. Size of the current FileChannel
long count = srcFileChannel.size();
while(count > 0) {
/**=============================================================
* 3. Write bytes from the source file's FileChannel to the target FileChannel
* 1. srcFileChannel.position(): the starting position in the source file, cannot be negative
* 2. count: the maximum number of bytes transferred, cannot be negative
* 3. dstFileChannel: the target file
*==============================================================*/
long transferred = srcFileChannel.transferTo(srcFileChannel.position(),
count, dstFileChannel);
// 4. After the transfer is complete, change the position of the original file to the new one
srcFileChannel.position(srcFileChannel.position() + transferred);
// 5. Calculate how many bytes are left to transfer
count -= transferred;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
Код за Java NIO.2 :
public static void main(String[] args) {
long currentMills = System.currentTimeMillis();
long startMills = currentMills;
Path sourceDirectory = Paths.get("/Users/IdeaProjects/testFolder/test.txt");
Path targetDirectory = Paths.get("/Users/IdeaProjects/testFolder/test3.txt");
Files.copy(sourceDirectory, targetDirectory);
currentMills = System.currentTimeMillis();
System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}
Да започнем с малкия файл.
Времето за изпълнение на Java IO е средно 1 мorсекунда. Пускайки теста няколко пъти, получаваме резултати от 0 до 2 мorсекунди.
Времето за изпълнение на Java NIO е много по-дълго. Средното време е 11 мorсекунди. Резултатите варират от 9 до 16. Това е така, защото Java IO работи по различен начин от нашата операционна система. IO премества и обработва файлове един по един, но операционната система изпраща данните в едно голямо парче. NIO се представи зле, защото е ориентиран към буфер, а не към поток като IO .
И нека изпълним нашия тест за Java NIO.2 . NIO.2 има подобрено управление на файлове в сравнение с Java NIO . Ето защо актуализираната библиотека дава толкова различни резултати:
Сега нека се опитаме да тестваме нашия голям файл, 521 MB видео. Задачата ще бъде абсолютно същата: копирайте file в друга папка. Виж!
Резултати за Java IO :
И ето резултата за Java NIO :
Java NIO се справи с file 9 пъти по-бързо в първия тест. Повторните тестове показаха приблизително същите резултати.
И ние също ще опитаме нашия тест на Java NIO.2 :
Защо този резултат? Просто защото няма много смисъл да сравняваме ефективността между тях, тъй като служат за различни цели. NIO е по-абстрактен I/O на ниско ниво, докато NIO.2 е ориентиран към управление на файлове.
Резюме
Можем спокойно да кажем, че Java NIO е значително по-ефективен при работа с файлове благодарение на използването вътре в блокове. Друг плюс е, че NIO библиотеката е разделена на две части: една за работа с файлове, друга за работа с мрежата.
Новият API на Java NIO.2 за работа с файлове предлага много полезни функции:
-
много по-полезно addressиране на файловата система с помощта на Path ,
-
значително подобрена обработка на ZIP файлове с помощта на потребителски доставчик на файлова система,
-
достъп до специални файлови атрибути,
-
много удобни методи, като четене на цял файл с един оператор, копиране на файл с един оператор и т.н.
Всичко е свързано с файлове и файлови системи и всичко е на доста високо ниво.
Реалността днес е, че Java NIO представлява приблизително 80-90% от работата с файлове, въпреки че делът на Java IO все още е значителен.
💡 PS Тези тестове бяха проведени на MacBook Pro 14" 16/512. Резултатите от тестовете може да се различават в зависимост от спецификациите на операционната система и работната станция.
GO TO FULL VERSION