java comparison
Tung Bui Thanh (TungDaDev)15 mins read

Đây là mấy cặp khái niệm Java/Spring hay bị hỏi khi phỏng vấn Senior. Nghe thì cơ bản, nhưng dùng sai trên production thì không hiếm, và câu trả lời "học thuộc" thường thiếu đúng mấy chi tiết gây bug. Mỗi mục dưới đây có code minh hoạ, kèm vài dòng về cơ chế và chỗ hay vấp.
1. == vs equals()
Với object, == so sánh reference (có phải cùng một object trong memory không), còn equals() so sánh giá trị (nội dung có giống nhau không). Với kiểu primitive thì == so sánh giá trị luôn.
String a = new String("hello");
String b = new String("hello");
a == b; // false — 2 objects khác nhau trong heap
a.equals(b); // true — nội dung giống nhau
// String pool exception
String c = "hello";
String d = "hello";
c == d; // true — cùng reference trong String pool (compile-time optimization)
// Integer caching (-128 to 127)
Integer x = 127;
Integer y = 127;
x == y; // true — cached
Integer m = 128;
Integer n = 128;
m == n; // false — ngoài cache range, different objects
m.equals(n); // true
// Best practice: LUÔN dùng equals() cho objects, == chỉ cho primitives và null check
Objects.equals(a, b); // Null-safe equalsCái bẫy của Integer cache là code chạy đúng khi test với số nhỏ, lên production gặp ID lớn hơn 127 mới sai. Giới hạn trên của cache còn chỉnh được bằng -XX:AutoBoxCacheMax, nên đừng bao giờ dựa vào nó. Nhớ thêm: override equals() thì phải override hashCode() theo, không thì object bỏ vào HashMap/HashSet sẽ "mất tích" dù equals trả về true.
2. final vs finally vs finalize
Ba keywords đều bắt đầu bằng "final" nhưng khác nhau hoàn toàn.
// final — constant/immutable
final int MAX = 100; // Variable: không reassign được
final class Utility { } // Class: không extend được
final void process() { } // Method: không override được
// final trong modern Java: effectively final cho lambdas
String name = "John"; // Effectively final (không reassign)
Runnable r = () -> System.out.println(name); // OK
// finally — cleanup code (luôn chạy sau try/catch)
try {
connection = dataSource.getConnection();
// ... do work
} catch (SQLException e) {
log.error("DB error", e);
} finally {
if (connection != null) connection.close(); // LUÔN chạy
}
// Modern: try-with-resources thay thế finally cho AutoCloseable
// finalize — DEPRECATED (Java 9+), deprecated for removal từ Java 18 (JEP 421), DO NOT USE
// Chạy trước GC collect object — unpredictable timing, performance killer
// Thay thế: Cleaner API hoặc try-with-resourcesĐoạn finally ở trên là kiểu viết trước Java 7, và nó còn một chỗ khó chịu: connection.close() cũng ném SQLException, nên phải bọc thêm try-catch bên trong finally. Nếu cả khối try lẫn close() cùng ném lỗi thì lỗi của close() đè mất lỗi gốc. Try-with-resources xử lý được chuyện này: lỗi từ close() được gắn vào lỗi gốc dưới dạng suppressed exception (getSuppressed()).
3. abstract class vs interface
Trước Java 8, ranh giới giữa hai thứ này rõ ràng. Từ Java 8+ (default methods) thì mờ hơn, nhưng semantic vẫn khác nhau.
// Interface — contract, "can do" relationship (CAN-DO behavior)
public interface Auditable {
LocalDateTime getCreatedAt();
String getCreatedBy();
// Default method (Java 8+): behavior chung, override được
default String getAuditInfo() {
return getCreatedBy() + " at " + getCreatedAt();
}
// Static method
static Auditable empty() { return new EmptyAuditable(); }
}
// Abstract class — shared implementation, "is-a" relationship
@MappedSuperclass
public abstract class BaseEntity {
@Id
private UUID id;
private LocalDateTime createdAt;
// Concrete method: shared behavior
public boolean isNew() { return id == null; }
// Abstract method: subclass must implement
public abstract String getEntityType();
// Constructor (interface KHÔNG có)
// (Đừng gán id ở đây: isNew() ở trên sẽ luôn trả về false)
protected BaseEntity() { this.createdAt = LocalDateTime.now(); }
// State (fields) — interface chỉ có constants
private int version;
}
// Class implement nhiều interfaces, extend chỉ 1 abstract class
public class Product extends BaseEntity implements Auditable, Serializable { }Khi nào dùng gì:
| Criteria | Interface | Abstract Class |
|---|---|---|
| Multiple inheritance | ✓ (implements nhiều) | ✗ (extends 1) |
| Has state (fields) | ✗ (chỉ constants) | ✓ |
| Has constructor | ✗ | ✓ |
| Forces "is-a" relationship | ✗ | ✓ |
| Evolving API | ✓ (default methods) | ✓ |
| Template Method pattern | ✗ | ✓ (preferred) |
| Strategy/Capability | ✓ (preferred) | ✗ |
4. Checked vs Unchecked Exceptions
// Checked Exception — compiler FORCE bạn handle (extends Exception)
// "Recoverable errors" — caller có thể xử lý
public class InsufficientFundsException extends Exception {
private final BigDecimal balance;
private final BigDecimal amount;
public InsufficientFundsException(BigDecimal balance, BigDecimal amount) {
super("Insufficient funds: balance=" + balance + ", amount=" + amount);
this.balance = balance;
this.amount = amount;
}
}
// Phải try-catch hoặc throws
public void transfer(BigDecimal amount) throws InsufficientFundsException {
if (balance.compareTo(amount) < 0) throw new InsufficientFundsException(balance, amount);
}
// Unchecked Exception — compiler KHÔNG force handle (extends RuntimeException)
// "Programming errors" — caller thường không thể recover
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(UUID id) {
super("Product not found: " + id);
}
}
// Không cần try-catch
public ProductDTO getById(UUID id) {
return repo.findById(id).orElseThrow(() -> new ProductNotFoundException(id));
}Spring convention: dùng UNCHECKED exceptions cho mọi business/application errors. Lý do:
@Transactionalmặc định chỉ rollback khi gặpRuntimeException(vàError). Ném checked exception thì transaction vẫn commit, trừ khi khai báorollbackFor- Method signature không bị kéo dài bởi chuỗi
throwstruyền qua mọi tầng @ControllerAdvicebắt tập trung ở một chỗ và map sang HTTP status
Ranh giới "recoverable vs programming error" nghe gọn nhưng ngoài đời khá mờ. Spring chọn unchecked cho gần như mọi thứ (DataAccessException cũng là unchecked), và đa số code Spring Boot hiện nay đi theo hướng đó.
5. HashMap vs ConcurrentHashMap vs LinkedHashMap vs TreeMap
// HashMap — fastest, unordered, NOT thread-safe
Map<String, Product> cache = new HashMap<>();
// O(1) get/put, null keys/values allowed
// ❌ KHÔNG dùng trong multi-threaded context
// ConcurrentHashMap — thread-safe, NO locks on reads
Map<String, Product> concurrentCache = new ConcurrentHashMap<>();
// Thread-safe, null keys/values NOT allowed
// Java 7: segment-level locking; Java 8+: CAS + synchronized trên từng bin
// ✅ Dùng cho shared state giữa threads
// LinkedHashMap — insertion order preserved
Map<String, Product> orderedMap = new LinkedHashMap<>();
// Access order mode → LRU cache implementation
Map<String, Product> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Product> eldest) {
return size() > 100; // Max 100 entries
}
};
// TreeMap — sorted by key (Red-Black tree)
Map<String, Product> sortedMap = new TreeMap<>();
// O(log n) get/put, keys must be Comparable
// Use case: range queries, "give me all products between A-M"
NavigableMap<LocalDate, BigDecimal> dailyRevenue = new TreeMap<>();
dailyRevenue.subMap(startDate, endDate); // Range query, [startDate, endDate) — không gồm endDateConcurrentHashMap thread-safe với từng thao tác, chứ không phải với chuỗi thao tác. Kiểu if (!map.containsKey(k)) map.put(k, v) vẫn có race: hai thread cùng thấy chưa có key rồi cùng put. Muốn atomic thì dùng putIfAbsent, computeIfAbsent, merge. Tương tự, LRU cache bằng LinkedHashMap ở trên không thread-safe, mà ở chế độ access order thì ngay cả get() cũng sửa cấu trúc bên trong (đưa entry về cuối), nên dùng chung giữa nhiều thread là hỏng. Cần cache dùng chung thì dùng Caffeine. Với TreeMap, key phải Comparable hoặc truyền Comparator vào constructor; muốn lấy cả hai đầu mút thì dùng subMap(from, true, to, true).
6. ArrayList vs LinkedList vs CopyOnWriteArrayList
// ArrayList — dynamic array, default choice 99% of the time
List<Product> products = new ArrayList<>();
// O(1) random access, O(1) amortized add, O(n) insert/remove middle
// Cache-friendly (contiguous memory)
// LinkedList — doubly-linked list, RARELY better choice
List<Product> linkedProducts = new LinkedList<>();
// O(n) random access, O(1) add/remove at head/tail
// ❌ Almost NEVER faster than ArrayList in practice (cache misses)
// Only use case: frequent add/remove at both ends (Deque interface)
// CopyOnWriteArrayList — thread-safe, optimized for reads
List<EventListener> listeners = new CopyOnWriteArrayList<>();
// Every write creates new internal array copy → expensive writes
// Reads never block, never throw ConcurrentModificationException
// ✅ Use for: listener lists, rarely-modified collections read by many threadsNgay cả khi cần thêm/xoá ở hai đầu, ArrayDeque thường vẫn nhanh hơn LinkedList, vì mỗi node của LinkedList là một object riêng nằm rải rác trên heap (tốn thêm memory cho hai con trỏ, lại hay cache miss). Iterator của CopyOnWriteArrayList duyệt trên bản chụp tại lúc tạo iterator, nên không ném ConcurrentModificationException, nhưng cũng không thấy phần tử được thêm sau đó.
7. synchronized vs ReentrantLock vs volatile vs Atomic
// synchronized — implicit lock, simple
public class Counter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
// Pros: Simple, auto-release
// Cons: No tryLock, no timeout, no fairness control
}
// ReentrantLock — explicit lock, more control
public class AdvancedCounter {
private final ReentrantLock lock = new ReentrantLock(true); // fair
private int count = 0;
public void increment() throws InterruptedException { // tryLock có timeout ném InterruptedException
if (lock.tryLock(5, TimeUnit.SECONDS)) { // Timeout!
try { count++; }
finally { lock.unlock(); } // Manual unlock
} else {
// Không lấy được lock: phải xử lý, đừng lặng lẽ bỏ qua lần tăng
throw new IllegalStateException("Timeout waiting for lock");
}
}
// Pros: tryLock, timeout, fairness, multiple conditions
// Cons: Verbose, must remember unlock in finally
}
// volatile — visibility guarantee, NO atomicity
public class Flag {
private volatile boolean running = true; // Visible across threads immediately
public void stop() { running = false; } // Write visible to all readers
public void run() { while (running) { /* work */ } }
// Pros: Lightweight, no locking
// Cons: Only works for single write/read — NOT for compound operations (count++)
}
// AtomicInteger — lock-free atomic operations (CAS)
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); } // Atomic!
public int get() { return count.get(); }
// Pros: Lock-free, high performance under contention
// Cons: Only single variable, no compound atomicity
}Decision tree:
- Chỉ cần visibility (flag, config) →
volatile - Single variable atomic ops →
AtomicInteger/AtomicReference - Simple mutual exclusion →
synchronized - Cần tryLock/timeout/fairness →
ReentrantLock
Fair lock (new ReentrantLock(true)) chia lượt công bằng nhưng throughput thấp hơn rõ rệt, chỉ bật khi thật sự có thread bị "đói". Còn một chuyện từ khi có virtual thread (Java 21): ở Java 21 tới 23, virtual thread chạy vào khối synchronized mà phải chờ (I/O, wait()) sẽ bị ghim (pin) vào carrier thread, nên lời khuyên hồi đó là đổi sang ReentrantLock. Từ Java 24, JEP 491 đã gỡ hạn chế này, synchronized dùng với virtual thread bình thường.
8. @Component vs @Service vs @Repository vs @Controller
Về mặt kỹ thuật thì tất cả đều là @Component, Spring đều scan và đăng ký bean. Khác biệt nằm ở semantic và special behavior.
@Component // Generic Spring bean (utility, helper)
@Service // Business logic layer — KHÔNG có behavior đặc biệt, chỉ semantic
@Repository // Data access layer — Exception translation (DataAccessException)
@Controller // Web layer — view resolution
@RestController // Web layer — = @Controller + @ResponseBody
@Configuration // Config class — CGLIB proxy, @Bean method interception@Repository đặc biệt: Spring tự động translate exception riêng của từng database hay ORM (ví dụ SQLIntegrityConstraintViolationException, PersistenceException của JPA) thành cây DataAccessException của Spring, như DataIntegrityViolationException. Nhờ vậy code xử lý lỗi không phụ thuộc loại database. Việc dịch này do PersistenceExceptionTranslationPostProcessor làm (Spring Boot tự đăng ký), nó bọc bean @Repository trong một proxy. Repository của Spring Data JPA thì được dịch sẵn dù bạn không gắn annotation.
9. PUT vs PATCH: HTTP semantics
// PUT — replace TOÀN BỘ resource (idempotent)
// Client gửi complete representation
@PutMapping("/{id}")
public ProductDTO update(@PathVariable UUID id, @Valid @RequestBody UpdateProductRequest request) {
// request PHẢI chứa TẤT CẢ fields
// Fields không gửi = set null/default
Product product = productRepository.findById(id).orElseThrow();
product.setName(request.getName()); // Bắt buộc
product.setPrice(request.getPrice()); // Bắt buộc
product.setDescription(request.getDescription()); // null nếu không gửi → xóa description
return toDTO(productRepository.save(product));
}
// PATCH — update MỘT PHẦN resource (chỉ fields gửi lên)
// Client gửi partial representation
@PatchMapping("/{id}")
public ProductDTO patch(@PathVariable UUID id, @RequestBody Map<String, Object> updates) {
Product product = productRepository.findById(id).orElseThrow();
// Chỉ update fields có trong request
updates.forEach((key, value) -> {
switch (key) {
case "name" -> product.setName((String) value);
case "price" -> product.setPrice(new BigDecimal(value.toString()));
case "description" -> product.setDescription((String) value);
}
});
return toDTO(productRepository.save(product));
}| PUT | PATCH | |
|---|---|---|
| Semantics | Replace full resource | Partial update |
| Body | Complete object | Only changed fields |
| Missing fields | Set to null/default | Leave unchanged |
| Idempotent | ✓ | Not guaranteed |
| Validation | Validate all required fields | Validate only sent fields |
PATCH bằng Map<String, Object> như trên là cách nhanh nhất, nhưng bỏ qua hết @Valid và dễ lỗi ép kiểu (price gửi lên dạng số sẽ là Integer hoặc Double tuỳ giá trị, nên code phải qua toString()). Có hai chuẩn cho PATCH đáng biết: JSON Merge Patch (RFC 7396, gửi object chứa field cần đổi, null nghĩa là xoá) và JSON Patch (RFC 6902, gửi danh sách thao tác add/replace/remove). Một cách khác hay gặp là DTO với các field nullable, nhưng khi đó không phân biệt được "không gửi" với "gửi null để xoá", trừ khi dùng thêm Optional hoặc JsonNullable.
10. @RequestParam vs @PathVariable vs @RequestBody
// @PathVariable — resource identifier, part of URL path
// GET /api/products/550e8400-e29b-41d4-a716-446655440000
@GetMapping("/{id}")
public ProductDTO get(@PathVariable UUID id) { ... }
// Khi nào: identifying specific resource
// @RequestParam — filtering, pagination, search criteria
// GET /api/products?status=ACTIVE&page=0&size=20&keyword=laptop
@GetMapping
public Page<ProductDTO> list(
@RequestParam(required = false) ProductStatus status,
@RequestParam(defaultValue = "0") int page) { ... }
// Khi nào: optional filters, query parameters
// @RequestBody — JSON payload (POST/PUT/PATCH body)
// POST /api/products with JSON body
@PostMapping
public ProductDTO create(@Valid @RequestBody CreateProductRequest request) { ... }
// Khi nào: sending complex object, create/update operations11. Comparator vs Comparable
// Comparable — "natural ordering" (defined IN the class)
public class Product implements Comparable<Product> {
private String name;
private BigDecimal price;
@Override
public int compareTo(Product other) {
return this.name.compareTo(other.name); // Natural order: by name
}
}
Collections.sort(products); // Uses compareTo
// Comparator — "custom ordering" (defined OUTSIDE the class)
// Nhiều cách sort khác nhau cho cùng 1 class
Comparator<Product> byPrice = Comparator.comparing(Product::getPrice);
Comparator<Product> byPriceDesc = Comparator.comparing(Product::getPrice).reversed();
Comparator<Product> byNameThenPrice = Comparator.comparing(Product::getName)
.thenComparing(Product::getPrice);
// Null-safe
Comparator<Product> nullSafe = Comparator.comparing(
Product::getCategory, Comparator.nullsLast(Comparator.naturalOrder()));
products.sort(byPrice);
products.stream().sorted(byNameThenPrice).toList();Rule: implement Comparable khi có một thứ tự tự nhiên hiển nhiên (String theo bảng chữ cái, ngày theo thời gian). Các cách sắp xếp khác thì dùng Comparator.
Javadoc khuyên compareTo nên nhất quán với equals (trả về 0 khi và chỉ khi equals là true). Lệch nhau thì TreeSet/TreeMap sẽ hành xử khác HashSet/HashMap. Ví dụ kinh điển là BigDecimal: new BigDecimal("2.0").equals(new BigDecimal("2.00")) là false (khác scale), nhưng compareTo trả về 0. Bỏ hai số đó vào HashSet được 2 phần tử, vào TreeSet chỉ còn 1. So sánh tiền thì dùng compareTo. Product ở trên so theo name nên hai sản phẩm trùng tên sẽ bị TreeSet coi là một.
12. StringBuilder vs StringBuffer vs String concatenation
// String concatenation (+) — compiler optimizes simple cases
String result = "Hello " + name + "!";
// Java 9+: compiler uses invokedynamic → efficient cho simple cases
// ❌ KHÔNG dùng trong loops
// ❌ Anti-pattern: concatenation in loop
String csv = "";
for (Product p : products) {
csv += p.getName() + ","; // Tạo new String object mỗi iteration → O(n²)
}
// ✅ StringBuilder — NOT thread-safe, fast (single-threaded)
StringBuilder sb = new StringBuilder();
for (Product p : products) {
sb.append(p.getName()).append(",");
}
String csv = sb.toString();
// StringBuffer — thread-safe (synchronized), slower
StringBuffer buffer = new StringBuffer(); // Legacy, HIẾM KHI cần
// Chỉ khi nhiều threads cùng build 1 string (rất rare)
// ✅ Modern approach: String.join / Collectors
String csv = products.stream()
.map(Product::getName)
.collect(Collectors.joining(","));
String csv = String.join(",", names);Chọn thế nào: biểu thức đơn giản thì cứ dùng +, từ Java 9 compiler sinh invokedynamic (JEP 280) và JVM tự chọn cách nối hiệu quả. Nối trong vòng lặp hay ghép phức tạp thì StringBuilder. Nối một collection thì String.join/Collectors.joining. StringBuffer thì gần như không bao giờ cần.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.