Java OO & Functional/Bounded Types và Generic Invariance — vì sao List<Integer> không phải List<Number>
25/38
Bài 25 / 38~22 phútGenerics & CollectionsMiễn phí lượt xem

Bounded Types và Generic Invariance — vì sao List<Integer> không phải List<Number>

Upper bounded type parameters, multiple bounds, generic invariance vs array covariance, workaround qua wildcard, và cách JVM xử lý erasure đến bound thay vì Object.

TL;DR: Bounded type parameter <T extends Number> giới hạn T chỉ được là Number hoặc subtype của nó — giúp gọi T.doubleValue() mà không cần cast. Generic invariance nghĩa là List<Integer> KHÔNG phải subtype của List<Number>Integer extends Number — Java chủ đích thiết kế vậy để tránh type safety break lúc runtime. Ngược lại, Java arrays là covariant (Integer[] có thể gán cho Number[]) nhưng đây là thiết kế cũ có lỗ hổng (ArrayStoreException). Bài này giải thích invariance từ góc độ type safety, cách workaround qua wildcard, và khi nào bounded type parameter ảnh hưởng bytecode.

1. Scenario — hàm tính tổng generic

Bạn muốn viết một hàm tính tổng các phần tử trong List. Thử với type parameter không bị giới hạn:

public static <T> double sum(List<T> list) {
    double total = 0;
    for (T item : list) {
        total += item.doubleValue();  // compile error: T khong co method doubleValue()
    }
    return total;
}

Compiler từ chối vì T có thể là bất kỳ type nào, kể cả String, Boolean — những type này không có doubleValue(). Để nói với compiler "T chỉ được là kiểu số", cần bounded type parameter.

2. Bounded type parameter — <T extends Bound>

Bounded type parameter (JLS §4.5.1) là cú pháp giới hạn kiểu T phải là subtype (hoặc chính xác là) một kiểu cụ thể. Từ khoá extends dùng cho cả class lẫn interface:

// T phai la Number hoac subtype cua Number (Integer, Long, Double, BigDecimal...)
public static <T extends Number> double sum(List<T> list) {
    double total = 0;
    for (T item : list) {
        total += item.doubleValue();  // OK -- Number co doubleValue()
    }
    return total;
}

// Su dung:
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5, 3.5);
List<BigDecimal> bigs = List.of(new BigDecimal("1.1"), new BigDecimal("2.2"));

System.out.println(sum(ints));     // 6.0
System.out.println(sum(doubles));  // 7.5
System.out.println(sum(bigs));     // 3.3

Khi bạn viết <T extends Number>, compiler cho phép gọi tất cả public method của Number trên biến kiểu T. Đây là mấu chốt: bounded type mở rộng những gì bạn có thể làm với T trong method body.

2.1 Ví dụ thực tế: bounded trong Collections

Comparable là interface được dùng rất nhiều làm bound:

// Tim phan tu nho nhat trong list -- yeu cau T co the so sanh voi chinh no
public static <T extends Comparable<T>> T min(List<T> list) {
    if (list.isEmpty()) throw new NoSuchElementException();
    T result = list.get(0);
    for (T item : list) {
        if (item.compareTo(result) < 0) {
            result = item;
        }
    }
    return result;
}

// Hoat dong voi moi Comparable type
String minStr = min(List.of("banana", "apple", "cherry"));  // "apple"
Integer minInt = min(List.of(3, 1, 4, 1, 5));               // 1

3. Multiple bounds — <T extends A & B>

T có thể bị giới hạn bởi nhiều type cùng lúc bằng &:

// T phai implement ca Comparable va Serializable
public static <T extends Comparable<T> & Serializable> void processAndStore(List<T> list) {
    Collections.sort(list);   // OK vi T extends Comparable
    // serialize(list)         // OK vi T extends Serializable
}

Ví dụ thực tiễn trong các thư viện và hệ thống phân tán

