Java IO neden bu kadar kötü?

IO (Input & Output) API, geliştiricilerin akışlarla çalışmasını kolaylaştıran bir Java API'sidir. Diyelim ki bazı veriler aldık (örneğin, ad, ikinci ad, soyadı) ve bunları bir dosyaya yazmamız gerekiyor - java.io'yu kullanma zamanı geldi .

java.io kitaplığının yapısı

Ancak Java IO'nun dezavantajları vardır, bu yüzden sırasıyla her biri hakkında konuşalım:

  1. Giriş/çıkış için erişimi engelleme. Sorun şu ki, bir geliştirici Java IO kullanarak bir dosyaya bir şeyler okumaya veya yazmaya çalıştığında , dosyayı kilitler ve iş tamamlanana kadar dosyaya erişimi engeller.
  2. Sanal dosya sistemleri için destek yok.
  3. Bağlantılar için destek yok.
  4. Çok ve çok sayıda kontrol edilen istisna.

Dosyalarla çalışmak her zaman istisnalarla çalışmayı gerektirir: örneğin, zaten var olan yeni bir dosya oluşturmaya çalışmak bir IOException hatası verir . Bu durumda uygulama çalışmaya devam etmeli ve dosyanın neden oluşturulamadığı kullanıcıya bildirilmelidir.


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

Burada , dosya oluşturulamadığında createTempFile yönteminin bir IOException attığını görüyoruz . Bu istisna uygun şekilde ele alınmalıdır. Bu yöntemi bir try-catch bloğunun dışında çağırmaya çalışırsak , derleyici bir hata oluşturur ve bunu düzeltmek için iki seçenek önerir: yöntemi bir try-catch bloğuna sarın veya File.createTempFile'ı çağıran yöntemin bir IOException ( böylece daha yüksek bir seviyede ele alınabilir).

Java NIO'ya varış ve Java IO ile karşılaştırması

Java NIO veya Java Engellemeyen G/Ç (veya bazen Java Yeni G/Ç), yüksek performanslı G/Ç işlemleri için tasarlanmıştır.

Java IO yöntemlerini ve bunların yerini alan yöntemleri karşılaştıralım .

Öncelikle Java IO ile çalışma hakkında konuşalım :

InputStream sınıfı


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 sınıfı , bir dosyadan veri okumak içindir. InputStream sınıfını devralır ve bu nedenle tüm yöntemlerini uygular. Dosya açılamıyorsa, bir FileNotFoundException atılır.

OutputStream sınıfı


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

Bir dosyaya bayt yazmak için FileOutputStream sınıfı . OutputStream sınıfından türetilir .

Okuyucu ve Yazar sınıfları

FileReader sınıfı , akışlardan karakter verilerini okumamıza izin verir ve FileWriter sınıfı, karakter akışlarını yazmak için kullanılır. Aşağıdaki kod, bir dosyadan nasıl yazılacağını ve okunacağını gösterir:


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

Şimdi Java NIO hakkında konuşalım :

Kanal

Java IO'da kullanılan akışların aksine , Kanal iki yönlü bir arayüzdür, yani hem okuyabilir hem de yazabilir. Bir Java NIO kanalı, hem engelleme hem de engellemememe modlarında eşzamansız veri akışını destekler.


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

Burada bir FileChannel kullandık . Bir dosyadan veri okumak için bir dosya kanalı kullanırız. Bir dosya kanalı nesnesi yalnızca bir dosya nesnesinde getChannel() yöntemi çağrılarak oluşturulabilir — doğrudan bir dosya kanalı nesnesi oluşturmanın bir yolu yoktur.

FileChannel'a ek olarak , başka kanal uygulamalarımız da var:

  • FileChannel — dosyalarla çalışmak için

  • DatagramChannel — bir UDP bağlantısı üzerinden çalışmak için bir kanal

  • SocketChannel — bir TCP bağlantısı üzerinden çalışmak için bir kanal

  • ServerSocketChannel bir SocketChannel içerirve bir web sunucusunun çalışma şekline benzer

Lütfen dikkat: FileChannel engellemesiz moda geçirilemez. Java NIO'nun engellememe modu, bir kanaldan okuma verisi istemenize ve yalnızca mevcut olanı almanıza (veya henüz veri yoksa hiçbir şey almamanıza) olanak tanır. Bununla birlikte, SelectableChannel ve uygulamaları , connect() yöntemi kullanılarak engellemesiz moda alınabilir .

seçici

Java NIO, hangi kanalın veri yazmaya ve okumaya hazır olduğunu bilen ve söz konusu kanalı işleyebilen bir iş parçacığı oluşturma yeteneğini tanıttı. Bu yetenek, Selector sınıfı kullanılarak uygulanır.

Kanalları bir seçiciye bağlama


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

Böylece Selector'ımızı oluşturup SelectableChannel'a bağlarız .

Bir seçici ile birlikte kullanılmak için, bir kanalın engellemesiz modda olması gerekir. Bu, FileChannel'i bir seçiciyle kullanamayacağınız anlamına gelir , çünkü FileChannel engellemesiz moda alınamaz. Ancak soket kanalları iyi çalışacaktır.

Burada, örneğimizde SelectionKey'in bir kanal üzerinde gerçekleştirilebilen bir dizi işlem olduğunu belirtelim. Seçim tuşu, bir kanalın durumunu bilmemizi sağlar.

Seçim Anahtarı Türleri

  • SelectionKey.OP_CONNECT, sunucuya bağlanmaya hazır bir kanalı belirtir.

  • SelectionKey.OP_ACCEPT , gelen bağlantıları kabul etmeye hazır bir kanaldır.

  • SelectionKey.OP_READ, verileri okumaya hazır bir kanalı belirtir.

  • SelectionKey.OP_WRITE, veri yazmaya hazır bir kanalı belirtir.

Tampon

Veriler daha fazla işlenmek üzere bir ara belleğe okunur. Bir geliştirici, verileri işlerken bize biraz daha fazla esneklik sağlayan arabellek üzerinde ileri geri hareket edebilir. Aynı zamanda, tamponun doğru işleme için gerekli miktarda veri içerip içermediğini kontrol etmemiz gerekir. Ayrıca, ara belleğe veri okurken, henüz işlenmemiş mevcut verileri yok etmediğinizden emin olun.


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

Bir tamponun temel özellikleri:

Temel özellikler
kapasite Dizinin uzunluğu olan arabellek boyutu.
konum Verilerle çalışmak için başlangıç ​​konumu.
limit Çalışma sınırı. Okuma işlemleri için sınır, okunabilecek veri miktarıdır, ancak yazma işlemleri için sınır, yazma için mevcut olan kapasite veya kotadır.
işaret reset() yöntemi çağrıldığında konum parametresinin sıfırlanacağı değerin dizini .

Şimdi biraz Java NIO.2'deki yeniliklerden bahsedelim .

Yol

Yol, dosya sistemindeki bir yolu temsil eder. Bir dosyanın adını ve ona giden yolu tanımlayan bir dizin listesini içerir.


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

Yollar, tek bir statik yöntemi olan çok basit bir sınıftır: get() . Yalnızca iletilen dizeden veya URI'den bir Yol nesnesi almak için oluşturuldu


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

Dosyalar

Dosyalar, bir dosyanın boyutunu doğrudan almamıza, dosyaları kopyalamamıza ve daha pek çok şeye izin veren bir yardımcı program sınıfıdır.


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

Dosya sistemi

FileSystem, dosya sistemine bir arayüz sağlar. FileSystem, çeşitli nesneler oluşturmak için bir fabrika gibi çalışır (Yol,Yol Eşleştirici,Dosyalar). Dosya sistemindeki dosyalara ve diğer nesnelere erişmemize yardımcı olur.


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

Performans testi

Bu test için iki dosya alalım. İlki küçük bir metin dosyası, ikincisi ise büyük bir video.

Bir dosya oluşturacağız ve birkaç kelime ve karakter ekleyeceğiz:

% text.txt'e dokunun

