Chuyển tới nội dung chính

Inner Classes (Lớp lồng nhau)

Bài trước: Interface — Đã học cách định nghĩa contract với interface. Bài này sẽ học về inner classes - các class được định nghĩa bên trong class khác để tổ chức code tốt hơn.

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Phân biệt 4 loại inner class: Member, Static Nested, Local, và Anonymous
  • Sử dụng member inner class để access instance members của outer class
  • Tạo static nested class cho helper classes không cần outer instance
  • Áp dụng local inner class và anonymous inner class cho one-time use
  • So sánh lambda expression vs anonymous inner class và biết khi nào dùng loại nào

Tại sao cần Inner Classes?

Inner class (nested class) là class được định nghĩa bên trong một class khác.

Lý do sử dụng

// Ví dụ: Node chỉ có ý nghĩa trong LinkedList
class LinkedList {
// Inner class - Node chỉ dùng trong LinkedList
private class Node {
int data;
Node next;

Node(int data) {
this.data = data;
this.next = null;
}
}

private Node head;

public void add(int data) {
Node newNode = new Node(data); // Tạo Node ở đây
if (head == null) {
head = newNode;
} else {
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
}
}
Lợi ích của Inner Classes
  • Logical grouping: Nhóm các classes liên quan với nhau
  • Encapsulation: Ẩn implementation details
  • Code readability: Code gọn gàng, dễ đọc hơn
  • Access to outer class: Truy cập được private members của outer class
  • Implement callbacks: Sử dụng trong event handling

Các loại Inner Classes

Java có 4 loại inner classes:

LoạiĐặc điểmKhi nào dùng
Member Inner ClassNon-static, member của outer classCần access instance members của outer class
Static Nested ClassStatic member của outer classKhông cần access instance members
Local Inner ClassĐịnh nghĩa trong method/blockChỉ dùng trong method đó
Anonymous Inner ClassKhông có tên, tạo tại chỗOne-time use, implement interface/extend class

1. Member Inner Class (Non-static Nested Class)

Class non-static được định nghĩa như một member của outer class.

Đặc điểm

class OuterClass {
private int outerData = 10;
private static int staticData = 20;

// Member inner class
class InnerClass {
private int innerData = 30;

public void display() {
// Truy cập được tất cả members của outer class
System.out.println("Outer data: " + outerData);
System.out.println("Static data: " + staticData);
System.out.println("Inner data: " + innerData);
}

public void accessOuterMethod() {
outerMethod(); // Gọi method của outer class
}
}

public void outerMethod() {
System.out.println("Outer method called");
}

public void createInner() {
InnerClass inner = new InnerClass();
inner.display();
}
}

// Sử dụng
public class TestMemberInner {
public static void main(String[] args) {
// Tạo outer object trước
OuterClass outer = new OuterClass();

// Tạo inner object thông qua outer
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();

// Hoặc gọi method của outer
outer.createInner();
}
}

Ví dụ thực tế: Car và Engine

class Car {
private String brand;
private int year;

public Car(String brand, int year) {
this.brand = brand;
this.year = year;
}

// Inner class Engine
class Engine {
private int horsepower;
private String type;

public Engine(int horsepower, String type) {
this.horsepower = horsepower;
this.type = type;
}

public void start() {
// Truy cập được private members của Car
System.out.println(brand + " " + year + " engine starting...");
System.out.println("Type: " + type);
System.out.println("Power: " + horsepower + " HP");
}

public void displayCarInfo() {
System.out.println("This engine belongs to " + brand);
}
}

public void createEngine(int hp, String type) {
Engine engine = new Engine(hp, type);
engine.start();
}
}

public class CarDemo {
public static void main(String[] args) {
Car car = new Car("Toyota", 2023);

// Tạo engine thông qua car object
Car.Engine engine = car.new Engine(200, "V6");
engine.start();
engine.displayCarInfo();
}
}
Lưu ý
  • Member inner class không thể có static members (trừ static final constants)
  • Cần outer class instance để tạo inner class instance
  • Inner class có thể access tất cả members của outer (kể cả private)

2. Static Nested Class

Class static được định nghĩa bên trong outer class.

Đặc điểm

class OuterClass {
private int outerData = 10;
private static int staticData = 20;

// Static nested class
static class StaticNestedClass {
private int nestedData = 30;

public void display() {
// Chỉ truy cập static members của outer class
// System.out.println(outerData); // ❌ COMPILE ERROR
System.out.println("Static data: " + staticData); // ✅ OK
System.out.println("Nested data: " + nestedData);
}

public static void staticMethod() {
System.out.println("Static nested class static method");
}
}
}

// Sử dụng
public class TestStaticNested {
public static void main(String[] args) {
// Không cần outer instance
OuterClass.StaticNestedClass nested =
new OuterClass.StaticNestedClass();
nested.display();

// Gọi static method
OuterClass.StaticNestedClass.staticMethod();
}
}

Ví dụ thực tế: Builder Pattern

class Person {
// Required parameters
private final String firstName;
private final String lastName;

// Optional parameters
private final int age;
private final String phone;
private final String address;

// Private constructor - chỉ Builder có thể gọi
private Person(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}

@Override
public String toString() {
return "Person{" +
"name='" + firstName + " " + lastName + "', " +
"age=" + age + ", " +
"phone='" + phone + "', " +
"address='" + address + "'}";
}

// Static nested Builder class
public static class Builder {
// Required parameters
private final String firstName;
private final String lastName;

// Optional parameters - initialized to default values
private int age = 0;
private String phone = "";
private String address = "";

public Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public Builder age(int age) {
this.age = age;
return this;
}

public Builder phone(String phone) {
this.phone = phone;
return this;
}

public Builder address(String address) {
this.address = address;
return this;
}

public Person build() {
return new Person(this);
}
}
}

// Sử dụng Builder
public class BuilderDemo {
public static void main(String[] args) {
// Fluent API
Person person1 = new Person.Builder("John", "Doe")
.age(30)
.phone("123-456-7890")
.address("123 Main St")
.build();

Person person2 = new Person.Builder("Jane", "Smith")
.age(25)
.build(); // Không cần set tất cả optional fields

System.out.println(person1);
System.out.println(person2);
}
}

Member Inner vs Static Nested

Đặc điểmMember Inner ClassStatic Nested Class
static keyword❌ Không✅ Có
Access outer instance members✅ Có❌ Không
Access outer static members✅ Có✅ Có
Tạo instanceCần outer instanceKhông cần outer instance
static members❌ Không (trừ constants)✅ Có

3. Local Inner Class

Class được định nghĩa bên trong một method hoặc scope block.

Đặc điểm

class OuterClass {
private int outerData = 10;

public void outerMethod() {
final int localVar = 20; // Effectively final

// Local inner class - chỉ visible trong method này
class LocalInnerClass {
public void display() {
System.out.println("Outer data: " + outerData);
System.out.println("Local var: " + localVar);
}
}

// Sử dụng local inner class
LocalInnerClass local = new LocalInnerClass();
local.display();
}

public void anotherMethod() {
// LocalInnerClass không visible ở đây
// LocalInnerClass local = new LocalInnerClass(); // ❌ ERROR
}
}

public class TestLocalInner {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.outerMethod();
}
}

