เหตุใด Java IO จึงแย่

API IO (อินพุตและเอาต์พุต) เป็น Java API ที่ช่วยให้นักพัฒนาทำงานกับสตรีมได้ง่าย สมมติว่าเราได้รับข้อมูลบางอย่าง (เช่น ชื่อ ชื่อกลาง นามสกุล) และเราจำเป็นต้องเขียนลงในไฟล์ — ถึงเวลาแล้วที่จะใช้java.io

โครงสร้างของไลบรารี java.io

แต่Java IOก็มีข้อเสีย ดังนั้นเรามาพูดถึงแต่ละข้อกัน:

  1. การบล็อกการเข้าถึงอินพุต/เอาต์พุต ปัญหาคือเมื่อนักพัฒนาพยายามอ่านหรือเขียนบางอย่างลงในไฟล์โดยใช้Java IOมันจะล็อกไฟล์และบล็อกการเข้าถึงจนกว่างานจะเสร็จสิ้น
  2. ไม่รองรับระบบไฟล์เสมือน
  3. ไม่รองรับลิงค์
  4. ข้อยกเว้นที่ตรวจสอบจำนวนมากและมากมาย

การทำงาน กับไฟล์เกี่ยวข้องกับการทำงานโดยมีข้อยกเว้นเสมอ ตัวอย่างเช่น การพยายามสร้างไฟล์ใหม่ที่มีอยู่แล้วจะโยนIOException ในกรณีนี้ แอปพลิเคชันควรทำงานต่อไป และผู้ใช้ควรได้รับแจ้งสาเหตุที่สร้างไฟล์ไม่ได้


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

ที่นี่เราเห็นว่า เมธอด createTempFileส่งIOExceptionเมื่อไม่สามารถสร้างไฟล์ได้ ข้อยกเว้นนี้ต้องได้รับการจัดการอย่างเหมาะสม หากเราพยายามเรียกใช้เมธอดนี้นอก บล็อก try-catchคอมไพเลอร์จะสร้างข้อผิดพลาดและแนะนำสองตัวเลือกสำหรับการแก้ไข: ล้อมเมธอดไว้ในบล็อกtry-catchหรือทำให้เมธอดที่เรียกFile.createTempFileโยนIOException ( จึงจะจัดการได้ในระดับที่สูงขึ้น)

มาถึง Java NIO และวิธีเปรียบเทียบกับ Java IO

Java NIOหรือ Java Non-Blocking I/O (หรือบางครั้ง Java New I/O) ได้รับการออกแบบมาสำหรับการดำเนินการ I/O ที่มีประสิทธิภาพสูง

ลองเปรียบเทียบ เมธอด Java IOกับเมธอดที่มาแทนที่

ก่อนอื่นเรามาพูดถึงการทำงานกับJava IO กัน :

คลาส 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());
}

คลาส FileInputStream ใช้สำหรับอ่าน ข้อมูลจากไฟล์ มันสืบทอด คลาส InputStreamและใช้วิธีทั้งหมดของมัน หากไม่สามารถเปิดไฟล์ได้FileNotFoundExceptionจะถูกส่งออกไป

คลาสเอาท์พุตสตรีม


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

คลาส FileOutputStream สำหรับเขียนไบต์ลงในไฟล์ มันมาจากคลาสOutputStream

ชั้นเรียนผู้อ่านและนักเขียน

คลาส FileReader ช่วยให้เราอ่านข้อมูลอักขระจากสตรีมได้ และ คลาส FileWriterใช้เพื่อเขียนสตรีมอักขระ รหัสต่อไปนี้แสดงวิธีการเขียนและอ่านจากไฟล์:


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

ตอนนี้เรามาพูดถึงJava NIO :

ช่อง

ไม่เหมือนกับสตรีมที่ใช้ในJava IOช่องเป็นอินเทอร์เฟซแบบสองทาง นั่นคือสามารถอ่านและเขียนได้ ช่อง Java NIOสนับสนุนโฟลว์ข้อมูลแบบอะซิงโครนัสทั้งในโหมดบล็อกและโหมดไม่บล็อก


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

