0
0
Lập trình
Hưng Nguyễn Xuân 1
Hưng Nguyễn Xuân 1xuanhungptithcm

Hướng Dẫn Hoạt Động với Map trong Java: Từ Cơ Bản đến Nâng Cao

Đăng vào 7 tháng trước

• 7 phút đọc

Hướng Dẫn Hoạt Động với Map trong Java: Từ Cơ Bản đến Nâng Cao

Mục Lục

  1. Giới thiệu về Map trong Java
  2. Các phương thức cơ bản của Map mà mọi lập trình viên nên biết
  3. Các mẫu nâng cao và thực tiễn tốt nhất
  4. Ví dụ thực tế và trường hợp sử dụng
  5. Cân nhắc về hiệu suất
  6. Những cạm bẫy thường gặp và cách tránh chúng
  7. Kết luận và bước tiếp theo

Giới thiệu về Map trong Java

Giao diện java.util.Map là một trong những cấu trúc dữ liệu cơ bản nhất trong lập trình Java. Trong khi nhiều lập trình viên đã quen thuộc với các thao tác cơ bản như put()get(), các phiên bản Java 8 trở đi đã giới thiệu nhiều phương thức mạnh mẽ mới có thể đơn giản hóa mã của bạn và khiến nó biểu đạt hơn.

Bạn sẽ học được gì

Cuối cùng hướng dẫn này, bạn sẽ hiểu:

  • Cách loại bỏ mã kiểm tra null rườm rà
  • Các thao tác nguyên tử hoạt động an toàn trong môi trường đồng thời
  • Các mẫu hiện đại giúp mã của bạn dễ đọc và bảo trì hơn
  • Các tác động về hiệu suất của các thao tác Map khác nhau

Yêu cầu

  • Kiến thức cơ bản về các tập hợp Java
  • Làm quen với biểu thức lambda (Java 8+)
  • Hiểu biết về kiểu tổng quát

Các phương thức cơ bản của Map mà mọi lập trình viên nên biết

1. computeIfAbsent: Khởi tạo thông minh

Vấn đề:
Bạn đã bao giờ viết mã như thế này chưa?

java Copy
Map<String, List<String>> groups = new HashMap<>();

if (!groups.containsKey("admins")) {
    groups.put("admins", new ArrayList<>());
}
groups.get("admins").add("Alice");

if (!groups.containsKey("users")) {
    groups.put("users", new ArrayList<>());
}
groups.get("users").add("Bob");

Cách làm này:

  • Rườm rà: 3 dòng cho một thao tác đơn giản
  • Dễ gây lỗi: Dễ dàng quên kiểm tra null
  • Lặp lại: Mẫu này xuất hiện khắp nơi

Giải pháp hiện đại:

java Copy
Map<String, List<String>> groups = new HashMap<>();

groups.computeIfAbsent("admins", k -> new ArrayList<>()).add("Alice");
groups.computeIfAbsent("users", k -> new ArrayList<>()).add("Bob");

Cách hoạt động:

  • computeIfAbsent(key, function) kiểm tra xem khóa có tồn tại không
  • Nếu khóa không tồn tại, nó gọi hàm để tạo giá trị mới
  • Nếu khóa đã tồn tại, nó trả về giá trị hiện có
  • Lambda k -> new ArrayList<>() chỉ tạo danh sách mới khi cần thiết

Ví dụ thực tế - Xây dựng chỉ mục:

java Copy
public class WordIndexer {
    public Map<String, List<Integer>> buildIndex(String text) {
        Map<String, List<Integer>> wordIndex = new HashMap<>();
        String[] words = text.split("\\s+");

        for (int i = 0; i < words.length; i++) {
            String word = words[i].toLowerCase();
            wordIndex.computeIfAbsent(word, k -> new ArrayList<>()).add(i);
        }

        return wordIndex;
    }
}

// Sử dụng
WordIndexer indexer = new WordIndexer();
Map<String, List<Integer>> index = indexer.buildIndex("con cáo nâu nhanh nhẹn nhảy qua con chó lười");
// Kết quả: {"con" -> [0, 6], "cáo" -> [1], "nâu" -> [2], ...}

2. compute: Cập nhật tổng quát

Khi nào sử dụng:
Sử dụng compute khi bạn cần tạo một giá trị mới HOẶC cập nhật một giá trị hiện có, và giá trị mới phụ thuộc vào cả khóa và giá trị hiện tại.

Chữ ký:

java Copy
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

Điểm chính:

  • Hàm nhận cả khóagiá trị hiện tại (null nếu không có)
  • Giá trị trả về của hàm trở thành giá trị mới
  • Nếu hàm trả về null, mục nhập sẽ bị xóa

Ví dụ 1: Đếm số lần xuất hiện với giá trị mặc định

java Copy
Map<String, Integer> wordCount = new HashMap<>();

String[] words = {"táo", "chuối", "táo", "anh đào", "chuối", "táo"};

for (String word : words) {
    wordCount.compute(word, (key, currentCount) -> {
        if (currentCount == null) {
            return 1; // Lần xuất hiện đầu tiên
        } else {
            return currentCount + 1; // Tăng số đếm hiện có
        }
    });
}

// Kết quả: {"táo" -> 3, "chuối" -> 2, "anh đào" -> 1}

3. merge: Kết hợp

Vấn đề:
Bạn cần kết hợp một giá trị mới với một giá trị hiện có, hoặc chèn giá trị mới nếu khóa không tồn tại.

Chữ ký:

java Copy
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)