Ví dụ thực tế: Validator

class FormValidator {
public boolean validate(String input, String type) {
// Local inner class cho validation logic
class EmailValidator {
public boolean isValid(String email) {
return email.contains("@") && email.contains(".");
}
}

class PhoneValidator {
public boolean isValid(String phone) {
return phone.matches("\\d{3}-\\d{3}-\\d{4}");
}
}

// Sử dụng local classes
switch (type.toLowerCase()) {
case "email":
EmailValidator emailValidator = new EmailValidator();
return emailValidator.isValid(input);
case "phone":
PhoneValidator phoneValidator = new PhoneValidator();
return phoneValidator.isValid(input);
default:
return false;
}
}
}

public class ValidatorDemo {
public static void main(String[] args) {
FormValidator validator = new FormValidator();

System.out.println(validator.validate("[email protected]", "email")); // true
System.out.println(validator.validate("invalid-email", "email")); // false
System.out.println(validator.validate("123-456-7890", "phone")); // true
System.out.println(validator.validate("123456", "phone")); // false
}
}

Effectively Final — Giải thích chi tiết

Effectively final là biến không được khai báo final nhưng không bao giờ thay đổi giá trị sau khi khởi tạo.

public class EffectivelyFinalDemo {
public void demonstrateEffectivelyFinal() {
int effectivelyFinalVar = 10; // Không có final keyword
// effectivelyFinalVar = 20; // ❌ Nếu uncomment → không còn effectively final

final int explicitFinalVar = 20; // Có final keyword

int notFinalVar = 30;

class LocalClass {
public void print() {
// ✅ OK - effectively final
System.out.println(effectivelyFinalVar);

// ✅ OK - explicit final
System.out.println(explicitFinalVar);

// ❌ Compile error nếu notFinalVar bị reassign
System.out.println(notFinalVar);
}
}

notFinalVar = 40; // ❌ Nếu uncomment → LocalClass.print() compile error

LocalClass local = new LocalClass();
local.print();
}
}
📖 Theo JLS §4.12.4

