Java Objects.checkIndex: ตรวจ index ก่อนแตะ array และ ByteBuffer
โค้ดอ่าน array มักมีเงื่อนไข index < 0 || index >= length ซ้ำหลายที่ ถ้าบางจุดเผลอใช้ > แทน >= เราจะปล่อยค่าเท่ากับ length ผ่าน ทั้งที่ index สุดท้ายคือ length - 1 Objects.checkIndex ทำ contract 0 <= index < length ให้เป็นมาตรฐานและคืน index เดิมเมื่อผ่าน จึงวางใน expression ได้ครับ
WHY: ขอบเขตคือรางลิ้นชัก
ให้นึกถึงลิ้นชักเรียงบนราง ถ้ามีห้าช่อง หมายเลขที่ใช้ได้คือศูนย์ถึงสี่ ค่าเป็นลบหลุดซ้าย และค่าห้าแตะตัวหยุดขวาแต่ไม่ใช่ลิ้นชัก การใช้ helper มาตรฐานทำให้ทุกจุดตีความรางแบบเดียวกันและโยน IndexOutOfBoundsException เมื่อผิด
HOW: ตรวจครั้งเดียวตรง boundary
import java.util.Objects;
public class Main {
static byte readByte(byte[] data, int index) {
return data[Objects.checkIndex(index, data.length)];
}
public static void main(String[] args) {
byte[] data = {10, 20, 30};
assert readByte(data, 0) == 10;
assert readByte(data, 2) == 30;
boolean rejectedLength = false;
try {
readByte(data, data.length);
} catch (IndexOutOfBoundsException expected) {
rejectedLength = true;
}
assert rejectedLength;
boolean rejectedNegative = false;
try {
readByte(data, -1);
} catch (IndexOutOfBoundsException expected) {
rejectedNegative = true;
}
assert rejectedNegative;
System.out.println("Objects.checkIndex checks passed");
}
}
การคืน index เดิมทำให้เขียน data[Objects.checkIndex(index, data.length)] ได้ แต่ถ้า expression ยาวจนอ่านยาก แยก int checked = ... ก็ชัดกว่า เป้าหมายคือรวม validation ไม่ใช่ย่อทุกอย่างเหลือบรรทัดเดียว
เลือก helper ให้ตรงช่วง
checkIndex(index, length) ใช้กับสมาชิกเดียว checkFromToIndex(from, to, length) ใช้ช่วงครึ่งเปิด [from, to) และยอมให้ from == to เป็นช่วงว่าง ส่วน checkFromIndexSize(from, size, length) ใช้เมื่อรับจุดเริ่มกับจำนวนสมาชิก ความต่างสำคัญเพราะ from + size อาจ overflow หากคำนวณเอง Helper มาตรฐานจัด contract และ exception ให้สม่ำเสมอกว่า
อย่าใช้ checkIndex กับจำนวน byte ที่จะอ่านหลายตัวแล้วตรวจแค่จุดเริ่ม ตัวอย่างเช่นอ่าน integer สี่ byteที่ index สุดท้ายอาจผ่านจุดเริ่มแต่ล้นท้าย ต้องใช้ checkFromIndexSize(index, 4, length)
Exception ภายในกับ API ภายนอก
ใน library ภายใน IndexOutOfBoundsException บอก programming error ได้ดี แต่ HTTP/API boundary ไม่ควรปล่อย stack trace หรือชื่อ class ภายในออกไป แปลงเป็น validation response ที่บอก field และช่วงที่ยอมรับโดยไม่สะท้อนข้อมูลลับทั้งหมด และเก็บ cause ไว้ใน log ที่ควบคุมการเข้าถึง
การตรวจ index ไม่แก้ race หาก array/list ที่อ้าง length เปลี่ยนระหว่าง check กับ access Array มี length คงที่แต่เนื้อหาอาจถูกแก้ข้าม thread ส่วน List ขนาดอาจเปลี่ยน ต้องคุม ownership หรือ lock operation ทั้งชุด อย่าคิดว่า helper ตัวเดียวทำให้ thread-safe
Recap
- สมาชิกเดียวใช้
checkIndex:0 <= index < length - ช่วง
[from, to)ใช้checkFromToIndex - จุดเริ่ม + จำนวน ใช้
checkFromIndexSize - ตรวจทั้ง operation ไม่ใช่แค่ byte แรกของข้อมูลหลาย byte
- แปลง exception ที่ external boundary และอย่าเผย stack trace
ลองแทน boundary check ที่ซ้ำที่สุดหนึ่งจุด แล้วเพิ่ม test ค่า -1, 0, length - 1 และ length ครับ สี่ค่านี้จับ off-by-one ได้เกือบทุกครั้ง