Mengapa Java IO sangat teruk?

API IO (Input & Output) ialah API Java yang memudahkan pembangun bekerja dengan strim. Katakan kami menerima beberapa data (contohnya, nama pertama, nama tengah, nama keluarga) dan kami perlu menulisnya ke fail — masanya telah tiba untuk menggunakan java.io .

Struktur perpustakaan java.io

Tetapi Java IO mempunyai kelemahannya, jadi mari kita bincangkan setiap satu daripada mereka secara bergilir:

  1. Menyekat akses untuk input/output. Masalahnya ialah apabila pembangun cuba membaca atau menulis sesuatu pada fail menggunakan Java IO , ia mengunci fail dan menyekat akses kepadanya sehingga kerja selesai.
  2. Tiada sokongan untuk sistem fail maya.
  3. Tiada sokongan untuk pautan.
  4. Banyak dan banyak pengecualian yang diperiksa.

Bekerja dengan fail sentiasa memerlukan kerja dengan pengecualian: contohnya, cuba mencipta fail baharu yang sudah wujud akan membuang IOException . Dalam kes ini, aplikasi harus terus berjalan dan pengguna harus dimaklumkan mengapa fail tidak dapat dibuat.


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

Di sini kita melihat bahawa kaedah createTempFile membuang IOException apabila fail tidak boleh dibuat. Pengecualian ini mesti dikendalikan dengan sewajarnya. Jika kita cuba memanggil kaedah ini di luar blok cuba-tangkap , pengkompil akan menghasilkan ralat dan mencadangkan dua pilihan untuk membetulkannya: balut kaedah dalam blok cuba-tangkap atau buat kaedah yang memanggil File.createTempFile membuang IOException ( jadi ia boleh dikendalikan pada tahap yang lebih tinggi).

Tiba di Java NIO dan bagaimana ia dibandingkan dengan Java IO

Java NIO , atau Java Non-Blocking I/O (atau kadangkala Java New I/O) direka untuk operasi I/O berprestasi tinggi.

Mari kita bandingkan kaedah Java IO dan kaedah yang menggantikannya.

Mula-mula, mari kita bercakap tentang bekerja dengan Java IO :

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

Kelas FileInputStream adalah untuk membaca data daripada fail. Ia mewarisi kelas InputStream dan oleh itu melaksanakan semua kaedahnya. Jika fail tidak boleh dibuka, FileNotFoundException dilemparkan.

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

Kelas FileOutputStream untuk menulis bait pada fail. Ia berasal daripada kelas OutputStream .

Kelas Pembaca dan Penulis

Kelas FileReader membolehkan kami membaca data aksara daripada strim, dan kelas FileWriter digunakan untuk menulis strim aksara . Kod berikut menunjukkan cara menulis dan membaca daripada fail:


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

Sekarang mari kita bercakap tentang Java NIO :

Saluran

Tidak seperti strim yang digunakan dalam Java IO , Saluran ialah antara muka dua hala, iaitu, ia boleh membaca dan menulis. Saluran Java NIO menyokong aliran data tak segerak dalam kedua-dua mod menyekat dan tidak menyekat.


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

Di sini kami menggunakan FileChannel . Kami menggunakan saluran fail untuk membaca data daripada fail. Objek saluran fail hanya boleh dibuat dengan memanggil kaedah getChannel() pada objek fail — tiada cara untuk terus mencipta objek saluran fail.

Sebagai tambahan FileChannel , kami mempunyai pelaksanaan saluran lain:

  • FileChannel — untuk bekerja dengan fail

  • DatagramChannel — saluran untuk bekerja melalui sambungan UDP

  • SocketChannel — saluran untuk bekerja melalui sambungan TCP

  • ServerSocketChannel mengandungi SocketChannel dan serupa dengan cara pelayan web berfungsi

