นากกายต้นฉบับรวมถั่วจากบัตร key ซ้ำสองใบลงในช่องเดียวบนกระดานมิ้นต์

Java Collectors.toMap: กำหนดชะตา key ซ้ำก่อน pipeline พังกลางทาง

พี่โชว์Engineering

การแปลง stream เป็น map ดูเหมือนเป็นงานตรงไปตรงมา จนข้อมูลสองรายการสร้าง key เดียวกัน overload สองอาร์กิวเมนต์ของ Collectors.toMap จะโยน IllegalStateException เมื่อชนกัน เพราะไลบรารีเดาไม่ได้ว่าเราต้องเก็บตัวแรก ตัวหลัง หรือรวมค่าครับ

ทางแก้ที่ดีไม่ใช่ catch exception รอบปลาย pipeline แต่คือเขียน merge policy ให้เป็นส่วนหนึ่งของ collector ตัวอย่างยอดขายนี้รวมจำนวนของ SKU เดียวกัน จึงใช้ Integer::sum อย่างตรงความหมาย

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {
    record Line(String sku, int quantity) {}

    static Map<String, Integer> quantities(List<Line> lines) {
        return lines.stream().collect(Collectors.toMap(
            Line::sku,
            Line::quantity,
            Integer::sum
        ));
    }

    public static void main(String[] args) {
        var totals = quantities(List.of(
            new Line("tea", 2),
            new Line("coffee", 1),
            new Line("tea", 3)
        ));
        assert totals.equals(Map.of("tea", 5, "coffee", 1));
    }
}

merge function รับ (oldValue, newValue) ถ้าต้องการ keep-first ใช้ (oldValue, ignored) -> oldValue; ถ้าต้องการ keep-last ใช้ (ignored, newValue) -> newValue แต่ทั้งสองแบบอาจซ่อนข้อมูลซ้ำที่จริงเป็น schema error เช่น user ID ที่ควร unique กรณีนั้นปล่อยให้ collector โยน หรือ validate แล้วคืน error ที่มีบริบทจะปลอดภัยกว่า

ถ้าต้องรักษาลำดับ insertion หรือเลือก implementation ของ map ใช้ overload สี่อาร์กิวเมนต์และส่ง supplier เช่น LinkedHashMap::new อย่าพึ่งพาชนิด map ของ overload ปกติ เพราะสัญญาไม่ได้รับรองชนิด ความเปลี่ยนแปลง และ thread safety

Recap

  • ตัดสินใจความหมายของ key ซ้ำก่อนเลือก merge function
  • ใช้ sum เฉพาะข้อมูลที่รวมกันได้จริง
  • อย่าใช้ keep-last กลบ violation ของ unique key
  • ส่ง map supplier เมื่อ downstream ต้องพึ่งลำดับหรือชนิด