Skip to content

บทที่ 7 — Collections + Generics

← บทที่ 6 | สารบัญ | บทที่ 8: Exceptions →

Array (int[], String[]) ที่เราใช้มาแล้วมีข้อจำกัด — ขนาดตายตัว, add/remove ยาก

Java มี Collections Framework — ชุด data structure ที่ใช้บ่อย: List, Set, Map ฯลฯ

หลังจบบท คุณจะ:

  • เลือก collection ที่ถูกต้องตามงาน (List vs Set vs Map)
  • ใช้ ArrayList, HashMap, HashSet, LinkedList ได้
  • เข้าใจ Generics — List<String> กับ <T> ทำงานยังไง
  • (ขั้นสูง — ข้ามได้สำหรับมือใหม่) รู้จัก wildcard (?, extends, super)
  • ใช้ iterator + for-each + แก้ collection ระหว่าง iterate ปลอดภัย

1. ทบทวน Array

java
int[] numbers = new int[5];      // ขนาดคงที่ 5 ตัว
numbers[0] = 10;
numbers[1] = 20;

int[] init = {10, 20, 30};       // กำหนดค่าเริ่มต้น

int len = numbers.length;        // ความยาว

for (int n : numbers) { ... }    // วน loop

ข้อจำกัด:

  • ขนาดตายตัว — ขยายไม่ได้
  • ไม่มี method ช่วย — ไม่มี add, remove, contains
  • เก็บ type เดียวint[] เก็บได้แค่ int

2. List — ตามลำดับ มี index

List คือ "array ที่ขยายได้ + มี method ช่วย"

2.1 ArrayList — ใช้บ่อยสุด

java
import java.util.ArrayList;
import java.util.List;

List<String> names = new ArrayList<>();
names.add("Anna");
names.add("Ben");
names.add("Cara");

System.out.println(names);           // [Anna, Ben, Cara]
System.out.println(names.size());    // 3
System.out.println(names.get(0));    // Anna
System.out.println(names.contains("Ben"));    // true

names.remove("Ben");
names.set(0, "Anna Updated");
names.clear();    // ลบทั้งหมด

Method ใช้บ่อย

java
list.add(e)              // เพิ่มต่อท้าย
list.add(index, e)       // เพิ่มที่ตำแหน่ง
list.get(index)          // ดึงค่า
list.set(index, e)       // แทนค่า
list.remove(index)       // ลบโดย index
list.remove(e)           // ลบโดยค่า (ตัวแรกที่เจอ)
list.size()              // ความยาว
list.isEmpty()           // ว่างไหม
list.contains(e)         // มีไหม
list.indexOf(e)          // ตำแหน่ง (หรือ -1)
list.clear()             // ล้าง

2.2 ทำไม List<String> ไม่ใช่ List

<String> = generic type — บอกว่า list เก็บ String

ถ้าไม่ใส่ → list เก็บ Object ทั่วไป เรียกว่า raw type

📖 raw type = List ที่ไม่ระบุชนิดข้อมูล — ไม่ปลอดภัยเพราะ compiler ไม่ตรวจ type ให้ อาจเกิด ClassCastException ตอนรัน (ดูตัวอย่างใน §5.1)

📖 ทำไม raw type ยังคอมไพล์ผ่านได้? — type erasure (อ่านเพิ่มเติม)

Java ใช้ type erasure — ข้อมูล generic type (<String> ฯลฯ) ถูกลบออกตอน compile ทำให้ List<String> และ List กลายเป็น List เหมือนกันใน bytecode นี่คือเหตุผลที่ raw type ยังคอมไพล์ผ่านได้แต่สูญเสีย type safety — compiler ไม่รู้ type จริง ๆ อีกต่อไป

กฎ: เขียน <Type> เสมอ — เช่น List<String>, List<Integer> — compiler จะบังคับให้ใส่เฉพาะ type ที่กำหนด (ดู §5 ตัวอย่าง raw type vs generics)

2.3 Diamond operator <>

java
List<String> names = new ArrayList<String>();   // เขียนเต็ม
List<String> names = new ArrayList<>();         // Java 7+ — Java เดา type ด้านขวา

ใช้แบบหลังเสมอ

2.4 Immutable list (Java 9+)

java
List<String> immutableNames = List.of("Anna", "Ben", "Cara");
immutableNames.add("Dee");    // ❌ UnsupportedOperationException

List.of สร้าง list ที่ "เปลี่ยนไม่ได้" — เหมาะกับค่าคงที่

2.5 LinkedList vs ArrayList

📖 อ่านตาราง Big-O (สั้น ๆ — ไม่ต้องเรียน CS formal — เคยเห็นใน O(n²) บทที่ 2 แล้ว ตรงนี้คือคำอธิบายเต็ม):

  • O(1) = "คงที่" — เร็วเท่าเดิมไม่ว่าจะมี 10 ตัวหรือ 1 ล้านตัว
  • O(n) = "แปรตามจำนวน" — มี 1 ล้านตัว = ทำงานช้ากว่ามี 10 ตัว ~ 100,000 เท่า
  • O(log n) = "ครึ่งทุกรอบ" — เร็วมาก (1 ล้านตัว = แค่ ~20 รอบ)
  • O(n²) = "n คูณ n" — มี 1,000 ตัว ทำงาน ~ 1,000,000 รอบ (ช้า) — ไม่ปรากฏในตารางด้านล่าง กล่าวถึงเพื่อครบชุด Big-O Overview
ArrayListLinkedList
ภายในarraydoubly linked list (รายการเชื่อมโยงสองทิศทาง — แต่ละโหนดเชื่อมทั้งหน้าและหลัง)
get(i)O(1) เร็วO(n) ช้า
add() ท้ายO(1)O(1)
add(0, e) หัวO(n) ช้าO(1) เร็ว*
remove(i) กลางO(n)O(n) (ต้อง traverse)

* Big-O ของ LinkedList ดูดี แต่ในทางปฏิบัติมักช้ากว่า ArrayList เพราะ pointer-chasing ทำให้เกิด cache miss ทุก node — ดูเพิ่มใน tip ด้านล่าง

