Miért olyan rossz a Java IO?

Az IO (Input & Output) API egy Java API, amely megkönnyíti a fejlesztők számára az adatfolyamokkal való munkát. Tegyük fel, hogy kapunk néhány adatot (például keresztnév, középső név, vezetéknév), és be kell írnunk egy fájlba – eljött az idő a java.io használatára .

A java.io könyvtár felépítése

De a Java IO-nak vannak hátrányai, ezért beszéljünk mindegyikről sorra:

  1. A bemeneti/kimeneti hozzáférés blokkolása. A probléma az, hogy amikor a fejlesztő megpróbál valamit olvasni vagy írni egy fájlba a Java IO segítségével , zárolja a fájlt, és blokkolja a hozzáférést a feladat elvégzéséig.
  2. Nem támogatja a virtuális fájlrendszereket.
  3. Nincs támogatás a hivatkozásokhoz.
  4. Sok-sok ellenőrzött kivétel.

A fájlokkal való munkavégzés mindig kivételekkel jár: például egy már létező új fájl létrehozása IOExceptiont eredményez . Ebben az esetben az alkalmazásnak tovább kell futnia, és értesíteni kell a felhasználót, hogy miért nem sikerült létrehozni a fájlt.


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 {
...
}

Itt azt látjuk, hogy a createTempFile metódus IOExceptiont dob , ha a fájl nem hozható létre. Ezt a kivételt megfelelően kell kezelni. Ha megpróbáljuk ezt a metódust a try-catch blokkon kívül hívni , a fordító hibát generál, és két megoldást javasol a javításra: csomagolja a metódust try -catch blokkba, vagy a File.createTempFile- t hívó metódus IOException ( így magasabb szinten is kezelhető).

Megérkezés a Java NIO-hoz és összehasonlítása a Java IO-val

A Java NIO vagy Java Non-Blocking I/O (vagy néha a Java New I/O) nagy teljesítményű I/O műveletekhez készült.

Hasonlítsuk össze a Java IO- módszereket és azokat, amelyek helyettesítik őket.

Először beszéljünk a Java IO- val való együttműködésről :

InputStream osztály


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());
}

A FileInputStream osztály az adatok fájlból történő olvasására szolgál. Örökli az InputStream osztályt, és ezért minden metódusát megvalósítja. Ha a fájl nem nyitható meg, a rendszer FileNotFoundException kivételt dob.

OutputStream osztály


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());
}

A FileOutputStream osztály a bájtok fájlba írásához. Az OutputStream osztályból származik .

Olvasó és író órák

A FileReader osztály lehetővé teszi karakteradatok olvasását folyamokból, a FileWriter osztály pedig karakterfolyamok írására szolgál. A következő kód megmutatja, hogyan kell írni és olvasni egy fájlból:


        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();
        }

Most beszéljünk a Java NIO- ról :

Csatorna

A Java IO- ban használt streamekkel ellentétben a Channel kétirányú interfész, azaz tud olvasni és írni is . A Java NIO csatorna blokkoló és nem blokkoló módban is támogatja az aszinkron adatáramlást.


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();

Itt egy FileChannel-t használtunk . Fájlcsatornát használunk az adatok kiolvasására egy fájlból. Fájlcsatorna objektum csak a getChannel() metódus meghívásával hozható létre egy fájlobjektumban – nincs mód a fájlcsatorna objektum közvetlen létrehozására.

A FileChannel mellett más csatornamegvalósításaink is vannak:

  • FileChannel – fájlokkal való munkavégzéshez

  • DatagramChannel – UDP-kapcsolaton keresztüli munkavégzésre szolgáló csatorna

  • SocketChannel – egy csatorna a TCP-kapcsolaton keresztüli munkavégzéshez

  • A ServerSocketChannel tartalmaz egy SocketChannel-t , és hasonló a webszerver működéséhez

Megjegyzés: A FileChannel nem kapcsolható nem blokkoló módba. A Java NIO nem blokkoló üzemmódja lehetővé teszi, hogy olvasási adatokat kérjen egy csatornától, és csak azt kapja, ami jelenleg elérhető (vagy semmit sem, ha még nem állnak rendelkezésre adatok). Ennek ellenére a SelectableChannel és megvalósításai nem blokkoló módba helyezhetők a connect() metódus segítségével.

