นากสีมิ้นต์ต้นฉบับกำลังใส่กล่องใบใหม่ลงช่องว่างในตู้ลิ้นชักสีเขียวอ่อน

Java computeIfAbsent: สร้างค่าตอนจำเป็นโดยไม่ซ่อน side effect

พี่โชว์Engineering

โค้ด grouping แบบ get-check-put เปิดช่องให้สร้าง list ซ้ำและทำให้ intent กระจายหลายบรรทัดครับ Map.computeIfAbsent รวมการตรวจ key กับการสร้างค่าไว้ที่ boundary เดียว แต่คำว่า absent รวมทั้งไม่มี mapping และ mapping ที่ค่าเป็น null จึงต้องเข้าใจ contract ก่อนใช้เป็น cache

WHY: factory ควรสร้างค่า ไม่ควรขับโลกภายนอก

mapping function จะถูกเรียกเมื่อ key ยังไม่มีค่าที่ไม่ใช่ null และจะ insert เฉพาะผลที่ไม่ใช่ null เหมือนตู้เก็บของที่เรียกช่างมาสร้างกล่องเมื่อช่องว่าง แต่ถ้าช่างกลับมามือเปล่า ช่องนั้นก็ยังว่าง

factory ที่ทำ network call, ส่ง event หรือแก้ map เดิมซ้อนอยู่ทำให้ retry และ concurrency คาดเดายาก ใช้ฟังก์ชันสร้าง object ที่เล็กและไม่มี side effect แล้วทำ I/O ในชั้น service ที่เห็น failure policy ชัดกว่า

HOW: grouping แบบอ่าน intent ได้ทันที

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    static Map<String, List<String>> group(List<String> words) {
        Map<String, List<String>> byInitial = new HashMap<>();
        for (String word : words) {
            String key = word.substring(0, 1);
            byInitial.computeIfAbsent(key, ignored -> new ArrayList<>()).add(word);
        }
        return byInitial;
    }

    public static void main(String[] args) {
        var grouped = group(List.of("mint", "map", "cache"));
        assert grouped.get("m").equals(List.of("mint", "map"));
        assert grouped.get("c").equals(List.of("cache"));

        var map = new HashMap<String, String>();
        map.computeIfAbsent("missing", key -> null);
        assert !map.containsKey("missing");
    }
}

ตัวอย่างทำงานดีเพราะแต่ละ iteration สร้าง list เฉพาะครั้งแรก ส่วน add เกิดหลัง compute จบ ไม่มีการแก้ map เดิมใน factory

HashMap ไม่ใช่ cache พร้อมใช้

default method ของ Map ไม่รับประกัน atomicity สำหรับ implementation ทั่วไป หากหลาย thread เขียนต้องอ่านสัญญาของ concrete map เช่น ConcurrentHashMap และจำไว้ว่าการคำนวณอาจ block key อื่นตาม implementation งานหนักควรใช้ cache library ที่รองรับ eviction, timeout, failure และ metrics

หาก null เป็นค่าธุรกิจที่มีความหมาย computeIfAbsent แยกไม่ได้ระหว่าง cached null กับ absent ใช้ Optional หรือ sentinel type ที่ชัดเจน และอย่าปล่อย cache โตไม่จำกัดเพียงเพราะ syntax สั้น

Recap

  • computeIfAbsent เรียก factory เมื่อไม่มีค่าที่ไม่ใช่ null
  • ผล null จะไม่ถูกบันทึก
  • จำกัด factory ให้เล็ก เร็ว และไม่มี side effect
  • ตรวจ atomicity จาก concrete Map ไม่ใช่เดาจากชื่อ method
  • cache production ต้องมี eviction และ failure policy เพิ่ม

เริ่มจาก grouping ใน memory ที่ single-threaded ก่อน แล้วค่อยขยายไป cache หลังเขียน concurrency contract ครับ