Default ใช้ ArrayList — เร็วในกรณีส่วนใหญ่ ใช้ LinkedList เมื่อจำเป็นต้อง add/remove หัวบ่อย ๆ

💡 ในทางปฏิบัติ ถ้าต้องการ add/remove หัว-ท้ายเร็ว ควรพิจารณา ArrayDeque แทน LinkedList เพราะ cache-friendly กว่า (LinkedList มี pointer overhead ทุก node ทำให้ใช้ memory มากกว่าและเข้าถึงข้อมูลช้ากว่า)


3. Set — ไม่ซ้ำ ไม่มีลำดับ (ส่วนใหญ่)

java
import java.util.HashSet;
import java.util.Set;

Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("spring");
tags.add("java");    // ซ้ำ — ไม่ add

System.out.println(tags);          // e.g. [java, spring] — ลำดับ NOT guaranteed (อาจได้ [spring, java] ก็ได้)
System.out.println(tags.size());   // 2
System.out.println(tags.contains("java"));   // true
tags.remove("java");

Set แบบต่าง ๆ

Classลำดับความเร็ว
HashSetไม่มีเร็วสุด (O(1))
LinkedHashSetตามลำดับที่ insertเกือบเท่า HashSet
TreeSetเรียงตาม natural orderO(log n)
java
import java.util.TreeSet;

Set<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.add(2);
System.out.println(sorted);    // [1, 2, 3]

ใช้เมื่อไหร่

  • "อยากเก็บแค่ค่าที่ไม่ซ้ำ" → Set
  • "อยากเช็คว่า X อยู่ในชุดไหม" → Set (เร็วกว่า List)
  • "อยาก unique + เรียง" → TreeSet

4. Map — key → value

java
import java.util.HashMap;
import java.util.Map;

Map<String, Integer> ages = new HashMap<>();
ages.put("Anna", 25);
ages.put("Ben", 30);
ages.put("Cara", 28);

System.out.println(ages.get("Anna"));    // 25
System.out.println(ages.get("Zoe"));     // null (ไม่มี key)
System.out.println(ages.containsKey("Ben"));    // true
System.out.println(ages.size());         // 3
ages.remove("Ben");

4.1 Iterate

📖 Lambda -> คืออะไร — ตลอดบทนี้จะเห็น syntax (x, y) -> ... ซึ่งคือ "function สั้น ๆ ที่บอกว่ารับอะไรแล้วทำอะไร" เช่น (name, age) -> System.out.println(name) แปลว่า "รับ name และ age แล้ว print name" — จะเรียนละเอียดในบทที่ 9 ตอนนี้แค่อ่านให้ออกก็พอ

วิธีอ่าน lambda เร็ว ๆ (พอสำหรับบทนี้):

  • (a, b) -> body = "รับ a และ b แล้วทำ body"
  • name -> System.out.println(name) = "รับ name แล้ว print"
  • (a, b) -> Integer.compare(a, b) = "รับ a, b แล้วเปรียบเทียบ"
java
// loop keys
for (String name : ages.keySet()) {
    System.out.println(name);
}

// loop values
for (int age : ages.values()) {
    System.out.println(age);
}

// loop entries (key + value) — ใช้บ่อยสุด
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

// Java 8+ forEach
ages.forEach((name, age) -> System.out.println(name + " = " + age));
// ↑ (name, age) -> ... คือ "lambda" — function สั้น ๆ ที่บอกว่า "รับอะไร แล้วทำอะไร"
//   จะเรียนละเอียดในบทที่ 9 — ตอนนี้อ่านเป็น "สำหรับแต่ละ (name, age) ทำ ..."

4.2 getOrDefault

java
int age = ages.getOrDefault("Zoe", 0);   // 0 ถ้าไม่มี key

4.3 putIfAbsent + computeIfAbsent

java
import java.util.ArrayList;   // ต้องอยู่บรรทัดบนสุดของไฟล์

ages.putIfAbsent("Anna", 99);    // ไม่ทำอะไรเพราะมี Anna แล้ว

Map<String, List<String>> grouping = new HashMap<>();
grouping.computeIfAbsent("vowels", k -> new ArrayList<>()).add("a");
grouping.computeIfAbsent("vowels", k -> new ArrayList<>()).add("e");
// grouping = {vowels=[a, e]}

📌 Counting pattern: count.put(w, count.getOrDefault(w, 0) + 1) มีรูปสั้นกว่าคือ count.merge(w, 1, Integer::sum)Integer::sum คือ shortcut ของ (a, b) -> a + b

  • ถ้าใช้ HashMap ใน single-thread (1 thread) ทั้งสองแบบใช้ได้เหมือนกัน
  • ถ้าใช้ ConcurrentHashMap ใน multi-thread (หลาย thread): getOrDefault + put ไม่ atomic (ทำสำเร็จในคำสั่งเดียวโดยไม่มีการขัดจังหวะ) จะเกิด race condition (เมื่อหลาย thread แข่งกันแก้ข้อมูลพร้อมกัน ทำให้ผลลัพธ์ผิด) — ใช้ merge แทน รายละเอียดใน §4.9 และ บทที่ 12

4.4 Map แบบต่าง ๆ

Classลำดับ keyความเร็ว
HashMapไม่มีO(1) เร็วสุด
LinkedHashMapตามลำดับ insertเกือบเท่า HashMap
TreeMapเรียงตาม keyO(log n)

4.5 Immutable map (Java 9+)

java
Map<String, Integer> fixed = Map.of(
    "Anna", 25,
    "Ben", 30
);
// แก้ไม่ได้

4.6 TreeMap — เรียง key + navigation

java
import java.util.TreeMap;

TreeMap<Integer, String> tm = new TreeMap<>();
tm.put(10, "a"); tm.put(20, "b"); tm.put(30, "c"); tm.put(40, "d");

// navigation method
tm.firstKey();              // 10
tm.lastKey();               // 40
tm.floorKey(25);            // 20  (key มากสุดที่ <= 25)
tm.ceilingKey(25);          // 30  (key น้อยสุดที่ >= 25)
tm.lowerKey(20);            // 10  (key มากสุดที่ < 20 — strict)
tm.higherKey(20);           // 30  (key น้อยสุดที่ > 20 — strict)

