โหมดมืด
บทที่ 7 — Collections + Generics
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"); // ❌ UnsupportedOperationExceptionList.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
| ArrayList | LinkedList | |
|---|---|---|
| ภายใน | array | doubly 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 order | O(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 ถ้าไม่มี key4.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 | เรียงตาม key | O(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 value —put(null, v)หรือput(k, null)จะ throwNullPointerExceptionทันที (ต่างจาก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/elementthrow exception ถ้าเต็ม/ว่าง;offer/poll/peekreturn 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();เลือกตัวไหน?
| Comparable | Comparator | |
|---|---|---|
| อยู่ใน 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); // ไม่ต้อง cast5.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— TypeE— Element (ใน collection)K,V— Key, Value (ใน map)N— NumberR— 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 Number5.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-fast | Fail-safe | |
|---|---|---|
| ทำอะไรเมื่อ collection ถูกแก้ระหว่าง iterate | throw ConcurrentModificationException ทันที | เงียบ — iterate ต่อโดยไม่ throw |
| Collection ที่ใช้ | ArrayList, HashMap, HashSet, LinkedHashMap | CopyOnWriteArrayList (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,reversedSequencedSet<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 หรือ nulldelete(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, ลบจาก toppeek()— 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/Deque — ArrayDeque แทน 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)