Mengapa Java IO begitu buruk?

API IO (Input & Output) adalah API Java yang memudahkan pengembang untuk bekerja dengan aliran. Katakanlah kita menerima beberapa data (misalnya, nama depan, nama tengah, nama belakang) dan kita perlu menulisnya ke file — waktunya telah tiba untuk menggunakan java.io .

Struktur perpustakaan java.io

Tetapi Java IO memiliki kekurangannya, jadi mari kita bahas masing-masing secara bergantian:

  1. Memblokir akses untuk input/output. Masalahnya adalah ketika pengembang mencoba membaca atau menulis sesuatu ke file menggunakan Java IO , itu mengunci file dan memblokir akses ke sana hingga pekerjaan selesai.
  2. Tidak ada dukungan untuk sistem file virtual.
  3. Tidak ada dukungan untuk tautan.
  4. Banyak sekali pengecualian yang diperiksa.

Bekerja dengan file selalu memerlukan bekerja dengan pengecualian: misalnya, mencoba membuat file baru yang sudah ada akan menghasilkan IOException . Dalam hal ini, aplikasi harus terus berjalan dan pengguna harus diberi tahu mengapa file 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 bahwa metode createTempFile melontarkan IOException ketika file tidak dapat dibuat. Pengecualian ini harus ditangani dengan tepat. Jika kami mencoba memanggil metode ini di luar blok coba-tangkap , kompiler akan menghasilkan kesalahan dan menyarankan dua opsi untuk memperbaikinya: bungkus metode dalam blok coba-tangkap atau buat metode yang memanggil File.createTempFile melempar IOException ( sehingga dapat ditangani pada tingkat yang lebih tinggi).

Sesampainya di Java NIO dan bagaimana perbandingannya dengan Java IO

Java NIO , atau Java Non-Blocking I/O (atau terkadang Java New I/O) dirancang untuk operasi I/O berperforma tinggi.

Mari bandingkan metode Java IO dan yang menggantikannya.

Pertama, mari kita bicara 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 dari file. Itu mewarisi kelas InputStream dan karenanya mengimplementasikan semua metodenya. Jika file tidak dapat 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 byte ke file. Itu berasal dari kelas OutputStream .

kelas Reader dan Writer

Kelas FileReader memungkinkan kita membaca data karakter dari aliran, dan kelas FileWriter digunakan untuk menulis aliran karakter . Kode berikut menunjukkan cara menulis dan membaca dari file:


        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 bicara tentang Java NIO :

Saluran

Berbeda dengan aliran yang digunakan di Java IO , Saluran adalah antarmuka dua arah, yaitu dapat membaca dan menulis. Saluran Java NIO mendukung aliran data asinkron dalam mode pemblokiran dan non-pemblokiran.


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 file untuk membaca data dari file. Objek saluran file hanya dapat dibuat dengan memanggil metode getChannel() pada objek file — tidak ada cara untuk membuat objek saluran file secara langsung.

Selain FileChannel , kami memiliki implementasi saluran lainnya:

  • FileChannel — untuk bekerja dengan file

  • DatagramChannel — saluran untuk bekerja melalui koneksi UDP

  • SocketChannel — saluran untuk bekerja melalui koneksi TCP

  • ServerSocketChannel berisi SocketChannel dan mirip dengan cara kerja server web

Harap diperhatikan: FileChannel tidak dapat dialihkan ke mode non-pemblokiran. Mode non-blocking Java NIO memungkinkan Anda meminta membaca data dari saluran dan hanya menerima apa yang saat ini tersedia (atau tidak sama sekali jika belum ada data yang tersedia). Yang mengatakan, SelectableChannel dan implementasinya dapat diletakkan dalam mode non-blocking menggunakan metode connect() .

Pemilih

Java NIO memperkenalkan kemampuan untuk membuat utas yang mengetahui saluran mana yang siap untuk menulis dan membaca data dan dapat memproses saluran tersebut. Kemampuan ini diimplementasikan menggunakan kelas Selector .

Menghubungkan saluran ke pemilih


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

Jadi kami membuat Selector kami dan menghubungkannya ke SelectableChannel .

Untuk digunakan dengan pemilih, saluran harus dalam mode non-pemblokiran. Ini berarti Anda tidak dapat menggunakan FileChannel dengan pemilih, karena FileChannel tidak dapat dimasukkan ke mode non-pemblokiran. Tetapi saluran soket akan berfungsi dengan baik.

Di sini, mari kita sebutkan bahwa dalam contoh kami, SelectionKey adalah serangkaian operasi yang dapat dilakukan di saluran. Tombol pilihan memberi tahu kami status saluran.

Jenis-jenis SelectionKey

  • SelectionKey.OP_CONNECT menandakan saluran yang siap terhubung ke server.

  • SelectionKey.OP_ACCEPT adalah saluran yang siap menerima koneksi masuk.

  • SelectionKey.OP_READ menandakan saluran yang siap membaca data.

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

Penyangga

Data dibaca ke buffer untuk diproses lebih lanjut. Pengembang dapat bergerak bolak-balik di buffer, yang memberi kami sedikit lebih banyak fleksibilitas saat memproses data. Pada saat yang sama, kita perlu memeriksa apakah buffer berisi jumlah data yang diperlukan untuk pemrosesan yang benar. Selain itu, saat membaca data ke dalam buffer, pastikan Anda tidak menghancurkan data yang 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

