Inheritance (Kế thừa)
Sau bài này, bạn sẽ:
- Hiểu khái niệm inheritance và mối quan hệ IS-A trong OOP
- Sử dụng từ khóa extends và super để kế thừa và gọi members của class cha
- Áp dụng method overriding và annotation @Override đúng cách
- Override các method quan trọng của Object class (toString, equals, hashCode)
- Nắm vững constructor chaining và thứ tự khởi tạo trong inheritance
Kế thừa là gì?
Inheritance (Kế thừa) là một cơ chế trong OOP cho phép một class (class con - subclass/child class) kế thừa các thuộc tính và phương thức từ một class khác (class cha - superclass/parent class).
IS-A Relationship
Kế thừa thể hiện mối quan hệ IS-A (là một):
ManagerIS-AEmployee(Manager là một Employee)CircleIS-AShape(Circle là một Shape)DogIS-AAnimal(Dog là một Animal)
- Tái sử dụng code: Không cần viết lại code đã có ở class cha
- Dễ bảo trì: Thay đổi ở class cha tự động áp dụng cho các class con
- Tính mở rộng: Dễ dàng thêm chức năng mới thông qua class con
- Đa hình: Cho phép xử lý các đối tượng cùng loại một cách linh hoạt
Cú pháp: extends keyword
Sử dụng từ khóa extends để kế thừa:
// Class cha (Superclass/Parent class)
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void work() {
System.out.println(name + " is working...");
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
}
// Class con (Subclass/Child class)
class Manager extends Employee {
private String department;
public Manager(String name, double salary, String department) {
super(name, salary); // Gọi constructor của class cha
this.department = department;
}
public void manage() {
System.out.println(getName() + " is managing " + department);
}
public String getDepartment() {
return department;
}
}
super keyword
Từ khóa super được dùng để:
1. Gọi constructor của class cha
class Animal {
private String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal constructor called");
}
public String getName() {
return name;
}
}
class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // Phải gọi đầu tiên trong constructor
this.breed = breed;
System.out.println("Dog constructor called");
}
}
super()phải là câu lệnh đầu tiên trong constructor của class con- Nếu không gọi
super()tường minh, Java tự động gọisuper()(constructor không tham số của class cha) - Nếu class cha không có constructor không tham số, bạn phải gọi
super()với tham số phù hợp
2. Gọi method của class cha
class Vehicle {
protected int maxSpeed = 100;
public void displayInfo() {
System.out.println("Max speed: " + maxSpeed + " km/h");
}
}
class Car extends Vehicle {
private String brand;
public Car(String brand) {
this.brand = brand;
}
@Override
public void displayInfo() {
super.displayInfo(); // Gọi method của class cha
System.out.println("Brand: " + brand);
}
}
Method Overriding
Method Overriding là việc class con định nghĩa lại method đã có ở class cha với cùng signature.
Quy tắc Overriding
class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
public double getArea() {
return 0.0;
}
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override // Annotation khuyến khích sử dụng
public void draw() {
System.out.println("Drawing a circle");
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
@Override
public double getArea() {
return width * height;
}
}
Quy tắc quan trọng khi Override
| Yêu cầu | Giải thích |
|---|---|
| Cùng tên method | Tên method phải giống hệt class cha |
| Cùng parameters | Số lượng, kiểu dữ liệu, thứ tự tham số phải giống |
| Cùng hoặc rộng hơn access modifier | private → ❌, protected → protected/public, public → public |
| Cùng hoặc hẹp hơn return type | Có thể return subtype (covariant return type) |
| Không được throw checked exception mới | Có thể throw ít exception hơn hoặc subtype exception |
Nên luôn sử dụng @Override annotation khi override method:
- Compiler sẽ báo lỗi nếu method không thực sự override
- Giúp code dễ đọc và maintain hơn
- Tránh lỗi đánh máy tên method
Object Class
Trong Java, tất cả các class đều kế thừa từ java.lang.Object (ngầm định).
Các method quan trọng của Object class
class Student {
private String id;
private String name;
private int age;
public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// Override toString() để in thông tin đối tượng dễ đọc
@Override
public String toString() {
return "Student{id='" + id + "', name='" + name + "', age=" + age + "}";
}
// Override equals() để so sánh nội dung đối tượng
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // Cùng reference
if (obj == null || getClass() != obj.getClass()) return false;
Student student = (Student) obj;
return age == student.age &&
id.equals(student.id) &&
name.equals(student.name);
}
// Override hashCode() khi override equals()
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
}
Sử dụng
public class TestStudent {
public static void main(String[] args) {
Student s1 = new Student("S001", "Nguyen Van A", 20);
Student s2 = new Student("S001", "Nguyen Van A", 20);
Student s3 = s1;
// toString()
System.out.println(s1); // Student{id='S001', name='Nguyen Van A', age=20}
// equals()
System.out.println(s1.equals(s2)); // true (nội dung giống nhau)
System.out.println(s1 == s2); // false (khác reference)
System.out.println(s1 == s3); // true (cùng reference)
// hashCode()
System.out.println(s1.hashCode() == s2.hashCode()); // true
}
}
Khi override equals(), phải override hashCode() để đảm bảo:
- Nếu
a.equals(b)làtruethìa.hashCode() == b.hashCode() - Điều này quan trọng khi dùng object trong
HashSet,HashMap
Single Inheritance trong Java
Java chỉ hỗ trợ single inheritance (kế thừa đơn): một class chỉ có thể kế thừa từ một class cha duy nhất.
// ✅ Hợp lệ
class A { }
class B extends A { }
class C extends B { }
// ❌ Không hợp lệ - Multiple inheritance
class D extends A, B { } // COMPILE ERROR
Tại sao Java không cho phép Multiple Inheritance?
Để tránh Diamond Problem:
Animal
/ \
Dog Cat
\ /
DogCat ← Nếu Dog và Cat đều override eat(), DogCat kế thừa eat() nào?
Java cho phép implement nhiều interface để đạt được tính linh hoạt tương tự multiple inheritance (sẽ học ở bài Interface).
Constructor Chaining trong Kế thừa
Constructor chaining là quá trình gọi constructor từ class con đến class cha theo chuỗi.
class Animal {
public Animal() {
System.out.println("1. Animal constructor");
}
}
class Mammal extends Animal {
public Mammal() {
super(); // Tự động gọi nếu không viết
System.out.println("2. Mammal constructor");
}
}
class Dog extends Mammal {
public Dog() {
super(); // Tự động gọi nếu không viết
System.out.println("3. Dog constructor");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
// Output:
// 1. Animal constructor
// 2. Mammal constructor
// 3. Dog constructor
}
}
Thứ tự khởi tạo chi tiết
Theo JLS §12.5, quá trình khởi tạo trong kế thừa diễn ra theo thứ tự:
- Static blocks của class cha → class con (chỉ 1 lần khi load class)
- Constructor của class cha (từ trên xuống theo hierarchy)
- Instance initializer blocks của class con
- Instance variables của class con
- Constructor body của class con
class Parent {
static { System.out.println("1. Parent static block"); }
{ System.out.println("3. Parent instance block"); }
public Parent() {
System.out.println("4. Parent constructor");
}
}
class Child extends Parent {
static { System.out.println("2. Child static block"); }
{ System.out.println("5. Child instance block"); }
public Child() {
System.out.println("6. Child constructor");
}
}
public class Test {
public static void main(String[] args) {
Child c = new Child();
// Output theo thứ tự 1 → 6
}
}
Initialization Order: Ví dụ phức tạp
class Grandparent {
static int staticGP = initStaticGP();
int instanceGP = initInstanceGP();
static int initStaticGP() {
System.out.println("1. Grandparent static init");
return 1;
}
int initInstanceGP() {
System.out.println("4. Grandparent instance init");
return 1;
}
public Grandparent() {
System.out.println("5. Grandparent constructor");
}
}
class Parent extends Grandparent {
static int staticP = initStaticP();
int instanceP = initInstanceP();
static int initStaticP() {
System.out.println("2. Parent static init");
return 2;
}
int initInstanceP() {
System.out.println("6. Parent instance init");
return 2;
}
public Parent() {
System.out.println("7. Parent constructor");
}
}
class Child extends Parent {
static int staticC = initStaticC();
int instanceC = initInstanceC();
static int initStaticC() {
System.out.println("3. Child static init");
return 3;
}
int initInstanceC() {
System.out.println("8. Child instance init");
return 3;
}
public Child() {
System.out.println("9. Child constructor");
}
}
public class InitializationOrderDemo {
public static void main(String[] args) {
System.out.println("Creating first Child...");
Child c1 = new Child();
// Output: 1→2→3→4→5→6→7→8→9
System.out.println("\nCreating second Child...");
Child c2 = new Child();
// Static blocks không chạy lại! Output: 4→5→6→7→8→9
}
}
Initialization order đảm bảo rằng superclass luôn được khởi tạo trước subclass, và static initialization chỉ chạy 1 lần khi class được load vào JVM lần đầu.
Giống như xây nhà: phải có móng (static) trước khi xây tường (instance), và phải xây tầng dưới (cha) trước tầng trên (con).
Ví dụ thực tế
Ví dụ 1: Employee Hierarchy
class Employee {
protected String name;
protected String id;
protected double baseSalary;
public Employee(String name, String id, double baseSalary) {
this.name = name;
this.id = id;
this.baseSalary = baseSalary;
}
public double calculateSalary() {
return baseSalary;
}
public void displayInfo() {
System.out.println("ID: " + id);
System.out.println("Name: " + name);
System.out.println("Salary: $" + calculateSalary());
}
}
class Manager extends Employee {
private double bonus;
private String department;
public Manager(String name, String id, double baseSalary,
double bonus, String department) {
super(name, id, baseSalary);
this.bonus = bonus;
this.department = department;
}
@Override
public double calculateSalary() {
return baseSalary + bonus;
}
@Override
public void displayInfo() {
super.displayInfo();
System.out.println("Department: " + department);
System.out.println("Bonus: $" + bonus);
}
}
class Developer extends Employee {
private String programmingLanguage;
private int projectCount;
public Developer(String name, String id, double baseSalary,
String programmingLanguage, int projectCount) {
super(name, id, baseSalary);
this.programmingLanguage = programmingLanguage;
this.projectCount = projectCount;
}
@Override
public double calculateSalary() {
return baseSalary + (projectCount * 500); // Bonus per project
}
@Override
public void displayInfo() {
super.displayInfo();
System.out.println("Language: " + programmingLanguage);
System.out.println("Projects: " + projectCount);
}
}
Ví dụ 2: Shape Hierarchy
abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
public abstract double getArea();
public abstract double getPerimeter();
public void displayInfo() {
System.out.println("Color: " + color);
System.out.println("Area: " + getArea());
System.out.println("Perimeter: " + getPerimeter());
}
}
class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public double getPerimeter() {
return 2 * Math.PI * radius;
}
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
@Override
public double getPerimeter() {
return 2 * (width + height);
}
}
Bài tập thực hành
Bài 1: Vehicle Hierarchy
Tạo hệ thống quản lý phương tiện:
- Class
Vehicle:brand,year,price - Class
CarextendsVehicle: thêmnumberOfDoors,fuelType - Class
MotorcycleextendsVehicle: thêmengineCapacity - Override
toString()vàequals()cho tất cả các class - Tạo method
displayInfo()cho mỗi class
Bài 2: Bank Account System
Tạo hệ thống tài khoản ngân hàng:
- Class
BankAccount:accountNumber,balance,ownerName- Methods:
deposit(),withdraw(),getBalance()
- Methods:
- Class
SavingsAccountextendsBankAccount: thêminterestRate- Override
withdraw(): limit số lần withdraw mỗi tháng - Thêm method
calculateInterest()
- Override
- Class
CheckingAccountextendsBankAccount: thêmoverdraftLimit- Override
withdraw(): cho phép rút âm trong giới hạn
- Override
Bài 3: Person Hierarchy
Tạo class hierarchy cho người:
- Class
Person:name,age,address - Class
StudentextendsPerson:studentId,major,gpa - Class
TeacherextendsPerson:teacherId,subject,salary - Implement
toString(),equals(), vàhashCode()đúng cách
Inheritance vs Composition
Theo Effective Java Item 18 ("Favor composition over inheritance"), kế thừa không phải lúc nào cũng là lựa chọn tốt nhất.
Khi nào dùng Inheritance?
Dùng kế thừa khi:
- Có mối quan hệ IS-A rõ ràng (Dog IS-A Animal)
- Subclass là specialized version của superclass
- Superclass được thiết kế for inheritance (documented và stable)
Khi nào dùng Composition?
Dùng composition khi:
- Có mối quan hệ HAS-A (Car HAS-A Engine)
- Muốn tái sử dụng code mà không cần IS-A
- Superclass có thể thay đổi trong tương lai
// ❌ Inheritance không phù hợp - Stack không phải là Vector
class Stack extends Vector {
public void push(Object item) {
addElement(item);
}
// Vấn đề: Stack expose tất cả Vector methods (add, remove, get...)
// → Phá vỡ encapsulation của Stack!
}
// ✅ Composition tốt hơn
class Stack {
private List<Object> elements = new ArrayList<>();
public void push(Object item) {
elements.add(item);
}
public Object pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
// Chỉ expose methods cần thiết, ẩn implementation
}
Fragile Base Class Problem
Vấn đề: Thay đổi ở superclass có thể phá vỡ subclass mà không ai biết.
Hãy tưởng tượng bạn mua một chiếc tủ (superclass) và đặt đồ vào ngăn kéo (subclass). Một ngày, nhà sản xuất thay đổi kích thước ngăn kéo — đồ của bạn không vừa nữa mà bạn không hề được thông báo. Đó chính là Fragile Base Class Problem.
class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c); // ❌ BUG! addAll() gọi add() internally
}
public int getAddCount() {
return addCount;
}
}
// Test
InstrumentedHashSet<String> set = new InstrumentedHashSet<>();
set.addAll(List.of("A", "B", "C"));
System.out.println(set.getAddCount()); // Output: 6 (expected 3!)
// Giải thích: addAll() cộng 3, rồi gọi add() 3 lần → mỗi lần cộng 1 → tổng 6
Câu hỏi OCP: Tại sao InstrumentedHashSet.addAll(List.of("A", "B", "C")) cho addCount = 6?
Đáp án: HashSet.addAll() internally gọi add() cho mỗi element. Vì add() đã bị override và tăng count, nên mỗi element được đếm 2 lần: 1 lần trong addAll(), 1 lần trong add().
Giải pháp: Dùng composition thay vì inheritance để kiểm soát đầy đủ behavior.
Method Hiding vs Method Overriding
Method hiding (ẩn method) xảy ra với static methods, khác với overriding (instance methods).
class Parent {
// Instance method - có thể override
public void instanceMethod() {
System.out.println("Parent instance method");
}
// Static method - bị hide, không override
public static void staticMethod() {
System.out.println("Parent static method");
}
}
class Child extends Parent {
@Override
public void instanceMethod() {
System.out.println("Child instance method");
}
// Method hiding - không phải override
public static void staticMethod() {
System.out.println("Child static method");
}
}
public class HidingDemo {
public static void main(String[] args) {
Parent p = new Child();
// Runtime polymorphism - gọi Child's method
p.instanceMethod(); // Output: Child instance method
// Compile-time binding - gọi Parent's method
p.staticMethod(); // Output: Parent static method (!)
// Static methods được gọi theo reference type, không phải object type
Child.staticMethod(); // Output: Child static method
}
}
Static methods cannot be overridden, chỉ có thể bị hidden. Method call resolution cho static methods diễn ra tại compile-time dựa trên reference type, không phải object type.
Static methods: "Tĩnh" như ngôi nhà cố định — xác định địa chỉ ngay khi xây (compile). Instance methods: "Động" như taxi — chỉ biết đích đến khi đang chạy (runtime).
Sealed Classes (Java 17+)
Sealed classes (class niêm phong) cho phép kiểm soát ai được phép kế thừa class.
// Sealed class - chỉ cho phép 3 subclasses
public sealed class Shape
permits Circle, Rectangle, Triangle {
public abstract double getArea();
}
// Các subclasses phải là: final, sealed, hoặc non-sealed
public final class Circle extends Shape {
private double radius;
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
public non-sealed class Rectangle extends Shape {
// non-sealed → cho phép subclasses khác kế thừa tiếp
private double width, height;
@Override
public double getArea() {
return width * height;
}
}
public sealed class Triangle extends Shape
permits EquilateralTriangle {
// Sealed → chỉ EquilateralTriangle được kế thừa
@Override
public double getArea() {
// implementation
return 0;
}
}
// ❌ Compile error - Square không được phép kế thừa Shape
// public class Square extends Shape { }
Lợi ích của Sealed Classes
- Exhaustive switch (Java 17+): Compiler kiểm tra tất cả cases
- Domain modeling: Mô hình hóa tập hợp hữu hạn các subclasses
- Security: Ngăn chặn inheritance không mong muốn
public double calculateArea(Shape shape) {
// Compiler biết tất cả subclasses → kiểm tra exhaustiveness
return switch (shape) {
case Circle c -> c.getArea();
case Rectangle r -> r.getArea();
case Triangle t -> t.getArea();
// Không cần default - compiler đảm bảo đã cover hết
};
}
Bẫy 1: Implicit super() call
class Parent {
public Parent(String name) {
// Không có no-arg constructor
}
}
class Child extends Parent {
public Child() {
// ❌ Compile error! Implicit super() không tìm thấy no-arg constructor
}
}
// ✅ Phải gọi super(String) tường minh
class Child extends Parent {
public Child() {
super("default"); // OK
}
}
Bẫy 2: Constructors are NOT inherited
class Parent {
public Parent(int x) { }
}
class Child extends Parent {
// ❌ Child không tự động có constructor(int x)
// Phải tự định nghĩa
}
// Test
Child c = new Child(5); // ❌ Compile error!
Bẫy 3: Method overriding không thể ném broader checked exception
class Parent {
public void read() throws IOException { }
}
class Child extends Parent {
// ✅ OK — cùng exception
public void read() throws IOException { }
// ✅ OK — hẹp hơn (subtype)
// public void read() throws FileNotFoundException { }
// ✅ OK — không throw gì cả
// public void read() { }
// ❌ Compile error! — broader exception
// public void read() throws Exception { }
}
Chi tiết 6 scenarios ở bài Exception Handling.
Bẫy 4: final class và final method
final class FinalClass { }
// ❌ Compile error - không thể extend final class
class SubClass extends FinalClass { }
class Parent {
public final void finalMethod() { }
}
class Child extends Parent {
// ❌ Compile error - không thể override final method
@Override
public void finalMethod() { }
}
Protected Access xuyên Package
protected member có thể truy cập từ:
- Cùng package — bất kỳ class nào
- Khác package — chỉ qua subclass, và chỉ qua reference của chính subclass đó
Quy tắc thứ 2 hay gây nhầm lẫn. Hãy xem 6 scenarios:
// package com.parent
public class Parent {
protected int value = 42;
protected void show() {
System.out.println("Parent show");
}
}
// package com.child — KHÁC package với Parent
public class Child extends Parent {
public void test() {
// Scenario 1: Qua this → ✅ OK
System.out.println(this.value);
this.show();
// Scenario 2: Qua Child reference → ✅ OK
Child other = new Child();
System.out.println(other.value);
other.show();
// Scenario 3: Implicit this → ✅ OK
System.out.println(value);
show();
// Scenario 4: Qua Parent reference → ❌ COMPILE ERROR!
Parent p = new Parent();
// System.out.println(p.value); // ❌
// p.show(); // ❌
// Scenario 5: Qua Parent ref trỏ đến Child → ❌ vẫn ERROR!
Parent p2 = new Child();
// System.out.println(p2.value); // ❌
// Scenario 6: Qua sibling subclass reference → ❌ ERROR!
// SiblingChild s = new SiblingChild();
// System.out.println(s.value); // ❌
}
}
| Scenario | Access qua | Kết quả | Lý do |
|---|---|---|---|
this.value | this (Child) | ✅ OK | Subclass truy cập inherited member |
other.value | Child ref | ✅ OK | Cùng class type |
value | Implicit this | ✅ OK | Tương đương this.value |
p.value | Parent ref | ❌ FAIL | Khác package, không qua subclass ref |
p2.value | Parent ref | ❌ FAIL | Compiler nhìn reference type, không phải object type |
sibling.value | SiblingChild ref | ❌ FAIL | Khác nhánh subclass |
Quy tắc vàng: Protected access từ khác package chỉ hoạt động khi reference type là chính class đó hoặc subclass — không phải superclass reference. Compiler kiểm tra reference type, không phải object type.
Clone và Cloneable
Object.clone() tạo bản sao của object. Nhưng cơ chế clone trong Java có nhiều "bẫy".
Shallow Copy vs Deep Copy
public class Address implements Cloneable {
String city;
public Address(String city) { this.city = city; }
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
}
public class Person implements Cloneable {
String name;
Address address; // Reference type
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
// Shallow copy — address vẫn trỏ đến cùng object
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
Person p1 = new Person("An", new Address("Hanoi"));
Person p2 = p1.clone();
p2.address.city = "HCMC";
System.out.println(p1.address.city); // HCMC! ← p1 cũng bị ảnh hưởng
Deep copy — clone cả các reference bên trong:
@Override
public Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = this.address.clone(); // Deep copy address
return cloned;
}
Tại sao clone() là protected?
Object.clone() được khai báo protected — nghĩa là bạn không thể gọi obj.clone() từ bên ngoài class trừ khi class đó override clone() thành public.
Quy tắc khi dùng Cloneable
- Class phải
implements Cloneable(marker interface — không có method) - Override
clone()và gọisuper.clone() - Nếu không implement
Cloneable→CloneNotSupportedExceptiontại runtime - Nên dùng copy constructor hoặc static factory thay vì clone (Effective Java Item 13)
// ✅ Copy constructor — an toàn hơn clone
public class Person {
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address.city); // Deep copy
}
}
Trong OCP, câu hỏi về clone() thường kiểm tra: (1) Cloneable là marker interface, (2) shallow vs deep copy, (3) CloneNotSupportedException khi thiếu implements Cloneable.
Tổng kết
| Khái niệm | Mô tả |
|---|---|
| Inheritance | Cơ chế cho phép class con kế thừa từ class cha |
| extends | Từ khóa để kế thừa class |
| super | Từ khóa để truy cập members của class cha |
| Method Overriding | Định nghĩa lại instance method của class cha (runtime polymorphism) |
| Method Hiding | Static method ở subclass che static method của superclass (compile-time) |
| Object class | Class cha mặc định của tất cả các class |
| Single Inheritance | Java chỉ cho phép kế thừa từ một class cha |
| Constructor Chaining | Quá trình gọi constructor từ class con → cha |
| Sealed Classes | Kiểm soát subclass nào được phép kế thừa (Java 17+) |
| Composition over Inheritance | Ưu tiên HAS-A hơn IS-A để tránh fragile base class |
| Protected xuyên package | Chỉ access qua subclass reference, không qua parent reference |
| Clone / Cloneable | Shallow copy mặc định, cần deep copy thủ công cho reference types |
- Luôn sử dụng
@Overrideannotation - Override
hashCode()khi overrideequals() - Sử dụng kế thừa khi có mối quan hệ IS-A rõ ràng
- Ưu tiên composition over inheritance khi có thể (Effective Java Item 18)
- Constructor của superclass phải được gọi đầu tiên (implicit hoặc explicit)
- Static methods bị hidden, không bị overridden