Sila ambil perhatian: FileChannel tidak boleh ditukar kepada mod tidak menyekat. Mod tanpa sekatan Java NIO membolehkan anda meminta data bacaan daripada saluran dan menerima hanya apa yang tersedia pada masa ini (atau tiada langsung jika tiada data tersedia lagi) . Yang berkata, SelectableChannel dan pelaksanaannya boleh diletakkan dalam mod tidak menyekat menggunakan kaedah connect() .

Pemilih

Java NIO memperkenalkan keupayaan untuk mencipta rangkaian yang mengetahui saluran mana yang sedia untuk menulis dan membaca data dan boleh memproses saluran tertentu itu. Keupayaan ini dilaksanakan menggunakan kelas Selector .

Menyambung saluran kepada pemilih


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

Oleh itu, kami mencipta Pemilih kami dan menyambungkannya ke Saluran Boleh Dipilih .

Untuk digunakan dengan pemilih, saluran mestilah dalam mod tidak menyekat. Ini bermakna anda tidak boleh menggunakan FileChannel dengan pemilih, kerana FileChannel tidak boleh dimasukkan ke dalam mod bukan sekatan. Tetapi saluran soket akan berfungsi dengan baik.

Di sini mari kita sebutkan bahawa dalam contoh kami SelectionKey ialah satu set operasi yang boleh dilakukan pada saluran. Kekunci pilihan membolehkan kami mengetahui status saluran.

Jenis SelectionKey

  • SelectionKey.OP_CONNECT menandakan saluran yang sedia untuk disambungkan ke pelayan.

  • SelectionKey.OP_ACCEPT ialah saluran yang sedia menerima sambungan masuk.

  • SelectionKey.OP_READ menandakan saluran yang sedia untuk membaca data.

  • SelectionKey.OP_WRITE menandakan saluran yang bersedia untuk menulis data.

Penampan

Data dibaca ke dalam penimbal untuk pemprosesan selanjutnya. Pembangun boleh bergerak ke sana ke mari pada penimbal, yang memberikan kita sedikit lebih fleksibiliti semasa memproses data. Pada masa yang sama, kita perlu menyemak sama ada penimbal mengandungi jumlah data yang diperlukan untuk pemprosesan yang betul. Selain itu, apabila membaca data ke dalam penimbal pastikan anda tidak memusnahkan data sedia ada yang belum diproses.


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

Sifat asas penimbal:

Atribut asas
kapasiti Saiz penimbal, iaitu panjang tatasusunan.
kedudukan Kedudukan permulaan untuk bekerja dengan data.
had Had operasi. Untuk operasi baca, had ialah jumlah data yang boleh dibaca, tetapi untuk operasi tulis, ia adalah kapasiti atau kuota yang tersedia untuk menulis.
tanda Indeks nilai yang parameter kedudukan akan ditetapkan semula apabila kaedah reset() dipanggil.

Sekarang mari kita bercakap sedikit tentang perkara baharu dalam Java NIO.2 .

Laluan

Laluan mewakili laluan dalam sistem fail. Ia mengandungi nama fail dan senarai direktori yang menentukan laluan kepadanya.


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

Paths ialah kelas yang sangat mudah dengan satu kaedah statik: get() . Ia dicipta semata-mata untuk mendapatkan objek Path daripada rentetan atau URI yang diluluskan.


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

Fail

Fail ialah kelas utiliti yang membolehkan kami mendapatkan terus saiz fail, menyalin fail dan banyak lagi.


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

Sistem fail

FileSystem menyediakan antara muka kepada sistem fail. FileSystem berfungsi seperti kilang untuk mencipta pelbagai objek (Laluan,PathMatcher,Fail). Ia membantu kami mengakses fail dan objek lain dalam sistem fail.


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

Ujian prestasi

Untuk ujian ini, mari ambil dua fail. Yang pertama ialah fail teks kecil, dan yang kedua ialah video besar.