// sub-range (view — แก้ต้นทาง = ผลกระทบ)
tm.subMap(15, 35);          // {20=b, 30=c}
tm.headMap(30);             // {10=a, 20=b}
tm.tailMap(30);             // {30=c, 40=d}

// descending
tm.descendingMap();         // {40=d, 30=c, 20=b, 10=a}

ใช้ทำ: time-series lookup, "nearest event", routing table, leaderboard

4.7 LinkedHashMap — LRU Cache เขียนได้ใน 5 บรรทัด

⚠️ ส่วนนี้ใช้ Generics (<K, V>) ที่จะอธิบายใน §5 — ถ้ายังไม่คุ้นสัญลักษณ์ K, V ข้ามได้แล้วกลับมาหลังอ่าน §5

LRU (Least Recently Used — ของที่ใช้ล่าสุดน้อยสุด) cache คือ "ที่เก็บของจำกัดขนาด — เมื่อเต็มแล้วจะเตะตัวที่ไม่ได้ใช้นานสุดออก"

เปรียบเหมือน ชั้นวางหนังสือ 3 ช่อง — ถ้าอยากวางเล่มที่ 4 แต่เต็มแล้ว ก็เอาเล่มที่ไม่ได้หยิบนานที่สุดออกก่อน แล้วค่อยวางเล่มใหม่เข้าไป

LinkedHashMap จำลำดับ insert ปกติ — แต่ถ้าใช้ "access order" + override removeEldestEntry → ได้ LRU cache:

java
import java.util.LinkedHashMap;
import java.util.Map;

// ⚠️ NOT thread-safe — single-thread only. Multi-thread ใช้ Caffeine แทน
public class LruCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    public LruCache(int capacity) {
        super(capacity, 0.75f, true);              // true = access order (เรียงตามลำดับที่เข้าถึงล่าสุด แทนที่จะเรียงตามลำดับที่ใส่เข้ามา)
        // ↑ argument 3 ตัว: (ความจุเริ่มต้น, load factor = 0.75 คือ default ที่สมดุล, access order)
        this.capacity = capacity;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;                  // เกินแล้วทิ้งตัวเก่าสุด
    }
}

LruCache<String, String> cache = new LruCache<>(3);
cache.put("a", "1"); cache.put("b", "2"); cache.put("c", "3");
cache.get("a");                                    // access → a ขึ้นเป็นใหม่
cache.put("d", "4");                               // b ถูกเตะออก (เก่าสุดที่ไม่ได้ access)
System.out.println(cache);                         // {c=3, a=1, d=4}

สำหรับ production cache → ใช้ Caffeine library (เร็วกว่า, มี TTL (Time To Live — กำหนดอายุของข้อมูลใน cache), มี stats (สถิติการใช้งาน เช่น hit rate)) — ดูบทที่ 11 ของ Spring Boot หมายเหตุ: Caffeine (github.com/ben-manes/caffeine) เป็น third-party library ไม่ได้มากับ Java — ต้องเพิ่ม dependency ใน Maven/Gradle ก่อนใช้

4.8 EnumMap + EnumSet — เร็วและประหยัดเมื่อ key เป็น enum

📖 enum = type ที่มีค่าได้แค่ชุดที่กำหนด เช่น สี, วัน, สถานะ — ประกาศว่า enum Color { RED, GREEN, BLUE } แล้วใช้ Color.RED ตอนเรียกค่า Java บังคับให้ใส่แค่ค่าที่กำหนดไว้เท่านั้น (เรียนเรื่อง enum เต็ม ๆ ในบทที่ 10)

java
import java.util.EnumMap;
import java.util.EnumSet;

enum Color { RED, GREEN, BLUE }   // ⬅ ตัวอย่างต่อ ๆ ไปในส่วนนี้ใช้ Color enum ตัวนี้

// EnumMap — ใช้ array ภายใน → เร็วและกินที่น้อยกว่า HashMap
EnumMap<Color, Integer> count = new EnumMap<>(Color.class);
count.put(Color.RED, 10);
count.put(Color.GREEN, 5);
count.getOrDefault(Color.BLUE, 0);                 // 0

// EnumSet — bitfield (เก็บข้อมูลเป็น bit 0/1 แทนที่จะเก็บเป็น object — ประหยัดหน่วยความจำและเร็วกว่ามาก) ภายใน → เร็วมาก
EnumSet<Color> primary = EnumSet.of(Color.RED, Color.BLUE);
EnumSet<Color> all = EnumSet.allOf(Color.class);
EnumSet<Color> none = EnumSet.noneOf(Color.class);
EnumSet<Color> notRed = EnumSet.complementOf(EnumSet.of(Color.RED));

primary.contains(Color.RED);                       // true (O(1) bit test)

💡 ถ้า key เป็น enum เสมอใช้ EnumMap/EnumSet — เร็วกว่า HashMap/HashSet 5-10 เท่า

4.9 ConcurrentHashMap — Map สำหรับ multi-thread

java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;  // (เรียนบทที่ 12)

ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();

// ❌ ไม่ปลอดภัย — read-modify-write 2 ตัว ไม่ atomic
int v = counts.getOrDefault("a", 0);
counts.put("a", v + 1);

// ✅ atomic
counts.merge("a", 1, Integer::sum);                // ไม่มี → 1; มี → +1
counts.compute("a", (k, v) -> (v == null ? 0 : v) + 1);

// ถ้าต้องการ AtomicInteger ต้องประกาศ Map แยก (AtomicInteger ≠ Integer)
ConcurrentHashMap<String, AtomicInteger> atomicCounts = new ConcurrentHashMap<>();
atomicCounts.computeIfAbsent("b", k -> new AtomicInteger()).incrementAndGet();