ที่นี่ เราใช้FileChannel เราใช้ช่องไฟล์เพื่ออ่านข้อมูลจากไฟล์ สามารถสร้างวัตถุช่องไฟล์ได้โดยการเรียกเมธอดgetChannel()บนวัตถุไฟล์เท่านั้น ไม่มีทางที่จะสร้างวัตถุช่องไฟล์ได้โดยตรง

นอกจากFileChannelเรายังมีการใช้งานช่องอื่นๆ:

  • FileChannel — สำหรับการทำงานกับไฟล์

  • DatagramChannel — ช่องทางสำหรับการทำงานผ่านการเชื่อมต่อ UDP

  • SocketChannel — ช่องทางสำหรับการทำงานผ่านการเชื่อมต่อ TCP

  • ServerSocketChannelมี SocketChannelและคล้ายกับวิธีการทำงานของเว็บเซิร์ฟเวอร์

โปรดทราบ: ไม่สามารถเปลี่ยนFileChannel เป็นโหมดไม่ปิดกั้นได้ โหมดไม่บล็อกของ Java NIO ช่วยให้คุณขอข้อมูลการอ่านจากช่องสัญญาณและรับเฉพาะสิ่งที่มีอยู่ในปัจจุบัน (หรือไม่มีอะไรเลยหากยังไม่มีข้อมูล) ที่กล่าวว่าSelectableChannelและการใช้งานสามารถใส่ในโหมดไม่ปิดกั้นโดยใช้เมธอดconnect()

ตัวเลือก

Java NIOนำเสนอความสามารถในการสร้างเธรดที่รู้ว่าแชนเนลใดพร้อมที่จะเขียนและอ่านข้อมูลและสามารถประมวลผลแชนเนลนั้น ความสามารถนี้ใช้งานโดยใช้คลาส Selector

การเชื่อมต่อช่องสัญญาณกับตัวเลือก


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

ดังนั้นเราจึงสร้างSelector ของ เราและเชื่อมต่อกับSelectableChannel

หากต้องการใช้กับตัวเลือก ช่องสัญญาณจะต้องอยู่ในโหมดไม่ปิดกั้น ซึ่งหมายความว่าคุณไม่สามารถใช้FileChannelกับตัวเลือกได้ เนื่องจากFileChannelไม่สามารถเข้าสู่โหมดไม่บล็อกได้ แต่ช่องซ็อกเก็ตจะทำงานได้ดี

ในที่นี้ขอกล่าวถึงว่าSelectionKey ในตัวอย่างของเรา คือชุดของการดำเนินการที่สามารถดำเนินการได้บนช่องสัญญาณ ปุ่มเลือกช่วยให้เราทราบสถานะของช่อง

ประเภทของ SelectionKey

  • SelectionKey.OP_CONNECTหมายถึงช่องสัญญาณที่พร้อมเชื่อมต่อกับเซิร์ฟเวอร์

  • SelectionKey.OP_ACCEPTเป็นช่องสัญญาณที่พร้อมรับการเชื่อมต่อที่เข้ามา

  • SelectionKey.OP_READหมายถึง ช่องสัญญาณที่พร้อมอ่านข้อมูล

  • SelectionKey.OP_WRITEหมายถึงช่องสัญญาณที่พร้อมเขียนข้อมูล

กันชน

ข้อมูลจะถูกอ่านลงในบัฟเฟอร์สำหรับการประมวลผลต่อไป นักพัฒนาสามารถเลื่อนไปมาบนบัฟเฟอร์ได้ ซึ่งช่วยให้เรามีความยืดหยุ่นมากขึ้นเล็กน้อยเมื่อประมวลผลข้อมูล ในเวลาเดียวกัน เราต้องตรวจสอบว่าบัฟเฟอร์มีจำนวนข้อมูลที่จำเป็นสำหรับการประมวลผลที่ถูกต้องหรือไม่ นอกจากนี้ เมื่ออ่านข้อมูลลงในบัฟเฟอร์ ต้องแน่ใจว่าคุณไม่ได้ทำลายข้อมูลที่มีอยู่ซึ่งยังไม่ได้ประมวลผล


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