Local variable được capture bởi local/anonymous class phải final hoặc effectively final để đảm bảo thread-safetyconsistency.

💡 Cách nhớ Effectively Final

Giống hợp đồng ngầm: Biến không cần ký "final" trên giấy, nhưng hành vi của nó như đã ký — không thay đổi sau khi khởi tạo.

Tại sao cần Effectively Final?

public Runnable createRunnable() {
int counter = 0;

// ❌ Nếu cho phép capture non-final variable
return new Runnable() {
@Override
public void run() {
System.out.println(counter); // counter value nào?
}
};

// counter có thể thay đổi sau khi Runnable được tạo
// → Giá trị counter trong run() không xác định
}

// ✅ Với effectively final
public Runnable createRunnable() {
int counter = 0; // Effectively final

return new Runnable() {
@Override
public void run() {
System.out.println(counter); // Luôn = 0
}
};
}
Local Inner Class Restrictions
  • Chỉ visible trong method/block định nghĩa nó
  • Có thể access local variables nếu chúng là final hoặc effectively final
  • Không thể có access modifiers (public, private, protected)
  • Không thể là static

4. Anonymous Inner Class

Class không có tên, được tạo và sử dụng tại chỗ.

Implement Interface

interface Greeting {
void greet(String name);
}

public class AnonymousDemo {
public static void main(String[] args) {
// Anonymous inner class implement interface
Greeting greeting = new Greeting() {
@Override
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
};

greeting.greet("John"); // Hello, John!

// Sử dụng trực tiếp
new Greeting() {
@Override
public void greet(String name) {
System.out.println("Hi, " + name + "!");
}
}.greet("Jane"); // Hi, Jane!
}
}

Extend Class

abstract class Animal {
abstract void makeSound();
}

public class AnonymousExtendDemo {
public static void main(String[] args) {
// Anonymous inner class extend abstract class
Animal dog = new Animal() {
@Override
void makeSound() {
System.out.println("Woof! Woof!");
}
};

Animal cat = new Animal() {
@Override
void makeSound() {
System.out.println("Meow! Meow!");
}
};

dog.makeSound(); // Woof! Woof!
cat.makeSound(); // Meow! Meow!
}
}

Ví dụ thực tế: Event Handling

interface ClickListener {
void onClick();
}

class Button {
private String label;
private ClickListener clickListener;

public Button(String label) {
this.label = label;
}

public void setOnClickListener(ClickListener listener) {
this.clickListener = listener;
}

public void click() {
System.out.println("Button '" + label + "' clicked");
if (clickListener != null) {
clickListener.onClick();
}
}
}