Dosyamız bellekte toplam 42 bayt yer kaplıyor:

Şimdi dosyamızı bir klasörden diğerine kopyalayacak kodu yazalım. IO ve NIO ve NIO.2 hızlarını karşılaştırmak için küçük ve büyük dosyalar üzerinde test edelim .

Kopyalama kodu, Java IO kullanılarak yazılmıştır :


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

Ve işte Java NIO'nun kodu :


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 için kod :


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

Küçük dosyayla başlayalım.

Java IO için yürütme süresi ortalama 1 milisaniyeydi. Testi birkaç kez çalıştırarak 0 ila 2 milisaniye arasında sonuç alıyoruz.

Milisaniye cinsinden yürütme süresi: 1

Java NIO'nun yürütme süresi çok daha uzundur. Ortalama süre 11 milisaniyedir. Sonuçlar 9 ile 16 arasında değişiyordu. Bunun nedeni Java IO'nun bizim işletim sistemimizden farklı çalışmasıdır. IO , dosyaları birer birer taşır ve işler, ancak işletim sistemi verileri büyük bir yığın halinde gönderir. NIO, IO gibi akış yönelimli değil, arabellek yönelimli olduğu için düşük performans gösterdi .

Milisaniye cinsinden yürütme süresi: 12

Java NIO.2 için testimizi de çalıştıralım . NIO.2, Java NIO'ya kıyasla gelişmiş dosya yönetimine sahiptir . Güncellenen kitaplığın bu kadar farklı sonuçlar vermesinin nedeni budur:

Milisaniye cinsinden yürütme süresi: 3

Şimdi büyük dosyamız olan 521 MB'lık bir videoyu test etmeye çalışalım. Görev tamamen aynı olacaktır: dosyayı başka bir klasöre kopyalayın. Bakmak!

Java IO için sonuçlar :

Milisaniye cinsinden yürütme süresi: 1866

Ve işte Java NIO için sonuç :

Milisaniye cinsinden yürütme süresi: 205

Java NIO, ilk testte dosyayı 9 kat daha hızlı işledi. Tekrarlanan testler yaklaşık olarak aynı sonuçları gösterdi.

Ayrıca testimizi Java NIO.2 üzerinde de deneyeceğiz :

Milisaniye cinsinden yürütme süresi: 360

Neden bu sonuç? Basitçe, çünkü farklı amaçlara hizmet ettikleri için aralarındaki performansı karşılaştırmamız bizim için pek mantıklı değil. NIO daha soyut, düşük seviyeli G/Ç'dir, NIO.2 ise dosya yönetimine yöneliktir.

Özet

Java NIO'nun blokların içinde kullanılması sayesinde dosyalarla çalışırken çok daha verimli olduğunu rahatlıkla söyleyebiliriz . Diğer bir artı, NIO kitaplığının iki bölüme ayrılmış olmasıdır : biri dosyalarla çalışmak için, diğeri ağ ile çalışmak için.

Java NIO.2'nin dosyalarla çalışmaya yönelik yeni API'si birçok yararlı özellik sunar:

  • Path kullanarak çok daha kullanışlı dosya sistemi adresleme ,

  • Özel bir dosya sistemi sağlayıcısı kullanılarak ZIP dosyalarının işlenmesi önemli ölçüde iyileştirildi,

  • özel dosya özniteliklerine erişim,

  • tek bir ifade ile tüm bir dosyayı okumak, tek bir ifade ile bir dosyayı kopyalamak gibi birçok uygun yöntem.

Her şey dosyalar ve dosya sistemleri ile ilgili ve hepsi oldukça yüksek seviyede.

Bugünün gerçeği, Java IO'nun payı hala önemli olmasına rağmen, Java NIO'nun dosyalarla yapılan çalışmaların kabaca %80-90'ını oluşturmasıdır .

💡 Not: Bu testler bir MacBook Pro 14" 16/512 üzerinde yapılmıştır. Test sonuçları, işletim sistemi ve iş istasyonu özelliklerine göre farklılık gösterebilir.