Защо Java IO е толкова лош?

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

Структура на библиотеката java.io

Но Java IO има своите недостатъци, така че нека поговорим за всеки от тях на свой ред:

  1. Блокиране на достъпа за вход/изход. Проблемът е, че когато програмист се опита да прочете or запише нещо във файл с помощта на Java IO , той заключва file и блокира достъпа до него, докато работата не бъде свършена.
  2. Няма поддръжка за виртуални файлови системи.
  3. Няма поддръжка за връзки.
  4. Много, много проверени изключения.

Работата с файлове винаги включва работа с изключения: например опитът за създаване на нов файл, който вече съществува, ще предизвика 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. Първият е малък текстов файл, а вторият е голям видеоклип.

Ще създадем файл и ще добавим няколко думи и знаци:

% докосване на text.txt

Нашият файл заема общо 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секунди.

Време за изпълнение в мorсекунди: 1

Времето за изпълнение на Java NIO е много по-дълго. Средното време е 11 мorсекунди. Резултатите варират от 9 до 16. Това е така, защото Java IO работи по различен начин от нашата операционна система. IO премества и обработва файлове един по един, но операционната система изпраща данните в едно голямо парче. NIO се представи зле, защото е ориентиран към буфер, а не към поток като IO .

Време за изпълнение в мorсекунди: 12

И нека изпълним нашия тест за Java NIO.2 . NIO.2 има подобрено управление на файлове в сравнение с Java NIO . Ето защо актуализираната библиотека дава толкова различни резултати:

Време за изпълнение в мorсекунди: 3

Сега нека се опитаме да тестваме нашия голям файл, 521 MB видео. Задачата ще бъде абсолютно същата: копирайте file в друга папка. Виж!

Резултати за Java IO :

Време за изпълнение в мorсекунди: 1866

И ето резултата за Java NIO :

Време за изпълнение в мorсекунди: 205

Java NIO се справи с file 9 пъти по-бързо в първия тест. Повторните тестове показаха приблизително същите резултати.

И ние също ще опитаме нашия тест на Java NIO.2 :

Време за изпълнение в мorсекунди: 360

Защо този резултат? Просто защото няма много смисъл да сравняваме ефективността между тях, тъй като служат за различни цели. 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. Резултатите от тестовете може да се различават в зависимост от спецификациите на операционната система и работната станция.