- vừa được xem lúc

[OCP21-02] Sử dụng cách tiếp cận hướng đối tượng trong Java - Phần 2

0 0 3

Người đăng: Coong Laam

Theo Viblo Asia

Phần 2: Khám phá sâu hơn về Java - Phạm vi, Kế thừa và Đa hình

Trong Phần 1, chúng ta đã tìm hiểu về các thành phần cơ bản của lớp và đối tượng, cùng với cách thức truyền đối số giữa các phương thức. Hôm nay, chúng ta sẽ đào sâu vào những khái niệm quan trọng hơn, giúp bạn viết mã Java mạnh mẽ, linh hoạt và dễ bảo trì: phạm vi biến, kế thừa, giao diện, và sức mạnh của đa hình.


1. Phạm vi biến (Variable Scope)

Phạm vi của một biến trong Java đề cập đến khả năng hiển thị và truy cập của biến đó trong mã. Hiểu rõ phạm vi là điều cần thiết để tránh lỗi và viết mã rõ ràng. Có bốn loại phạm vi chính trong Java:

  • Biến cục bộ (Local variables):

    • Được khai báo bên trong một phương thức hoặc một khối mã ({}).
    • Chỉ có thể truy cập được trong khối mà nó được khai báo.
    • Phạm vi bắt đầu tại thời điểm khai báo và kết thúc khi khối bao bọc kết thúc.
    • Ví dụ:
      public void myMethod() { int count = 0; // 'count' là biến cục bộ if (count == 0) { String message = "Hello"; // 'message' là biến cục bộ, chỉ trong khối if } // System.out.println(message); // Lỗi: 'message' không thể truy cập ở đây
      }
      
    • Lưu ý quan trọng: Biến cục bộ phải được khởi tạo rõ ràng trước khi sử dụng.
  • Tham số phương thức (Method parameters):

    • Cũng được coi là biến cục bộ nhưng với phạm vi bao phủ toàn bộ thân phương thức.
    • Phạm vi bắt đầu khi phương thức được gọi và kết thúc khi phương thức hoàn thành.
    • Ví dụ:
      public void greet(String name) { // 'name' là tham số phương thức System.out.println("Hello, " + name);
      }
      
  • Trường (Fields) hay biến thể hiện (Instance variables):

    • Các biến được khai báo ở cấp độ lớp, bên ngoài bất kỳ phương thức hoặc khối nào.
    • Mỗi đối tượng (instance) của lớp sẽ có một bản sao riêng của các biến này.
    • Phạm vi bắt đầu khi đối tượng được khởi tạo và tồn tại chừng nào đối tượng còn trong bộ nhớ.
    • Ví dụ:
      public class Car { String color; // 'color' là biến thể hiện int speed; // 'speed' là biến thể hiện
      }
      
    • Lưu ý: Trường tự động nhận giá trị mặc định nếu không được khởi tạo rõ ràng (ví dụ: null cho kiểu đối tượng, 0 cho số, false cho boolean).
  • Biến lớp (Class variables) hay trường tĩnh (Static fields):

    • Các biến được khai báo ở cấp độ lớp với từ khóa static.
    • Chỉ có một bản sao duy nhất của biến này được chia sẻ bởi tất cả các thể hiện của lớp.
    • Phạm vi bắt đầu khi lớp được tải vào bộ nhớ và tồn tại cho đến khi chương trình kết thúc.
    • Ví dụ:
      public class ApplicationConfig { public static final String APP_NAME = "MyAwesomeApp"; // 'APP_NAME' là biến lớp
      }
      
  • Từ khóa var (Local Variable Type Inference):

    • Cho phép khai báo biến cục bộ mà không cần chỉ định rõ kiểu dữ liệu.
    • Trình biên dịch sẽ tự động suy luận kiểu dựa trên biểu thức khởi tạo.
    • Ví dụ:
      var greeting = "Hello, Java!"; // Trình biên dịch suy luận 'greeting' là String
      var number = 100; // Trình biên dịch suy luận 'number' là int
      
    • Lưu ý: var chỉ dùng cho biến cục bộ và phải được khởi tạo ngay lập tức.

