Bakit napakasama ng Java IO?

Ang IO (Input & Output) API ay isang Java API na ginagawang madali para sa mga developer na magtrabaho sa mga stream. Sabihin nating nakatanggap kami ng ilang data (halimbawa, unang pangalan, gitnang pangalan, apelyido) at kailangan namin itong isulat sa isang file — dumating na ang oras para gamitin ang java.io .

Istraktura ng java.io library

Ngunit ang Java IO ay may mga kakulangan nito, kaya pag-usapan natin ang bawat isa sa kanila nang magkakasunod:

  1. Hinaharang ang access para sa input/output. Ang problema ay kapag sinubukan ng isang developer na magbasa o magsulat ng isang bagay sa isang file gamit ang Java IO , ni-lock nito ang file at hinaharangan ang access dito hanggang sa matapos ang trabaho.
  2. Walang suporta para sa mga virtual file system.
  3. Walang suporta para sa mga link.
  4. Napakaraming nasuri na mga eksepsiyon.

Ang pagtatrabaho sa mga file ay palaging nangangailangan ng pagtatrabaho sa mga pagbubukod: halimbawa, ang pagsisikap na lumikha ng isang bagong file na umiiral na ay magtapon ng isang IOException . Sa kasong ito, dapat magpatuloy sa pagtakbo ang application at dapat maabisuhan ang user kung bakit hindi magawa ang file.


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

Dito makikita natin na ang paraan ng createTempFile ay naghagis ng IOException kapag hindi magawa ang file. Ang pagbubukod na ito ay dapat pangasiwaan nang naaangkop. Kung susubukan naming tawagan ang pamamaraang ito sa labas ng try-catch block, bubuo ang compiler ng error at magmumungkahi ng dalawang opsyon para sa pag-aayos nito: balutin ang paraan sa try-catch block o gawin ang paraan na tumatawag sa File.createTempFile na magtapon ng IOException ( kaya maaari itong mahawakan sa mas mataas na antas).

Pagdating sa Java NIO at kung paano ito maihahambing sa Java IO

Ang Java NIO , o Java Non-Blocking I/O (o kung minsan ay Java New I/O) ay idinisenyo para sa mataas na pagganap ng mga operasyon ng I/O.

Ihambing natin ang mga pamamaraan ng Java IO at ang mga pumapalit sa kanila.

Una, pag-usapan natin ang tungkol sa pagtatrabaho sa Java IO :

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

Ang klase ng FileInputStream ay para sa pagbabasa ng data mula sa isang file. Namana nito ang klase ng InputStream at samakatuwid ay ipinapatupad ang lahat ng mga pamamaraan nito. Kung ang file ay hindi mabuksan, ang isang FileNotFoundException ay itatapon.

OutputStream klase


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

Ang klase ng FileOutputStream para sa pagsusulat ng mga byte sa isang file. Nagmula ito sa klase ng OutputStream .

Mga klase ng Reader at Writer

Hinahayaan tayo ng klase ng FileReader na magbasa ng data ng character mula sa mga stream, at ang klase ng FileWriter ay ginagamit upang magsulat ng mga stream ng character. Ipinapakita ng sumusunod na code kung paano magsulat at magbasa mula sa isang 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();
        }

Ngayon pag-usapan natin ang Java NIO :

Channel

Hindi tulad ng mga stream na ginagamit sa Java IO , ang Channel ay two-way na interface, ibig sabihin, maaari itong magbasa at magsulat. Sinusuportahan ng channel ng Java NIO ang asynchronous na daloy ng data sa parehong mga mode ng pagharang at hindi pag-block.


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

Dito ginamit namin ang isang FileChannel . Gumagamit kami ng file channel para magbasa ng data mula sa isang file. Ang isang file channel object ay maaari lamang gawin sa pamamagitan ng pagtawag sa getChannel() method sa isang file object — walang paraan para direktang gumawa ng file channel object.

Bilang karagdagan sa FileChannel , mayroon kaming iba pang mga pagpapatupad ng channel:

  • FileChannel — para sa pagtatrabaho sa mga file

  • DatagramChannel — isang channel para sa pagtatrabaho sa isang koneksyon sa UDP

  • SocketChannel — isang channel para sa pagtatrabaho sa isang koneksyon sa TCP

  • Ang ServerSocketChannel ay naglalaman ng isang SocketChannel at katulad ng kung paano gumagana ang isang web server