Választó

A Java NIO bevezette egy olyan szál létrehozásának lehetőségét, amely tudja, melyik csatorna készen áll az adatok írására és olvasására, és képes feldolgozni az adott csatornát. Ezt a képességet a Selector osztály segítségével valósítjuk meg.

Csatornák csatlakoztatása választóhoz


Selector selector = Selector.open();
channel.configureBlocking(false); // Non-blocking mode
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Így létrehozzuk a Selector- unkat , és összekapcsoljuk egy SelectableChannel- nel .

A szelektorral való használathoz a csatornának nem blokkoló módban kell lennie. Ez azt jelenti, hogy a FileChannel nem használható választóval, mivel a FileChannel nem helyezhető nem blokkoló módba. De a socket csatornák jól működnek.

Itt említsük meg, hogy példánkban a SelectionKey egy csatornán végrehajtható műveletek halmaza. A választógomb segítségével tudjuk megtudni a csatorna állapotát.

A SelectionKey típusai

  • A SelectionKey.OP_CONNECT olyan csatornát jelöl, amely készen áll a szerverhez való csatlakozásra.

  • A SelectionKey.OP_ACCEPT egy csatorna, amely készen áll a bejövő kapcsolatok fogadására.

  • A SelectionKey.OP_READ olyan csatornát jelöl, amely készen áll az adatok olvasására.

  • A SelectionKey.OP_WRITE olyan csatornát jelöl, amely készen áll az adatok írására.

Puffer

Az adatokat egy pufferbe olvassa be további feldolgozás céljából. A fejlesztők oda-vissza mozoghatnak a pufferen, ami egy kis rugalmasságot biztosít számunkra az adatok feldolgozásakor. Ugyanakkor ellenőriznünk kell, hogy a puffer tartalmazza-e a helyes feldolgozáshoz szükséges mennyiségű adatot. Ezenkívül, amikor adatokat olvas be egy pufferbe, ügyeljen arra, hogy ne semmisítse meg a meglévő, még fel nem dolgozott adatokat.


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

A puffer alapvető tulajdonságai:

Alapvető tulajdonságok
kapacitás A puffer mérete, ami a tömb hossza.
pozíció Az adatokkal való munka kiindulópontja.
határ A működési határ. Az olvasási műveleteknél a korlát az olvasható adatok mennyisége, az írási műveleteknél viszont az írásra rendelkezésre álló kapacitás vagy kvóta.
Mark Annak az értéknek az indexe, amelyre a pozíció paraméter visszaáll a reset() metódus meghívásakor.

Most beszéljünk egy kicsit a Java NIO.2 újdonságairól .

Pálya

Az elérési út egy elérési út a fájlrendszerben. Tartalmazza a fájl nevét és a könyvtárak listáját, amelyek meghatározzák az elérési utat.


Path relative = Paths.get("Main.java");
System.out.println("File: " + relative);
// Get the file system
System.out.println(relative.getFileSystem());

A Paths egy nagyon egyszerű osztály egyetlen statikus metódussal: get() . Kizárólag azért jött létre, hogyaz átadott karakterláncból vagy URI-ból Path objektumot kapjon.


Path path = Paths.get("c:\\data\\file.txt");

Fájlok

A Files egy segédprogram osztály, amely lehetővé teszi számunkra, hogy közvetlenül megkapjuk a fájl méretét, másoljunk fájlokat stb.


Path path = Paths.get("files/file.txt");
boolean pathExists = Files.exists(path);

Fájlrendszer

A FileSystem interfészt biztosít a fájlrendszerhez. A FileSystem gyárként működik különféle objektumok létrehozásához (Pálya,PathMatcher,Fájlok). Segít a fájlrendszerben lévő fájlok és egyéb objektumok elérésében.


try {
      FileSystem filesystem = FileSystems.getDefault();
      for (Path rootdir : filesystem.getRootDirectories()) {
          System.out.println(rootdir.toString());
      }
  } catch (Exception e) {
      e.printStackTrace();
  }