ConcurrentHashMap ไม่ใช่แค่ "HashMap ที่ใส่ synchronized" — ใช้ lock striping (แบ่ง map ออกเป็นส่วน ๆ แต่ละส่วนมี lock ของตัวเอง ทำให้หลาย thread เขียนคนละส่วนได้พร้อมกัน) → หลาย thread เขียนคนละ bucket ได้พร้อมกัน

⚠️ ConcurrentHashMap ไม่รับ null key หรือ null valueput(null, v) หรือ put(k, null) จะ throw NullPointerException ทันที (ต่างจาก HashMap ที่รับ null ได้) ต้องตรวจ null ก่อน put เสมอ

⚠️ lambda ใน compute/merge คืน null = ลบ key ออก — ถ้า lambda คืน null แทนค่า ConcurrentHashMap จะ remove key นั้นทันที ระวัง conditional return ที่อาจคืน null โดยไม่ตั้งใจ

⚠️ ConcurrentHashMap.size() ไม่ใช่ atomic — ค่าที่ได้อาจไม่แม่นยำถ้ามี thread อื่น add/remove อยู่พร้อมกัน ถ้าต้องการ exact count ให้ใช้ LongAdder แยกต่างหาก (เรียนบทที่ 12)

รายละเอียดเพิ่มใน บทที่ 12 — Concurrency

4.10 ⚠️ Pitfall: ใช้ object เป็น key

ถ้าใช้ class ของเราเป็น key → ต้อง override equals + hashCode ไม่งั้นจะหาไม่เจอ:

java
class User {
    String id;
    User(String id) { this.id = id; }   // constructor
    // ไม่ override equals/hashCode
}

Map<User, String> map = new HashMap<>();
map.put(new User("123"), "Anna");
map.get(new User("123"));    // null! (Java เทียบ reference, ไม่เจอ)

แก้: override equals + hashCode (หรือใช้ record — บทที่ 10)

⚠️ กับดักที่เกี่ยวข้องกัน: == ใน Java เทียบ reference (คนละ object ในหน่วยความจำถือว่าไม่เท่ากัน แม้ค่าข้างในจะเหมือนกัน) ไม่ใช่เทียบค่า — แม้แต่ String ก็เช่นกัน new String("a") == new String("a") ได้ false เพราะเป็นคนละ object แต่ "a".equals("a") ได้ true เสมอ (เทียบค่าจริง) กฎจำง่าย ๆ: ต้องการเทียบว่า "ค่าเหมือนกันไหม" ให้ใช้ .equals() เสมอ ห้ามใช้ == กับ object (ยกเว้นตั้งใจเช็คว่าเป็น object ตัวเดียวกันจริง ๆ)


4.11 Queue / Deque / Stack — เมื่อ "ลำดับ" สำคัญ

Queue — FIFO (เข้าก่อนออกก่อน)

java
import java.util.Queue;
import java.util.ArrayDeque;

Queue<String> q = new ArrayDeque<>();               // ArrayDeque ดีกว่า LinkedList สำหรับ Queue — cache-friendly กว่า
q.offer("a"); q.offer("b"); q.offer("c");          // เพิ่มท้าย
String first = q.poll();                            // "a" — ลบ + return หัว
String peek = q.peek();                             // "b" — ดู ไม่ลบ
boolean empty = q.isEmpty();

⚠️ add/remove/element throw exception ถ้าเต็ม/ว่าง; offer/poll/peek return null/false — ใน production เลือก offer/poll/peek เสมอ

Deque + ArrayDeque — Queue 2 ทาง + Stack ที่เร็วกว่า

📖 Deque อ่านว่า "deck" — ย่อมาจาก double-ended queue (คิวสองทาง)

java
import java.util.ArrayDeque;
import java.util.Deque;

Deque<Integer> dq = new ArrayDeque<>();

// ใช้เป็น Queue (FIFO)
dq.offerLast(1); dq.offerLast(2);
dq.pollFirst();                                     // 1

// ใช้เป็น Stack (LIFO) — เร็วกว่า java.util.Stack
dq.push(10);                                        // = addFirst
dq.push(20);
dq.pop();                                           // 20 = removeFirst
dq.peek();                                          // 10

⚠️ ห้ามใช้ java.util.Stack — เป็น legacy (Vector subclass, synchronized ทุก method โดยไม่จำเป็น ทำให้ช้า) — ใช้ Deque แทน

PriorityQueue — heap (ตัวเล็กสุดออกก่อน)

java
import java.util.PriorityQueue;
import java.util.Comparator;
// ⚠️ โค้ดทั้งหมดนี้ต้องอยู่ใน method เช่น public static void main(String[] args) { ... }

PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(3); minHeap.offer(1); minHeap.offer(4); minHeap.offer(1); minHeap.offer(5);
while (!minHeap.isEmpty()) System.out.print(minHeap.poll() + " ");   // 1 1 3 4 5

// max-heap — ใส่ comparator กลับ (Comparator = ตัวบอกวิธีเรียง — ดูหัวข้อถัดไป §4.12)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

// priority task scheduler
// ใช้ class ธรรมดาเก็บ task — record แบบย่อจะเรียนบทที่ 10
class Task {
    String name; int priority;
    Task(String name, int priority) { this.name = name; this.priority = priority; }
}
PriorityQueue<Task> tasks = new PriorityQueue<>(Comparator.comparingInt(t -> t.priority));
tasks.offer(new Task("clean", 5));
tasks.offer(new Task("fire alarm!", 1));
tasks.offer(new Task("email", 3));
tasks.poll();                                       // fire alarm! (priority 1)

ใช้ทำ: Dijkstra/A* (อัลกอริทึมหาเส้นทางสั้นสุด), task scheduler (คิวงานตามความเร่งด่วน เช่น เคสฉุกเฉินก่อน), top-K (หา K อันดับสูงสุด เช่น 10 คะแนนสูงสุด), event simulator

⚠️ PriorityQueue ไม่ thread-safe — multi-thread ใช้ PriorityBlockingQueue


4.12 Comparable vs Comparator — เรียงยังไง?

Comparable<T> — "natural order" ของ class

java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

record Person(String name, int age) implements Comparable<Person> {
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);    // เรียงตามอายุ default
    }
}