Cách hoạt động:

  1. Nếu khóa không tồn tại hoặc ánh xạ tới null: chèn giá trị được cung cấp
  2. Nếu khóa đã tồn tại: gọi hàm với (oldValue, newValue) và sử dụng kết quả

Ví dụ 1: Đếm chữ cái trong một từ

java Copy
Map<String, Integer> counter = new HashMap<>();

String word = "xin chào";
for (char c : word.toCharArray()) {
    String letter = String.valueOf(c);
    counter.merge(letter, 1, Integer::sum);
}

// Kết quả: {"x" -> 1, "i" -> 1, "n" -> 1, " " -> 3, "c" -> 1, "h" -> 1, "á" -> 1, "o" -> 1}

4. getOrDefault: Lấy giá trị an toàn

Vấn đề:
Lỗi Null Pointer khi truy cập các giá trị bản đồ.

java Copy
Map<String, Integer> scores = new HashMap<>();
int aliceScore = scores.get("Alice"); // NullPointerException nếu không tìm thấy "Alice"

Giải pháp:

java Copy
int aliceScore = scores.getOrDefault("Alice", 0); // Trả về 0 nếu không tìm thấy

5. forEach: Lặp lại biểu đạt

Cách truyền thống:

java Copy
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);

for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " đạt được " + entry.getValue());
}

Cách hiện đại:

java Copy
scores.forEach((name, score) -> 
    System.out.println(name + " đạt được " + score));

6. replaceAll: Biến đổi hàng loạt

Khi nào sử dụng:
Khi bạn cần biến đổi tất cả các giá trị trong một bản đồ dựa trên giá trị và/hoặc khóa hiện tại.

java Copy
Map<String, Double> prices = new HashMap<>();
prices.put("máy tính xách tay", 999.99);
prices.put("chuột", 29.99);

prices.replaceAll((item, price) -> price * 0.9);
// Kết quả: {"máy tính xách tay" -> 899.99, "chuột" -> 26.99}

Các mẫu nâng cao và thực tiễn tốt nhất

Làm việc với Bản đồ không thay đổi

Tạo các bản đồ không thay đổi nhỏ:

java Copy
Map<String, Integer> httpStatusCodes = Map.of(
    "OK", 200,
    "NOT_FOUND", 404,
    "INTERNAL_ERROR", 500
);

Các thao tác an toàn với ConcurrentHashMap

Tại sao sử dụng ConcurrentHashMap?
HashMap thông thường không an toàn. ConcurrentHashMap cung cấp các thao tác an toàn mà không cần đồng bộ hóa bên ngoài.

java Copy
import java.util.concurrent.ConcurrentHashMap;

public class ThreadSafeCounter {
    private final ConcurrentHashMap<String, Integer> counters = new ConcurrentHashMap<>();

    public void increment(String key) {
        counters.merge(key, 1, Integer::sum);
    }
}

Ví dụ thực tế và trường hợp sử dụng

Ví dụ 1: Hệ thống phân tích nhật ký

java Copy
public class LogAnalyzer {
    private final Map<String, Integer> errorCounts = new ConcurrentHashMap<>();

    public void processLogEntry(String logLevel, String message) {
        if ("ERROR".equals(logLevel)) {
            errorCounts.merge(message, 1, Integer::sum);
        }
    }
}

Ví dụ 2: Hệ thống giỏ hàng

java Copy
public class ShoppingCart {
    private final Map<String, Integer> items = new HashMap<>();
    private final Map<String, Double> prices = Map.of(
        "táo", 0.99,
        "chuối", 0.59
    );

    public void addItem(String item, int quantity) {
        items.merge(item, quantity, Integer::sum);
    }
}

Cân nhắc về hiệu suất

Tóm tắt độ phức tạp thời gian

Thao tác HashMap TreeMap LinkedHashMap
get() O(1) O(log n) O(1)
put() O(1) O(log n) O(1)
computeIfAbsent() O(1) O(log n) O(1)
merge() O(1) O(log n) O(1)

Những cạm bẫy thường gặp và cách tránh chúng

Cạm bẫy 1: Sửa đổi bản đồ trong khi lặp lại

java Copy
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
for (String key : map.keySet()) {
    if (map.get(key) == 2) {
        map.remove(key); // ConcurrentModificationException!
    }
}

Cạm bẫy 2: Giả định computeIfAbsent luôn tính toán

java Copy
Map<String, List<String>> map = new HashMap<>();
map.put("key", null);
List<String> list = map.computeIfAbsent("key", k -> new ArrayList<>()); // Trả về null!

Kết luận và bước tiếp theo

Những điểm chính

  1. Sử dụng computeIfAbsent thay vì các kiểm tra null thủ công
  2. Sử dụng merge cho các bộ đếm và kết hợp giá trị
  3. Sử dụng getOrDefault để tránh lỗi null pointer

Bài tập thực hành

  1. Xây dựng một bộ phân tích tần suất từ trong một tệp văn bản
  2. Tạo một tiện ích nhóm mà nhóm các đối tượng theo nhiều tiêu chí
  3. Triển khai một bộ nhớ đệm đơn giản với thời gian hết hạn sử dụng các thao tác Map

Tài liệu tham khảo thêm

  • Khám phá API java.util.stream để xử lý tập hợp nâng cao
  • Học về các thực hiện Map chuyên biệt như EnumMapIdentityHashMap

Bạn đã sẵn sàng để hiện đại hóa mã Java của mình? Bắt đầu bằng cách xác định các thao tác Map rườm rà trong các dự án hiện tại của bạn và tái cấu trúc chúng bằng các kỹ thuật này. Bạn và đồng đội sẽ cảm ơn bạn!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào