นากกายกำลังติดลิ้นชักข้อมูลเฉพาะให้แม่พิมพ์แต่ละชิ้นโดยไม่ปะปนกัน

Java ClassValue: ผูก cache กับ Class โดยไม่สร้าง static map ถือ class loader ไว้

พี่โชว์Engineering

Framework ที่คำนวณ metadata จาก Class<?> มักเริ่มด้วย static ConcurrentHashMap<Class<?>, ...> แต่ map นั้นอาจถือ class และ class loader ไว้นานกว่าที่ต้องการครับ ClassValue<T> ผูกค่ากับ Class โดย runtime จัด lifecycle ให้เหมาะกับการ unload มากกว่า map ที่เราเป็นเจ้าของเอง

WHY: cache key นี้มี lifecycle พิเศษ

get(type) คำนวณแบบ lazy ผ่าน computeValue เมื่อยังไม่มีค่า แล้วคืนค่าที่ผูกกับ Class นั้น การ remove(type) ทำให้ครั้งถัดไปคำนวณใหม่ เหมือนแม่พิมพ์แต่ละชิ้นมีลิ้นชักของตัวเอง แทนการทำสมุดกลางที่ลืมลบชื่อแม่พิมพ์เก่า

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
  static final AtomicInteger computes = new AtomicInteger();

  static final ClassValue<String> SIMPLE_NAME = new ClassValue<>() {
    @Override
    protected String computeValue(Class<?> type) {
      computes.incrementAndGet();
      return type.getSimpleName();
    }
  };

  public static void main(String[] args) {
    assert SIMPLE_NAME.get(String.class).equals("String");
    assert SIMPLE_NAME.get(String.class).equals("String");
    assert computes.get() == 1 : computes.get();

    assert SIMPLE_NAME.get(Integer.class).equals("Integer");
    assert computes.get() == 2 : computes.get();

    SIMPLE_NAME.remove(String.class);
    assert SIMPLE_NAME.get(String.class).equals("String");
    assert computes.get() == 3 : computes.get();
  }
}

Runtime อาจเรียก computeValue มากกว่าหนึ่งครั้งภายใต้ race แต่จะ publish ค่าเดียวให้ get สำเร็จ ดังนั้นการคำนวณควรไม่มี side effect สำคัญและผลควรเทียบเท่ากัน อย่าเพิ่ม billing record หรือแก้ระบบภายนอกจากเมธอดนี้

ClassValue เหมาะกับ metadata ที่ derive จาก Class เช่น serializer plan หรือ reflection lookup ไม่เหมาะกับ cache ที่ต้อง enumerate ทุก key, จำกัดขนาดรวม หรือ expire ตามเวลา เพราะ API ตั้งใจซ่อน mapping ถ้าต้องการ policy เหล่านั้นให้ใช้ cache ที่ออกแบบมาสำหรับงานนั้น

Recap

  • ใช้ ClassValue กับค่าที่ผูก lifecycle กับ Class
  • ทำ computeValue ให้ idempotent และไม่มี side effect สำคัญ
  • เรียก remove เมื่อข้อมูลต้องคำนวณใหม่
  • ใช้ cache อื่นหากต้อง enumerate, TTL หรือ capacity limit