2. Kế thừa (Inheritance)

Kế thừa là một trong những trụ cột của lập trình hướng đối tượng (OOP), cho phép một lớp mới (lớp con/subclass) dựa trên một lớp hiện có (lớp cha/superclass), thừa hưởng các thuộc tính và phương thức của nó.

  • Từ khóa extends: Được sử dụng để tạo một lớp con.

    • Ví dụ:
      class Animal { // Lớp cha void eat() { System.out.println("Animal is eating"); }
      } class Dog extends Animal { // Lớp con kế thừa từ Animal void bark() { System.out.println("Dog is barking"); }
      }
      
  • Lớp trừu tượng (Abstract classes):

    • Các lớp không thể được khởi tạo trực tiếp (không thể tạo đối tượng từ chúng).
    • Được thiết kế để làm cơ sở cho các lớp con.
    • Có thể chứa các phương thức trừu tượng (abstract methods), là những phương thức không có phần thân implementation trong lớp trừu tượng và phải được triển khai bởi các lớp con cụ thể (concrete subclasses).
    • Ví dụ:
      abstract class Shape { // Lớp trừu tượng abstract double getArea(); // Phương thức trừu tượng void display() { System.out.println("This is a shape."); }
      } class Circle extends Shape { double radius; // Phải triển khai getArea() @Override double getArea() { return Math.PI * radius * radius; }
      }
      
  • Giao diện (Interfaces):

    • Định nghĩa một "hợp đồng" về các phương thức mà một lớp phải triển khai.
    • Chỉ chứa các định nghĩa phương thức (trước Java 8), hoặc có thể có phương thức default, static từ Java 8 trở đi.
    • Từ khóa implements: Được sử dụng để một lớp triển khai một giao diện.
    • Một lớp có thể triển khai nhiều giao diện.
    • Ví dụ:
      interface Flyable { // Giao diện void fly();
      } class Bird implements Flyable { // Bird triển khai giao diện Flyable @Override public void fly() { System.out.println("Bird is flying."); }
      }
      
  • Lớp niêm phong (Sealed classes) (Từ Java 15 trở đi):

    • Hạn chế các lớp khác có thể kế thừa chúng.
    • Các lớp con được phép được chỉ định bằng từ khóa permits.
    • Các lớp con của một lớp niêm phong phải được khai báo là final, sealed, hoặc non-sealed.
    • Ví dụ:
      public abstract sealed class Vehicle permits Car, Truck { /* ... */ } public final class Car extends Vehicle { /* ... */ }
      public non-sealed class Truck extends Vehicle { /* ... */ }
      // class Bicycle extends Vehicle { /* Lỗi: Bicycle không được phép */ }
      
  • Từ khóa this:

    • Một tham chiếu đến thể hiện (instance) hiện tại của lớp.
    • Công dụng:
      • Phân biệt giữa biến cục bộ và biến thể hiện khi chúng có cùng tên.
      • Truyền thể hiện hiện tại như một đối số cho phương thức khác.
      • Gọi một constructor khác từ bên trong một constructor (phải là câu lệnh đầu tiên).
    • Ví dụ:
      public class Point { int x, y; public Point(int x, int y) { this.x = x; // Phân biệt tham số 'x' và trường 'x' this.y = y; } public void printCoords() { System.out.println("Coords: (" + this.x + ", " + this.y + ")"); }
      }
      
  • Từ khóa super:

    • Một tham chiếu đến lớp cha (superclass) của lớp hiện tại.
    • Công dụng:
      • Truy cập các thành viên (biến, phương thức) của lớp cha.
      • Gọi constructor của lớp cha từ constructor của lớp con (phải là câu lệnh đầu tiên).
    • Ví dụ:
      class Animal { String sound = "Generic Sound"; Animal(String s) { this.sound = s; } void makeSound() { System.out.println(sound); }
      } class Cat extends Animal { Cat() { super("Meow"); // Gọi constructor của lớp cha } @Override void makeSound() { super.makeSound(); // Gọi phương thức makeSound() của lớp cha System.out.println("Purrr..."); }
      }
      

3. Đa hình (Polymorphism)

Đa hình là một khái niệm mạnh mẽ trong OOP, cho phép bạn đối xử với các đối tượng của các lớp con khác nhau như thể chúng là các đối tượng của cùng một lớp cha.

  • Ví dụ:

    Animal myDog = new Dog(); // myDog là kiểu Animal, nhưng đối tượng thực tế là Dog
    myDog.eat(); // Gọi phương thức eat() của Dog (nếu Dog override nó)
    
  • Ghi đè phương thức (Method Overriding):

    • Một khái niệm cốt lõi trong đa hình, nơi một lớp con cung cấp một triển khai riêng của một phương thức đã được định nghĩa trong lớp cha.
    • Quy tắc ghi đè:
      • Phương thức trong lớp con phải có cùng tên, kiểu trả về và danh sách tham số với phương thức trong lớp cha.
      • Bạn có thể làm cho access modifier rộng hơn trong lớp con (ví dụ: protected thành public), nhưng không thể hạn chế hơn (public thành private).
      • Phương thức ghi đè chỉ có thể khai báo các ngoại lệ (exceptions) tương tự hoặc cụ thể hơn so với các ngoại lệ được khai báo bởi phương thức lớp cha.
      • Một phương thức ghi đè được phép có kiểu trả về đồng biến (covariant return type), nghĩa là kiểu trả về có thể là một lớp con của kiểu trả về được khai báo trong phương thức lớp cha.
    • Ví dụ về Covariant Return Type:
      class Product { Product getProduct() { return new Product(); }
      }
      class Book extends Product { @Override Book getProduct() { return new Book(); } // Covariant return type: Book là subclass của Product
      }
      
  • Chú thích @Override:

    • Tường minh đánh dấu các phương thức được dùng để ghi đè một phương thức của lớp cha.
    • Cung cấp một biện pháp bảo vệ chống lại các lỗi vô tình (ví dụ: gõ sai tên phương thức, hoặc sai tham số). Nếu phương thức không thực sự ghi đè, trình biên dịch sẽ báo lỗi.
  • Khác biệt giữa Overriding, Hiding và Shadowing:

    • Ghi đè phương thức private: Việc khai báo lại một phương thức private từ lớp cha trong lớp con không được coi là ghi đè. Các phương thức private không được kế thừa, do đó, lớp con đang định nghĩa một phương thức hoàn toàn mới.
    • Ẩn phương thức static (Method Hiding): Việc khai báo lại một phương thức static trong lớp con được gọi là ẩn (hiding), không phải ghi đè. Phương thức của lớp con "ẩn" phương thức của lớp cha, nhưng không thực sự ghi đè nó. Hành vi được gọi sẽ phụ thuộc vào kiểu tham chiếu, không phải kiểu đối tượng thực tế.
      • Ví dụ:
        class Parent { static void display() { System.out.println("Parent static method"); }
        }
        class Child extends Parent { static void display() { System.out.println("Child static method"); } // Hiding, not overriding
        }
        // Parent.display(); // Parent static method
        // Child.display(); // Child static method
        // Parent p = new Child();
        // p.display(); // Parent static method (do kiểu tham chiếu là Parent)
        
    • Ẩn biến (Variable Hiding/Shadowing): Các biến cũng có thể bị ẩn trong lớp con nếu một lớp con khai báo một biến với cùng tên với một biến trong lớp cha.
      • Ví dụ:
        class Base { String name = "Base Name";
        }
        class Derived extends Base { String name = "Derived Name"; // Hiding the 'name' from Base void printNames() { System.out.println("Derived name: " + this.name); System.out.println("Base name: " + super.name); }
        }
        

4. Truy cập đối tượng và Ép kiểu (Object Access and Type Casting)

Có ba cách chính để truy cập một đối tượng trong Java:

  1. Sử dụng một tham chiếu có cùng kiểu với đối tượng:
    • Ví dụ: Dog myDog = new Dog();
  2. Sử dụng một tham chiếu là lớp cha (superclass) của kiểu đối tượng:
    • Ví dụ: Animal myAnimal = new Dog(); (Đây là trọng tâm của đa hình.)
  3. Sử dụng một tham chiếu định nghĩa một giao diện mà lớp của đối tượng triển khai hoặc kế thừa:
    • Ví dụ: Flyable myBird = new Bird();
  • Ép kiểu (Type Casting):

    • Cho phép bạn gán giá trị của một kiểu dữ liệu nguyên thủy này sang kiểu khác, hoặc xử lý một đối tượng của một lớp này như một đối tượng của lớp khác, miễn là có mối quan hệ kế thừa giữa hai lớp đó.
    • Upcasting (ép kiểu từ subtype lên supertype):
      • Không yêu cầu ép kiểu tường minh. An toàn và tự động.
      • Ví dụ: Animal animal = new Dog(); (biến kiểu Dog tự động được ép lên Animal).
    • Downcasting (ép kiểu từ supertype xuống subtype):
      • Yêu cầu ép kiểu tường minh. Có thể gây ra ClassCastException nếu đối tượng thực tế không phải là kiểu con đó.
      • Ví dụ: Dog dog = (Dog) animal; (chỉ hợp lệ nếu animal thực sự là một đối tượng Dog).
  • Toán tử instanceof:

    • Được sử dụng để kiểm tra xem một đối tượng có phải là một thể hiện của một lớp cụ thể hay triển khai một giao diện cụ thể hay không.
    • Trả về giá trị boolean (true hoặc false).
    • Ví dụ:
      Animal someAnimal = new Dog();
      if (someAnimal instanceof Dog) { System.out.println("It's a Dog!");
      }
      
  • Đối sánh mẫu với instanceof (Pattern matching for instanceof) (Từ Java 16 trở đi):

    • Cho phép bạn kết hợp kiểm tra kiểu và ép kiểu trong một thao tác duy nhất, giảm mã lặp lại.
    • Ví dụ:
      Object obj = "Hello Java";
      if (obj instanceof String s) { // Nếu obj là String, thì ép kiểu sang s và gán giá trị System.out.println("String length: " + s.length());
      }
      

Phần 2 này đã cung cấp một cái nhìn sâu sắc hơn về cách các thành phần trong Java hoạt động cùng nhau để tạo ra các ứng dụng có cấu trúc và linh hoạt. Việc nắm vững các khái niệm về phạm vi, kế thừa và đa hình là nền tảng để bạn phát triển khả năng lập trình hướng đối tượng của mình.

Hãy đón đọc phần tiếp theo để khám phá những khía cạnh thú vị khác của Java nhé!

Source: https://ocpj21.javastudyguide.com

Bình luận

Bài viết tương tự

- vừa được xem lúc

Chương 5 Object oriented programming

Chương 5 Object oriented programming. Tôi lần đầu tiên được giới thiệu về lập trình hướng đối tượng ở trường cao đẳng nơi tôi đã có một giới thiệu tóm tắc về c++.

0 0 36

- vừa được xem lúc

SOLID trong OOP và ví dụ dễ hiểu bằng Python

Thế SOLID là gì? SOLID là cứng . Đùa tí Đây là các nguyên lý thiết kế trong OOP, được ghép lại từ các chữ cái đầu của Single Responsibility, Open Close Principle, Liskov Substitution Principle, Interf

0 0 43

- vừa được xem lúc

002: Object và Class trong OOP

Bài viết nằm trong series Object-Oriented Design from real life to software. Về mặt ý tưởng, OOP nói đến việc áp dụng từ thế giới thực vào thế giới lập trình.

0 0 47

- vừa được xem lúc

001: Procedural programming và Object-Oriented programming

Bài viết nằm trong series Object-Oriented Design from real life to software. 1) Procedural programming.

0 0 43

- vừa được xem lúc

003: Các tính chất cơ bản trong OOP P1

Bài viết nằm trong series Object-Oriented Design from real life to software. . . Abstraction.

0 0 58

- vừa được xem lúc

004: Các tính chất cơ bản trong OOP P2

Bài viết nằm trong series Object-Oriented Design from real life to software. . . Inheritance.

0 0 53