== kiểm tra identity còn .equals() kiểm tra equality theo logic của class. Với primitive như int hay boolean, == so sánh giá trị thực. Với object, == so sánh reference — hai biến có trỏ tới cùng một instance trên heap hay không — còn .equals() là method để class tự định nghĩa hai object được coi là bằng nhau khi nào.
Cơ chế khác nhau ở chỗ == là toán tử do JLS quy định và không thể override, còn .equals() là method của java.lang.Object. Cài đặt mặc định trong Object là return this == obj, nên nếu class không override thì .equals() cũng chỉ so sánh reference. Các class trong JDK như String, Integer, LocalDate, List đã override .equals() để so sánh nội dung: String so từng ký tự, Integer so giá trị int bên trong, List so từng phần tử theo thứ tự. Khi bạn override .equals(), bạn phải override hashCode() để giữ contract: nếu a.equals(b) là true thì a.hashCode() == b.hashCode() phải đúng, nếu không HashMap và HashSet sẽ không tìm thấy key.
String a = new String("hi");
String b = new String("hi");
String c = "hi";
String d = "hi";
a == b; // false — hai instance khác nhau trên heap
a.equals(b); // true — String so sánh từng ký tự
c == d; // true — cùng tham chiếu trong string pool do interning
a == c; // false — nội dung giống nhau nhưng khác instanceLỗi thường gặp:
- Gọi a.equals(b) khi a có thể null sẽ ném NullPointerException; dùng Objects.equals(a, b) để xử lý null an toàn.
- So wrapper bằng == do autoboxing: Integer x = 127; Integer y = 127; thì x == y là true nhờ cache -128 đến 127, nhưng với 128 thì false; luôn dùng .equals() cho wrapper.
- == lại là cách đúng để so sánh enum và để check if (obj == null) vì enum đảm bảo mỗi hằng số chỉ có một instance.
Lưu ý: Với object, mặc định dùng .equals() để so sánh nội dung; chỉ dùng == khi bạn thực sự muốn hỏi có phải cùng một object hay không, như so sánh enum, check null, hoặc tối ưu identity check trong chính cài đặt .equals().