Trong các hệ thống phân tán lớn (như Apache Spark, Apache Flink, Hazelcast) hoặc khi chúng ta cần định nghĩa các thuật toán thực thi trên mạng lưới cluster, dữ liệu cần truyền tải qua lại giữa các máy chủ (node). Để thực hiện việc này, các đối tượng nghiệp vụ không chỉ cần có khả năng sắp xếp hoặc so sánh (cần Comparable) để phân chia phân vùng (partitioning) hay gộp nhóm (shuffling), mà còn bắt buộc phải có khả năng tuần tự hóa (serialization) để chuyển đổi sang mảng byte và gửi qua mạng (cần Serializable).

Dưới đây là một ví dụ thực tế mô phỏng một Utility Method trong thư viện lưu trữ cache phân tán (Distributed Cache):

import java.io.*;
import java.util.*;

public class DistributedDataProcessor {

    // Method yeu cau T phai vua so sanh duoc (de sort) vua phai Serializable (de ghi ra file hoac truyen qua mang)
    public static <T extends Comparable<? super T> & Serializable> void sortAndPersist(
            List<T> data, 
            OutputStream outputStream
    ) throws IOException {
        
        // Step 1: Sap xep du lieu. Hop le vi T extends Comparable
        Collections.sort(data);
        
        // Step 2: Tuan tu hoa du lieu ra luong ghi. Hop le vi T extends Serializable
        try (ObjectOutputStream oos = new ObjectOutputStream(outputStream)) {
            oos.writeObject(data); 
        }
    }
}

Nếu một developer cố tình truyền vào một class có implement Comparable nhưng không implement Serializable (hoặc ngược lại), compiler sẽ ngay lập tức chặn lại và báo lỗi compile-time. Điều này loại bỏ hoàn toàn các lỗi runtime tai hại như NotSerializableException khi chương trình đang chạy trên môi trường cluster production thực tế.

Quy tắc quan trọng với multiple bounds:

  • Tối đa 1 class, các phần còn lại phải là interface.
  • Class phải đứng đầu tiên trong danh sách.
// DUNG: class truoc, interface sau
<T extends AbstractList<E> & Serializable & Cloneable>

// SAI: 2 class -- compile error (T khong the extend 2 class)
<T extends ArrayList<E> & LinkedList<E>>  // compile error

Lý do không thể có 2 class: Java không có multiple class inheritance. T extend 2 class đồng nghĩa với multiple inheritance — không cho phép.

4. Bounded type và erasure — ảnh hưởng bytecode

Khi compiler xử lý generic type, nó thực hiện type erasure — xoá thông tin generic và thay bằng type thực tế. Điều quan trọng: với bounded type parameter, erasure đến bound, không phải Object:

// Source code:
public static <T extends Number> double sum(List<T> list) {
    for (T item : list) {
        total += item.doubleValue();
    }
}

// Sau erasure (bytecode tuong duong):
public static double sum(List list) {
    for (Number item : list) {          // T -> Number (bound), khong phai Object
        total += item.doubleValue();    // goi truc tiep, khong can cast
    }
}

So sánh với unbounded <T>:

// Source:
public static <T> void print(List<T> list) {
    for (T item : list) { System.out.println(item); }
}

// Sau erasure:
public static void print(List list) {
    for (Object item : list) {           // T -> Object (khong co bound)
        System.out.println(item);
    }
}

Hệ quả thực tế: bounded type parameter giúp compiler tạo bytecode hiệu quả hơn (không cần checkcast instruction với method call trên bound), và bắt lỗi type sớm hơn tại compile time.

5. Generic invariance — định nghĩa và lý do

Generic invariance là tính chất: nếu A là subtype của B, thì Generic<A> không phải subtype của Generic<B>. Cụ thể:

  • Integer extends Number — đúng
  • Nhưng List<Integer> KHÔNG phải subtype của List<Number>

Điều này nghe có vẻ phi lý. Vì sao Java thiết kế vậy?

Xét đoạn code giả định nếu invariance KHÔNG được áp dụng:

// Gia su (SAI -- Java khong cho phep):
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> nums = ints;                     // neu cho phep...

nums.add(3.14);   // Double la Number, nen add OK...
nums.add(Long.MAX_VALUE);  // Long cung la Number...

// Gio ints la List<Integer> nhung chua Double va Long
Integer x = ints.get(3);  // ClassCastException tai runtime!