List<Person> people = new ArrayList<>(List.of(
    new Person("Anna", 25),
    new Person("Ben", 18),
    new Person("Cara", 32)
));
Collections.sort(people);                           // ใช้ natural order
// หรือ (อ่านชัดกว่า)
people.sort(Comparator.naturalOrder());             // แนะนำแทน people.sort(null) ที่ดูเหมือน bug

กฎ compareTo:

  • < 0 → this < other
  • = 0 → equal
  • > 0 → this > other
  • ⚠️ อย่าเขียน a - b ตรง ๆ — ถ้า a = Integer.MIN_VALUE และ b = 1 → overflow → ได้ค่าบวกแทนลบ → sort พัง ใช้ Integer.compare(a, b) เสมอ (เทียบโดยไม่ subtract)
    • สำหรับ long ใช้ Long.compare(a, b) — สำหรับ double ใช้ Double.compare(a, b) (อย่าใช้ a - b กับ double: double ไม่ overflow แต่มีปัญหาอื่น — NaN - x = NaN แล้ว cast เป็น int ได้ 0 แสดงว่า equal ทั้งที่ไม่ใช่; และ -0.0 กับ +0.0 เท่ากันด้วย == แต่ควร sort ต่างกัน — Double.compare จัดการทุกกรณีนี้ให้)

Comparator<T> — order ภายนอก (ไม่ผูกกับ class)

java
import java.util.Comparator;

Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> byAgeDesc = Comparator.comparingInt(Person::age).reversed();
Comparator<Person> byNameThenAge = Comparator
    .comparing(Person::name)
    .thenComparingInt(Person::age);

people.sort(byName);
people.sort(byAgeDesc);

// nullsFirst / nullsLast
Comparator<String> safe = Comparator.nullsFirst(Comparator.naturalOrder());

// reverse
Comparator<Integer> desc = Comparator.reverseOrder();

เลือกตัวไหน?

ComparableComparator
อยู่ใน class❌ — ภายนอก
มีแค่ตัวเดียวต่อ class✅ (natural order)❌ — มีได้หลายตัว
ใช้เมื่อorder "เป็นธรรมชาติของ class" (เช่น Date, Integer)order ตาม use case (เรียงตามชื่อ/อายุ/...)

4.13 Unmodifiable view vs Immutable

java
List<String> mutable = new ArrayList<>(List.of("a", "b"));

// 1. Immutable factory (Java 9+) — มี element เป็น immutable
List<String> immut = List.of("a", "b");
immut.add("c");                                    // ❌ UnsupportedOperationException

// 2. Unmodifiable wrapper — view ที่แก้ไม่ได้ แต่ source ยังแก้ได้
List<String> view = Collections.unmodifiableList(mutable);
view.add("c");                                     // ❌ throw
mutable.add("c");                                  // ✅ OK — และ view เห็นด้วย!

// 3. Copy แล้ว unmod — Java 10+
List<String> copy = List.copyOf(mutable);          // independent + immutable
mutable.add("d");                                  // copy ไม่กระทบ

⚠️ unmodifiableList(x) ไม่ได้ copy — เป็นแค่ wrapper; ถ้าต้องการ "ของจริง" ใช้ List.copyOf(x)


5. Generics — type parameter

📖 Generics คืออะไร — analogy: Template หรือ "แม่พิมพ์" ที่รับ type เป็น argument

  • เทียบ TypeScript: function<T>(x: T): T { ... } — function ที่รับ type T เข้ามา
  • เทียบ Python (3.12+): def foo[T](x: T) -> T: ...
  • ใน Java: <T> = placeholder ของ type ที่จะ "แทน" ตอนใช้งาน

เปรียบกับเครื่องเอทีเอ็ม: "เครื่องนี้รับบัตรอะไรก็ได้" → ตอนใช้ระบุไปว่ารับบัตรของ "ธนาคาร X" → กลายเป็นเครื่องรับบัตรธนาคาร X เฉพาะ เครื่อง = generic class / บัตร = type parameter <T>

<T> คืออะไรกันแน่?

5.1 ปัญหาก่อนมี Generics (Java 1.4 ลงไป)

📖 "raw type" = ใช้ collection โดยไม่ใส่ <...> — ปล่อยให้มันยอมรับอะไรก็ได้ (Object) คำว่า "raw" หมายถึง "ดิบ" (ยังไม่ได้ specify type)

java
List names = new ArrayList();        // ⬅ raw type — ไม่บอกว่าเก็บอะไร
names.add("Anna");
String s = (String) names.get(0);    // ต้อง cast เพราะ get() คืน Object

// ถ้าใส่ผิด:
names.add(123);
String s2 = (String) names.get(1);   // ❌ ClassCastException ตอน run

⚠️ raw type ยังคอมไพล์ผ่านใน Java 25 — แต่ compiler จะเตือน unchecked ทุกครั้ง หลีกเลี่ยง @SuppressWarnings("unchecked") เพื่อปิดปาก เว้นแต่พิสูจน์ได้แน่ว่า cast type-safe จริง ๆ (และต้องเพิ่ม comment อธิบายเหตุผล)

  • @SuppressWarnings มาจาก java.langไม่ต้อง import เป็น annotation ที่มีใน Java ทุก version (annotation เรียนละเอียดบทที่ 14)
  • ถ้าปิด warning เงียบ ๆ จะเกิด ClassCastException ตอน run โดยไม่มีคำเตือน — เขียน <Type> ให้ถูกตั้งแต่แรกดีกว่า

5.2 Generics แก้ปัญหา (Java 5+)

java
List<String> names = new ArrayList<>();
names.add("Anna");
names.add(123);    // ❌ compile error — เจอตั้งแต่ก่อน run
String s = names.get(0);    // ไม่ต้อง cast

5.3 สร้าง class แบบ generic เอง

java
public class Box<T> {
    private T content;
    
    public void set(T content) {
        this.content = content;
    }
    
    public T get() {
        return content;
    }
}

// ใช้
Box<String> sBox = new Box<>();
sBox.set("hello");
String s = sBox.get();