คุณสมบัติพื้นฐานของบัฟเฟอร์:

คุณลักษณะพื้นฐาน
ความจุ ขนาดบัฟเฟอร์ซึ่งเป็นความยาวของอาร์เรย์
ตำแหน่ง ตำแหน่งเริ่มต้นสำหรับการทำงานกับข้อมูล
จำกัด ขีด จำกัด การดำเนินงาน สำหรับการดำเนินการอ่าน ขีดจำกัดคือจำนวนข้อมูลที่สามารถอ่านได้ แต่สำหรับการดำเนินการเขียน ขีดจำกัดคือความจุหรือโควต้าที่พร้อมใช้งานสำหรับการเขียน
เครื่องหมาย ดัชนีของค่าที่ พารามิเตอร์ ตำแหน่งจะถูกรีเซ็ตเมื่อเรียกเมธอดreset()

ตอนนี้เรามาพูดถึงสิ่งใหม่ๆ ในJava NIO.2 กันสักหน่อย

เส้นทาง

เส้นทางแสดงเส้นทางในระบบไฟล์ ประกอบด้วยชื่อไฟล์และรายการไดเร็กทอรีที่กำหนดพาธไปยังไฟล์นั้น


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

Pathsเป็นคลาสที่ง่ายมากด้วยเมธอดสแตติกเดียว: get( ) มันถูกสร้างขึ้นเพื่อรับ วัตถุ เส้นทางจากสตริงหรือ URI ที่ส่งผ่าน


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

ไฟล์

Filesคือคลาสยูทิลิตี้ที่ช่วยให้เราได้ขนาดไฟล์ คัดลอกไฟล์ และอื่นๆ ได้โดยตรง


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

ระบบไฟล์

FileSystemมีส่วนต่อประสานกับระบบไฟล์ ระบบไฟล์ทำงานเหมือนโรงงานสำหรับสร้างวัตถุต่างๆ (เส้นทาง,PathMatcher,ไฟล์). ช่วยให้เราเข้าถึงไฟล์และวัตถุอื่นๆ ในระบบไฟล์


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

การทดสอบประสิทธิภาพ

สำหรับการทดสอบนี้ เราจะใช้สองไฟล์ ไฟล์แรกเป็นไฟล์ข้อความขนาดเล็ก และไฟล์ที่สองเป็นวิดีโอขนาดใหญ่

เราจะสร้างไฟล์และเพิ่มคำและอักขระสองสามตัว:

% แตะ text.txt

ไฟล์ของเราใช้หน่วยความจำทั้งหมด 42 ไบต์:

ตอนนี้มาเขียนโค้ดที่จะคัดลอกไฟล์ของเราจากโฟลเดอร์หนึ่งไปอีกโฟลเดอร์หนึ่ง มาทดสอบกับไฟล์ขนาด เล็กและขนาดใหญ่เพื่อเปรียบเทียบความเร็วของIOและNIOและNIO.2

รหัสสำหรับการคัดลอก เขียนโดยใช้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();
        }
    }

และนี่คือรหัสสำหรับ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();
        }
    }

รหัสสำหรับ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));
}

เริ่มจากไฟล์เล็กก่อน

เวลาดำเนินการสำหรับJava IOคือ 1 มิลลิวินาทีโดยเฉลี่ย เรียกใช้การทดสอบหลายๆ ครั้ง เราจะได้ผลลัพธ์ตั้งแต่ 0 ถึง 2 มิลลิวินาที

เวลาดำเนินการเป็นมิลลิวินาที: 1

เวลาดำเนินการสำหรับJava NIOนั้นนานกว่ามาก เวลาเฉลี่ยอยู่ที่ 11 มิลลิวินาที ผลลัพธ์มีค่าตั้งแต่ 9 ถึง 16 เนื่องจากJava IOทำงานแตกต่างจากระบบปฏิบัติการของเรา IOย้ายและประมวลผลไฟล์ทีละไฟล์ แต่ระบบปฏิบัติการส่งข้อมูลเป็นกลุ่มใหญ่ NIO ทำงานได้ ไม่ดีเพราะเน้นบัฟเฟอร์ ไม่ใช่สตรีมเหมือนIO

