1. Giới thiệu
Trong đời sống thực tế, ta thường gặp tình huống mỗi “khóa” (key) duy nhất cần được ánh xạ tới một “giá trị” (value) nào đó. Danh bạ điện thoại lưu số điện thoại và tên người, từ điển liên kết từ với bản dịch của nó, còn trong bảng điểm thì mỗi sinh viên có tên riêng và số điểm tương ứng.
Trong Java, cho những bài toán như vậy có giao diện Map. Đây là một collection lưu các cặp “khóa — giá trị” (key–value pair).
Các thuộc tính chính của Map:
- Mỗi khóa là duy nhất (không cho phép trùng lặp).
- Mỗi khóa chỉ ánh xạ tới một giá trị.
- Giá trị có thể lặp lại.
Hãy hình dung một phép so sánh. Nếu danh sách (List) giống như hàng chờ ở căng-tin (mỗi người đứng ở một vị trí, có thể truy cập theo số thứ tự), thì map (Map) giống như tủ có nhiều ngăn: mỗi ngăn có số (khóa) và bên trong là thứ gì đó của riêng nó (giá trị).
Giao diện Map: các thao tác cơ bản
Giao diện Map khai báo các phương thức chính để làm việc với các cặp khóa–giá trị:
| Phương thức | Mô tả |
|---|---|
|
Thêm/thay thế giá trị theo khóa |
|
Lấy giá trị theo khóa |
|
Xóa cặp theo khóa |
|
Kiểm tra có khóa đó hay không |
|
Kiểm tra có giá trị đó hay không |
|
Số cặp trong Map |
|
Kiểm tra Map có trống không |
|
Xóa tất cả các cặp |
Các kiểu K và V là tham số generic: K (Key) — kiểu của khóa, V (Value) — kiểu của giá trị.
3. Lớp HashMap: truy cập nhanh theo khóa
HashMap là gì?
HashMap là hiện thực phổ biến nhất của giao diện Map. Nó cung cấp khả năng truy cập nhanh tới giá trị theo khóa.
Quan trọng: HashMap không đảm bảo thứ tự lưu trữ phần tử! Nếu bạn thêm khóa theo một thứ tự nào đó, khi duyệt chúng có thể đi theo thứ tự khác.
Khởi tạo HashMap thế nào?
import java.util.HashMap;
import java.util.Map;
public class Example {
public static void main(String[] args) {
// Tạo map: khóa — String, giá trị — Integer
Map<String, Integer> ages = new HashMap<>();
// Thêm phần tử
ages.put("Vasya", 25);
ages.put("Petya", 30);
ages.put("Masha", 22);
// Lấy giá trị theo khóa
int vasyaAge = ages.get("Vasya");
System.out.println("Tuổi của Vasya: " + vasyaAge); // 25
// Kiểm tra sự tồn tại của khóa
if (ages.containsKey("Masha")) {
System.out.println("Masha có trong danh sách!");
}
// Xóa phần tử
ages.remove("Petya");
// Duyệt tất cả các cặp khóa–giá trị
for (String name : ages.keySet()) {
System.out.println(name + ": " + ages.get(name));
}
}
}
Kết quả:
Tuổi của Vasya: 25
Masha có trong danh sách!
Vasya: 25
Masha: 22
Đặc điểm của HashMap
Điều quan trọng cần nhớ: các khóa trong HashMap luôn duy nhất. Nếu bạn đặt một phần tử mới với khóa đã tồn tại, giá trị cũ sẽ bị thay thế bởi giá trị mới.
Giá trị có thể trùng: nhiều khóa khác nhau có thể trỏ tới cùng một giá trị.
Và một điểm quan trọng nữa — thứ tự phần tử. HashMap không quan tâm tới thứ tự thêm vào. Khi in ra, các bản ghi có thể bị trộn — đó là hành vi bình thường.
4. Lớp TreeMap: sắp xếp theo khóa
Khác với HashMap, lớp TreeMap lưu các phần tử theo thứ tự sắp xếp của khóa.
Khi nào dùng TreeMap?
Khi bạn cần các phần tử đi theo thứ tự tăng (hoặc giảm) của khóa. Ví dụ, nếu bạn muốn in danh bạ theo thứ tự chữ cái.
Ví dụ:
import java.util.Map;
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
Map<String, String> phoneBook = new TreeMap<>();
phoneBook.put("Vasya", "+1-900-123-45-67");
phoneBook.put("Masha", "+1-900-555-55-55");
phoneBook.put("Petya", "+1-900-222-33-44");
for (String name : phoneBook.keySet()) {
System.out.println(name + ": " + phoneBook.get(name));
}
}
}
Kết quả:
Masha: +1-900-555-55-55
Petya: +1-900-222-33-44
Vasya: +1-900-123-45-67
Lưu ý: các khóa được sắp xếp theo thứ tự chữ cái.
5. Các thao tác chính với Map
Thêm và thay thế phần tử
Map<String, Integer> scores = new HashMap<>();
scores.put("Anna", 90);
scores.put("Ivan", 85);
scores.put("Anna", 95); // Sẽ ghi đè giá trị cho "Anna"
Lấy giá trị
Integer annaScore = scores.get("Anna"); // 95
Integer unknown = scores.get("Vasya"); // null nếu không có khóa như vậy
Kiểm tra sự tồn tại của khóa hoặc giá trị
scores.containsKey("Ivan"); // true
scores.containsValue(85); // true
Xóa cặp theo khóa
scores.remove("Ivan");
Kích thước map và làm sạch
int size = scores.size();
scores.clear(); // Xóa tất cả phần tử
5. Duyệt phần tử của Map
Map không phải danh sách, ở đây không có chỉ số. Nhưng bạn có thể duyệt:
Theo khóa:
for (String key : scores.keySet()) {
System.out.println("Khóa: " + key + ", Giá trị: " + scores.get(key));
}
Theo giá trị:
for (Integer value : scores.values()) {
System.out.println("Giá trị: " + value);
}
Theo cặp khóa–giá trị (cách tốt nhất):
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " => " + value);
}
Khi nào dùng HashMap, và khi nào dùng TreeMap?
HashMap là lựa chọn “mặc định” đa dụng. Nếu không cần thứ tự khóa và quan trọng nhất là tốc độ thao tác, hầu như luôn chọn nó.
TreeMap hữu ích khi cần thứ tự. Nó tự động lưu khóa đã được sắp xếp và cho phép nhanh chóng tìm khóa nhỏ nhất/lớn nhất hoặc làm việc với khoảng khóa.
Tổng kết: trong 90% trường hợp dùng HashMap. Khi dữ liệu cần có “thứ tự” ngay từ đầu, hãy dùng TreeMap.
6. Ví dụ sử dụng Map
Ví dụ 1: Danh bạ điện thoại
Map<String, String> phoneBook = new HashMap<>();
phoneBook.put("Katya", "+1-999-111-22-33");
phoneBook.put("Oleg", "+1-999-222-33-44");
phoneBook.put("Katya", "+1-999-555-66-77"); // Số cũ của Katya sẽ được thay bằng số mới
for (Map.Entry<String, String> entry : phoneBook.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
Kết quả:
Oleg: +1-999-222-33-44
Katya: +1-999-555-66-77
Ví dụ 2: Đếm số lần xuất hiện của từ
Giả sử chúng ta có một danh sách các từ và muốn biết mỗi từ xuất hiện bao nhiêu lần:
import java.util.*;
public class WordCount {
public static void main(String[] args) {
List<String> words = Arrays.asList("táo", "chuối", "táo", "lê", "chuối", "táo");
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
int oldCount = counts.getOrDefault(word, 0); // nếu không có khóa — 0
counts.put(word, oldCount + 1);
}
System.out.println(counts); // {lê=1, táo=3, chuối=2}
}
}
7. Lỗi thường gặp khi làm việc với Map
Lỗi 1: Nhầm lẫn giữa khóa và giá trị. Người mới học thường cố lấy giá trị theo chỉ số như trong danh sách, hoặc quên rằng khóa phải là duy nhất. Trong Map không có chỉ số — chỉ có khóa.
Lỗi 2: Dùng khóa và giá trị null. Trong HashMap cho phép khóa null, còn trong TreeMap — không (sẽ ném NullPointerException). Giá trị có thể là null ở cả hai hiện thực, nhưng hiếm khi hữu ích.
Lỗi 3: Kỳ vọng có thứ tự phần tử trong HashMap. HashMap không đảm bảo bất kỳ thứ tự nào. Nếu cần thứ tự — hãy dùng LinkedHashMap (giữ thứ tự thêm vào) hoặc TreeMap (sắp xếp theo khóa).
Lỗi 4: Sửa đổi Map trong khi duyệt. Nếu bạn đang duyệt Map trong vòng lặp và đồng thời thêm/xóa phần tử — có thể phát sinh ConcurrentModificationException. Với các tác vụ như vậy, hãy dùng iterator với phương thức remove() hoặc các collection chuyên dụng.
Lỗi 5: So sánh khóa và giá trị bằng == thay vì equals. Map dùng phương thức equals để so sánh khóa (và giá trị). Nếu bạn tạo lớp làm khóa của riêng mình, nhất định phải override equals và hashCode.
GO TO FULL VERSION