Box<Integer> iBox = new Box<>();
iBox.set(42);
int n = iBox.get();

T = type parameter — placeholder ที่ใช้เป็น type จริงตอน new

⚠️ ข้อจำกัดสำคัญของ generics ใน Java (มาจาก type erasure ที่อธิบายไปแล้ว §2.2): เพราะ compiler ลบข้อมูล <T> ออกตอน compile runtime จึงไม่รู้ว่า T คือ type อะไรจริง ๆ ผลที่ตามมา:

  • เขียน new T() ข้างใน generic class/method ไม่ได้ — compiler ไม่รู้ว่าจะ new อะไร
  • ClassCastException (ถ้ามี) จะเกิด ตอน get ค่าออกมา ไม่ใช่ตอน add ผิด — เพราะตอน add ไม่มีการตรวจ type จริงที่ runtime
  • เก็บ primitive (int, double) ตรง ๆ ใน generic ไม่ได้ — ต้องใช้ตัวห่อ (wrapper) อย่าง Integer, Double เสมอ (เช่น List<Integer> ไม่ใช่ List<int>) ซึ่งมี overhead จากการ boxing/unboxing

Convention:

  • T — Type
  • E — Element (ใน collection)
  • K, V — Key, Value (ใน map)
  • N — Number
  • R — Result

5.4 Generic method

java
public static <T> T firstNonNull(T a, T b) {
    return a != null ? a : b;
}

String s = firstNonNull(null, "default");
Integer n = firstNonNull(null, 42);

5.5 Bounded type — จำกัด type

java
public static <T extends Number> double sum(List<T> nums) {
    double total = 0;
    for (T n : nums) {
        total += n.doubleValue();
    }
    return total;
}

sum(List.of(1, 2, 3));        // int (extends Number) ✅
sum(List.of(1.5, 2.5));       // double ✅
sum(List.of("a", "b"));       // ❌ String ไม่ extend Number

5.6 Wildcard (?, extends, super)

ลึกเกินสำหรับมือใหม่ — รู้ว่าเจอแบบนี้คืออะไรพอ เปิดดูเมื่อพร้อม:

📖 Wildcard รายละเอียด (ขั้นสูง — ข้ามได้รอบแรก)
java
// ? = unknown type
List<?> anything;        // อ่านได้, เขียนไม่ได้

// ? extends X = X หรือลูกของ X
List<? extends Number> readOnly;

// ? super X = X หรือพ่อของ X
List<? super Integer> writeOnly;

PECS rule: Producer (ผู้ผลิต/ส่งออก) Extends, Consumer (ผู้บริโภค/รับเข้า) Super

  • extends = method ที่ อ่านข้อมูลออกจาก list (list ทำหน้าที่ผลิต/produce ข้อมูลให้)
  • super = method ที่ เขียนข้อมูลเข้าไปใน list (list ทำหน้าที่บริโภค/consume ข้อมูล)

ตัวอย่างสั้น ๆ:

java
// ฝั่งอ่าน → extends;  ฝั่งเขียน → super
void copyNumbers(List<? extends Number> src, List<? super Number> dest) {
    for (Number n : src) dest.add(n);
}
// ? super Number หมายถึง List ที่ยอมรับ Number (หรือ subtype ของ Number) เข้าไปได้
// เช่น List<Number> หรือ List<Object> ก็รับได้ — type system รับประกันว่าใส่ Number เข้าไปได้เสมอ

6. Iterator + concurrent modification

6.1 Iterator

ทุก collection มี Iterator:

java
import java.util.Iterator;

List<String> names = new ArrayList<>(List.of("Anna", "Ben", "Cara"));
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    String name = it.next();
    System.out.println(name);
}

for-each จริง ๆ ใช้ iterator ข้างใต้

6.2 ⚠️ ห้ามแก้ collection ระหว่าง for-each

java
List<String> names = new ArrayList<>(List.of("Anna", "Ben", "Cara"));
for (String name : names) {
    if (name.equals("Ben")) {
        names.remove(name);    // ❌ ConcurrentModificationException
    }
}

แก้: ใช้ iterator + iterator.remove():

java
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().equals("Ben")) {
        it.remove();    // ✅
    }
}

หรือใช้ removeIf (Java 8+):

java
names.removeIf(name -> name.equals("Ben"));

6.3 Fail-fast vs Fail-safe iterator

Fail-fast (ล้มเร็ว — โยน error ทันทีถ้ามีการแก้ไขระหว่าง iterate) vs Fail-safe (ปลอดภัยจากความผิดพลาด — ไม่โยน error แต่อาจเห็นข้อมูลเก่า)

Fail-fastFail-safe
ทำอะไรเมื่อ collection ถูกแก้ระหว่าง iteratethrow ConcurrentModificationException ทันทีเงียบ — iterate ต่อโดยไม่ throw
Collection ที่ใช้ArrayList, HashMap, HashSet, LinkedHashMapCopyOnWriteArrayList (snapshot จริง), ConcurrentHashMap, ConcurrentSkipListMap (weakly consistent — อาจเห็นหรือไม่เห็นการแก้ที่เกิดระหว่าง iterate ไม่รับประกัน แต่ไม่โยน error)
ภายในทำยังไงนับ modCount (ตัวนับจำนวนครั้งที่ collection ถูกแก้ไข — ถ้าค่าเปลี่ยนระหว่าง iterate = มีการแก้ไขเกิดขึ้น) — ถ้าเปลี่ยน throwดูรายละเอียดด้านล่าง
Memoryน้อยสูงกว่า (เก็บ snapshot)
ใช้กรณีsingle-thread, "อยากรู้ทันทีถ้า bug"multi-thread อ่านบ่อย-เขียนน้อย

รายละเอียด "ภายในทำยังไง" ของฝั่ง Fail-safe (แยกเป็นข้อ ๆ เพราะแต่ละ class ทำงานคนละแบบ):

  • CopyOnWriteArrayList — iterate บน snapshot ของ array จริง ๆ (copy ตอนเริ่ม iterate) จึงไม่เห็นการแก้ไขที่เกิดขึ้นระหว่างนั้นเลย
  • ConcurrentHashMap / ConcurrentSkipListMap — เป็น weakly consistent แปลว่า "อาจเห็นหรือไม่เห็นการแก้ไขที่เกิดระหว่าง iterate ก็ได้ ไม่มีการรับประกัน" แต่ไม่ throw error แน่นอน