เวลาดำเนินการเป็นมิลลิวินาที: 12

และเรียกใช้การทดสอบJava NIO.2 ของเรา ด้วย NIO.2 ได้ปรับปรุงการ จัดการไฟล์เมื่อเทียบกับJava NIO นี่คือสาเหตุที่ไลบรารีที่อัปเดตสร้างผลลัพธ์ที่แตกต่างกันดังกล่าว:

เวลาดำเนินการเป็นมิลลิวินาที: 3

ตอนนี้มาลองทดสอบไฟล์ขนาดใหญ่ของเรา ซึ่งเป็นวิดีโอขนาด 521 MB งานจะเหมือนกันทุกประการ: คัดลอกไฟล์ไปยังโฟลเดอร์อื่น ดู!

ผลลัพธ์สำหรับJava IO :

เวลาดำเนินการเป็นมิลลิวินาที: 1866

และนี่คือผลลัพธ์สำหรับJava NIO :

เวลาดำเนินการเป็นมิลลิวินาที: 205

Java NIOจัดการไฟล์เร็วขึ้น 9 เท่าในการทดสอบครั้งแรก การทดสอบซ้ำ ๆ แสดงให้เห็นผลลัพธ์ที่เหมือนกันโดยประมาณ

และเราจะลองทดสอบกับJava NIO.2 :

เวลาดำเนินการเป็นมิลลิวินาที: 360

ทำไมผลลัพธ์นี้ เพียงเพราะมันไม่มีเหตุผลที่เราจะเปรียบเทียบประสิทธิภาพระหว่างพวกเขา เนื่องจากพวกเขาตอบสนองวัตถุประสงค์ที่แตกต่างกัน NIOเป็น I/O ระดับต่ำที่เป็นนามธรรมมากกว่า ในขณะที่NIO.2มุ่งเน้นไปที่การจัดการไฟล์

สรุป

เราสามารถพูดได้อย่างปลอดภัยว่าJava NIOมีประสิทธิภาพมากกว่าอย่างมากเมื่อทำงานกับไฟล์ ต้องขอบคุณการใช้งานภายในบล็อก ข้อดีอีกอย่างคือ ไลบรารี NIOแบ่งออกเป็นสองส่วน: ส่วนหนึ่งสำหรับการทำงานกับไฟล์และอีกส่วนหนึ่งสำหรับการทำงานกับเครือข่าย

API ใหม่ของ Java NIO.2 สำหรับการทำงานกับไฟล์มีคุณสมบัติที่มีประโยชน์มากมาย:

  • การกำหนดที่อยู่ระบบไฟล์ที่มีประโยชน์มากกว่าโดยใช้Path ,

  • ปรับปรุงการจัดการไฟล์ ZIP อย่างมีนัยสำคัญโดยใช้ผู้ให้บริการระบบไฟล์แบบกำหนดเอง

  • เข้าถึงแอตทริบิวต์ไฟล์พิเศษ

  • วิธีที่สะดวกมากมาย เช่น การอ่านไฟล์ทั้งหมดด้วยคำสั่งเดียว การคัดลอกไฟล์ด้วยคำสั่งเดียว เป็นต้น

มันคือทั้งหมดที่เกี่ยวกับไฟล์และระบบไฟล์ และทั้งหมดนี้อยู่ในระดับสูงทีเดียว

ความจริงในปัจจุบันคือJava NIOคิดเป็นประมาณ 80-90% ของงานกับไฟล์ แม้ว่าส่วนแบ่งของJava IO จะยังมีนัยสำคัญ

💡 ป.ล. การทดสอบเหล่านี้ดำเนินการบน MacBook Pro 14" 16/512 ผลการทดสอบอาจแตกต่างกันไปตามข้อกำหนดของระบบปฏิบัติการและเวิร์กสเตชัน