Pakitandaan: Ang FileChannel ay hindi maaaring ilipat sa non-blocking mode. Hinahayaan ka ng non-blocking mode ng Java NIO na humiling ng pagbabasa ng data mula sa isang channel at tumanggap lamang ng kung ano ang kasalukuyang available (o wala pa kung wala pang available na data). Sabi nga, ang SelectableChannel at ang mga pagpapatupad nito ay maaaring ilagay sa non-blocking mode gamit ang connect() method.

Tagapili

Ipinakilala ng Java NIO ang kakayahang lumikha ng thread na nakakaalam kung aling channel ang handang sumulat at magbasa ng data at maaaring magproseso ng partikular na channel na iyon. Ang kakayahang ito ay ipinatupad gamit ang Selector class.

Pagkonekta ng mga channel sa isang tagapili


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

Kaya ginagawa namin ang aming Selector at ikinonekta ito sa isang SelectableChannel .

Upang magamit sa isang tagapili, ang isang channel ay dapat na nasa non-blocking mode. Nangangahulugan ito na hindi mo magagamit ang FileChannel sa isang tagapili, dahil ang FileChannel ay hindi maaaring ilagay sa non-blocking mode. Ngunit ang mga socket channel ay gagana nang maayos.

Dito natin banggitin na sa ating halimbawa ang SelectionKey ay isang hanay ng mga operasyon na maaaring isagawa sa isang channel. Ipinapaalam sa amin ng selection key ang status ng isang channel.

Mga Uri ng SelectionKey

  • Ang SelectionKey.OP_CONNECT ay nangangahulugang isang channel na handang kumonekta sa server.

  • SelectionKey.OP_ACCEPT ay isang channel na handang tumanggap ng mga papasok na koneksyon.

  • Ang SelectionKey.OP_READ ay nangangahulugang isang channel na handang magbasa ng data.

  • Ang SelectionKey.OP_WRITE ay nangangahulugang isang channel na handang magsulat ng data.

Buffer

Ang data ay binabasa sa isang buffer para sa karagdagang pagproseso. Ang isang developer ay maaaring magpalipat-lipat sa buffer, na nagbibigay sa amin ng kaunting flexibility kapag nagpoproseso ng data. Kasabay nito, kailangan nating suriin kung ang buffer ay naglalaman ng dami ng data na kinakailangan para sa tamang pagproseso. Gayundin, kapag nagbabasa ng data sa isang buffer, siguraduhing hindi mo sisirain ang umiiral na data na hindi pa naproseso.


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

Mga pangunahing katangian ng isang buffer:

Mga pangunahing katangian
kapasidad Ang laki ng buffer, na siyang haba ng array.
posisyon Ang panimulang posisyon para sa pagtatrabaho sa data.
limitasyon Ang limitasyon sa pagpapatakbo. Para sa mga read operation, ang limitasyon ay ang dami ng data na maaaring basahin, ngunit para sa write operations, ito ay ang kapasidad o quota na magagamit para sa pagsusulat.
marka Ang index ng halaga kung saan ire-reset ang parameter ng posisyon kapag tinawag ang reset() na paraan.

Ngayon, pag-usapan natin ang tungkol sa kung ano ang bago sa Java NIO.2 .

Daan

Ang landas ay kumakatawan sa isang landas sa file system. Naglalaman ito ng pangalan ng isang file at isang listahan ng mga direktoryo na tumutukoy sa landas patungo dito.


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

Ang Paths ay isang napaka-simpleng klase na may isang solong static na pamamaraan: get() . Ito ay ginawa lamang upang makakuha ng Path object mula sa ipinasang string o URI.


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

Mga file

Ang mga file ay isang utility class na nagbibigay-daan sa amin na direktang makuha ang laki ng isang file, kopyahin ang mga file, at higit pa.


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

FileSystem

Nagbibigay ang FileSystem ng interface sa file system. Ang FileSystem ay gumagana tulad ng isang pabrika para sa paglikha ng iba't ibang mga bagay (Daan,PathMatcher,Mga file). Nakakatulong ito sa amin na ma-access ang mga file at iba pang mga bagay sa file system.


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

Pagsubok sa pagganap

Para sa pagsusulit na ito, kumuha tayo ng dalawang file. Ang una ay isang maliit na text file, at ang pangalawa ay isang malaking video.

Gagawa kami ng file at magdagdag ng ilang salita at character:

% touch text.txt

Ang aming file ay sumasakop ng kabuuang 42 byte sa memorya:

Ngayon magsulat tayo ng code na kokopya sa ating file mula sa isang folder patungo sa isa pa. Subukan natin ito sa maliit at malalaking file upang maihambing ang bilis ng IO at NIO at NIO.2 .

Code para sa pagkopya, nakasulat gamit ang 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();
        }
    }

At narito ang code para sa 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();
        }
    }

Code para sa 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));
}

Magsimula tayo sa maliit na file.

Ang oras ng pagpapatupad para sa Java IO ay 1 millisecond sa karaniwan. Sa pamamagitan ng pagpapatakbo ng pagsubok nang maraming beses, nakakakuha kami ng mga resulta mula 0 hanggang 2 millisecond.

Oras ng pagpapatupad sa millisecond: 1

Ang oras ng pagpapatupad para sa Java NIO ay mas matagal. Ang average na oras ay 11 millisecond. Ang mga resulta ay mula 9 hanggang 16. Ito ay dahil ang Java IO ay gumagana nang iba kaysa sa aming operating system. Inilipat at pinoproseso ng IO ang mga file nang paisa-isa, ngunit ipinapadala ng operating system ang data sa isang malaking tipak. Hindi maganda ang performance ng NIO dahil ito ay buffer-oriented, hindi stream-oriented tulad ng IO .

Oras ng pagpapatupad sa millisecond: 12

At patakbuhin din natin ang ating pagsubok para sa Java NIO.2 . Pinahusay ng NIO.2 ang pamamahala ng file kumpara sa Java NIO . Ito ang dahilan kung bakit ang na-update na library ay gumagawa ng iba't ibang mga resulta:

Oras ng pagpapatupad sa millisecond: 3

Ngayon subukan nating subukan ang aming malaking file, isang 521 MB na video. Ang gawain ay magiging eksaktong pareho: kopyahin ang file sa isa pang folder. Tingnan mo!

Mga resulta para sa Java IO :

Oras ng pagpapatupad sa millisecond: 1866

At narito ang resulta para sa Java NIO :

Oras ng pagpapatupad sa millisecond: 205

Hinawakan ng Java NIO ang file nang 9 na beses nang mas mabilis sa unang pagsubok. Ang mga paulit-ulit na pagsusuri ay nagpakita ng humigit-kumulang sa parehong mga resulta.

At susubukan din namin ang aming pagsubok sa Java NIO.2 :

Oras ng pagpapatupad sa millisecond: 360

Bakit ganito ang resulta? Dahil lang sa hindi gaanong makatwiran para sa amin na paghambingin ang pagganap sa pagitan nila, dahil may iba't ibang layunin ang mga ito. Ang NIO ay mas abstract na mababang antas ng I/O, habang ang NIO.2 ay nakatuon sa pamamahala ng file.

Buod

Maaari naming ligtas na sabihin na ang Java NIO ay makabuluhang mas mahusay kapag nagtatrabaho sa mga file salamat sa paggamit sa loob ng mga bloke. Ang isa pang plus ay ang NIO library ay nahahati sa dalawang bahagi: isa para sa pagtatrabaho sa mga file, isa pa para sa pagtatrabaho sa network.

Ang bagong API ng Java NIO.2 para sa pagtatrabaho sa mga file ay nag-aalok ng maraming kapaki-pakinabang na tampok:

  • mas kapaki-pakinabang na pag-address ng file system gamit ang Path ,

  • makabuluhang pinabuting paghawak ng mga ZIP file gamit ang isang custom na file system provider,

  • access sa mga espesyal na katangian ng file,

  • maraming maginhawang pamamaraan, tulad ng pagbabasa ng isang buong file na may isang pahayag, pagkopya ng isang file na may isang solong pahayag, atbp.

Lahat ito ay tungkol sa mga file at file system, at lahat ito ay medyo mataas na antas.

Ang katotohanan ngayon ay ang Java NIO ay nagkakahalaga ng humigit-kumulang 80-90% ng trabaho sa mga file, kahit na ang bahagi ng Java IO ay mahalaga pa rin.

💡 PS Ang mga pagsubok na ito ay pinatakbo sa isang MacBook Pro 14" 16/512. Maaaring mag-iba ang mga resulta ng pagsubok batay sa mga detalye ng operating system at workstation.