java
import java.util.concurrent.CopyOnWriteArrayList;

// Fail-fast — ArrayList
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) list.remove(s);              // ❌ CME

// Fail-safe — CopyOnWriteArrayList
List<String> safe = new CopyOnWriteArrayList<>(List.of("a", "b", "c"));
for (String s : safe) safe.remove(s);              // ✅ ไม่ throw
                                                    // แต่ iteration เห็นของเดิม (snapshot)

💡 fail-fast เป็น "best-effort detection" — ไม่ guarantee 100% ใน multi-thread; เป็น diagnostic tool ไม่ใช่ safety feature


7. Conversion ระหว่าง collection

ในงานจริงเราต้องแปลงไปมาระหว่างชนิด collection บ่อย — array↔List, List↔Set, List↔Map ส่วนนี้รวมวิธีแปลงที่ใช้บ่อย พร้อมข้อควรระวัง (เช่น Arrays.asList ได้ list ที่ขนาดตายตัว):

Array ↔ List

java
import java.util.Arrays;

// array → list
Integer[] arr = {1, 2, 3};
List<Integer> list = Arrays.asList(arr);         // mutable (set ได้) แต่ size ตายตัว — ดู Pitfall ด้านล่าง
List<Integer> mutable = new ArrayList<>(Arrays.asList(arr));   // mutable + ขยายได้

⚠️ Pitfall: Arrays.asList ได้ list ที่ขนาดตายตัว

  • list.set(i, v) — แก้ค่าได้ปกติ และแก้ array ต้นทางด้วย (wrap โดยตรง ไม่ copy)
  • list.add(...) / list.remove(...) — ❌ UnsupportedOperationException เพราะ size ตายตัว
  • ถ้าต้องการ list ที่ add/remove ได้ ให้ new ArrayList<>(Arrays.asList(arr)) เสมอ
java
// ⚠️ Pitfall: Arrays.asList(int[]) ไม่ได้คืน List<Integer>!
// int[] arr2 = {1, 2, 3};
// Arrays.asList(arr2) → คืน List<int[]> (list มี 1 ตัวคือ array ทั้งก้อน ไม่ใช่ list ของตัวเลข)
// แก้: ใช้ Integer[] เสมอ ไม่ใช่ int[] ถ้าจะแปลงเป็น List<Integer>

// list → array
List<String> names = new ArrayList<>(List.of("a", "b"));
String[] arr2 = names.toArray(new String[0]);

List ↔ Set

java
List<String> list = new ArrayList<>(List.of("a", "b", "a", "c"));
Set<String> set = new HashSet<>(list);    // [a, b, c] (deduplicate)

List<String> back = new ArrayList<>(set);

Map → entries

java
Set<Map.Entry<String, Integer>> entries = ages.entrySet();

7.5 Sequenced Collections (Java 21+) — API ใหม่สำหรับ "ของที่มีลำดับ"

ก่อน Java 21 — ทำงานกับ "ตัวแรก/ตัวสุดท้าย" ของ collection ที่มีลำดับลำบาก:

java
// แบบเก่า — ยุ่งและไม่ uniform
list.get(0);                              // first ของ List
list.get(list.size() - 1);                // last ของ List
((LinkedHashSet<String>) set).iterator().next();   // first ของ LinkedHashSet — งง
deque.peekFirst();                        // first ของ Deque (อีก API)

Java 21 (JEP 431) เพิ่ม interface ใหม่ ที่ครอบทุก collection ที่ "มีลำดับ":

  • SequencedCollection<E> — เพิ่ม getFirst, getLast, addFirst, addLast, removeFirst, removeLast, reversed
  • SequencedSet<E> — interface ที่สืบทอดทั้ง SequencedCollection และ Set; LinkedHashSet เป็น SequencedSet (มีลำดับ + ไม่ซ้ำ) ส่วน List เป็น SequencedCollection เหมือนกัน (มี getFirst/getLast ใช้ได้) แต่ไม่ใช่ SequencedSet เพราะ List อนุญาตค่าซ้ำได้ (ไม่ใช่ Set)
  • SequencedMap<K,V> — เพิ่ม firstEntry, lastEntry, putFirst, putLast, reversed, sequencedKeySet, ...

ตัวอย่าง

java
List<String> names = new ArrayList<>(List.of("Anna", "Ben", "Cara"));
names.getFirst();              // "Anna"   (เดิม: names.get(0))
names.getLast();               // "Cara"   (เดิม: names.get(names.size()-1))
names.addFirst("Zoe");         // ใส่หัว — [Zoe, Anna, Ben, Cara]
names.addLast("Dee");          // ใส่ท้าย
names.removeFirst();
List<String> rev = names.reversed();   // view กลับด้าน (lazy — ไม่ copy)

LinkedHashSet<Integer> seen = new LinkedHashSet<>(List.of(1, 2, 3));
seen.getFirst();               // 1 (ลำดับที่ insert)
seen.getLast();                // 3

LinkedHashMap<String, Integer> ages = new LinkedHashMap<>();
ages.put("Anna", 25);
ages.put("Ben", 30);
ages.firstEntry();             // Anna=25
ages.lastEntry();              // Ben=30
ages.reversed();               // SequencedMap view กลับด้าน

TreeMap<String, Integer> sorted = new TreeMap<>();
// TreeMap ก็เป็น SequencedMap — first/last = key เล็กสุด/ใหญ่สุด

ทำไมสำคัญ

  • API uniform — ไม่ต้องจำว่า List ใช้ .get(0), Deque ใช้ .peekFirst(), LinkedHashSet ต้อง cast
  • .reversed() เป็น view — O(1) ไม่ copy, แก้ต้นทาง = ผลกระทบทันที
  • Collections.unmodifiableSequencedCollection(...) ห่อให้ immutable ได้