Nếu Java cho phép List<Integer> gán cho List<Number>, bạn có thể thêm Double (hợp lệ với List<Number>) vào một list thực tế chứa Integer — và đọc ra bằng Integer sẽ ném ClassCastException lúc runtime. Generic invariance ngăn điều này xảy ra, đảm bảo lỗi được bắt tại compile time.

// Thuc te -- Java tu choi ngay tai compile time:
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> nums = ints;  // compile error: incompatible types
Lỗi thường gặp

Người mới Java hay thắc mắc "tại sao method của tôi nhận List<Number> nhưng không nhận được List<Integer>?". Đây chính là invariance. Giải pháp: dùng upper bounded wildcard List<? extends Number> — sẽ thấy ở section 7.

6. Arrays là covariant — thiết kế cũ, lỗ hổng runtime

Java arrays được thiết kế covariant (từ Java 1.0): nếu A extends B thì A[] là subtype của B[]. Đây là thiết kế trước khi generics ra đời (Java 5), nhằm giúp viết generic algorithms:

Integer[] ints = {1, 2, 3};
Number[] nums = ints;       // OK -- covariant, compiler cho phep

nums[0] = 3.14;             // ArrayStoreException tai runtime!
// -- nguoi ta dang ghi Double vao Integer[], JVM phat hien va throw

JVM bắt lỗi này bằng arraystore check — mỗi lần ghi vào array, JVM kiểm tra xem kiểu thực của phần tử có compatible với kiểu thực của array không. Nếu không: ArrayStoreException.

So sánh với generics:

ArraysGenerics
CovarianceCó (Integer[] extends Number[])Không (List<Integer> không phải List<Number>)
Kiểm tra typeRuntime (ArrayStoreException)Compile time (compile error)
Hiệu năngOverhead mỗi lần ghi (arraystore check)Không overhead runtime
Khi nào phát hiện lỗiMuộn (runtime)Sớm (compile time)

Generics rút kinh nghiệm từ covariant arrays và chọn invariance để bắt lỗi sớm hơn. Đây là quyết định thiết kế có chủ đích.

flowchart TD
    A["Integer extends Number"] --> B["Arrays: Integer[] extends Number[]<br/>(covariant)"]
    A --> C["Generics: List&lt;Integer&gt; NOT subtype of List&lt;Number&gt;<br/>(invariant)"]
    B --> D["Error: ArrayStoreException at runtime"]
    C --> E["Error: compile error -- safe"]

7. Workaround invariance — wildcard

Nếu cần viết method nhận cả List<Integer> lẫn List<Double> (và mọi list số), dùng upper bounded wildcard ? extends Number:

// Voi invariance, method nay chi nhan dung List<Number>:
public static double sumNumbers(List<Number> list) {
    return list.stream().mapToDouble(Number::doubleValue).sum();
}

sumNumbers(List.of(1, 2, 3));          // compile error: List<Integer> khong phai List<Number>
sumNumbers(List.of(1.5, 2.5));         // compile error: List<Double> khong phai List<Number>

// FIX: dung upper bounded wildcard
public static double sumNumbers(List<? extends Number> list) {
    return list.stream().mapToDouble(Number::doubleValue).sum();
}

sumNumbers(List.of(1, 2, 3));          // OK -- List<Integer>
sumNumbers(List.of(1.5, 2.5));         // OK -- List<Double>
sumNumbers(List.of(new BigDecimal("1"))); // OK -- List<BigDecimal>

List<? extends Number> đọc là "list của phần tử có kiểu không rõ, nhưng biết là subtype của Number". Đây là covariance qua wildcard — an toàn hơn array covariance vì compiler ngăn ghi vào list (chỉ đọc được).

Tại sao không thể ghi vào List<? extends Number>?

List<? extends Number> list = new ArrayList<Integer>();
list.add(3.14);   // compile error -- compiler khong biet exact type la gi
list.add(1);      // compile error -- tuong tu

Compiler không biết ?Integer hay Double hay Long — nếu cho add 3.14 vào List<Integer>, là sai. Nên compiler từ chối tất cả write operations trên List<? extends T>.

Đây là PECS (Producer Extends, Consumer Super) đã học ở bài 03 — ? extends T phù hợp cho producer (chỉ đọc ra), ? super T phù hợp cho consumer (chỉ ghi vào).

