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

Java Math.clamp: บีบค่าให้อยู่ในช่วงในบรรทัดเดียว และแปลง long เป็น int แบบไม่ล้น

น้องกายEngineering

แอปที่รับค่า pageSize=999999 จากลิงก์ แล้วส่งตรงเข้า query ฐานข้อมูล คือวิธีทำให้เซิร์ฟเวอร์หอบได้ง่ายที่สุดวิธีหนึ่ง ทางกันพื้นฐานคือ “บีบค่า” ให้อยู่ในช่วงที่ยอมรับได้ เช่น 1 ถึง 100 ตั้งแต่ Java 21 มีเมธอด Math.clamp ทำเรื่องนี้ในบรรทัดเดียวครับ งานนี้ถูกเพิ่มผ่าน JDK-8301226

WHY: ทำไมไม่เขียน max/min ซ้อนกันเหมือนเดิม

สูตรเดิมคือ Math.max(min, Math.min(max, x)) ใช้ได้ แต่มีจุดพลาดที่เจอบ่อย:

  • สลับ max กับ min แล้วได้ผลผิดแบบเงียบๆ ไม่มี error
  • ถ้าช่วงเพี้ยน (min > max) เพราะ config ผิด สูตรเดิมก็ยังคืนค่าออกมา ไม่บอกใคร
  • อ่านแล้วต้องหยุดคิดว่าตัวไหนครอบตัวไหน

Math.clamp(value, min, max) เขียนชื่อสิ่งที่ตั้งใจทำตรงๆ และตามเอกสาร ถ้า min > max จะโยน IllegalArgumentException ทันที เหมือนรั้วกั้นที่ติดตั้งผิดด้านแล้วส่งเสียงเตือน แทนที่จะปล่อยรถผ่าน

HOW: 4 แบบที่มีให้ใช้

ตามเอกสาร Java 21 มี 4 overload:

รูปแบบใช้เมื่อ
int clamp(long value, int min, int max)แปลง long เป็น int แบบไม่ล้น (saturation)
long clamp(long value, long min, long max)ค่าขนาดใหญ่ เช่น byte, millisecond
double clamp(double value, double min, double max)เปอร์เซ็นต์ ความทึบ ระดับเสียง
float clamp(float value, float min, float max)กราฟิก เกม

แบบแรกน่าสนใจที่สุด เอกสารบอกว่าเพราะขอบเขตเป็น int ผลลัพธ์จึงอยู่ในช่วง int เสมอ ใช้แปลง long เป็น int อย่างปลอดภัยได้ ต่างจากการ cast (int) ที่ถ้าค่าเกินจะวนไปเป็นเลขแปลกๆ

จุดที่ต้องรู้กับ double

  • ถ้า value เป็น NaN ผลก็เป็น NaN (ไม่ถูกบีบ) — ต้องเช็ก NaN เองถ้าค่ามาจากผู้ใช้
  • ถ้า min หรือ max เป็น NaN จะโยน IllegalArgumentException
  • เมธอดนี้ถือว่า -0.0 เล็กกว่า 0.0 จริงๆ เช่น clamp(-0.0, 0.0, 1.0) คืน 0.0

โค้ดพร้อม assertions

public class Main {
    static int safePageSize(long requested) {
        // ลิงก์อาจส่งเลขอะไรก็ได้ บีบให้อยู่ 1..100 และได้ int เสมอ
        return Math.clamp(requested, 1, 100);
    }

    static double volume(double v) {
        if (Double.isNaN(v)) return 0.5; // clamp ไม่จัดการ NaN ให้
        return Math.clamp(v, 0.0, 1.0);
    }

    public static void main(String[] args) {
        assert safePageSize(999_999L) == 100;
        assert safePageSize(0L) == 1;
        assert safePageSize(25L) == 25;
        assert safePageSize(Long.MAX_VALUE) == 100;

        long big = 5_000_000_000L;
        assert (int) big != Integer.MAX_VALUE;          // cast ตรงๆ ได้เลขเพี้ยน
        assert Math.clamp(big, Integer.MIN_VALUE, Integer.MAX_VALUE) == Integer.MAX_VALUE;

        assert volume(1.7) == 1.0;
        assert volume(-3.0) == 0.0;
        assert volume(Double.NaN) == 0.5;
        assert Double.isNaN(Math.clamp(Double.NaN, 0.0, 1.0));

        boolean threw = false;
        try {
            Math.clamp(5L, 10L, 1L); // min > max: config ผิด
        } catch (IllegalArgumentException e) {
            threw = true;
        }
        assert threw;

        System.out.println("ok");
    }
}

ตอนตรวจบทความ โค้ดนี้คอมไพล์ด้วย javac และรันด้วย java -ea (เปิด assertions) บน JDK 21 ของเครื่องนี้

ใช้ตรงไหนในงานจริง

งานตัวอย่าง
API paginationpageSize จาก query string บีบ 1..100
ตั้งค่าจากไฟล์ configtimeout, retry count ไม่ให้ต่ำหรือสูงเกิน
UI/เกมความทึบ 0.0..1.0, ตำแหน่งตัวละครไม่ให้หลุดจอ
แปลงขนาดไฟล์/เวลาlong → int แบบไม่ล้น

ข้อควรระวัง: clamp คือ “ปรับให้อยู่ในช่วง” ไม่ใช่ “ตรวจว่าถูกต้อง” ถ้าค่าที่ผิดควรถูกปฏิเสธพร้อมบอกผู้ใช้ (เช่น จำนวนเงินโอน) อย่า clamp เงียบๆ ให้ตอบ error แทน

Recap

  • Java 21 ขึ้นไป ใช้ Math.clamp(value, min, max) แทน max/min ซ้อนกัน
  • ใช้ Math.clamp(longValue, intMin, intMax) เพื่อแปลง long เป็น int แบบไม่ล้น
  • จำไว้ว่า min > max จะโยน IllegalArgumentException — ดีต่อการจับ config ผิด
  • ค่า double ที่เป็น NaN ไม่ถูกบีบ ต้องเช็กเอง
  • งานเงินหรือค่าที่ผิดแล้วต้องแจ้งผู้ใช้ อย่า clamp เงียบๆ
  • รันโค้ดด้านบนด้วย java -ea เพื่อเปิด assertions