Properti dasar buffer:

Atribut dasar
kapasitas Ukuran buffer, yang merupakan panjang dari array.
posisi Posisi awal untuk bekerja dengan data.
membatasi Batas operasi. Untuk operasi baca, batasannya adalah jumlah data yang dapat dibaca, tetapi untuk operasi tulis, adalah kapasitas atau kuota yang tersedia untuk menulis.
tanda Indeks nilai yang parameter posisi akan disetel ulang saat metode reset() dipanggil.

Sekarang mari kita bicara sedikit tentang apa yang baru di Java NIO.2 .

Jalur

Path mewakili jalur dalam sistem file. Ini berisi nama file dan daftar direktori yang menentukan jalur ke sana.


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

Paths adalah kelas yang sangat sederhana dengan satu metode statis: get() . Itu dibuat semata-mata untuk mendapatkan objek Path dari string atau URI yang diteruskan.


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

File

File adalah kelas utilitas yang memungkinkan kita secara langsung mendapatkan ukuran file, menyalin file, dan banyak lagi.


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

Berkas sistem

FileSystem menyediakan antarmuka ke sistem file. FileSystem berfungsi seperti pabrik untuk membuat berbagai objek (Jalur,PathMatcher,File). Ini membantu kami mengakses file dan objek lain di sistem file.


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

Uji kinerja

Untuk tes ini, mari ambil dua file. Yang pertama adalah file teks kecil, dan yang kedua adalah video besar.

Kami akan membuat file dan menambahkan beberapa kata dan karakter:

% sentuh teks.txt

File kami menempati total 42 byte dalam memori:

Sekarang mari kita menulis kode yang akan menyalin file kita dari satu folder ke folder lainnya. Mari kita uji pada file kecil dan besar untuk membandingkan kecepatan IO dan NIO dan NIO.2 .

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

Kode 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 mulai dengan file kecil.

Waktu eksekusi untuk Java IO rata-rata adalah 1 milidetik. Dengan menjalankan pengujian beberapa kali, kami mendapatkan hasil dari 0 hingga 2 milidetik.

Waktu eksekusi dalam milidetik: 1

Waktu eksekusi untuk Java NIO jauh lebih lama. Waktu rata-rata adalah 11 milidetik. Hasilnya berkisar antara 9 hingga 16. Ini karena Java IO bekerja secara berbeda dari sistem operasi kami. IO memindahkan dan memproses file satu per satu, tetapi sistem operasi mengirimkan data dalam satu potongan besar. NIO berkinerja buruk karena berorientasi buffer, bukan berorientasi aliran seperti IO .

Waktu eksekusi dalam milidetik: 12

Dan mari kita jalankan pengujian kita untuk Java NIO.2 . NIO.2 telah meningkatkan manajemen file dibandingkan dengan Java NIO . Inilah mengapa perpustakaan yang diperbarui menghasilkan hasil yang berbeda:

Waktu eksekusi dalam milidetik: 3

Sekarang mari kita coba untuk menguji file besar kita, sebuah video berukuran 521 MB. Tugasnya akan persis sama: salin file ke folder lain. Lihat!

Hasil untuk Java IO :

Waktu eksekusi dalam milidetik: 1866

Dan inilah hasil untuk Java NIO :

Waktu eksekusi dalam milidetik: 205

Java NIO menangani file 9 kali lebih cepat pada pengujian pertama. Tes berulang menunjukkan hasil yang kira-kira sama.

Dan kami juga akan mencoba pengujian kami di Java NIO.2 :

Waktu eksekusi dalam milidetik: 360

Mengapa hasil ini? Hanya karena tidak masuk akal bagi kami untuk membandingkan kinerja di antara mereka, karena mereka memiliki tujuan yang berbeda. NIO adalah I/O tingkat rendah yang lebih abstrak, sedangkan NIO.2 berorientasi pada manajemen file.

Ringkasan

Kami dapat dengan aman mengatakan bahwa Java NIO secara signifikan lebih efisien saat bekerja dengan file berkat penggunaan di dalam blok. Kelebihan lainnya adalah pustaka NIO dibagi menjadi dua bagian: satu untuk bekerja dengan file, satu lagi untuk bekerja dengan jaringan.

API baru Java NIO.2 untuk bekerja dengan file menawarkan banyak fitur berguna:

  • pengalamatan sistem file yang jauh lebih berguna menggunakan Path ,

  • penanganan file ZIP yang meningkat secara signifikan menggunakan penyedia sistem file kustom,

  • akses ke atribut file khusus,

  • banyak metode mudah, seperti membaca seluruh file dengan satu pernyataan, menyalin file dengan satu pernyataan, dll.

Ini semua tentang file dan sistem file, dan itu semua level yang cukup tinggi.

Kenyataan saat ini adalah bahwa Java NIO menyumbang sekitar 80-90% pekerjaan dengan file, meskipun pangsa Java IO masih signifikan.

💡 PS Tes ini dijalankan pada MacBook Pro 14" 16/512. Hasil tes mungkin berbeda berdasarkan spesifikasi sistem operasi dan workstation.