8. Pitfall 1 — cố gắng ghi vào List<? extends Number>

public static void addNumbers(List<? extends Number> list) {
    list.add(42);       // compile error!
    list.add(null);     // null la ngoai le, cho phep -- nhung it dung
}

Nếu cần method vừa đọc vừa ghi, phải dùng:

  • Type parameter cụ thể: <T extends Number> void addToList(List<T> list, T item)
  • Hoặc lower bounded wildcard List<? super Integer> nếu chỉ cần ghi Integer vào list
// Vua doc vua ghi -- dung type parameter
public static <T extends Number> void addIfPositive(List<T> list, T item) {
    if (item.doubleValue() > 0) {
        list.add(item);  // OK -- list la List<T>, item la T
    }
}

9. Pitfall 2 — recursive bound <T extends Comparable<T>> và đa hình trong kế thừa nghiệp vụ

Một dạng bounded type đặc biệt thường gây nhầm lẫn:

// Sai cach doc: "T extend Comparable cua T"
// Dung cach doc: "T la type co the so sanh voi chinh no"
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

<T extends Comparable<T>>recursive generic bound — T phải implement Comparable<T>, tức là T biết cách so sánh với đối tượng cùng kiểu T. String, Integer, LocalDate đều thỏa mãn bound này.

Phân biệt với <T extends Comparable> (raw type, không nên dùng) — làm mất type safety của method compareTo.

Liên kết bài toán Kế thừa Nghiệp vụ & Đa hình (Liên hệ Bài học số 7)

Trong Bài học số 7, chúng ta đã học cách dùng Comparable để định nghĩa thứ tự tự nhiên (natural ordering) của một class. Tuy nhiên, khi áp dụng vào các bài toán thiết kế nghiệp vụ thực tế có sử dụng kế thừa (inheritance), cấu trúc <T extends Comparable<T>> sẽ nhanh chóng bị gãy đổ do tính chất invariant của generic.

Giả sử chúng ta thiết kế hệ thống quản lý nhân sự với class Employee implements Comparable<Employee> để sắp xếp theo thâm niên công tác:

public class Employee implements Comparable<Employee> {
    private String name;
    private int yearsOfService;

    @Override
    public int compareTo(Employee other) {
        return Integer.compare(this.yearsOfService, other.yearsOfService);
    }
}

Sau đó, chúng ta định nghĩa một class con là Manager kế thừa từ Employee:

public class Manager extends Employee {
    private double departmentBudget;
    // Ke thua compareTo(Employee) tu Employee
}

Bây giờ, chúng ta có một List<Manager> và muốn tìm Manager có thâm niên lớn nhất bằng method max đã định nghĩa ở trên:

List<Manager> managers = List.of(new Manager(...), new Manager(...));
Manager seniorManager = max(managers); // COMPILE ERROR!

Tại sao lại bị Compile Error?

  • Compiler cố gắng map kiểu: T được suy diễn là Manager.
  • Do đó, bound <T extends Comparable<T>> trở thành <Manager extends Comparable<Manager>>.
  • Tuy nhiên, class Manager kế thừa từ Employee nên nó chỉ implements Comparable<Employee>, chứ không implements Comparable<Manager>.
  • Do generic trong Java là invariant, Comparable<Employee> không phải là subtype của Comparable<Manager>, compiler từ chối biên dịch vì Manager không thỏa mãn bound.

Giải pháp: <T extends Comparable<? super T>>

Để giải quyết triệt để vấn đề đa hình trong kế thừa dữ liệu nghiệp vụ, JDK cung cấp cấu trúc linh hoạt hơn:

// DUNG -- Cho phep T compare voi bat ky supertype nao cua T
public static <T extends Comparable<? super T>> T max(List<T> list) {
    if (list.isEmpty()) throw new NoSuchElementException();
    T result = list.get(0);
    for (T item : list) {
        if (item.compareTo(result) > 0) {
            result = item;
        }
    }
    return result;
}