8. Collections utility class

คลาส Collections (มี s) รวมเมธอด static ช่วยจัดการ collection ที่ใช้บ่อย — เรียง (sort), กลับด้าน (reverse), สุ่ม (shuffle), หาค่ามาก/น้อย ไม่ต้องเขียน loop เอง:

java
import java.util.Collections;

List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5));

Collections.sort(nums);             // เรียงน้อยไปมาก
Collections.reverse(nums);          // กลับด้าน
Collections.shuffle(nums);          // สุ่ม
Collections.max(nums);              // หา max
Collections.min(nums);              // หา min
Collections.frequency(nums, 1);     // นับว่า 1 มีกี่ตัว

// custom sort
nums.sort(Comparator.reverseOrder());          // มากไปน้อย
// หรือ: nums.sort((a, b) -> Integer.compare(b, a));  // ห้ามเขียน b - a (overflow ได้ — ดู §4.12)

9. Checkpoint

🛠️ Checkpoint 7.1 — Word count

รับ string จาก args[0] แล้วนับว่าแต่ละคำเจอกี่ครั้ง

ทดสอบ: java Main "the quick brown fox the lazy dog" ผลลัพธ์:

text
the=2
quick=1
brown=1
fox=1
lazy=1
dog=1
📖 เฉลย
java
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        String[] words = args[0].split(" ");
        Map<String, Integer> count = new HashMap<>();
        
        for (String w : words) {
            count.merge(w, 1, Integer::sum);
            // เทียบเท่า:
            // count.put(w, count.getOrDefault(w, 0) + 1);
        }
        
        count.forEach((word, n) -> System.out.println(word + "=" + n));
    }
}

🛠️ Checkpoint 7.2 — Unique numbers

รับเลขจาก args (หลายตัว) เก็บเฉพาะตัวไม่ซ้ำ + เรียง

ทดสอบ: java Main 3 1 4 1 5 9 2 6 5 3[1, 2, 3, 4, 5, 6, 9]

📖 เฉลย
java
import java.util.TreeSet;

public class Main {
    public static void main(String[] args) {
        TreeSet<Integer> unique = new TreeSet<>();
        for (String s : args) {
            unique.add(Integer.parseInt(s));
        }
        System.out.println(unique);
    }
}

TreeSet เรียงให้อัตโนมัติ + ไม่ซ้ำ — perfect!

🛠️ Checkpoint 7.3 — Phone book

สร้าง class PhoneBook ที่:

  • add(name, number) — เพิ่ม
  • lookup(name) — return number หรือ null
  • delete(name) — ลบ
  • listAll() — แสดงทั้งหมดเรียงตามชื่อ
📖 เฉลย
java
import java.util.TreeMap;

public class PhoneBook {
    private TreeMap<String, String> contacts = new TreeMap<>();
    
    public void add(String name, String number) {
        contacts.put(name, number);
    }
    
    public String lookup(String name) {
        return contacts.get(name);
    }
    
    public void delete(String name) {
        contacts.remove(name);
    }
    
    public void listAll() {
        contacts.forEach((name, num) -> 
            System.out.println(name + ": " + num));
    }
}

TreeMap เรียงตาม key อัตโนมัติ

🛠️ Checkpoint 7.4 — Generic Stack

สร้าง class GenericStack<T> ของเราเอง (ตั้งชื่อ GenericStack เพื่อไม่ชนกับ java.util.Stack ที่มีอยู่แล้ว):

  • push(T item)
  • pop() — return T, ลบจาก top
  • peek() — return T, ไม่ลบ
  • isEmpty()

(ภายในใช้ ArrayList หรือ LinkedList)

📖 เฉลย
java
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

public class GenericStack<T> {
    private List<T> items = new ArrayList<>();
    
    public void push(T item) {
        items.add(item);
    }
    
    public T pop() {
        if (isEmpty()) throw new NoSuchElementException("empty");
        return items.remove(items.size() - 1);
    }
    
    public T peek() {
        if (isEmpty()) throw new NoSuchElementException("empty");
        return items.get(items.size() - 1);
    }
    
    public boolean isEmpty() {
        return items.isEmpty();
    }
    
    public int size() {
        return items.size();
    }
}

// ใช้
public class Main {
    public static void main(String[] args) {
        GenericStack<Integer> stack = new GenericStack<>();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack.pop());    // 3
        System.out.println(stack.peek());   // 2
    }
}

10. สรุปบท

List = ลำดับ + index → ArrayList (default), LinkedList (เฉพาะ add/remove หัวบ่อย)
Set = ไม่ซ้ำ → HashSet (เร็ว), LinkedHashSet (insertion order), TreeSet (เรียง + navigation)
Map = key→value → HashMap, LinkedHashMap (insertion/access order — LRU!), TreeMap (เรียง + floor/ceiling)
EnumMap/EnumSet — เร็วและประหยัดเมื่อ key เป็น enum (เสมอใช้แทน HashMap/HashSet)
Queue/DequeArrayDeque แทน Stack (legacy); offer/poll/peek แทน add/remove/element
PriorityQueue — heap; min-heap default, max-heap = Comparator.reverseOrder()
ConcurrentHashMap — multi-thread; ใช้ merge/compute แทน get-then-put
Comparable (natural order ใน class) vs Comparator (order ภายนอก, มีหลายตัวได้)
Generics <T> — type safety ตอน compile, ไม่ต้อง cast; PECS: extends = ผู้ผลิต, super = ผู้บริโภค
Immutable vs unmodifiable view: List.of/List.copyOf = ของจริง; Collections.unmodifiableList = wrapper เห็นการแก้ source
Sequenced Collections (Java 21+) — getFirst/getLast/reversed() ที่ uniform
✅ ห้ามแก้ collection ระหว่าง for-each — ใช้ iterator.remove(), removeIf, หรือ CopyOnWriteArrayList (fail-safe)
✅ ใช้ object เป็น key ใน Map → ต้อง override equals + hashCode (หรือใช้ record)

→ ไปบทที่ 8: Exception Handling