public class EventHandlingDemo {
public static void main(String[] args) {
Button submitButton = new Button("Submit");
Button cancelButton = new Button("Cancel");

// Anonymous inner class cho event handling
submitButton.setOnClickListener(new ClickListener() {
@Override
public void onClick() {
System.out.println("Form submitted!");
}
});

cancelButton.setOnClickListener(new ClickListener() {
@Override
public void onClick() {
System.out.println("Form cancelled!");
}
});

submitButton.click();
cancelButton.click();
}
}

Ví dụ: Thread với Anonymous Class

public class ThreadDemo {
public static void main(String[] args) {
// Anonymous inner class implement Runnable
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread 1: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread 2: " + i);
try {
Thread.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

thread1.start();
thread2.start();
}
}

So sánh các loại Inner Classes

LoạiĐịnh nghĩaAccess outer membersAccess modifiersKhi nào dùng
Member InnerTrong outer classInstance + static✅ CóCần access outer instance
Static NestedTrong outer class (static)Chỉ static✅ CóKhông cần outer instance
Local InnerTrong method/blockCó (final/effectively final locals)❌ KhôngChỉ dùng trong method
AnonymousTại chỗ❌ KhôngOne-time use, callbacks

Scope Rules — Inner Class Accessing Outer Class Members

Member inner class có thể access tất cả members của outer class (kể cả private):

Qualified this

class Outer {
private String outerField = "Outer field";

class Inner {
private String innerField = "Inner field";

// Nếu Inner cũng có field cùng tên
private String outerField = "Inner's outerField";

public void demonstrate() {
// this → Inner's instance
System.out.println(this.outerField); // Output: Inner's outerField

// Outer.this → Outer's instance
System.out.println(Outer.this.outerField); // Output: Outer field

// Có thể gọi Outer's methods
Outer.this.outerMethod();
}
}

private void outerMethod() {
System.out.println("Outer method called from Inner");
}

public void createInner() {
Inner inner = new Inner();
inner.demonstrate();
}
}

public class ScopeDemo {
public static void main(String[] args) {
Outer outer = new Outer();
outer.createInner();
}
}

Static Nested Class vs Inner Class — Access Differences

class OuterAccess {
private static int staticField = 10;
private int instanceField = 20;

private static void staticMethod() {
System.out.println("Static method");
}

private void instanceMethod() {
System.out.println("Instance method");
}

// Static nested class
static class StaticNested {
public void access() {
System.out.println(staticField); // ✅ OK
staticMethod(); // ✅ OK

// System.out.println(instanceField); // ❌ Compile error!
// instanceMethod(); // ❌ Compile error!
}
}

// Member inner class
class MemberInner {
public void access() {
System.out.println(staticField); // ✅ OK
staticMethod(); // ✅ OK
System.out.println(instanceField); // ✅ OK
instanceMethod(); // ✅ OK
}
}
}
💡 Cách nhớ: Static = Không biết outer instance

Static nested class giống "người thuê nhà" — biết địa chỉ nhà (static members) nhưng không vào được phòng riêng (instance members).

Member inner class giống "thành viên gia đình" — có chìa khóa tất cả phòng (access tất cả).

Serialization Issues với Inner Classes

Inner classes (đặc biệt là non-static) có vấn đề khi serialize:

import java.io.*;

class Outer implements Serializable {
private static final long serialVersionUID = 1L;
private String outerData = "Outer";

// ⚠️ Non-static inner class serialization có vấn đề
class Inner implements Serializable {
private static final long serialVersionUID = 1L;
private String innerData = "Inner";

// Inner class tự động giữ reference đến Outer instance
// → Serialize Inner cũng serialize Outer
}

// ✅ Static nested class OK cho serialization
static class StaticNested implements Serializable {
private static final long serialVersionUID = 1L;
private String data = "Static Nested";
}
}

public class SerializationDemo {
public static void main(String[] args) {
// ⚠️ Serialize inner class
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("inner.ser"))) {
out.writeObject(inner);
// Cảnh báo: Serialize inner cũng serialize outer instance
} catch (IOException e) {
e.printStackTrace();
}

// ✅ Serialize static nested class (không vấn đề)
Outer.StaticNested nested = new Outer.StaticNested();
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("nested.ser"))) {
out.writeObject(nested);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Serialization Best Practices
  • Tránh serialize non-static inner classes nếu không cần thiết
  • Nếu bắt buộc, đảm bảo outer class cũng Serializable
  • Ưu tiên static nested classes cho serializable objects
  • Xem xét dùng separate top-level classes cho serialization

Lambda Expressions vs Anonymous Inner Classes

Lambda expressions (Java 8+) là cách ngắn gọn hơn để viết anonymous inner class cho functional interface.

So sánh Chi tiết

Đặc điểmLambda ExpressionAnonymous Inner Class
SyntaxNgắn gọn: (a, b) -> a + bDài dòng: new Interface() { ... }
this referenceTrỏ đến enclosing classTrỏ đến anonymous class
Compile outputinvokedynamic bytecodeSeparate .class file
PerformanceNhanh hơn (lazy initialization)Chậm hơn (eager class loading)
Functional interface✅ Chỉ dùng với functional interface✅ Dùng với bất kỳ interface/class
State❌ Không có fields✅ Có thể có fields
Multiple methods❌ Chỉ 1 method✅ Nhiều methods

this Reference Difference

public class ThisReferenceDemo {
private String field = "Enclosing class";

public void demonstrateLambda() {
Runnable lambda = () -> {
// this → ThisReferenceDemo instance
System.out.println(this.field); // Output: Enclosing class
};
lambda.run();
}

public void demonstrateAnonymous() {
Runnable anonymous = new Runnable() {
private String field = "Anonymous class";

@Override
public void run() {
// this → Runnable anonymous class instance
System.out.println(this.field); // Output: Anonymous class

// Access enclosing class
System.out.println(ThisReferenceDemo.this.field); // Output: Enclosing class
}
};
anonymous.run();
}
}

Khi nào dùng Lambda vs Anonymous Class?

Dùng Lambda khi:

  • Interface là functional interface (1 abstract method)
  • Code đơn giản, ngắn gọn
  • Không cần access this của anonymous class
  • Performance quan trọng

Dùng Anonymous Class khi:

  • Interface có nhiều abstract methods
  • Cần implement multiple methods
  • Cần fields hoặc state
  • Cần access this của anonymous class
  • Extend abstract class
interface Worker {
void work();
void rest(); // Nhiều methods - không thể dùng lambda
}

public class LambdaLimitation {
public static void main(String[] args) {
// Phải dùng anonymous class
Worker worker = new Worker() {
@Override
public void work() {
System.out.println("Working...");
}

@Override
public void rest() {
System.out.println("Resting...");
}
};

worker.work();
worker.rest();
}
}
🔥 Bẫy OCP: Static Nested Class vs Inner Class

Bẫy 1: Access difference

class Outer {
private int instanceVar = 10;
private static int staticVar = 20;

static class StaticNested {
void access() {
// ❌ Compile error! Không access instance members
// System.out.println(instanceVar);

// ✅ OK - access static members
System.out.println(staticVar);
}
}

class Inner {
void access() {
// ✅ Access cả instance và static members
System.out.println(instanceVar);
System.out.println(staticVar);
}
}
}

Bẫy 2: this reference trong inner class

class Outer {
private String name = "Outer";

class Inner {
private String name = "Inner";

void print() {
System.out.println(name); // Output: Inner
System.out.println(this.name); // Output: Inner
System.out.println(Outer.this.name); // Output: Outer
}
}
}

Bẫy 3: Lambda vs Anonymous Class với effectively final

public void test() {
int count = 0;

// ❌ Compile error cho cả lambda và anonymous class
// count++; // Làm count không còn effectively final

Runnable lambda = () -> System.out.println(count); // ✅ OK

Runnable anonymous = new Runnable() {
@Override
public void run() {
System.out.println(count); // ✅ OK
}
};

// ❌ Nếu uncomment → cả lambda và anonymous class đều compile error
// count++;
}

Ví dụ thực tế: Callback Pattern

interface Callback {
void onSuccess(String result);
void onError(String error);
}

class AsyncTask {
public void execute(String task, Callback callback) {
System.out.println("Executing task: " + task);

// Simulate async operation
try {
Thread.sleep(1000);

// Simulate success or failure
if (Math.random() > 0.5) {
callback.onSuccess("Task completed: " + task);
} else {
callback.onError("Task failed: " + task);
}
} catch (InterruptedException e) {
callback.onError(e.getMessage());
}
}
}

public class CallbackDemo {
public static void main(String[] args) {
AsyncTask task = new AsyncTask();

// Anonymous inner class for callback
task.execute("Download file", new Callback() {
@Override
public void onSuccess(String result) {
System.out.println("✅ Success: " + result);
}

@Override
public void onError(String error) {
System.out.println("❌ Error: " + error);
}
});

// Wait for async task
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

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

Bài 1: University System

Tạo class University với inner classes:

  • Department: lưu tên khoa, số lượng giảng viên
  • Student: lưu thông tin sinh viên, thuộc department nào
  • Member methods để thêm department, thêm student, hiển thị thông tin

Bài 2: Stack Implementation

Implement Stack<T> với:

  • Static nested class Node<T> để lưu elements
  • Methods: push(), pop(), peek(), isEmpty()
  • Test với các kiểu dữ liệu khác nhau

Bài 3: GUI Event Handling

Tạo class TextField với:

  • Method setOnChangeListener(ChangeListener listener)
  • Interface ChangeListener với method onChange(String newValue)
  • Sử dụng anonymous inner class để handle events
  • Test với multiple TextFields

Bài 4: Compare Implementations

Implement interface Comparator<Person> bằng:

  1. Named class
  2. Anonymous inner class
  3. Lambda expression

So sánh Person theo: name, age, salary. Chạy và so sánh.

Compiler-generated Class Files

Khi compile, mỗi inner class tạo ra file .class riêng biệt:

Quy tắc đặt tên:

  • Member / Static nested: Outer$InnerName.class
  • Anonymous class: Outer$1.class, Outer$2.class,... (đánh số thứ tự)
  • Local class: Outer$1LocalName.class
OCP Tip

Nếu đề bài hỏi "chương trình tạo ra bao nhiêu .class file", hãy đếm: 1 cho outer class + 1 cho mỗi inner/nested/anonymous/local class.

OCP Trap

Local class không thể có static members (trừ static final constants):

void method() {
class LocalClass {
// ❌ Compile error! Local class không thể có static fields
// static int count = 0;

// ✅ OK — static final constant
static final int MAX = 100;

// ❌ Compile error! Local class không thể có static methods
// static void helper() { }
}
}

Lý do: Local class là inner class (non-static context), nên tuân theo cùng quy tắc với member inner class — không có static members.

Ngoại lệ (Java 16+): Từ Java 16, inner classes có thể có static members, nhưng OCP vẫn thường test theo quy tắc trước Java 16.

Tổng kết

Key Takeaways
  1. Inner classes giúp organize code và encapsulation tốt hơn
  2. Member inner class cần outer instance, access được instance members
  3. Static nested class không cần outer instance, chỉ access static members
  4. Local inner class chỉ dùng trong method, access final/effectively final locals
  5. Anonymous inner class dùng cho one-time implementation
  6. Lambda expressions ngắn gọn hơn anonymous class cho functional interface

Khi nào dùng loại nào?

Member Inner Class
└── Khi cần access instance members của outer class
└── Ví dụ: Engine trong Car, Node trong LinkedList

Static Nested Class
└── Khi không cần access instance members
└── Ví dụ: Builder pattern, Helper classes

Local Inner Class
└── Khi chỉ dùng trong một method
└── Ví dụ: Validators, Comparators trong method

Anonymous Inner Class
└── One-time use, implement interface/extend class
└── Ví dụ: Event handlers, callbacks, threads

Lambda Expression
└── Functional interface, code ngắn gọn
└── Ví dụ: Collections.sort(), Stream API

Inner classes là công cụ mạnh mẽ giúp code organized, encapsulated, và readable!

Đọc thêm