Bây giờ, khi gọi max(managers) với T = Manager:

  • Bound trở thành: Manager extends Comparable<? super Manager>.
  • ? super Manager có thể là Employee (vì Employee là cha của Manager).
  • Class Manager thực sự implements Comparable<Employee> (thông qua kế thừa từ Employee).
  • Điều kiện được thỏa mãn hoàn toàn! Chúng ta có thể so sánh và sắp xếp phân cấp các class con (Manager, Director, v.v.) dựa trên thuật toán so sánh gốc định nghĩa ở class cha Employee một cách trơn tru.

Đây là lý do tại sao các phương thức thư viện chuẩn của JDK như Collections.sort(List<T>) hay Stream.sorted() luôn khai báo bound là <T extends Comparable<? super T>> thay vì <T extends Comparable<T>>.

10. Tổng kết — bảng so sánh

Bounded <T extends B>Wildcard ? extends BWildcard ? super B
Dùng làm type parameter?Không (? không phải type)Không
Gọi method của B?Có (khi đọc)Không (type không rõ)
Có thể ghi vào?Không
Phù hợp choMethod body cần gọi method của boundParameter nhận nhiều subtype (producer)Parameter nhận supertype (consumer)
Erasure đếnBound (B)Bound (B)Object (lower bounded)

11. 📚 Deep Dive

  • JLS §4.5.1 — Type Arguments and Wildcardsdocs.oracle.com/.../jls-4.html#jls-4.5.1 — spec chính thức về wildcard và bounded type arguments, subtyping rules.
  • JLS §10.10 — Array Store Exceptiondocs.oracle.com/.../jls-10.html#jls-10.10 — spec về covariant arrays và ArrayStoreException.
  • Angelika Langer Generics FAQangelikalanger.com/GenericsFAQ — tài liệu tham khảo đầy đủ nhất về Java Generics, đặc biệt section về wildcards và variance.
  • Effective Java item 31 (Joshua Bloch, 3rd edition) — "Use bounded wildcards to increase API flexibility" — giải thích PECS, khi nào dùng extends vs super, và lý do wildcard làm API linh hoạt hơn.
  • JEP 300 (project Valhalla context) — giải thích vì sao generic invariance là thiết kế đúng và Valhalla không thay đổi điều đó dù có value types.

12. Self-check

