เพกเกจ java.nio เป็นอีกทางเลือกในอ่านและเขียนข้อมูลกับแหล่งข้อมูลโดยมีแนวคิดที่แตกต่างกับแพคเกจ java.io คือ แพคเกจ java.io ทำงานกับสตรีมของข้อมูลแต่ แพคเกจ java.nio ทำงานกับช่องทาง (channel) และบัฟเฟอร์ (buffer) โดยข้อมูลจะถูกอ่านจากช่องทางไปยังบัฟเฟอร์และถูกเขียนจากบัฟเฟอร์ไปยังช่องทาง โดยช่องทางจะเชื่อมกับแหล่งข้อมูล เช่น ไฟล์ เครือข่าย เป็นต้น
จากการทำงานร่วมกันของช่องทางกับบัฟเฟอร์ทำให้แพคเกจ java.nio อ่านและเขียนข้อมูลเป็นบล็อกตามขนาดของบัฟเฟอร์ที่กำหนดแทนที่จะเป็นทีละตัวอักษรหรือทีละไบต์ แพคเกจ java.nio มีความสามารถในการใช้งานแบบ non-blocking IO คือเธรดสามารถไปทำงานอื่นต่อได้หลังจากสั่งอ่านหรือเขียนข้อมูลและกลับมาทำงานต่อเมื่อการอ่านหรือเขียนข้อมูลเสร็จสิ้น และมีความสามารถในการตรวจสอบ (monitor) เหตุการณ์จากหลายๆช่องทางพร้อมกันผ่านการใช้งานออบเจกต์ซีเลกเตอร์ (selector)
บัฟเฟอร์
บัฟเฟอร์คือบล็อกของหน่วยความจำที่ถูกใช้เมื่อมีการอ่านและเขียนข้อมูลกับช่องทาง เมื่อถูกเขียนข้อมูลบัฟเฟอร์จะคอยดูปริมาณของข้อมูลที่เขียนลงไป และเมื่อเราต้องการอ่านข้อมูลเราต้องเปลี่ยนโหมดของบัฟเฟอร์จากการเขียนเป็นการอ่าน และเมื่อเราอ่านข้อมูลจากบัฟเฟอร์แล้วเราจะต้องล้างข้อมูลออกจากบัฟเฟอร์เพื่อรอรับการเขียนข้อมูลใหม่
บัฟเฟอร์มีคุณสมบัติที่สำคัญ 3 อย่างคือ ความจุ (capacity) ขนาดของข้อมูล (limit) และตัวชี้ตำแหน่ง (position) ความจุคือขนาดของหน่วยความจำที่ใช้เป็นบัฟเฟอร์ ตัวชี้ตำแหน่งคือตัวที่บอกตำแหน่งที่จะอ่านหรือเขียนข้อมูล ส่วนขนาดของข้อมูลคือปริมาณของข้อมูลที่สามารถอ่านหรือเขียนกับบัฟเฟอร์ได้ในขณะใดขณะหนึ่ง
เมื่อสร้างบัฟเฟอร์เปล่าขึ้นมาตัวชี้ตำแหน่งจะเริ่มที่ตำแหน่งแรกของบัฟเฟอร์คือตำแหน่งที่ 0 และเลื่อนไปยังตำแหน่งถัดไปเมื่อมีการเขียนข้อมูล และเมื่อเราเปลี่ยนโหมดจากการเขียนมาเป็นการอ่านตัวชี้ตำแหน่งจะเลื่อนกลับมาที่ตำแหน่งที่ 0 และเลื่อนไปยังตำแหน่งถัดไปเมื่อมีการอ่านข้อมูล ในโหมดการเขียนข้อมูลปริมาณของข้อมูลกับความจุจะเท่ากัน แต่เมื่อเราเปลี่ยนโหมดจากการเขียนมาเป็นการอ่าน ขนาดของข้อมูลจะเปลี่ยนมาชี้ที่ตำแหน่งสุดท้ายที่มีการเขียนข้อมูล (ข้อมูลที่จะถูกอ่านมีแค่นี้) ซึ่งในกรณีที่ข้อมูลมีน้อยกว่าขนาดของบัฟเฟอร์จะทำให้ขนาดของข้อมูลน้อยกว่าความจุที่มี
เราสามารถสรุปวงจรการใช้งานบัฟเฟอร์ได้ดังนี้
1. จองที่หน่วยความจำด้วยเมธอด allocate()
2. เขียนขัอมูลลงบัฟเฟอร์
3. เรียกใช้เมธอด buffer.flip เพื่อสลับโหมดจาการอ่านเป็นการเขียน
4. อ่านข้อมูลจากบัฟเฟอร์
5. เรียกใช้เมธอด buffer.clear() หรือ buffer.compact() เพื่อล้างข้อมูลจากบัฟเฟอร์
ชนิดของบัฟเฟอร์ที่มีให้ใช้งานประกอบด้วย ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer และ MappedByteBuffer การจองพื้นที่หน่วยความจำคือการสร้างออบเกจต์ของบัฟเฟอร์ขึ้นมาด้วยเมธอด allocate() จากตัวอย่างด้านล่างเป็นการจองหน่วยความจำขนาด 48 ไบต์ และขนาด 1024 อักขระ สังเกตุว่าหน่วยของจำนวนจะสอดคล้องกับชนิดของบัฟเฟอร์
ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer,allocate(1024);
การอ่านข้อมูลจากช่องทางลงในบัฟเฟอร์ทำได้โดยใช้เมธอด read() ของ
ออบเจกต์ช่องทาง หรือเขียนข้อมูลลงบัฟเฟอร์โดยตรงใช้เมธอด put() ของออบเจกต์บัฟเฟอร์ โดยเมธอด put() ของแต่ละชนิดของบัฟเฟอร์มีหลายเวอร์ชั่นซึ่งมีประโยชน์แตกต่างกันไป ตัวอย่างเช่น
int bytesRead = inChannel.read(buf);
buf.put(127);
การเขียนข้อมูลจากบัฟเฟอร์ไปยังช่องทางทำได้โดยใช้เมธอด write() ของ
ออบเจกต์ช่องทาง หรืออ่านข้อมูลจากบัฟเฟอร์โดยตรงใช้เมธอด get() ของ
ออบเจกต์บัฟเฟอร์ โดยเมธอด get() ของแต่ละชนิดของบัฟเฟอร์มีหลายเวอร์ชั่นซึ่งมีประโยชน์แตกต่างกันไป ตัวอย่างเช่น
int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();
การเปลี่ยนโหมดของบัฟเฟอร์จากการเขียนเป็นการอ่านใช้เมธอด flip()
ของออบเจกต์บัฟเฟอร์ซึ่งตัวชี้ตำแหน่งจะเลื่อนกลับมาที่ตำแหน่งที่ 0 หรือในกรณีที่เราอยู่ในโหมดอ่านอยู่แล้วและต้องการให้ตัวชี้ตำแหน่งจะเลื่อนกลับมาที่ตำแหน่งที่ 0 จะใช้เมธอด rewind()
การล้างข้อมูลออกจากบัฟเฟอร์ใช้เมธอด clear() ของออบเจกต์บัฟเฟอร์
ซึ่งเป็นการย้ายตัวชี้ตำแหน่งไปที่ตำแหน่งที่ 0 และกำหนดขนาดข้อมูลเท่ากับความจุเพื่อเตรียมสำหรับการเขียนข้อมูลตั้งแต่ตำแหน่งที่ 0 หรือใช้คำสั่ง compact() ออบเจกต์บัฟเฟอร์ซึ่งจะสำเนาข้อมูลที่ยังไม่ได้อ่านไปที่ส่วนต้นของบัฟเฟอร์ และย้ายตัวชี้ตำแหน่งไปที่ตำแหน่งถัดจากข้อมูลที่ยังไม่ได้อ่านและกำหนดขนาดข้อมูลเท่ากับความจุเพื่อเตรียมสำหรับการเขียนข้อมูลในพื้นที่ส่วนที่เหลือ สรุปคือการล้างข้อมูลเป็นการเตรียมบัฟเฟอร์ให้พร้อมสำหรับการเขียนข้อมูลโดยที่ข้อมูลเก่ายังคงอยู่จนกว่าจะโดนข้อมูลใหม่เขียนทับ
เราสามารถบันทึกตำแหน่งของตัวชี้ตำแหน่งไว้ด้วยเมธอด mark() ของออบเจกต์บัฟเฟอร์ และสั่งให้ตัวชี้ตำแหน่งเลื่อนไปยังตำแหน่งที่บันทึกไว้ด้วยเมธอด reset() ของออบเจกต์บัฟเฟอร์ และเราสามารถเปรียบเทียบบัฟเฟอร์ด้วยเมธอด equal() และเมธอด compareTo() ของออบเจกต์บัฟเฟอร์
ทำความเข้าใจการใช้บัฟเฟอร์
การเก็บข้อมูลแบบไบนารี่ในบัฟเฟอร์จะเก็บข้อมูลเป็นไบต์เรียงลำดับกันไป โดยเราสามารถอ้างอิงตำแหน่งของแต่ละไบต์ได้โดยเริ่มจากไบต์แรกเป็นตำแหน่งที่ 0 (แนวคิดเดียวกับอาเรย์) บัฟเฟอร์ในแพคเกจ java.nio มาพร้อมกับตัวบอกตำแหน่งซึ่งจะเลื่อนไปโดยอัตโนมัติเมื่อมีการอ่านหรือเขียนข้อมูลกับบัฟเฟอร์เพื่อที่จะได้อ่านหรือเขียนในตำแหน่งถัดไปได้อย่างถูกต้อง และยังมีความสามารถในการเก็บตำแหน่งที่ต้องการ (mark) เพื่อนำมาใช้ในภายหลังได้ด้วย
เราสามารถสร้างบัฟเฟอร์ได้ด้วยเมธอด allocate() หรือ จะห่อหุ้มอาเรย์ไบต์ด้วยออบเจกต์บัฟเฟอร์ เช่น ByteBuffer ดังตัวอย่างด้านล่าง ซึ่งทั้ง 2 วิธีจะได้บัฟเฟอร์ที่มีอาเรย์รองรับ (backed by array) ด้วยจำนวนสมาชิกในอาเรย์และจำนวนของไบต์ที่รองรับในบัฟเฟอร์ที่เท่ากัน แต่หากเราสร้างบัฟเฟอร์ด้วยเมธอด allocateDirect() จะได้บัฟเฟอร์ที่ไม่มีอาเรย์รองรับ
เราสามารถตรวจสอบว่าบัฟเฟอร์มีอาเรย์รองรับหรือไม่ด้วยเมธอด hasArray() ของออบเจกต์บัฟเฟอร์หรือตรวจสอบว่าบัฟเฟอร์เป็น direct allocation หรือไม่ด้วยเมธอด isDirect() ของออบเจกต์บัฟเฟอร์
เราสามารถเขียนข้อมูลลงในบัฟเฟอร์ได้ด้วยเมธอด put() ของออบเจกต์บัฟเฟอร์ หรือจะเขียนข้อมูลชนิด primitive data type ด้วยเมธอด เช่น putInt() putDouble() putChar() ของออบเจกต์บัฟเฟอร์ ซึ่งเมธอดจะแปลงข้อมูลเป็นไบต์ก่อนเขียนลงในบัฟเฟอร์ให้ และในการอ่านข้อมูลจากบัฟเฟอร์เราใช้เมธอด get() ของออบเจกต์บัฟเฟอร์ หรือใช้เมธอดสำหรับอ่านข้อมูลที่จะแปลงข้อมูลจากไบต์มาเป็นข้อมูลชนิด primitive data type ด้วยเมธอด เช่น getInt() getDouble() getChar() ของออบเจกต์บัฟเฟอร์
เมื่อมีการสร้างบัฟเฟอร์ขึ้นมาตัวบอกตำแหน่งจะอยู่ที่ตำแหน่งแรกคือตำแหน่งที่ 0 และเมื่อมีการเขียนข้อมูลตัวบอกตำแหน่งจะเลื่อนไปยังตำแหน่งถัดไปตราบเท่าที่ยังไม่สุดพื้นที่บัฟเฟอร์ที่จองไว้ ตัวอย่างด้านล่างเป็นการสร้างบัฟเฟอร์ขนาด 8 ไบต์เพื่อเก็บเลขจำนวนเต็ม 2 ค่า เมื่อเขียนค่าแรกตัวบอกตำแหน่งจะเลื่อนจากตำแหน่งที่ 0 ไปยังตำแหน่งที่ 1 และเลื่อนไปยังท้ายบัฟเฟอร์เมื่อเขียนค่าที่สอง และเมื่อเราสั่งอ่านค่าทันทีจะพบข้อผิดพลาด
BufferUnderflowException เพราะตัวบอกตำแหน่งเลื่อนไปอยู่ที่ท้ายบัฟเฟอร์แล้วจึงอ่านอะไรจากตำแหน่งนั้นไม่ได้
แต่เมื่อเราใช้เมธอด flip() ของออบเจกต์บัฟเฟอร์ ซึ่งตัวบอกตำแหน่งจะกลับมาที่ตำแหน่งที่ 0 จึงจะอ่านค่าได้
เมื่อเราอ่านข้อมูลตัวบอกตำแหน่งจะเลื่อนไปยังตำแหน่งถัดไปดังนั้นหากเราต้องการเขียนข้อมูลเราต้องใช้เมธอด clear() หรือเมธอด compact() ของออบเจกต์บัฟเฟอร์ แล้วแต่กรณีเพื่อเลื่อนตัวบอกตำแหน่งมาที่ตำแหน่งที่สามารถเขียนข้อมูลได้
ช่องทาง
ช่องทาง (channel) จะคล้ายๆกับสตรีมคือใช้เชื่อมต่อกับแหล่งข้อมูลเพื่ออ่านหรือเขียนข้อมูล แต่ช่องทางรองรับทั้งการอ่านและเขียนต่างจากสตรีมที่รองรับเพียงอย่างใดอย่างหนึ่ง เราสามารถอ่านและเขียนข้อมูลกับช่องทางได้โดยที่กิจกรรมไม่จำเป็นต้องสอดคล้องกัน (asynchronously) และการอ่านและเขียนข้อมูลจะทำกับบัฟเฟอร์เสมอ
ช่องทางที่ใช้กันบ่อยคือคลาส FileChannel เพื่ออ่านและเขียนข้อมูลกับไฟล์ คลาส DatagramChannel เพื่อรับและส่งข้อมูลผ่านโปรโตคอล UDP คลาส SocketChannel เพื่อรับและส่งข้อมูลผ่านโปรโตคอล TCP และคลาส ServerSocketChannel เพื่อรอรับการเชื่อมต่อ (listening) ผ่านโปรโตคอล TCP
ตัวอย่างด้านล่างเป็นการใช้งานช่องทาง FIleChannel โดยเราจะเขียนข้อความลงในไฟล์และอ่านข้อความเพื่อจะแสดงให้เห็นว่าเราสามารถใช้ช่องทางและบัฟเฟอร์เดียวกันสำหรับการอ่านและการเขียนข้อมูล ในบรรทัดที่ 12 เราสร้างออบเจกต์สตรีมของข้อมูลขึ้นมาก่อนเพื่อให้ครอบคลุมการสร้างไฟล์ในกรณีที่ไม่มีไฟล์ จากนั้นในบรรทัดที่ 13 จึงใช้เมธอด getChannel() ของออบเจกต์สตรีมเพื่อสร้างออบเจกต์ช่องทาง เราเลือกใช้คลาส RandomAccessFile ในการสร้างออบเจกต์สตรีมของข้อมูลเพราะรองรับทั้งการอ่านและเขียนไฟล์ ซึ่งสอดคล้องกับการใช้งานช่องทาง (ถึงแม้ว่าช่องทางจะรองรับทั้งการอ่านและการเขียน แต่ถ้าเราใช้ออบเจกต์สตรีมที่รองรับการอ่านหรือการเขียนเพียงอย่างเดียวเป็นตัวตั้งต้น ช่องทางก็จะทำได้เท่าที่สตรีมทำได้ ) ในบรรทัดที่ 14 เราเตรียมข้อมูลเป็นข้อความเพื่อเขียนลงในไฟล์ ในบรรทัดที่ 15 เราสร้างบัฟเฟอร์ขึ้นมาเพื่อใช้งานกับช่องทาง และในบรรทัดที่ 16 เราใช้เมธอด clear() ของออบเจกต์บัฟเฟอร์เพื่อเตรียมบัฟเฟอร์ให้ว่างเพื่อรับข้อมูลที่เราจะเขียนลงในไฟล์ บรรทัดที่ 17 เราเขียนข้อมูลลงลในบัฟเฟอร์โดยตรงด้วยเมธอด put() ของออบเจกต์บัฟเฟอร์ บรรทัดที่ 18 เราใช้เมธอด flip() ของออบเจกต์บัฟเฟอร์เพื่อเลื่อนตัวชี้ตำแหน่งไปที่ตำแหน่งแรกของบัฟเฟอร์ บรรทัดที่ 19 เรากำหนดการวนรอบเพื่อเขียนข้อมูลลงในไฟล์โดยตรวจสอบจากเมธอด hasRemaining() ของออบเจกต์บัฟเฟอร์เพื่อดูว่ายังมีข้อมูลในบัฟเฟอร์หรือไม่ และในบรรทัดที่ 20 เป็นการเขียนข้อมูลจากบัฟเฟอร์ลงในไฟล์ด้วยเมธอด write() ของออบเจกต์ช่องทาง
สำหรับการอ่านข้อมูล เริ่มจากบรรทัดที่ 23 เราเลื่อนตัวชี้ข้อมูลของไฟล์มาที่ตำแหน่งเริ่มต้น บรรทัดที่ 24 เราใช้เมธอด clear() ของออบเจกต์บัฟเฟอร์เพื่อเตรียมบัฟเฟอร์ให้ว่างเพื่อรับข้อมูลที่เราอ่านจากไฟล์ บรรทัดที่ 25 เราอ่านข้อมูลจากไฟล์มาที่บัฟเฟอร์ด้วยเมธอด read() ของออบเจกต์ช่องทาง ซึ่งจะคืนค่าเป็นจำนวนไบต์ที่อ่านเข้ามา บรรทัดที่ 27 เป็นการกำหนดการวนรอบเพื่ออ่านข้อมูลจากไฟล์มาที่บัฟเฟอร์โดยถ้าค่าที่อ่านจากไฟล์เป็น -1 หมายถึงสิ้นสุดข้อมูลในไฟล์แล้ว บรรทัดที่ 28 เราใช้เมธอด flip() ของออบเจกต์บัฟเฟอร์เพื่อเลื่อนตัวชี้ตำแหน่งไปที่ตำแหน่งแรกของบัฟเฟอร์ บรรทัดที่ 29 เป็นการกำหนดการวนรอบเพื่ออ่านข้อมูลทีละไบต์จากบัฟเฟอร์โดยตรวจสอบว่ายังมีข้อมูลหรือไม่ด้วยเมธอด hasRemaining() ของออบเจกต์บัฟเฟอร์ และในบรรทัดที่ 30 เราอ่านข้อมูลจากบัฟเฟอร์ด้วยเมธอด get() ของออบเจกต์บัฟเฟอร์ ในบรรทัดที่ 32 เมื่ออ่านข้อมูลหมดแล้วเราจะทำให้บัฟเฟอร์ว่างเพื่อรอรับการอ่านข้อมูลจากไฟล์ในรอบถัดไปด้วยเมธอด clear() ของออบเจกต์บัฟเฟอร์ จากนั้นจึงอ่านข้อมูลเข้ามาใหม่ดังบรรทัดที่ 33 และปิดการใช้งานช่องทางด้วยเมธอด close() ของออบเจกต์ช่องทางในบรรทัดที่ 35
เมธอดของออบเจกต์ FileChannel นอกจากเมธอด position() ที่ใช้กำหนดหรือถามตำแหน่งตำแหน่งในไฟล์แล้ว ยังมีเมธอด size() เพื่อสอบถามขนาดของไฟล์ เมธอด truncate() เพื่อลดขนาดของไฟล์ และเมธอด force() เพื่อสั่งให้เขียนข้อมูลที่อาจจะยังค้างอยู่ที่บัฟเฟอร์หรือแคชลงในไฟล์
สำหรับการใช้งาน SocketChannel สามารถเชื่อมต่อช่องทางกับแหล่งข้อมูลได้โดยตรงดังตัวอย่างด้านล่าง ส่วนการอ่านและเขียนข้อมูลจะใช้งานเหมือนกันกับ FileChannel และปิดการใช้ช่องทางด้วยเมธอด close()
ServerSocketChannel เป็นช่องทางที่ใช้รอรับการเชื่อมต่อ (listening) ด้วยโปรโตคอล TCP เราสามารถเชื่อมต่อช่องทางกับซอกเก็ตได้ดังตัวอย่างด้านล่าง และเราจะต้องคอยตรวจสอบการเชื่อมต่อด้วยเมธอด accept() และปิดการใช้ช่องทางด้วยเมธอด close() ตัวอย่างเช่น
DatagramChannel เป็นช่องทางที่ใช้ส่งและรับแพคเกจของโปรโตคอล UDP เราสามารถเชื่อมต่อช่องทางกับซอกเก็ตได้ดังตัวอย่างด้านล่าง การส่งและรับข้อมูลใช้เมธอด send() และ receive() และปิดการใช้ช่องทางด้วยเมธอด close()
เราสามารถส่งหรือรับข้อมูลโดยตรงระหว่างช่องทางแบบ FileChannel กับช่องทางแบบอื่นด้วยเมธอด transferTo() และ transferFrom() ดังตัวอย่างด้านล่าง
Scatter และ Gather
java.nio รองรับการอ่านข้อมูลจากช่องทางไปที่หลายบัฟเฟอร์เรียกว่า scatter และ รองรับการเขียนข้อมูลจากหลายบัฟเฟอร์ไปที่ช่องทางเรียกว่า gather ซึ่งทำได้โดยใช้นำเอาบัฟเฟอร์มาใส่อาเรย์และเขียนหรืออ่านข้อมูลไปตามลำดับของบัฟเฟอร์ในอาเรย์
การใช้ scatter ทำได้โดยกำหนดบัฟเฟอร์ตามที่ต้องการแล้วนำมาบรรจุในอาเรย์จากนั้นสั่งอ่านข้อมูลจากอาเรย์ ดังตัวอย่างด้านล่าง ซึ่งจะเป็นการอ่านข้อมูลจากช่องทาง 128 ไบต์แรกไปที่บัฟเฟอร์ header และอ่านอีก 1024 ไบต์ไปที่บัฟเฟอร์ body ดังนั้นขนาดของข้อมูลจากช่องทางจะต้องเป็นตามที่กำหนด
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[ ] bufferArray = { header, body };
channel.read(bufferArray);
การใช้ gather ทำได้โดยกำหนดบัฟเฟอร์ตามที่ต้องการแล้วนำมาบรรจุในอาเรย์จากนั้นสั่งเขียนข้อมูลจากอาเรย์ ดังตัวอย่างด้านล่าง ซึ่งจะเป็นการเขียนข้อมูลจากบัฟเฟอร์ header ตามด้วยบัฟเฟอร์ body ซึ่งในกรณีของการเขียนข้อมูล ข้อมูลจะถูกเขียนไปยังช่องทางเท่าที่มี ดังนั้นขนาดของข้อมูลที่ใช้ในการเขียนข้อมูลจะยืดหยุ่นกว่าการอ่านข้อมูล ดังตัวอย่างด้านล่าง
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
Selector
ความสามารถในการทำซีเล็กเตอร์ (selector) คือการใช้เธรดเดียวเพื่อเฝ้าดูเหตุการณ์ (event) ที่จะเกิดกับช่องทางหลายๆช่องทางพร้อมกัน การใช้ซีเล็กเตอร์ทำให้เราใช้เธรดน้อย ซึ่งยิ่งเราใช้เธรดน้อยก็ยิ่งประหยัดทรัพยากรของเครื่องคอมพิวเตอร์
เราใช้งานซีเล็กเตอร์โดยการสร้างออบเจกต์ซีเล็กเตอร์ขี้นมาและลงทะเบียนออบเจกต์ช่องทางกับออบเจกต์ซีเล็กเตอร์ โดยออบเจกต์ช่องทางจะต้องถูกกำหนดให้เป็น non-blocking ดังนั้นจะไม่สามารถใช้ไฟล์เป็นแหล่งข้อมูลเพราะไฟล์ไม่สามารถกำหนดให้เป็น non-blocking
ตัวอย่างด้านล่าง ในบรรทัดที่ 15 เราสร้างออบเจกต์ช่องทางโดยใช้เมธอด open() ของออบเจกต์ SocketChannel ในบรรทัดที่ 16 เราเชื่อมต่อกับ TCP ซอกเก็ตโดยใช้เมธอด connect ของออบเจกต์ SocketChannel และระบุเครื่องเซอร์ฟเวอร์และพอร์ตที่ต้องการเชื่อมต่อด้วยการสร้างออบเจกต์ InetSocketAddress และส่งเป็นพารามิเตอร์ให้กับเมธอด connect ในบรรทัดที่ 17 เราสร้างออบเจกต์ซีเล็กเตอร์โดยใช้เมธอด open() ของคลาส Selector ในบรรทัดที่ 18 เรากำหนดให้ช่องทางเป็น non-blocking โดยใช้เมธอด configureBlocking ของออบเจกต์ SocketChannel (ซึ่งสืบทอดมาจากคลาส AbstractSelectableChannel) บรรทัดที่ 19 เราลงทะเบียนออบเจกต์ช่องทางกับออบเจกต์ซีเล็กเตอร์โดยใช้เมธอด register() ของออบเจกต์ SocketChannel โดยระบุออบเจกต์ซีเล็กเตอร์ที่จะใช้และเหตุการณ์ที่ต้องการเฝ้าซึ่งเรียกว่า interest set ซึ่งจะได้ผลลัพธ์เป็นออบเจกต์ SelectionKey
แต่ละออบเจกต์ช่องทางที่ลงทะเบียนกับออบเจกต์ซีเล็กเตอร์จะมีออบเจกต์ SelectionKey เป็นของตนเอง โดยออบเจกต์ SelectionKey จะมีข้อมูล Interest set และ ready set
interest set คือเหตุการณ์ที่จะเกิดขึ้นกับช่องทางที่เราต้องการให้ซีเล็กเตอร์คอยเฝ้าดู ซึ่งมี 4 ชนิดคือ SelectionKey.OP_CONNECT, SelectionKey.OP_
ACCEPT, SelectionKey.OP_READ และ SelectionKey.OP_WRITE ส่วน ready set คือจะบอกว่าเหตุการณ์ใดเกิดขึ้นแล้ว
เราสามารถตรวจสอบว่าออบเจกต์ SelectionKey มีเหตุการณ์อะไรบ้างใน
interest set ได้ดังตัวอย่างด้านล่าง บรรทัดที่ 21 เราอ่านค่า interest set ด้วยเมธอด interestOps() ของออบเจกต์ SelectionKey บรรทัดที่ 22-25เราเอาค่าที่ได้ไป AND กับ ค่าที่กำหนดแล้วเทียบดูว่าเท่ากับค่าที่กำหนดหรือไม่ ซึ่งได้ผลลัพธ์เป็น true หรือ false
เราสามารถตรวจสอบช่องทางที่พร้อมใช้งานด้วยเมธอด select() ของออบเจกต์ซีเล็กเตอร์ ซึ่งเมธอดจะตอบกลับเป็นจำนวนช่องทางที่พร้อมให้ใช้งาน
เราสามารถตรวจสอบว่าช่องทางของเราพร้อมใช้งานหรือไม่ด้วยเมธอดของออบเจกต์ SelectionKey ตามตัวอย่างด้านล่าง
ในการสอบถามช่องทางที่พร้อมใช้งานด้วยเมธอด select() ค่าที่ได้จะเป็นจำนวนช่องทางที่พร้อมใช้งานที่เพิ่มเติมมาจากการเรียกใช้เมธอดครั้งก่อน ดังนั้นจำนวนที่ได้อาจจะไม่ใช่จำนวนช่องทางที่พร้อมใช้งานทั้งหมด ถ้าเราต้องการวนรอบเพื่อตรวจสอบและดำเนินการใดหากช่องทางนั้นพร้อมใช้งาน สามารถทำได้โดยอ่านช่องทางที่ลงทะเบียนไว้จาก selected key set โดยใช้เมธอด selectedKeys() ของออบเจกต์ซีเล็กเตอร์และวนรอบเพื่อทดสอบว่าแต่ละช่องทางพร้อมหรือไม่ ตามตัวอย่างด้านล่าง
เราสามารถแนบออบเจกต์ไปกับออบเจกต์ SelectionKey ได้ด้วยเมธอด attach() ของออบเจกต์ SelectionKey ตัวอย่างเช่น
selectionKey.attach(theObject);
เราใช้เมธอด close() ของออบเจกต์ SelectionKey ในการปิดใช้งานออบเจกต์ SelectionKey
การอ่านและเขียนข้อมูลอักขระด้วย java.nio
ในหัวข้อนี้เราจะอ่าน เขียน และใช้งานบัฟเฟอร์ด้วยแพคเกจ java.nio สำหรับ java.nio เราเรียกแหล่งข้อมูลไม่ว่าจะเป็น ไฟล์ เครือข่าย (socket) หรือแหล่งข้อมูลแบบอื่นๆว่า channel โดยเราจะใช้คลาส java.nio.channel ในการเชื่อมต่อกับแหล่งข้อมูล
แพคเกจ java.nio จะดำเนินการกับข้อมูลเป็นบล็อกซึ่งต่างจาก java.io ที่ดำเนินการกับข้อมูลทีละอักขระหรือไบต์ ด้วยแพคเกจ java.nio เราสามารถใช้เพียงออบเจกต์เดียวในการอ่านและเขียนข้อมูลโดยสามารถอ่านหรือเขียนข้อมูลลงในบัฟเฟอร์ได้โดยไม่ต้องอาศัยออบเจกต์อื่นมาช่วย ต่างกันกับแพคเกจ java.io ที่เราต้องใช้ 2 ออบเจกต์เพื่ออ่านและเขียนข้อมูลแยกจากกันและต้องใช้ออบเจกต์ที่เป็นบัฟฟเฟอร์มาห่อหุ้มเพื่อช่วยในการทำบัฟเฟอร์
ตัวอย่างการอ่านไฟล์ข้อความโดยใช้คลาสจากแพคเกจ java.nio
จากบรรทัดที่ 12 เรากำหนดการจัดการข้อผิดพลาดด้วยคีย์เวิร์ด throws เพราะเป็นการอ่านและเขียนข้อมูลกับไฟล์ซึ่งเป็น excected exception
บรรทัดที่ 13 เราสร้างออบเจกต์ Path โดยใช้ เมธอด getDefault() จากคลาส FileSystems ของแพคเกจ java.nio เพื่ออ่านค่าพาธปัจจุบันที่โปรแกรมทำงานอยู่ เมธอด getDefault() จะคืนค่าเป็นออบเจกต์ FileSystem จากนั้นใช้เมธอด getPath() ของออบเจกต์ FileSystem เพื่อกำหนดพาธที่เราต้องการใช้งานซึ่งจะคืนค่าเป็นออบเจกต์ Path ให้กับตัวแปรเพื่อนำไปใช้งานต่อไป
บรรทัดที่ 14 – 16 ใช้เมธอด writeString() ของคลาส Files เพื่อเขียนข้อความลงในไฟล์ อย่างไรก็ตามในการเขียนข้อมูลแต่ละครั้งจะมีการเปิดไฟล์ เขียนข้อมูล ปิดไฟล์ ดังนั้นหากมีการเขียนข้อมูลจำนวนมาก ควรรวบรวมด้วยออบเจกต์ StringBuilder ก่อนค่อยสั่งเขียนลงไฟล์เพื่อช่วยเพิ่มประสิทธิภาพในการใช้งาน
บรรทัดที่ 17 เป็นการอ่านข้อความในไฟล์ด้วยเมธอด readAllLines() ของคลาส Files ซึ่งจะได้ผลลัพธ์เป็นออบเจกต์ List ที่มีสมาชิกเป็นข้อความแต่ละบรรทัดในไฟล์
การเขียนและอ่านข้อมูลแบบไบนารี่ด้วย java.nio
แพคเกจ java.nio จะเชื่อมต่อกับแหล่งข้อมูลด้วยคลาส Channel ซึ่งเราสามารถสร้างออบเจกต์ Channel ได้ด้วยเมธอด open ของคลาส Channel เอง หรือใช้เมธอด getChannel() ของคลาส FileInputStream หรือคลาส FileOutputStream หรือคลาส RandomAccessFile อย่างไรก็ตามออบเจกต์ Channel ที่สร้างด้วยเมธอด getChannel() สามารถอ่านได้อย่างเดียวหรือเขียนได้อย่างเดียวหรือได้ทั้งอ่านและเขียนขึ้นอยู่กับว่าเราใช้คลาสอะไรเป็นตัวตั้ง
ในตัวอย่างด้านล่างเราใช้อาเรย์ของไบต์เพื่อเตรียมข้อมูลที่ต้องการเขียนและหุ้มห่อด้วยคลาส ByteBuffer เพื่อกำหนดข้อมูลในบัฟเฟอร์ด้วยข้อมูลในอาเรย์ การหุ้มห่อด้วยคลาส ByteBuffer ทำให้อาเรย์ของไบต์กับบัฟเฟอร์เป็นเหมือนตัวเดียวกัน ดังนั้นการเปลี่ยนแปลงใดๆที่เกิดขึ้นกับบัฟเฟอร์หรือเกิดขึ้นกับอาเรย์ของไบต์จะมีผลถึงอีกส่วนหนึ่งด้วย
ในบรรทัดที่ 10 เราใช้คีย์เวิร์ด throws เพื่อจัดการกับข้อผิดพลาด IOException
บรรทัดที่ 11 สร้างออบเจกต์ Path เพื่อระบุไฟล์ที่ต้องการใช้งาน
บรรทัดที่ 12 สร้างออบเจกต์ Channel โดยใช้เมธอด open()โดยระบุพารามิเตอร์เป็นออบเจกต์ Path ทางเลือกเป็น APPEND เพื่อบอกว่าเขียนข้อมูลต่อข้อมูลที่มีอยู่ หรือถ้าไม่มีไฟล์ก็จะสร้างไฟล์ขึ้นมา
บรรทัดที่ 13 กำหนดข้อมูลที่ต้องการเขียนในอาเรย์ของไบต์
บรรทัดที่ 14 ห่อหุ้มอาเรย์ของไบต์ด้วยคลาส ByteBuffer เพื่อกำหนดข้อมูลในบัฟเฟอร์ด้วยข้อมูลในอาเรย์
บรรทัดที่ 15 เป็นการสั่งเขียนข้อมูลในบัฟเฟอร์ด้วยเมธอด write() ซึ่งออบเกจต์ Channel ชี้ไปที่ออบเจกต์ Path ซึ่งระบุที่อยู่ของไฟล์ ซึ่งคือการเขียนข้อมูลลงในไฟล์นั่นเอง
บรรทัดที่ 19 กำหนดออบเจกต์ Channel เพื่อการอ่านข้อมูลจากไฟล์ตามที่ระบุใน Path สังเกตุว่ากำหนดทางเลือกเป็น READ สังเกตว่าเราใช้ออบเจกต์ Channel ตัวเดิมเพียงแต่เปลี่ยนให้ทำหน้าที่อ่านข้อมูล
บรรทัดที่ 21 เป็นการอ่านข้อมูลจากไฟล์ตามที่กำหนดในออบเจกต์ Path มาเก็บที่บัฟเฟอร์โดยใช้เมธอด read() ซึ่งข้อมูลก็จะถูกปรับปรุงในอาเรย์ของไบต์ไปด้วย
บรรทัดที่ 22 – 28 เป็นการวนอ่านข้อมูลมาแสดงโดยต้องคาสต์ตามชนิดข้อมูลที่ต้องการเพราะข้อมูลที่อ่านเข้ามาเป็นไบนารี่
บรรทัดที่ 29 เป็นการสั่งปิดออบเจกต์ Channel ด้วยเมธอด close() หรือหากเราเขียนโปรแกรมด้วยรูปแบบ Try-with-resources ก็ไม่ต้องมีบรรทัดนี้
การจัดการไฟล์ด้วย java.nio
แพคเกจ java.nio มีคลาสที่ให้เราใช้จัดการกับพาธและไฟล์ เช่น สำเนาไฟล์/ไดเร็คกทอรี่ ลบไฟล์/ไดเร็คกทอรี่ ย้ายไฟล์/ไดเร็คกทอรี่ เป็นต้น โดยคลาส
หลักๆคือ คลาส Path คลาส FileSystems คลาส FileSystem และคลาส Files
พาธคือเส้นทางที่แสดงว่าไฟล์หรือไดเร็คทอรี่ที่เราต้องการอยู่ที่ไหน โดยประกอบด้วยโหนดซึ่งเป็นตัวแทนของ root ไดเร็คทอรี่ และไฟล์ เรียงต่อกันโดยคั่นแต่ละโหนดด้วยตัวแบ่งโหนดคือ / หรือ \ ขึ้นอยู่กับชนิดของระบบปฏิบัติการ ตัวอย่างเช่น c:\directory\subdirectory\file.txt มี c: เป็น root มี directory และ subdirectory เป็นไดเร็คทอรี่และมี file.txt เป็นไฟล์ หรือ /home/user/file.txt มี / เป็น root มี home เป็นไดเร็คทอรี่และมี file.txt เป็นไฟล์
เราสามารถอ้างอิงพาธได้ทั้งแบบ absolute และ relative โดยการอ้างอิงในแบบ absolute คือการอ้างอิงตั้งแต่ root เช่น c:\directory\subdirectory\file.txt หรือ /home/user/file.txt ส่วนการอ้างอิงแบบ relative เป็นการอ้างอิงจากไดเร็คทอรี่ปัจจุบันที่ทำงานอยู่ (working directory) เช่น subdirectory\file.txt หรือ user/file.txt
จากตัวอย่างที่ผ่านมาเราใช้การอ้างอิงพาธแบบ relative โดยเราสร้างออบเจกต์ Path โดยใช้ เมธอด getDefault() จากคลาส FileSystems ของแพคเกจ java.nio เพื่ออ่านค่าไดเร็คทอรี่ปัจจุบันที่โปรแกรมทำงานอยู่ซึ่งเมธอด getDefault() จะคืนค่าเป็นออบเจกต์ FileSystem จากนั้นใช้เมธอด getPath() ของออบเจกต์ FileSystem ซึ่งเป็นการกำหนดพาธแบบ relative เพื่อกำหนดพาธและไฟล์ที่เราต้องการใช้งานซึ่งจะคืนค่าเป็นออบเจกต์ Path ซึ่งเก็บพาธแบบสมบูรณ์เพื่อนำไปใช้งานต่อไป
สำหรับการอ้างอิงพาธแบบ absolute ใช้เมธอด get() ของคลาส Paths หรือ เมธอด of() ของคลาส Path ตัวอย่างเช่น
เราสามารถระบุพาธแบบ relative โดยใช้เมธอด get() ของคลาส Paths ดังตัวอย่างด้านล่าง โดยเราระบุ “.” ซึ่งหมายถึงไดเร็คทอรี่ปัจจุบันที่โปรแกรมทำงานอยู่ หรือ “..” หมายถึงไดเร็คทอรี่ก่อนหน้า
เราสามารถดูพาธแบบ absolute ได้ด้วยเมธอด toAbsolutePath() ของออบเจกต์ Path
ใช้เมธอด toAbsolutePath() ของออบเกจต์ Path เพื่อแสดงพาธแบบ absolute
ข้อแตกต่างระหว่างคลาส FileSystem และ คลาส FileSystems คือเราใช้ คลาส FileSystems ในการสร้างออบเจกต์ FileSystem และการใช้งานจะเป็นการใช้งานออบเจกต์ FileSystem สำหรับคลาส Path และคลาส Paths ก็เช่นเดียวกัน เราใช้คลาส Paths ในการสร้างออบเจกต์ Path และการใช้งานจะเป็นการใช้งานออบเจกต์ Path
เราสามารถใช้คลาส Path ของแพคเกจ java.nio แทนการใช้คลาส File ของแพคเกจ java.io ในการอ้างอิงถึงไฟล์ที่ต้องทำงานด้วย แต่ยังคงใช้คลาสต่างๆในการอ่าน เขียน และบัฟเฟอร์จากแพคเกจ java.io อย่างไรก็ตามถึงแม้เราจะใช้งานแค่คลาส Path แทนคลาส File แต่ก็มีการใช้งานคลาสอื่นในแพคเกจ java.nio ด้วย เช่น ใช้งานคลาส FileSystems เพื่อกำหนดพาธและไฟล์ที่จะใช้ ใช้งานคลาส Files เพื่อสร้างออบเจกต์ BufferedWriter และ BufferedReader หรือเพื่อสร้างออบเจกต์ InputStream และ OutputStream เป็นต้น
ตัวอย่างด้านล่างเป็นการอ่านและเขียนสตรีมของอักขระโดยใช้แพคคเกจ java.io แต่ใช้การอ้่างอิงพาธจากแพคเกจ java.nio จากบรรทัดที่ 12 เรากำหนดการจัดการข้อผิดพลาดด้วยคีย์เวิร์ด throws เช่นเดียวกับการการใช้ java.io เพราะเป็นการอ่านและเขียนข้อมูลกับไฟล์ซึ่งเป็น excected exception
บรรทัดที่ 14 เราสร้างออบเจกต์ Path เพื่อเก็บพาธและไฟล์ที่ต้องการใช้งาน
บรรทัดที่ 15 เป็นการเขียนข้อมูลแบบอักขระลงในไฟล์ด้วยด้วยออบเจกต์ BufferedWriter ของแพคเกจ java.io โดยการสร้างออบเจกต์ BufferedWriter โดยใช้เมธอด newBufferedWriter() ของคลาส Files จากเพคเกจ java.nio โดยระบุพารามิเตอร์ด้วยออบเจกต์ Path (filePath) และระบุทางเลือกเป็นเขียนต่อท้ายข้อความเดิมที่มีอยู่
บรรทัดที่ 20 เป็นการอ่านข้อมูลอักขระจากไฟล์เพื่อมาจัดการด้วยออบเจกต์ Scanner โดยระบุแหล่งข้อมูลของออบเจกต์ Scanner เป็นออบเจกต์ BufferedReader ของแพคเกจ java.io ซึ่งสร้างด้วยเมธอด newBufferedReader ของคลาส Files จากเพคเกจ java.nio
บรรทัดที่ 25 เป็นการอ่านข้อมูลด้วยออบเจกต์ BufferedReader โดยการสร้างออบเจกต์ BufferedReader ใช้เมธอด newBufferedReader() ของคลาส Files จากเพคเกจ java.nio โดยระบุพารามิเตอร์ด้วยออบเจกต์ Path (filePath) จากนั้นจึงวนรอบอ่านข้อมูลทีละบรรทัด
ตัวอย่างด้านเป็นการอ่านและเขียนสตรีมของไบต์โดยใช้แพคคเกจ java.io แต่ใช้การอ้่างอิงพาธจากแพคเกจ java.nio ในบรรทัดที่ 60 – 74 เป็นการประกาศคลาส Car เพื่อใช้สร้างออบเจกต์ Car
บรรทัดที่ 36 เป็นการเขียนออบเจกต์ Car ลงในไฟล์ โดยสร้างออบเจกต์ OutputStream ด้วยคลาส Files เมธอด newOutputStream ของแพคเกจ java.nio ซึ่งใช้ออบเจกต์ Path เป็นพารามิเตอร์ ผลลัพธ์ที่ได้คือ ออบเจกต์ OutputStream ของแพคเกจ java.io เพื่อส่งเป็นพารามิเตอร์ให้กับคลาส BufferedOutputStream ของแพคเกจ java.io ต่อไป
บรรทัดที่ 42 เป็นการอ่านออบเจกต์ Car จากไฟล์ โดยสร้างออบเจกต์ InputStream ด้วยคลาส Files เมธอด newInputStream ของแพคเกจ java.nio ซึ่งใช้ออบเจกต์ Path เป็นพารามิเตอร์ ผลลัพธ์ที่ได้คือ ออบเจกต์ InputStream ของแพคเกจ java.io เพื่อส่งเป็นพารามิเตอร์ให้กับคลาส BufferedInputStream ของแพคเกจ java.io ต่อไป
จากตัวอย่างจะเห็นการใช้ออบเจกต์ Path แทนการใช้ออบเจกต์ File ส่วนการดำเนินการส่วนที่เหลือจะเหมือนกันกับในตัวอย่างที่ใช้ออบเจกต์ File ของแพคเกจ java.io ไม่ว่าจะเป็นการอ่านหรือเขียนอักขระหรือออบเกจต์และการใช้บัฟเฟอร์เพื่อเพิ่มประสิทธิภาพการทำงาน