Kami akan membuat fail dan menambah beberapa perkataan dan aksara:

% sentuh text.txt

Fail kami menduduki sejumlah 42 bait dalam ingatan:

Sekarang mari kita tulis kod yang akan menyalin fail kita dari satu folder ke folder yang lain. Mari kita uji pada fail kecil dan besar untuk membandingkan kelajuan IO dan NIO dan NIO.2 .

Kod untuk menyalin, ditulis menggunakan 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();
        }
    }

Dan inilah kod untuk 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();
        }
    }

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

Mari kita mulakan dengan fail kecil.

Masa pelaksanaan untuk Java IO ialah 1 milisaat secara purata. Dengan menjalankan ujian beberapa kali, kami mendapat keputusan dari 0 hingga 2 milisaat.

Masa pelaksanaan dalam milisaat: 1

Masa pelaksanaan untuk Java NIO adalah lebih lama. Purata masa ialah 11 milisaat. Hasilnya adalah antara 9 hingga 16. Ini kerana Java IO berfungsi secara berbeza daripada sistem pengendalian kami. IO memindahkan dan memproses fail satu demi satu, tetapi sistem pengendalian menghantar data dalam satu bahagian besar. NIO berprestasi buruk kerana ia berorientasikan penimbal, bukan berorientasikan aliran seperti IO .

Masa pelaksanaan dalam milisaat: 12

Dan mari jalankan juga ujian kami untuk Java NIO.2 . NIO.2 telah menambah baik pengurusan fail berbanding Java NIO . Inilah sebabnya mengapa perpustakaan yang dikemas kini menghasilkan hasil yang berbeza:

Masa pelaksanaan dalam milisaat: 3

Sekarang mari cuba menguji fail besar kami, video 521 MB. Tugasnya akan sama: salin fail ke folder lain. Lihatlah!

Keputusan untuk Java IO :

Masa pelaksanaan dalam milisaat: 1866

Dan inilah hasilnya untuk Java NIO :

Masa pelaksanaan dalam milisaat: 205

Java NIO mengendalikan fail 9 kali lebih pantas dalam ujian pertama. Ujian berulang menunjukkan keputusan yang lebih kurang sama.

Dan kami juga akan mencuba ujian kami pada Java NIO.2 :

Masa pelaksanaan dalam milisaat: 360

Mengapa keputusan ini? Semata-mata kerana tidak masuk akal untuk kita membandingkan prestasi antara mereka, kerana ia mempunyai tujuan yang berbeza. NIO adalah I/O peringkat rendah yang lebih abstrak, manakala NIO.2 berorientasikan pengurusan fail.

Ringkasan

Kami dengan selamat boleh mengatakan bahawa Java NIO adalah jauh lebih cekap apabila bekerja dengan fail berkat penggunaan di dalam blok. Satu lagi kelebihan ialah perpustakaan NIO dibahagikan kepada dua bahagian: satu untuk bekerja dengan fail, satu lagi untuk bekerja dengan rangkaian.

API baharu Java NIO.2 untuk bekerja dengan fail menawarkan banyak ciri berguna:

  • pengalamatan sistem fail yang jauh lebih berguna menggunakan Path ,

  • pengendalian fail ZIP dengan ketara menggunakan pembekal sistem fail tersuai,

  • akses kepada atribut fail khas,

  • banyak kaedah mudah, seperti membaca keseluruhan fail dengan satu pernyataan, menyalin fail dengan satu pernyataan, dsb.

Ini semua tentang fail dan sistem fail, dan semuanya adalah tahap yang cukup tinggi.

Realiti hari ini ialah Java NIO menyumbang kira-kira 80-90% kerja dengan fail, walaupun bahagian Java IO masih ketara.

💡 PS Ujian ini dijalankan pada MacBook Pro 14" 16/512. Keputusan ujian mungkin berbeza berdasarkan sistem pengendalian dan spesifikasi stesen kerja.