Teljesítményteszt

Ehhez a teszthez vegyünk két fájlt. Az első egy kis szövegfájl, a második pedig egy nagy videó.

Létrehozunk egy fájlt, és hozzáadunk néhány szót és karaktert:

% touch text.txt

Fájlunk összesen 42 bájtot foglal el a memóriában:

Most írjunk kódot, amely átmásolja a fájlunkat egyik mappából a másikba. Teszteljük a kis és nagy fájlokon, hogy összehasonlíthassuk az IO és a NIO és a NIO sebességét.2 .

Java IO- val írt másolási kód :


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();
        }
    }

És itt van a Java NIO kódja :


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 kód :


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));
}

Kezdjük a kis fájllal.

A Java IO végrehajtási ideje átlagosan 1 ezredmásodperc volt. A teszt többszöri lefuttatásával 0 és 2 ezredmásodperc közötti eredményt kapunk.

Végrehajtási idő ezredmásodpercben: 1

A Java NIO végrehajtási ideje sokkal hosszabb. Az átlagos idő 11 ezredmásodperc. Az eredmények 9 és 16 között változtak. Ennek az az oka, hogy a Java IO másként működik, mint a mi operációs rendszerünk. Az IO egyenként mozgatja és dolgozza fel a fájlokat, de az operációs rendszer egy nagy darabban küldi el az adatokat. A NIO gyengén teljesített, mert puffer-orientált, nem adatfolyam-orientált, mint az IO .

Végrehajtási idő ezredmásodpercben: 12

És futtassuk le a Java NIO.2 tesztünket is . A NIO.2 javított fájlkezelést kínál a Java NIO- hoz képest . Ez az oka annak, hogy a frissített könyvtár ilyen eltérő eredményeket produkál:

Végrehajtási idő ezredmásodpercben: 3

Most próbáljuk meg tesztelni a nagy fájlunkat, egy 521 MB-os videót. A feladat pontosan ugyanaz lesz: másolja át a fájlt egy másik mappába. Néz!

Eredmények a Java IO-ra :

Végrehajtási idő ezredmásodpercben: 1866

És itt van a Java NIO eredménye :

Végrehajtási idő ezredmásodpercben: 205

A Java NIO 9-szer gyorsabban kezelte a fájlt az első tesztben. Az ismételt tesztek megközelítőleg ugyanazt az eredményt mutatták.

És kipróbáljuk a tesztünket a Java NIO.2-n is :

Végrehajtási idő ezredmásodpercben: 360

Miért ez az eredmény? Egyszerűen azért, mert nincs sok értelme összehasonlítanunk a teljesítményüket, mivel különböző célokat szolgálnak. A NIO absztraktabb, alacsony szintű I/O, míg a NIO.2 a fájlkezelésre irányul.

Összegzés

Nyugodtan kijelenthetjük, hogy a Java NIO lényegesen hatékonyabb a fájlokkal való munka során, köszönhetően a blokkon belüli használatnak. További plusz, hogy a NIO könyvtár két részre oszlik: az egyik a fájlokkal, a másik a hálózattal való munkavégzéshez.

A Java NIO.2 új API-ja a fájlokkal való munkavégzéshez számos hasznos funkciót kínál:

  • sokkal hasznosabb fájlrendszer-címzés a Path használatával ,

  • jelentősen javított ZIP fájlok kezelése egyéni fájlrendszer-szolgáltató segítségével,

  • hozzáférés speciális fájlattribútumokhoz,

  • számos kényelmes módszer, mint például egy teljes fájl egyetlen utasítással történő olvasása, egy fájl másolása egyetlen utasítással stb.

Minden a fájlokról és fájlrendszerekről szól, és mindez elég magas szintű.

A valóság ma az, hogy a fájlokkal végzett munka nagyjából 80-90%-át a Java NIO teszi ki, bár a Java IO részesedése még mindig jelentős.

💡 PS Ezeket a teszteket MacBook Pro 14" 16/512 számítógépen futtatták. A teszteredmények az operációs rendszer és a munkaállomás specifikációitól függően eltérőek lehetnek.