# Lập trình hướng đối tượng với JAVA
## Lập trình hướng đối tượng là gì?
Hướng đối tượng (Object-oriented) là một phương pháp lập trình mà trong đó mọi thứ trong chương trình đều được xem như các đối tượng (objects) tương tác với nhau để thực hiện các nhiệm vụ cụ thể. Mỗi đối tượng có thuộc tính (properties) và phương thức (methods) riêng, cùng với các quan hệ với các đối tượng khác.
Trong hướng đối tượng, chương trình được phân chia thành các đối tượng riêng biệt, mỗi đối tượng đóng vai trò là một thực thể trong thế giới thực. Đối tượng có thể tương tác với nhau bằng cách gửi thông điệp qua các phương thức của chúng.
Hướng đối tượng giúp cho việc lập trình trở nên dễ dàng hơn bằng cách phân chia các chức năng của chương trình thành các đối tượng nhỏ hơn, tập trung vào từng chức năng riêng biệt. Điều này giúp tăng tính tái sử dụng của mã nguồn, giảm thiểu sự trùng lặp và giúp mã nguồn dễ bảo trì hơn.
Java là một ngôn ngữ lập trình hướng đối tượng, trong đó các đối tượng được tạo ra bằng cách khởi tạo các đối tượng từ các lớp (class). Các đối tượng này được sử dụng để thực hiện các chức năng cụ thể và tương tác với nhau thông qua các phương thức và thuộc tính của chúng.
## Đối tượng là gì?
Trong lập trình hướng đối tượng, đối tượng (object) là một **thực thể cụ thể**, có đặc tính và hành vi riêng. Nó được **tạo ra từ một lớp (class)** và có thể được sử dụng để thực hiện các chức năng cụ thể trong chương trình.
Mỗi đối tượng có một số **thuộc tính (properties)** và các **phương thức (methods)** để thực hiện các hành động. Các thuộc tính định nghĩa trạng thái của đối tượng, còn các phương thức xác định các hành động mà đối tượng đó có thể thực hiện.
Ví dụ, nếu bạn tạo một lớp (class) Car, mỗi đối tượng Car sẽ có các thuộc tính như tên, màu sắc, tốc độ, giá cả, v.v. Các phương thức của đối tượng Car có thể bao gồm đề phòng, khởi động, tăng tốc, giảm tốc, v.v.
Khi một đối tượng được tạo ra từ lớp, đó được gọi là một thể hiện (instance) của lớp đó. Bạn có thể tạo nhiều thể hiện của một lớp khác nhau, mỗi thể hiện có các thuộc tính và phương thức riêng của nó.
Ví dụ về một class Car trong java:
```java
class Car{
private double price;
private int speed;
private String color;
public void startUP(){
System.out.println("Bruhh bruhh Xe được khởi động!!");
}
public void speedUP(int speed){
this.speed = speed + 20;
System.out.println("Xe tanggg tocccccc!");
}
public void slowDown(int speed){
this.speed = speed - 10;
System.out.println("Xe dang di cham laiii!");
}
}
```
## Thuộc tính là gì??
thuộc tính (properties) là **các biến** hoặc **giá trị được lưu trữ trong một đối tượng** và **mô tả các đặc tính của đối tượng đó**. Thuộc tính có thể được khai báo trong một lớp (class) và được **truy cập thông qua các phương thức** của đối tượng tương ứng.
Ví dụ: con mèo có những thuộc tính như là tên, cân nặng, tuổi, màu lông,..
```java
class Cat{
private int age;
private int weight;
private String name;
private String color;
}
```
## Phương thức là gì??
Method (phương thức) là **các hàm trong một lớp (class)** được sử dụng để **thực hiện một tác vụ cụ thể hoặc tính toán một giá trị trả về**. Các phương thức được sử dụng để giảm sự lặp lại mã và tạo ra các chức năng có thể được sử dụng lại trong toàn bộ chương trình.
Hay ví dụ cụ thể phương thức thể hiện các HÀNH ĐỘNG của đối tượng đó.
Quay lại với class Cat. Giờ có thể các phương thức như là ăn, ngủ, đi vệ sinh, kêu,...
```java
class Cat{
private int age;
private int weight;
private String name;
private String color;
public void eat(){
this.weight = this.weight + 10;
// cân nặng của mèo tăng lên :V
}
public void sleep(){
System.out.println("Trẫm đang ngủ! cấm làm phiền!!!");
}
public void meow(){
System.out.println("Meooowwww Meooooowwwwwww!");
}
}
```
### Một số phương thức đặc biệt
#### **Constructer**
Constructor (hay còn được gọi là phương thức khởi tạo) là một phương thức đặc biệt trong Java được **sử dụng để khởi tạo đối tượng** của một lớp (class). **Khi một đối tượng được tạo bằng từ khóa "new"**, **constructor sẽ được gọi tự động để khởi tạo các giá trị ban đầu cho đối tượng**.
Ví dụ về cách khởi tạo một đối tượng trong java:
```java
Cat Tom = new Cat();
```
Constructor **có tên giống với tên của lớp** và **có thể có hoặc không có tham số**. Nếu một lớp không có constructor nào được định nghĩa, Java sẽ tạo ra một constructor mặc định (default constructor) không tham số.
Cụ thể một Contructor trông như thế nào?
```java
class Cat {
private int age;
private int weight;
private String name;
private String color;
public Cat(){
this.age = 0;
this.weight = 0;
this.name = "";
this.color = "";
}
public Cat(int age, int weight, String name, String color){
this.age = age;
this.weight = weight;
this.name = name;
this.color = color;
}
public void eat(){
this.weight = this.weight + 10;
// cân nặng của mèo tăng lên :V
}
public void sleep(){
System.out.println("Trẫm đang ngủ! cấm làm phiền!!!");
}
public void meow(){
System.out.println("Meooowwww Meooooowwwwwww!");
}
}
```
Còn đây là khi ta khởi tạo đối tượng:
```java
Cat Tom = new Cat();
Cat Tom = new Cat(17, 5, "Tom","Blue");
```
hoàn toàn có thể toạ mà không truyền các thuộc tính hoặc truyền thuộc tính.
Nếu một lớp không có constructor nào được định nghĩa, Java sẽ tạo ra một constructor mặc định (default constructor) không tham số
### Getter và Setter
Trước khi tìm hiểu Getter và Setter. Từ đầu bài giảng bạn có thắc mắc tại sao trước các thuộc tính, các phương thức có các từ khoá như là '**public**' '**private**'. vậy các từ khoá này là gì?
Trong java, public và private là các từ khóa quan trọng được sử dụng để quản lý sự truy cập của thành viên của một lớp (class).
public được sử dụng để chỉ ra rằng thành viên của lớp (class), ví dụ như một phương thức hay một thuộc tính, có thể được truy cập từ bên ngoài lớp đó. Nói cách khác, các thành viên public có thể được truy cập từ mọi nơi trong chương trình.
private được sử dụng để chỉ ra rằng thành viên của lớp (class) chỉ có thể được truy cập bên trong lớp đó và không thể được truy cập từ bên ngoài lớp đó. Các thành viên private thường được sử dụng để bảo vệ dữ liệu và đảm bảo rằng chúng không thể bị truy cập hoặc thay đổi từ bên ngoài lớp đó.
Ngoài ra còn có một số từ khoá khác:

Chính vì thế Getter và setter được sinh ra
getter là một phương thức được sử dụng để truy cập giá trị của một thuộc tính trong một lớp (class).
Getter cho phép các thuộc tính được truy cập mà không cần phải làm cho các thuộc tính đó trở thành public
ví dụ:
```java
public class Person {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
```
Ví dụ ở một class khác chúng ta khởi tạo một đối tượng Person:
```java
Class motClassNaoDo{
public static void main(String[] args){
Person D = new Person()
// với private chúng tha không thể System.out.println(A.name)
// Mà cần phải sử dụng Getter vì lúc này hàm getter là thành viên của class
// nên có thể truy xuất biến
System.out.println(D.getName());
}
}
```
Vẫn với ví dụ đó. Nếu từ một class khác chúng ta không thể thay đổi giá trị thuộc tính name của Person A. Mà ta cần phải có setter.
Setter là một phương thức được sử dụng để thiết lập (set) giá trị của một thuộc tính (property) của một đối tượng. Setter cho phép bạn thay đổi giá trị của thuộc tính của đối tượng trong suốt quá trình thực thi của chương trình
```java
A.setName("Chu Ho Binh Duonggggg")
```
## Tính đóng gói
Từ các khái niệm getter, setter chúng ta đi đến một trong 4 khái niệm quan trọng nhất của lập trình hướng đối tượng đó chính là tính đóng gói.
Tính đóng gói (Encapsulation) là một khái niệm quan trọng, cho phép **che dấu thông tin về trạng thái** và **hành vi của một đối tượng khỏi các đối tượng khác**. Tính đóng gói bảo vệ dữ liệu của một đối tượng khỏi sự can thiệp trái phép và đảm bảo tính nhất quán của chương trình.
Như ví dụ trên, ở một class khác chúng ta không thể xem hoặc sửa các thuộc tính hoặc các phương thức khi mà trước nó là từ khoá private.
Dưới đây là một ví dụ về tính đóng gói trong Java:
```java
public class BankAccount {
private String accountNumber;
private double balance;
public BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
}
}
}
```
Trong ví dụ này, lớp BankAccount đóng gói thông tin về số tài khoản và số dư của tài khoản trong các thuộc tính private accountNumber và balance.
Để truy cập hoặc thiết lập giá trị của các thuộc tính này, ta sử dụng phương thức Getter và Setter tương ứng getAccountNumber(), setAccountNumber(), getBalance(), và setBalance().
Nhờ tính đóng gói này, các đối tượng khác không thể truy cập trực tiếp vào các thuộc tính này, giúp bảo vệ dữ liệu của đối tượng và đảm bảo tính nhất quán của chương trình.
## Tính kế thừa
Trong lập trình hướng đối tượng, Tính kế thừa (Inheritance) cho phép ta **xây dựng các lớp mới trên cơ sở các lớp đã có sẵn**. **Lớp kế thừa (subclass) được tạo ra sẽ có tất cả các thuộc tính và phương thức của lớp cơ sở (superclass)** và **có thể mở rộng hoặc định nghĩa lại các thuộc tính và phương thức của lớp cơ sở**.
Tính kế thừa cho phép tạo ra một cấu trúc phân cấp (hierarchy) của các lớp, với các lớp con kế thừa các thuộc tính và phương thức của các lớp cha và có thể thêm mới hoặc thay đổi các thuộc tính và phương thức của chúng. Việc sử dụng tính kế thừa giúp tăng tính tái sử dụng mã nguồn, giảm độ phức tạp của chương trình và cải thiện tính bảo mật.
ví dụ như sau:
Đầu tiên chúng ta có một class Animal với các thuộc tính và các phương thức:
```java
public class Animal {
protected int age;
protected String name;
public Animal(int age, String name) {
this.age = age;
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
}
```
Có thể thấy đây là các thuộc tính chung của rất nhiều con vật khác như là mèo, chó,....
vậy nếu chúng ta viết lại đối tượng mèo hoặc chó như sau:
```java
public class Cat {
protected int age;
protected String name;
public Cat(int age, String name) {
this.age = age;
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
public void meow(){
System.out.println("meooow meooowwww");
}
```
Có thể thấy nhiều điểm chung mà ta phải viết lại. Thay vì viết lại chúng ta có thể kế thừa lại các thuộc tính, phương thức có sẵn của đối tượng animal với 'extends'. Có thể làm như sau:
```java
public class Cat extends Animal {
public Cat(int age, String name) {
super(age, name);
}
public void meow() {
System.out.println(name + " is meowing.");
}
}
```
Có thể thấy vô cùng ngắn gọn và lúc này class Cat hoàn toàn có các thuộc tính như là age, name hay các phương thức eat(), sleep() của Animal.
Chắc bạn đang thắc mắc hàm super().
Thì hàm super() được sử dụng để **gọi đến constructor** của lớp cha gần nhất. Khi tạo một đối tượng của lớp con.
Ngoài ra còn có thể gọi hàm của class cha với cách sau
```java
public class Cat extends Animal {
public Cat(int age, String name) {
super(age, name);
}
public void meow() {
super.makeSound();
System.out.println(name + " is meowing.");
}
}
```
## Override
Típ tục nào, khái niệm tiếp theo đó chính là Override.
Override là **cách mà lớp con định nghĩa lại một method của lớp cha**, với nội dung khác hẳn so với method của lớp cha.
Khi lớp con override một phương thức của lớp cha, phương thức của lớp cha được ghi đè và không được sử dụng nữa. Việc override giúp cho lớp con có thể tuỳ chỉnh lại hoặc mở rộng phương thức của lớp cha mà không cần viết lại toàn bộ phương thức.
Để override thì lớp con cần **thêm từ khoá @override trước method**
```java
public class Animal {
public void makeSound() {
System.out.println("The animal makes a sound.");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks.");
}
}
```
## Overload
Overload là một khái niệm trong lập trình hướng đối tượng, nó cho phép **một lớp có nhiều phương thức cùng tên nhưng khác nhau về số lượng hoặc kiểu dữ liệu của tham số đầu vào**. Khi gọi một phương thức, trình biên dịch sẽ xác định phương thức nào sẽ được gọi dựa trên kiểu và số lượng tham số truyền vào.
```java
public class Calculator {
public int add(int x, int y) {
return x + y;
}
public int add(int x, int y, int z) {
return x + y + z;
}
public double add(double x, double y) {
return x + y;
}
}
```
Trong ví dụ này, lớp Calculator có ba phương thức add() cùng tên nhưng khác nhau về số lượng hoặc kiểu dữ liệu của tham số đầu vào. Khi gọi phương thức add(), trình biên dịch sẽ xác định phương thức nào sẽ được gọi dựa trên kiểu và số lượng tham số truyền vào. Ví dụ:
```java
Calculator calc = new Calculator();
int sum1 = calc.add(1, 2); // gọi phương thức add(int x, int y)
int sum2 = calc.add(1, 2, 3); // gọi phương thức add(int x, int y, int z)
double sum3 = calc.add(1.5, 2.5); // gọi phương thức add(double x, double y)
```
## Upcasting và Downcasting
Upcasting là chuyển đổi một đối tượng từ lớp con sang lớp cha tương ứng. Khi upcasting đối tượng con sẽ được coi như đối tượng cha và có thể dụng như đối tượng lớp cha.
Upcasting được thực hiện ngầm định bởi Java.
Sau đây là ví dụ về Upcasting:
```java
public class Animal{
String name;
public void makeNoise(){
System.out.println("Animal makenoise");
}
}
public class Dog extends Animal{
@Override
public void makeNoise(){
System.out.println("Woof! Wooof!");
}
public void Growl(){
System.out.println("Grrrrr");
}
}
```
ở một class khác của chương trình. Ví dụ:
```java
public class upCasting{
public static void main(String args[]){
Animal myanimal = new Dog(); // đây chính là upcasting
myanimal.Growl();
// chúng ta không thẻ gọi hàm Growl() vì lúc này myanimal được coi như lớp cha.
}
}
```
Hmmm vậy upCasting để làm gì? Có tác dụng gì??
```java
public class upCasting{
public static void main(String args[]){
Animal myanimal = new Dog();
Dog dog = new Dog();
Cat cat = new Cat();
someAnimalMakeNoise(myanimal); //output: Woof Woofff!
someAnimalMakeNoise(dog); // output Woof Wooff;
someAnimalMakeNoise(cat); // output meoooww meoooww
}
public static void someAnimalMakeNoise(Animal animal){
animal.makeNoise();
}
}
```
Downcasting là **chuyển đổi một đối tượng từ lớp cha sang lớp con tương ứng**
Khi downcasting, đối tượng cha được chuyển đổi trở lại đối tượng con tương ứng, do đó cần phải **ép kiểu đối tượng cha thành kiểu đối tượng con**.
Nếu đối tượng cha không phải là một đối tượng của lớp con tương ứng, sẽ xảy ra lỗi ClassCastException.
Quay lại với ví dụ vừa rồi.
```java
public class upCasting{
public static void main(String args[]){
Animal myanimal = new Dog();
Dog dog = new Dog();
Cat cat = new Cat();
someAnimalMakeNoise(myanimal); //output: Woof Woofff!
someAnimalMakeNoise(dog); // output Woof Wooff;
someAnimalMakeNoise(cat); // output meoooww meoooww
}
public static void someAnimalMakeNoise(Animal animal){
animal.makeNoise();
// Nếu ở đây ta biết animal được truyền vào chắc chắn là đối tượng mèo
// thì hoàn toàn có thể downcast animal về kiểu mèo
Cat myCat = (Cat) animal;
myCat.meow(); // output: meow meooowww
}
}
```
Tuy nhiên nếu ta truyền vào đối tượng dog thì sẽ xảy ra lỗi.
Vậy nên có một từ khoá giúp chúng ta xác định đối tượng được truyền vào
```java
public class upCasting{
public static void main(String args[]){
Animal myanimal = new Dog();
Dog dog = new Dog();
Cat cat = new Cat();
someAnimalMakeNoise(myanimal); //output: Woof Woofff!
someAnimalMakeNoise(dog); // output Woof Wooff;
someAnimalMakeNoise(cat); // output meoooww meoooww
}
public static void someAnimalMakeNoise(Animal animal){
animal.makeNoise();
// Nếu ở đây ta biết animal được truyền vào chắc chắn là đối tượng mèo
// thì hoàn toàn có thể downcast animal về kiểu mèo
if (animal instanceof Cat){
Cat myCat = (Cat) animal;
myCat.meow(); // output: meow meooowww
}
}
}
```
## Tính trừu tượng
Tính trừu tượng (abstraction) trong lập trình hướng đối tượng là khả năng **tập trung vào các tính năng chính của đối tượng và ẩn đi các chi tiết cài đặt phức tạp**. Nó giúp cho việc lập trình trở nên dễ dàng hơn và dễ hiểu hơn bằng cách tạo ra các abstract class hoặc interface để định nghĩa các tính năng và các phương thức của đối tượng mà không cần quan tâm đến cách thức thực hiện bên trong.
### Abstract class là gì?
Một abstract class là một lớp trừu tượng (abstract) được định nghĩa bằng từ khóa abstract. Một abstract class có thể chứa các phương thức abstract (phương thức chỉ được khai báo và không có thân hàm), các phương thức được triển khai (được cài đặt), các thuộc tính, các hằng số và các constructor.
Một abstract class **không thể được khởi tạo bằng từ khóa new**, nó chỉ được sử dụng để **định nghĩa các thuộc tính, phương thức và chức năng chung cho các lớp con**. Lớp con của một abstract class có thể triển khai các **phương thức abstract** của lớp cha và cung cấp cài đặt cho các phương thức này.
```java
public abstract class Animal{
int age;
String name;
public abstract makeNoise();
public abstract eat();
public abstract sleep();
}
```
**Abstract method** là một phương thức trong Java chỉ khai báo phương thức mà không có thân hàm (body) được định nghĩa bên trong.
Trong Java, khi một **phương thức được khai báo là abstract, thì lớp chứa phương thức này cũng phải được định nghĩa là abstract**.
Các lớp con kế thừa abstract class thì phải override các abstract method nếu không sẽ xảy ra lỗi.
### Interface là gì?
Interface trong lập trình hướng đối tượng là một kiểu tham số hóa giúp định nghĩa các phương thức, thuộc tính, hằng số hoặc một nhóm phương thức mà các lớp khác phải triển khai. Interface định nghĩa các tiêu chuẩn về hành vi cho các lớp khác, cho phép một lớp triển khai nhiều interface cùng một lúc.
Một interface được định nghĩa bằng cách sử dụng từ khóa interface và có thể chứa các phương thức không có thân hàm (không có body) và các hằng số. Các phương thức được khai báo trong interface được xem như là một hợp đồng mà một lớp phải triển khai.
Một lớp triển khai interface phải cài đặt toàn bộ các phương thức được định nghĩa trong interface đó. Một lớp cũng có thể triển khai nhiều interface, cho phép chia sẻ và kế thừa các phương thức giữa các lớp.
```java
public interface Drawable {
public void draw();
public void resize(int width, int height);
}
```
Trong ví dụ này, Drawable là một interface, được định nghĩa bằng từ khóa interface. Interface này chứa hai phương thức không có thân hàm là draw() và resize(). Một lớp có thể triển khai interface Drawable và cài đặt các phương thức này để thực hiện hành động vẽ và thay đổi kích thước.
```java
public class Rectangle implements Drawable {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void draw() {
System.out.println("Drawing a rectangle");
}
public void resize(int width, int height) {
this.width = width;
this.height = height;
System.out.println("Resizing a rectangle");
}
}
```
Trong ví dụ này, lớp Rectangle triển khai interface Drawable và cài đặt hai phương thức draw() và resize().
### Sự khác biệt giữa interface và abstract class
Interface và Abstract Class đều là các khái niệm trong OOP Java và được sử dụng để thiết kế các lớp và đối tượng trong chương trình.
Một số điểm khác nhau giữa Interface và Abstract Class:
Interface là một tập hợp các phương thức mà không có bất kỳ thân hàm (body) nào được triển khai trong đó, trong khi Abstract Class là một lớp trừu tượng chứa ít nhất một phương thức trừu tượng.
Một lớp có thể triển khai nhiều Interface, trong khi một lớp chỉ có thể kế thừa một Abstract Class.
Interface không thể chứa bất kỳ biến hoặc hằng số nào, trong khi Abstract Class có thể chứa các biến và hằng số.
Tất cả các phương thức trong Interface đều là public và abstract, trong khi các phương thức trong Abstract Class có thể là public, private, protected, abstract hoặc có thân hàm (body).
Interface không cung cấp sự thừa kế các biến, trong khi Abstract Class có thể cung cấp sự thừa kế các biến.
Một lớp có thể triển khai các phương thức của Interface, trong khi phải triển khai tất cả các phương thức trừu tượng của Abstract Class.
Tóm lại, Interface thường được sử dụng để định nghĩa các hành vi, trong khi Abstract Class được sử dụng để định nghĩa các đặc tính và hành vi của các lớp con. Khi thiết kế một chương trình, chúng ta nên cân nhắc sử dụng Interface hoặc Abstract Class tùy thuộc vào nhu cầu của chương trình và thiết kế cụ thể của từng lớp.