Tự kiểm tra
Q1
Vì sao `<T>` không đủ khi muốn gọi `item.doubleValue()` trên `T`? Cần thêm gì?
`<T>` không giới hạn gì — `T` có thể là `String`, `Boolean`, bất kỳ type nào. Compiler không thể đảm bảo `T` có method `doubleValue()` nên từ chối gọi. Cần `<T extends Number>` — bound là `Number`, compiler biết mọi `T` đều có method của `Number` (bao gồm `doubleValue()`, `intValue()`, `longValue()`). Sau erasure, `T` được thay bằng `Number` trong bytecode nên gọi `item.doubleValue()` không cần cast thêm. Bounded type mở rộng interface của T mà compiler cho phép gọi trong method body.
Q2
Vì sao `List<Integer>` không phải subtype của `List<Number>` mặc dù `Integer extends Number`?
Nếu `List<Integer>` là subtype của `List<Number>`, ta có thể gán `List<Integer> ints` cho `List<Number> nums`, rồi `nums.add(3.14)` (Double là Number, hợp lệ với `List<Number>`). Nhưng thực tế bên dưới là `ArrayList<Integer>` — lúc đọc `ints.get(...)` và cast sang `Integer`, ta nhận `ClassCastException`. Generic invariance ngăn điều này bằng compile error thay vì runtime crash. Đây là quyết định thiết kế có chủ đích: bắt lỗi sớm tại compile time thay vì muộn tại runtime. Arrays không có bảo vệ này và phải dùng arraystore check mỗi lần ghi — tốn performance và vẫn chỉ phát hiện lúc runtime.
Q3
Tại sao `Integer[] ints = 3; Number[] nums = ints; nums[0] = 3.14;` ném `ArrayStoreException` thay vì compile error?
Arrays Java là covariant (`Integer[]` là subtype của `Number[]`) — quyết định từ Java 1.0 trước khi có generics. Gán `nums = ints` hợp lệ về kiểu. Tại compile time, compiler chỉ biết `nums` là `Number[]` và `3.14` là `Double extends Number` — hợp lệ. Nhưng JVM biết array thực tế là `Integer[]` và `Double` không compatible với slot `Integer`. JVM thực hiện arraystore check mỗi lần ghi vào array (chi phí runtime) và ném `ArrayStoreException` khi phát hiện không tương thích. Generics chọn invariance để tránh overhead này và bắt lỗi sớm hơn tại compile time.
Q4
Bạn có method `void printAll(List<Number> list)`. Bạn gọi `printAll(new ArrayList<Integer>())` — compile error hay OK?
Compile error. `List<Integer>` không phải subtype của `List<Number>` vì generic invariance. Method signature `List<Number>` chỉ nhận đúng `List<Number>`, không nhận `List<Integer>` hay `List<Double>`. Fix: đổi signature thành `void printAll(List<? extends Number> list)` — upper bounded wildcard cho phép nhận `List<Integer>`, `List<Double>`, `List<BigDecimal>`, bất kỳ `List<? extends Number>` nào. Trong body method, `? extends Number` chỉ cho phép đọc phần tử ra dưới dạng `Number`, không cho phép ghi — phù hợp với print (chỉ đọc).
Q5
Phân biệt `<T extends Comparable<T>>` và `<T extends Comparable<? super T>>`. Khi nào cần dạng thứ hai?
`<T extends Comparable<T>>` yêu cầu kiểu `T` phải tự implement `Comparable<T>` với chính nó. `<T extends Comparable<? super T>>` mở rộng linh hoạt hơn: `T` có thể so sánh được với bất kỳ supertype nào của nó. Chúng ta cần dạng thứ hai khi áp dụng **đa hình trong kế thừa dữ liệu nghiệp vụ (kế thừa class)**. Ví dụ (liên hệ Bài học 7): class cha `Employee implements Comparable<Employee>` và class con `Manager extends Employee`. `Manager` không tự implement `Comparable<Manager>` mà thừa kế phương thức so sánh từ `Employee`. Nếu dùng `Comparable<T>`, compiler sẽ báo lỗi khi truyền `List<Manager>` vì `Manager` không implements `Comparable<Manager>`. Dùng `Comparable<? super T>` cho phép khớp `?` với `Employee`, giúp chương trình biên dịch thành công và tái sử dụng được logic so sánh của class cha.
Q6
`<T extends A & B>` yêu cầu gì về A và B? Vì sao class phải đứng trước interface?
A và B là các type mà T phải là subtype của cả hai. Quy tắc: tối đa 1 class trong danh sách, phần còn lại phải là interface, và class phải đứng đầu. Thứ tự "class trước interface" là quy định cú pháp của JLS (JLS §4.4): nếu bound có chứa class thì nó bắt buộc viết đầu tiên — viết interface trước class là lỗi compile ở mức source, không liên quan đến hiệu năng runtime. Quy ước này cũng làm erasure đơn giản, dễ đoán: sau type erasure, `T` bị thay bằng bound đứng đầu (leftmost bound) — đặt class đầu tiên thì kiểu erasure chính là class đó, các interface còn lại được compiler xử lý bằng checkcast khi cần. Lý do chỉ 1 class: Java không có multiple class inheritance — T không thể extend 2 class cùng lúc.
Q7
Sau type erasure, bytecode của `<T extends Comparable<T>> T max(T a, T b)` trông như thế nào?
Sau erasure, `T` được thay bằng bound đầu tiên là `Comparable` (raw type). Bytecode tương đương: Comparable max(Comparable a, Comparable b) { return a.compareTo(b) >= 0 ? a : b; }. Compiler chèn thêm checkcast instruction tại các điểm return để đảm bảo kiểu trả về đúng. Điều này giải thích vì sao generic code không tạo ra class file riêng cho mỗi type argument — tất cả dùng chung 1 bytecode với `Comparable` (hay `Object` với unbounded). Performance implication: không có overhead tạo class mới (không như C++ templates), nhưng có overhead checkcast và không thể tối ưu specialization cho primitive types (lý do Valhalla đang giải quyết).

Bài tiếp theo

Bài 11 là Mini Challenge — LRU Cache: áp dụng tổng hợp LinkedHashMap, bounded generics, và immutable collections để implement LRU Cache generic từ đầu với complexity O(1) get và put.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên