[Java Core] B1: Tại sao nên học Java, syntax cơ bản

This entry is part 1 of 8 in the series Java Core

Giới thiêu về Java

Ngôn ngữ lập trình Java được thiết kế để trở thành một ngôn ngữ không phụ thuộc vào nền tảng (machine-independent). Java có thể chạy trên bất kỳ nền tảng nào miễn là có máy ảo Java (Java Virtual Machine – JVM). Máy ảo Java là một chương trình có thể chạy trên nhiều nền tảng khác nhau mà không cần phải biên dịch lại. Máy ảo Java có thể chạy trên các máy tính, điện thoại, máy tính bảng, máy chủ, … Máy ảo Java có thể được cài đặt trên các hệ điều hành khác nhau như Windows, Linux, Mac OS, …

Java vừa đủ mạnh với nhiều thư viện, tính năng, bảo đảm các sự chặt chẽ, nhưng cũng đồng thời chạy rất nhanh. Java có thể được sử dụng để phát triển các ứng dụng desktop, web, mobile, game, … Java cũng là một trong những ngôn ngữ lập trình được sử dụng nhiều nhất hiện nay.

Điều cần nói

  • Q: Tại sao Java ra đời, tại sao cần hướng đối tượng chứ không code hết vào 1 file
  • A: Trước khi Java ra đời, các ngôn ngữ lập trình khác như C/C++ đã ra đời. Tuy nhiên, các ngôn ngữ này có một số hạn chế như sau:
    • Các ngôn ngữ này không thể chạy trên nhiều nền tảng khác nhau, mà phải biên dịch lại cho từng nền tảng khác nhau. Ví dụ như C++ trên Windows và C++ trên Linux là 2 ngôn ngữ khác nhau, nên phải biên dịch lại.
    • Các ngôn ngữ này không có khái niệm về hướng đối tượng, nên không thể phát triển các ứng dụng lớn, phức tạp.
    • Cơ chế hướng đối tượng của Java giúp chương trình trở thành các đối tượng làm việc với nhau, giúp cho việc phát triển các ứng dụng lớn, phức tạp trở nên dễ dàng hơn. (Sẽ nói sâu hơn vào phần sau)

Ba thành phần nền tảng Java không thể thiếu và cách chúng hoạt động cùng nhau trong các ứng dụng Java của bạn. Cụ thể:

  • JDK (Java Development Kit – Bộ công cụ phát triển Java)
  • JRE (Java Runtime Environment – Môi trường thực thi Java)
  • JVM (Java Virtual Machine – Máy ảo Java)
JDK JRE JVM - quochung.cyou PTIT

Cách Java thực hiện một đoạn code như sau:

  • Với các file code ví dụ là Party.java, nó sẽ được biên dịch bởi trình biên dịch javac trong JDK để tạo ra file Party.class chứa các bytecode.
  • Các file .class và các thư viện cần có cho Java sẽ được tổng hợp bởi JRE
  • Từ các đoạn mã máy trên, JRE sẽ đưa cho JVM để thực thi, và chạy một “máy ảo” độc lập để thực thi chương trình của chúng ta.

Điều đó nghĩa là, chương trình chúng ta đang chạy “tách biệt” với hệ thống (chạy trong JVM). Vậy nên với Java ta chỉ cần viết một lần, biên dịch một lần và nó có thể chạy ở mọi nền tảng mà JVM hỗ trợ. Khả năng đó được lý giải vì khác với các ngôn ngữ như C hay C++, Java được thiết kế theo nhiều lớp khác nhau để tách biệt giữa chương trình Java và hệ thống.

Java Virtual Machine (JVM)

Không như C/C++ khi mà code được biên dịch thì sẽ tạo thành các mã lệnh được làm cho riêng các vi xử lý khác nhau. Code java đầu tiên được biên dịch thành một dạng tổng quát – bytecode, là ngôn ngữ cho JVM chạy. Sau đó JVM mới chạy thành các ngôn ngữ máy cho nền tảng đó.

image 3 - quochung.cyou PTIT

Trình tự hoạt động:

  • Bạn tạo ra một đoạn code (source) với đuôi .java
  • javac compiler biến nó thành 1 file .class
  • File .class sẽ được đọc bởi JVM, và chuyển thành bytecode để chạy trên các nền tảng khác nhau.
image 2 - quochung.cyou PTIT

Điều cần nói

  • Tóm lại, Java sẽ không compile code ra thẳng mã máy, mà sẽ compile ra dạng .class đặc biệt, gọi là bytecode. Sau đó, JVM sẽ đọc bytecode này, và chuyển nó thành mã máy để chạy trên nền tảng đó.

Ưu điểm:

  • Khả năng độc lập với nền tảng, chỉ cần có JVM là được. Người dùng cuối, thiết bị cuối không cần cài cả bộ mingw như C++, họ chỉ cần JVM (là một phần nhỏ trong bộ JDK để lập trình), tức là bộ chỉ dùng để chạy thôi, và bộ thư viện này có sẵn trên hầu hết các thiết bị hiện nay.
  • Do chạy trên máy ảo, nên có tính bảo mật cao hơn, do mỗi chương trình là một máy ảo khác nhau

Nhược điểm:

  • Chậm hơn C++, vì phải chạy trên máy ảo. Code không được ra thẳng mã máy để chạy cho nhanh, và phải qua 1 tầng nữa để JVM biên dịch thành mã máy. Ví dụ: các thiết bị Android theo cơ chế này nên chúng thường mở ứng dụng chậm hơn IOS, và dùng tài nguyên nhiều hơn. Tuy nhiên những lần mở sau thì nhanh hơn, vì JVM đã biên dịch thành mã máy rồi, nên chỉ cần chạy mã máy thôi.
  • Có thể nói sâu hơn nếu muốn: JVM ngày xưa thì chỉ có cơ chế JIT (Just In Time – vừa đúng lúc), tức là khi nào cần mở ứng dụng thì nó mới biên dịch .class thành mã máy, khi biên dịch nó sẽ tự động tối ưu code, tối giản file, … để lần sau nhanh hơn. Sau này nó có thêm cơ chế AOT (Ahead Of Time – trước thời gian), tức là khi cài đặt ứng dụng, nó sẽ biên dịch luôn thành mã máy, để lần sau mở ứng dụng sẽ nhanh hơn. Tuy nhiên, cơ chế này sẽ làm tăng dung lượng file cài đặt, và cũng làm tăng thời gian cài đặt.

Cấu trúc chương trình Java

  • Trong file source code, chứa “class” (lớp)
  • Mỗi “class” chứa nhiều “method” (hàm) khác nhau.
  • Mỗi “method” chứa nhiều “statements” (dòng lệnh) khác nhau.

Ví dụ 1 file class:

image 1 - quochung.cyou PTIT
``` 

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

```
  • Khi một dự án Java chạy, JVM sẽ tìm class bạn để là class đầu tiên khởi chạy, rồi sau đó tìm đến method main để chạy.
image - quochung.cyou PTIT
public static void main(String[] args) {
   // đây là hàm đầu tiên được chạy
}

2. Bắt đầu với Java

2.0 Sắp xếp các element trong 1 chương trình Java

ElementMô tảBắt buộc cóĐể ở đâu
Packagepackage abc;KhôngĐầu file
Importimport abc;KhôngSau package
khai báo classpublic class abc {Sau import
khai báo biếnint a;KhôngBất cứ đâu
khai báo hàmpublic void abc() {KhôngBất cứ đâu

Package là gì:

  • Tưởng tượng package như cây thư mục thôi, nó giúp chúng ta phân loại các file code của chúng ta, để dễ quản lý hơn. Ví dụ như chúng ta có 1 project lớn, có 1000 file code, thì chúng ta sẽ phân chúng thành các package nhỏ, ví dụ như package com.company.project1com.company.project2, … để dễ quản lý hơn.
JVM
  • Giả dụ ta có cấu trúc thư mục như sau

anhclb
└── anhfirstmeet
    ├── 2020
    │   ├── QuocHung.png
    │   └── PMA.png
    └── 2021
    │   ├── QuocHung.png
    │   └── PMA.png

  • Thì chỉ cần nhìn vào đây, có 2 file ảnh QuocHung, nhưng người ta biết ngay là 2 file ảnh khác nhau, vì nó nằm trong 2 thư mục khác nhau. Tương tự, package cũng giúp chúng ta phân loại các file code của chúng ta, để dễ quản lý hơn.
  • Ví dụ package thư mục chuyên chứa các class xử lí tác vụ (như java.util), các thư mục chuyên chứa code xử lí thuật toán (như algorithm trong C++)

2.1. Nhập, xuất

Ví dụ đơn giản về nhật xuất trong Java:

import java.util.Scanner;

publicclassSayHelloExample {
    publicstaticvoidmain(String[] args) {
        Scannerscanner=newScanner(System.in);

        System.out.print("Enter your name: ");
        Stringname= scanner.nextLine();

        System.out.println("Hello " + name + "!");
    }
}

Để bắt đầu ta sẽ đến với các hàm xuất:

  • System.in.println() để in ra và xuống dòng
  • System.in.print() để in nội dung thông thường

Với việc nhập ta sẽ cần khai báo một đối tượng Scanner thuộc package java.util. Sau đó, ta có thể sử dụng các hàm .next().nextInt(),… để lấy dữ liệu theo dạng token hoặc .nextLine() để lấy cả dòng.

Bài tập

  • Cho số bộ test, mỗi bộ test nhập vào 2 số a, b. In ra a/b làm tròn đến 2 dấu phẩy thập phân theo format như sau:
  • Bo test 0001: 1/2 = 0.50
  • Bo test 0002: 1/3 = 0.33
  • Yêu cầu sử dụng printf

Điều cần nói

  • Một chương trình nhập xuất cơ bản như trên cho ta mường tượng về sự hướng đối tượng chặt chẽ của Java
  • Ví dụ trong C++, để nhập ta có cin, để xuất có cout, trong Python để xuất có printf, trong C cũng là printf, sao Java lại phải dài tận System.out.printf như vậy ?
  • Đó là vì trong Java, mọi thứ đều là đối tượng, nên để xuất ra màn hình, ta phải gọi đến hàm printf của đối tượng System.out. Đây là một trong những điểm khác biệt giữa Java và các ngôn ngữ khác. Điều này thể hiện sự tường minh tuyệt đối
  • Ví dụ trong C++ hay Python như trên, cái gì đang in ra ? Hàm in ra được gọi từ hư không, chẳng có gì khởi nguồn cả, còn Java thì không, ta phải gọi đến hàm printf của đối tượng System.out, nó là một đối tượng thực sự, nó có một hàm printf, và ta gọi đến hàm printf của nó. Điều này thể hiện sự tường minh tuyệt đối của Java, mọi thứ đều là đối tượng, không có gì hư không cả.
  • Rõ ràng, chương trình bắt đầu với đối tượng là class Example, sau đó nó tự động chạy hàm main và đang giao tiếp với các đối tượng khác, ở đây là Scanner để nhập vào, và System.out để xuất ra. Điều này thể hiện sự hướng đối tượng chặt chẽ của Java, mọi thứ đều là đối tượng, và chúng đang nói chuyện, giao tiếp với nhau.

2.2. Khai báo biến, câu lệnh rẽ nhánh, vòng lặp

2.2.1. Biến

  • Biến trong Java chia làm 3 loại
    • Local Variables
    • Instance Variables
    • Static Variables
  • Ngoài ra biến trong Java còn dùng để chứa 2 kiểu dữ liệu primitives và references
  • Local Variable là biến được khai báo trong một phương thức, constructor hoặc một khối lệnh. Biến này chỉ có giá trị trong phương thức, constructor hoặc khối lệnh đó. Khi phương thức, constructor hoặc khối lệnh kết thúc, biến này sẽ bị hủy. (Ví dụ trong hàm main ở trên, biến name là một local variable, và chỉ dùng được trong hàm main đó)
  • Instance Variable là biến được khai báo trong một class, nhưng bên ngoài các phương thức, constructor hoặc khối lệnh. Biến này có thể được truy cập bởi bất kỳ phương thức, constructor hoặc khối lệnh nào của class đó.

Ví dụ:

publicclassExample {
    public String name; // instance variablepublicvoidsayHello() {
        Stringmessage="Hello " + name; // local variable
        System.out.println(message);
    }
}

  • Static Variable là biến được khai báo trong một class, nhưng bên ngoài các phương thức, constructor hoặc khối lệnh. Biến này có thể được truy cập bởi bất kỳ phương thức, constructor hoặc khối lệnh nào của class đó. Tuy nhiên, biến này chỉ có một bản thể duy nhất, không phải mỗi đối tượng sẽ có một bản thể riêng.

Ví dụ:

publicclassExample {
    publicstaticintcount=0; // static variablepublicExample() {
        count++;
    }
}

  • Như trên, nếu không có static thì mỗi class example tạo ra nhiều object con thì count là khác nhau, còn hiện tại, count là độc nhất, gắn chặt với class Example, không phải với object con của nó.

2.2.2. Câu lệnh rẽ nhánh

  • Nhìn chung, câu lệnh rẽ nhánh trong Java cũng giống với C và C++
if (condition1) {
    //do something
} elseif (condition2) {
    
} else {
    
}

Câu hỏi

  • Q: Có dùng được kiểu while(1) trong Java như C++ không?
  • A: Không, trong Java thì boolean và integer là 2 kiểu dữ liệu khác nhau, không thể dùng chung được. Trong Java, để tạo vòng lặp vô hạn, ta sẽ dùng while(true) thay cho while(1).
  • Lí do cho điều này là vì trong C++, 0 là false, 1 là true, 2 cũng là true, nói chung số dương là true. Điều này làm rối loạn cho người mới lập trình, không đủ tường minh, nên trong Java, boolean và integer là 2 kiểu dữ liệu khác nhau, không thể dùng chung được.

Câu hỏi

  • Q: Liệu viết nhiều class có phải viết hàm main hết cho chúng không
  • A: Không, chỉ cần viết một class có hàm main là được. Các class khác có thể không có hàm main, hoặc có hàm main nhưng không được gọi đến. Class chính có hàm main sẽ khởi động lên ,rồi gọi vào các class khác.
  • Một comment bắt đầu bằng hai dấu gạch //
// đây là comment

2.2.3. Vòng lặp

Nhìn chung, vòng lặp trong Java cũng giống với C và C++

  • Câu lệnh lặp for
for (int i = 0; i < 10; i++) {
    // thực hiện câu lệnh này 10 lần
}
  • Câu lệnh lặp while
while (true) {
    // thực hiện câu lệnh này mãi mãi
}
  • Câu lệnh lặp do-while
do {
    // thực hiện câu lệnh này ít nhất 1 lần
} while (true);

Lệnh lặp for gồm 3 phần trong ngoặc tròn:

  • Phần init dùng khởi tạo biến đếm
  • Phần condition chỉ định điều kiện lặp tiếp
  • Phần increment dùng tăng, giảm biến đếm, để tới lúc nào đó điều kiện trở thành false.

Java có một vòng lặp khác, gọi là foreach nhưng vẫn dùng từ khóa for, nhưng theo cú pháp khác.

int[] a = { 1, 2, 3 };
for (int e: a)

Các kiểu dữ liệu nguyên thủy

Kiểu dữ liệuKích thướcGiá trị tối thiểuGiá trị tối đaMô tả
byte1 byte-128127Lưu trữ các số nguyên có dấu
short2 bytes-32,76832,767Lưu trữ các số nguyên có dấu
int4 bytes-2,147,483,6482,147,483,647Lưu trữ các số nguyên có dấu
long8 bytes-9,223,372,036,854,775,8089,223,372,036,854,775,807Lưu trữ các số nguyên có dấu
float4 bytes1.40129846432481707e-453.40282346638528860e+38Lưu trữ các số thực
double8 bytes4.94065645841246544e-324d1.79769313486231570e+308dLưu trữ các số thực
boolean1 bittruefalseLưu trữ các giá trị logic
char2 bytes‘\u0000’ (or 0)‘\uffff’ (or 65,535 inclusive)Lưu trữ các ký tự/ chữ cái Unicode

Mảng trong Java

  • Mảng trong Java là một tập hợp các phần tử có cùng kiểu dữ liệu. Mảng trong Java có thể chứa các kiểu dữ liệu nguyên thủy như int, float, double, char, … hoặc các đối tượng như String, … Mảng trong Java có độ dài cố định, nghĩa là khi khai báo mảng, ta phải xác định được số lượng phần tử của mảng đó. Mảng trong Java có thể là mảng một chiều, mảng hai chiều, mảng ba chiều, … Mảng trong Java có thể được khai báo như sau:
// Khai báo mảng một chiềuint[] a = newint[10];

int[] b = {1, 2, 3, 4, 5};

// Khai báo mảng hai chiềuint[][] c = newint[10][10];

int[][] d = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

Điều cần nói

  • Một lần nữa ta thấy được sự chặt chẽ của Java. Ở đây, như ta nhớ, để khai báo 1 biến ta phải theo syntax sau:
  • <kiểu dữ liệu> <tên biến> = <giá trị>;
  • Vậy ở đây, int[], double[] coi cả cục này là kiểu dữ liệu “mảng” (array), và a, b là tên biến, và new int[10] là giá trị của biến đó. Vậy, ta có thể hiểu là a, b là 2 biến kiểu mảng, và giá trị của nó là 2 mảng có 10 phần tử, và các phần tử đó có kiểu int.
  • Điều này khác với C++, vì kiểu dữ liệu là kiểu dữ liệu, chứ không có chuyện trong kiểu dữ liệu lại có cả số lượng phần tử như c++

Trong C++ trông nó như sau:

int a[10];

Các phương thức so sánh

Phương thứcMô tả
<Nhỏ hơn
<=Nhỏ hơn hoặc bằng
>Lớn hơn
>=Lớn hơn hoặc bằng
==Bằng
!=Không bằng
&&
||Hoặc
!Phủ định
? :Toán tử 3 ngôi

Ôn lại cuối buổi

3. Tại sao Java lại ra đời, tại sao cần hướng đối tượng chứ không code hết vào 1 file

  • A: Trước khi Java ra đời, các ngôn ngữ lập trình khác như C/C++ đã ra đời. Tuy nhiên, các ngôn ngữ này có một số hạn chế như sau:
    • Các ngôn ngữ này không thể chạy trên nhiều nền tảng khác nhau, mà phải biên dịch lại cho từng nền tảng khác nhau. Ví dụ như C++ trên Windows và C++ trên Linux là 2 ngôn ngữ khác nhau, nên phải biên dịch lại.
    • Các ngôn ngữ này không có khái niệm về hướng đối tượng, nên không thể phát triển các ứng dụng lớn, phức tạp.
    • Cơ chế hướng đối tượng của Java giúp chương trình trở thành các đối tượng làm việc với nhau, giúp cho việc phát triển các ứng dụng lớn, phức tạp trở nên dễ dàng hơn. (Sẽ nói sâu hơn vào phần sau)
  • Ví dụ: thay vì code hết code vào 1 file HopThu, chuyên làm mọi việc xử lí email, … ta chia làm 3 class GuiThu, NhanThu, ThungRacThu, làm chỉ nhiệm vụ như tên của nó
  • Khi có lỗi xảy ra, ví dụ gửi thư, ta chỉ cần sửa class GuiThu, không cần sửa cả class NhanThu, ThungRacThu, … như vậy sẽ dễ dàng hơn rất nhiều. Giusp debug 1 luồng đơn giản hơn, dễ maintain, dễ scale hơn.
  • Nhược điểm là về lâu về lại, số lượng class sẽ tăng lên cao rất nhiều, và sẽ khó quản lý hơn.
  • Điều đó buộc chúng ta đặt tên file, đặt package sao cho chuẩn, cho tường minh là điều vô cùng quan trọng, để sau này dễ quản lý hơn.

4. Có thể có 2 hàm main trong 1 chương trình không

  • A: Có, nhưng khi chạy, JVM sẽ chỉ chạy hàm main đầu tiên, và bỏ qua các hàm main khác.

5. Tại sao cần chia JDK và JRE và JVM ra làm 3 phần

  • A: Để cho việc phát triển và chạy ứng dụng trở nên dễ dàng hơn. Ví dụ, khi ta phát triển ứng dụng, ta chỉ cần cài đặt JDK, và khi ta cần chạy ứng dụng, ta chỉ cần cài đặt JRE. Nếu ta cần chạy ứng dụng trên nhiều nền tảng khác nhau, ta chỉ cần cài đặt JVM cho từng nền tảng đó. Thiết bị cuối không cần cài toàn bộ thư viện để code, nó chỉ cần chạy thôi

6. Tại sao Java lại chạy trên máy ảo

  • A: Vì máy ảo có các tiện ích là:
    • Có thể chạy trên nhiều nền tảng khác nhau
    • Có thể chạy nhiều ứng dụng khác nhau cùng lúc

[Java Core] B2: Class và Object (Lớp và đối tượng)

This entry is part 2 of 8 in the series Java Core

Tổng quan về Class và Object

f0035 01 - quochung.cyou PTIT

Class và Object (Lớp và Đối tượng) là hai trong những khái niệm quan trọng nhất của ngôn ngữ lập trình hướng đối tượng (OOP).

Sự khác biệt chính giữa một Class và một Object trong Java là:

  • Class là một mô hình chi tiết để bạn sử dụng tạo ra các Object. Class định nghĩa tất cả các thuộc tính và các phương thức cần thiết của một Object.
  • Mỗi Object phải thuộc một Class nào đó. Và một Object là một thể hiện của Class. Tất cả các Object thuộc về cùng một Class có cùng các thuộc tính và các phương thức.

Syntac :

ClassAobj=newClassA();

Vậy thực chất Java đã làm những gì?

  • Khi ta sử dụng new Example(), Java sẽ dành ra một số lượng vừa đủ ô nhớ trong bộ nhớ để lưu trữ các giá trị của một đối tượng Example và sau đó khởi tạo đối tượng đó.
  • Sau khi khởi tạo, Java sẽ lấy địa chỉ của vùng nhớ đó, gán vào cho “biến” obj. Biến obj được định nghĩa là kiểu Example, hay tức là nếu ta truy cập vào địa chỉ mà biến obj lưu thì vùng nhớ đó sẽ là vùng nhớ của một đối tượng Example.
blog12 - quochung.cyou PTIT

Điều cần nói

  • Hiểu đơn giản, một object trong Java thực chất như 1 cái điều khiển, đang được cài đặt cho 1 cái tivi thực chất ở trong đó. Khi ta bấm nút trên điều khiển, thực chất ta đang gửi tín hiệu đến tivi, và tivi sẽ thực hiện hành động tương ứng. Vậy nên, khi ta gọi hàm obj.printInfo(), thực chất ta đang gửi tín hiệu đến đối tượng obj, và đối tượng obj sẽ thực hiện hành động tương ứng.
  • Lúc này, cứ tạm nhớ nó chỉ là cái điều khiển, chứ không phải cái tivi thật sự, sâu hơn ta sẽ tìm hiểu tiếp ở dưới. (Phần equal và hashcode)

Từ khóa this

  • Từ khoá this trong Java được sử dụng để tham chiếu đến đối tượng hiện tại. Từ khoá this có thể được sử dụng để tham chiếu bất kỳ biến nào của đối tượng hiện tại.

Ví dụ:

classPerson {
  String name;

  Person(String name) {
    this.name = name;
  }
}

  • Trong ví dụ trên, ta có một class Person, trong đó có thuộc tính name. Ta muốn gán giá trị cho thuộc tính name, ta sẽ tạo một hàm khởi tạo, và trong hàm khởi tạo đó, ta sẽ gán giá trị cho thuộc tính name. Vì tên của tham số trùng với tên của thuộc tính, nên ta sẽ dùng từ khóa this để tham chiếu đến thuộc tính name.
  • Từ khóa this cũng có thể được sử dụng để tham chiếu đến các phương thức của đối tượng hiện tại.
classPerson {
  String name;

  Person(String name) {
    this.name = name;
  }

  voidprintInfo() {
    System.out.println(this.name);
  }
}

Access modifier

access modifier trong java - quochung.cyou PTIT

Access modifier là gì

  • Access modifier là một từ khóa trong Java, nó được dùng để chỉ định quyền truy cập của một thuộc tính hoặc một phương thức. Có 4 loại access modifier trong Java, đó là public, private, protected, default.
  • Bài này chỉ cần nói default, private, public

Public

  • Khi ta dùng từ khóa public với một thuộc tính hoặc một phương thức, thì thuộc tính hoặc phương thức đó có thể được truy cập từ bên ngoài class.
publicclassPerson {
  public String name;
  publicvoidsetName(String name) {
    this.name = name;
  }
}

Private

  • Khi ta dùng từ khóa private với một thuộc tính hoặc một phương thức, thì thuộc tính hoặc phương thức đó chỉ có thể được truy cập từ bên trong class.
publicclassPerson {
  private String name;
  privatevoidsetName(String name) {
    this.name = name;
  }
}

Protected

  • Khi ta dùng từ khóa protected với một thuộc tính hoặc một phương thức, thì thuộc tính hoặc phương thức đó chỉ có thể được truy cập từ bên trong class, hoặc từ bên trong các class con của class đó.
publicclassPerson {
  protected String name;
  protectedvoidsetName(String name) {
    this.name = name;
  }
}

Default

  • Khi ta không dùng từ khóa nào với một thuộc tính hoặc một phương thức, thì thuộc tính hoặc phương thức đó chỉ có thể được truy cập từ bên trong class, hoặc từ bên trong các class cùng package với class đó.
publicclassPerson {
  String name;
  voidsetName(String name) {
    this.name = name;
  }
}

Tại sao cần dùng access modifier

  • Khi ta dùng access modifier, ta có thể kiểm soát được quyền truy cập của các thuộc tính và phương thức. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Ta muốn thuộc tính name này chỉ có thể được truy cập từ bên trong class Person, và không thể được truy cập từ bên ngoài class Person. Vì vậy, ta sẽ dùng từ khóa private với thuộc tính name.
publicclassPerson {
  private String name;
}

Điều cần nói

  • Trong thực tế, ví dụ như 1 cái ô tô, bên trong có rất nhiều cấu kiện tinh vi. Khi ta khởi động xe, các cấu kiện hoạt động với nhau, ta không biết chúng làm gì, làm như nào, chỉ biết là khi bật xe, thì xe chạy.
  • Các cấu kiện đó là những thứ “private”, chỉ bên trong một class “Oto” với nhau mới biết, bên ngoài không biết. Điều này giúp cho việc sử dụng xe dễ dàng hơn, thứ bên ngoài không cần biết quá nhiều về xe bên trong.

Getter và Setter

Getter

  • Getter là một hàm dùng để lấy giá trị của một thuộc tính. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Để lấy giá trị của thuộc tính name, ta sẽ tạo một hàm getName().
classPerson {
  String name;

  String getName() {
    return name;
  }
}

Setter

  • Setter là một hàm dùng để gán giá trị cho một thuộc tính. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Để gán giá trị cho thuộc tính name, ta sẽ tạo một hàm setName().
classPerson {
  String name;

  voidsetName(String name) {
    this.name = name;
  }
}

Tại sao cần dùng Getter và Setter

  • Khi ta dùng Getter và Setter, ta có thể kiểm soát được quyền truy cập của các thuộc tính. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Ta muốn thuộc tính name này chỉ có thể được truy cập từ bên trong class Person, và không thể được truy cập từ bên ngoài class Person. Vì vậy, ta sẽ dùng từ khóa private với thuộc tính name, và tạo một hàm getName() và setName() để lấy và gán giá trị cho thuộc tính name.
classPerson {
  private String name;

  String getName() {
    return name;
  }

  voidsetName(String name) {
    this.name = name;
  }
}

Điều cần nói

  • Hiểu đơn giản hơn, khi để tất cả thuộc tính private và chỉ getter setter public, vô hình chung ta sẽ dẫn mọi truy cập vào 1 class chỉ còn 1 con đường, đó là getter và setter
  • Điều này giúp ta có được “quyền kiểm soát”, “control” ở 1 class đó, ta có thể kiểm soát được các giá trị được gán vào thuộc tính, và các giá trị được lấy ra từ thuộc tính.
  • Ví dụ, ta có một class là “NhaHang”, nó có một phương thức lấy số điện thoại, tuy nhiên ta muốn khi lấy số điện thoại, ta sẽ chuẩn hoá lại giá trị đó, ví dụ như thêm dấu +84 vào đầu, hoặc thêm dấu – vào giữa, hoặc thêm dấu cách vào giữa, hoặc thêm dấu ngoặc vào đầu cuối, … Vậy nên, ta sẽ tạo một hàm getter, và trong hàm getter đó, ta sẽ chuẩn hoá lại giá trị trước khi trả về.
class NhaHang {
  private String soDienThoai;

  String getSoDienThoai() {
    // Chuẩn hoá lại số điện thoạireturn soDienThoai;
  }

  void setSoDienThoai(String soDienThoai) {
    this.soDienThoai = soDienThoai;
  }
}

  • Như này, ta đảm bảo số điện thoại được lấy ra dùng cho class khác luôn là số điện thoại chuẩn hoá, không cần phải chuẩn hoá lại nữa. Vì cách duy nhất để lấy số điện thoại ra là dùng hàm getter, và hàm getter đã chuẩn hoá rồi.
  • Tương tự với setter, giả sử ta có một class ConNguoi với thuộc tính chiều cao. Chiều cao thì không thể âm được, vậy nên ta sẽ tạo một hàm setter, trong đó ta sẽ kiểm tra giá trị trước khi gán vào thuộc tính.
class ConNguoi {
  private int chieuCao;

  void setChieuCao(int chieuCao) {
    if (chieuCao < 0) {
      this.chieuCao = 0;
    } else {
      this.chieuCao = chieuCao;
    }
  }
}

  • Điều này đảm bảo tính toàn vẹn của dữ liệu, giúp cho mọi thứ chính xác và chặt chẽ
  • Ngoài ra, nó cũng giúp tăng tính bảo mật, không lộ quá nhiều thông tin bên trong class ra bên ngoài, mà mọi thứ chỉ có thể truy cập, sử dụng qua các phương thức ta đã định nghĩa sẵn. (Tính chất này còn được gọi là encapsulation, đóng gói trong OOP)

Từ khoá Static

Static là gì

  • Static là một từ khóa trong Java, nó có thể được dùng với biến, hàm, class. Static có nghĩa là tĩnh, tức là nó chỉ tồn tại ở một vị trí duy nhất, tồn tại ngay cả khi chưa tạo ra đối tượng. Vì vậy, khi ta dùng static với biến, hàm, class, ta có thể gọi chúng mà không cần tạo ra đối tượng.
  • Ví dụ, trong các thư viện Java, ta thường thấy các hàm, biến, class static. Vì vậy, ta có thể gọi chúng mà không cần tạo ra đối tượng.

Math.sqrt(2);

Từ khoá static với biến

  • Khi ta dùng static với biến, thì biến đó sẽ được tạo ra ngay cả khi chưa tạo ra đối tượng. Vì vậy, ta có thể gọi biến đó mà không cần tạo ra đối tượng. Biến static này là tồn tại duy nhất, tức là nó chỉ có một giá trị duy nhất, và nó sẽ được sử dụng chung cho tất cả các đối tượng.
class Person {
  static int count=0;

  Person() {
    count++;
  }
}

Hàm main static

  • Hàm main() là một hàm static, nó được gọi khi chương trình bắt đầu chạy. Ví dụ:
public classHelloWorld {
  public staticvoidmain(String[] args) {
    System.out.println("Hello World");
  }
}

Lí do hàm main là static

  • Như ta biết, để gọi 1 hàm từ 1 đối tượng, ta phải tạo ra 1 đối tượng đó trước.
  • Vậy nếu hàm main không phải là static, thì ta phải tạo ra 1 đối tượng của class HelloWorld trước, rồi gọi hàm main từ đối tượng đó.
  • Vì vậy, hàm main static thường xuất hiện ở class khởi nguồn. Như vậy, Java có thể gọi thẳng hàm main luôn mà không cần khởi tạo class trước đó.

Lí do hàm static chỉ có thể gọi hàm static khác

  • Tuy nhiên, 1 hàm static chỉ có thể gọi các hàm static khác, và chỉ có thể truy cập các biến static khác. Lí do là vì, khi ta gọi hàm static, ta không cần tạo ra 1 đối tượng, vậy nên ta không thể truy cập các biến không phải static của đối tượng đó được. Và vì ta không tạo ra 1 đối tượng, nên ta không thể gọi các hàm không phải static của đối tượng đó được.
  • Cứ tưởng tượng đơn giản, các hàm, biến, class static luôn chỉ tồn tại độc nhất, tồn tại ngay cả khi chưa tạo ra đối tượng. Vì vậy, các hàm, biến, class static luôn có thể được gọi mà không cần tạo ra đối tượng. Nếu mà 1 hàm static gọi 1 hàm không static, thì lỡ đâu hàm không static đó lại cần phải truy cập đến các biến không static, thì làm sao mà truy cập được, vì chưa tạo ra đối tượng mà.

Về equal và hashcode, ==

  • Trong Java, có 2 cách để so sánh 2 đối tượng với nhau, đó là so sánh bằng toán tử == và so sánh bằng hàm equal(). Tuy nhiên, 2 cách này lại có 2 cách hoạt động khác nhau.
  • Khi ta so sánh 2 đối tượng bằng toán tử ==, ta đang so sánh 2 địa chỉ của 2 đối tượng đó. Nếu 2 đối tượng đó có cùng địa chỉ, thì toán tử == sẽ trả về true, ngược lại thì trả về false.
  • Khi ta so sánh 2 đối tượng bằng hàm equal(), ta đang so sánh 2 nội dung của 2 đối tượng đó. Nếu 2 đối tượng đó có cùng nội dung, thì hàm equal() sẽ trả về true, ngược lại thì trả về false.
  • equal() dùng để so sánh nội dung của 2 đối tượng. (Lưu ý: toán tử == sẽ là so sánh giá trị của 2 biến đó, hay chính xác hơn là so sánh địa chỉ, chứ không phải so sánh nội dung)
  • hashcode() dùng để xác định vị trí của đối tượng đó trong một bảng băm (cấu trúc dữ liệu mà đa số sẽ sử dụng)
- quochung.cyou PTIT
f0055 06 - quochung.cyou PTIT
f0057 01 - quochung.cyou PTIT
  • Vì như đã dạy ở trên, ta khi tạo 1 biến, thì thực chất nó chỉ là 1 tham chiếu
  • Vì vậy, các String có cùng 1 giá trị, dùng == để so sánh sẽ trả về false, vì chúng có các tham chiếu khác nhau
  • – Ta có thể thấy, khi gán 1 String khác, nếu không dùng new gán vào string cũ, chúng sẽ bằng nhau vì chúng cùng tham chiếu vào 1 địa chỉ
String str1=newString("Hello");
String str2=newString("Hello");

System.out.println(str1 == str2); // false



```java

String str1 = new String("Hello");
String str2 = str1;

System.out.println(str1 == str2); // true

  • Để giải quyết vấn đề này, ta dùng hàm equal() để so sánh nội dung của 2 String
String str1=newString("Hello");
String str2=newString("Hello");

System.out.println(str1.equal(str2)); // true
int a=5;
int b=5;

Integer a1=5;
Integer b1=5;

Integer a2=100;
Integer b2=100;

Integer a3=500;
Integer b3=500;



System.out.println(a == b); // true 
System.out.println(a1 == b1); // true vì đây là tham chiếu, nhưng mà Java có cơ chế cache Integer từ -128 đến 127, nên khi gán 2 số từ -128 đến 127, chúng sẽ cùng tham chiếu đến 1 địa chỉ, nên == sẽ trả về true
System.out.println(a2 == b2); // true vì đây là tham chiếu, nhưng mà Java có cơ chế cache Integer từ -128 đến 127, nên khi gán 2 số từ -128 đến 127, chúng sẽ cùng tham chiếu đến 1 địa chỉ, nên == sẽ trả về true
System.out.println(a3 == b3); // false
System.out.println(a3.equal(b3)); // true vì đây là so sánh nội dung, không phải so sánh tham chiếu

String và String Builder trong Java

  • String là một class trong Java, nó được sử dụng để lưu trữ một chuỗi các ký tự. Một đối tượng String được tạo ra bằng cách sử dụng từ khóa new và có thể được khởi tạo bằng một chuỗi ký tự hoặc một đối tượng String khác.
  • Lí do là vì, khi ta tạo biến String mới, ta chỉ đang tạo 1 tham chiếu đến vùng nhớ của String đó.
  • String là một dạng immutable, tức là nó không thể thay đổi được. Vì vậy, khi ta thay đổi giá trị của String, ta đang tạo ra một String mới, và tham chiếu đến nó.
String str="Hello";

str = "World";

  • Để nhập vào String từ bàn phím, ta sử dụng hàm nextLine() của Scanner
Scanner sc=newScanner(System.in);

String str= sc.nextLine();

  • Ngoài ra String có rất nhiều hàm hỗ trợ, dạy chính có thể đảo qua vài hàm chính
String str="Hello";

str = str.toLowerCase(); // "hello"

str = str.toUpperCase(); // "HELLO"

str = str.replace("l", "L"); // "HeLLo"Stringstr1="Hello    "

str1 = str1.trim(); // "Hello"
  • String Builder là một class trong Java, nó được sử dụng để lưu trữ một chuỗi các ký tự. Một đối tượng String Builder được tạo ra bằng cách sử dụng từ khóa new và có thể được khởi tạo bằng một chuỗi ký tự hoặc một đối tượng String khác.
  • String Builder là một dạng mutable, tức là nó có thể thay đổi được. Vì vậy, khi ta thay đổi giá trị của String Builder, ta không tạo ra một String Builder mới, mà chỉ thay đổi giá trị của String Builder đó.
StringBuilderstr=newStringBuilder("Hello");

str.append(" World"); // "Hello World"

str.deleteCharAt(0); // "ello World"

str.delete(0, 4); // " World"

str.insert(0, "Hello"); // "Hello World"

str.reverse(); // "dlroW olleH"
  • Để nhập vào String Builder từ bàn phím, ta sử dụng hàm nextLine() của Scanner như String
  • Vậy StringBuilder khác String ở chỗ nào
Stringstr="Hello";

str = str + " World";

  • Khi ta thực hiện phép cộng chuỗi, ta đang tạo ra một String mới, và tham chiếu đến nó. Vậy nên khi ta thực hiện phép cộng chuỗi nhiều lần, ta sẽ tạo ra rất nhiều String mới, và tham chiếu đến chúng. Điều này sẽ làm tốn rất nhiều bộ nhớ, và làm chậm chương trình.
  • Vì vậy, khi ta cần thay đổi giá trị của chuỗi nhiều lần, ta nên sử dụng StringBuilder, để tránh tạo ra quá nhiều String mới.
  • Bảng so sánh
StringStringBuilder
String là immutableStringBuilder là mutable
Khi thay đổi ít lần, String tốn ít bộ nhớ hơnKhi thay đổi ít lần, StringBuilder không được hết khả năng và tốn bộ nhớ nhiều hơn (lí do bên dưới)
Khi thay đổi nhiều lần, String tốn nhiều bộ nhớ hơn và thời gian hơnKhi thay đổi nhiều lần, StringBuilder tốn ít bộ nhớ hơn và thời gian hơn
inttotal=50000;
Strings=""; 
for (inti=0; i < total; i++) { s += String.valueOf(i); } 
// 4828msStringBuildersb=newStringBuilder(); 
for (inti=0; i < total; i++) { sb.append(String.valueOf(i)); } 
// 4ms
  • Lí do StringBuilder có thể động như vậy vì nó sử dụng cơ chế mảng động, tức là khi khai báo 1 String độ dài 10, thực chất Java dành ra nhiều ô nhớ hơn để khi thêm độ dài mới vào tốc độ sẽ nhanh hơn (do không phải di chuyển toàn bộ vùng nhớ sang phần khác). Trong khi đó, String thực chất là một mảng tĩnh, khi khai báo 1 String độ dài 10, Java chỉ dành ra đúng 10 ô nhớ, khi thêm độ dài mới vào, Java sẽ phải di chuyển toàn bộ vùng nhớ sang phần khác, điều này sẽ làm chậm tốc độ xử lí.
  • Tuy nhiên, do dùng ô nhớ ít hơn, String tốn ít bộ nhớ hơn StringBuilder. Vì vậy, khi ta cần thay đổi giá trị của chuỗi ít lần, ta nên sử dụng String, để tránh tốn bộ nhớ.

Kĩ thuật mảng động: Nhân đôi mảng

  • Hiểu đơn giản, mảng luôn được khởi tạo số lượng phần từ nhiều hơn thực tế, để khi thêm phần tử vào mảng, ta không cần phải di chuyển toàn bộ mảng sang phần khác.
  • Điều này thực chất đang hi sinh bộ nhớ nhiều hơn để có hiệu quả thời gian tốt hơn trong đa số trường hợp.
image 4 - quochung.cyou PTIT
image 5 - quochung.cyou PTIT

7. Inner class

Inner class là gì

  • Inner class là một class được khai báo bên trong một class khác. Inner class có thể được khai báo là static hoặc non-static. Inner class có thể truy cập tất cả các biến và phương thức của class bên ngoài nó.
classOuterClass {
  intx=5;

  classInnerClass {
    inty=10;
  }
}

Inner class static

  • Inner class static là một class được khai báo bên trong một class khác, và được khai báo là static. Inner class static có thể truy cập tất cả các biến và phương thức của class bên ngoài nó.
classOuterClass {
  intx=5;

  staticclassInnerClass {
    inty=10;
  }
}

Inner class non-static

  • Inner class non-static là một class được khai báo bên trong một class khác, và không được khai báo là static. Inner class non-static có thể truy cập tất cả các biến và phương thức của class bên ngoài nó.
classOuterClass {
  intx=5;

  classInnerClass {
    inty=10;
  }
}

Tại sao cần dùng Inner class

  • Inner class có thể được dùng để tạo ra một class chỉ được sử dụng bởi một class khác. Ví dụ, ta có một class Person, trong đó có một class Address. Class Address này chỉ được sử dụng bởi class Person, nên ta có thể tạo ra một inner class Address.
classPerson {
  String name;
  Address address;

  classAddress {
    String street;
    String city;
    String state;
  }
}

9. Sử dụng class từ file khác trong Java

  • từ việc import Scanner, import ArrayList, import String, … có thể import bất kì class nào trong Java, và sử dụng nó trong class của mình.
import java.util.Scanner;

classPerson {
  String name;
  int age;

  voidinput() {
    Scannersc=newScanner(System.in);
    name = sc.nextLine();
    age = sc.nextInt();
  }
}

  • Tuy nhiên, có thể tự tạo ra các class của riêng mình, và sử dụng nó trong class của mình. Ví dụ có các Class như QuanLySinhVien, SinhVien, MonHoc, … thì có thể tạo ra các file QuanLySinhVien.java, SinhVien.java, MonHoc.java, … và sử dụng chúng trong class của mình.
import java.util.Scanner;

classSinhVien {
  String name;
  int age;

  voidinput() {
    Scannersc=newScanner(System.in);
    name = sc.nextLine();
    age = sc.nextInt();
  }
}


classQuanLySinhVien {
  SinhVien[] danhSachSinhVien;

  voidinput() {
    Scannersc=newScanner(System.in);
    intn= sc.nextInt();
    danhSachSinhVien = newSinhVien[n];
    for (inti=0; i < n; i++) {
      danhSachSinhVien[i] = newSinhVien();
      danhSachSinhVien[i].input();
    }
  }
}

  • Lưu ý, trong 1 file thì chỉ có thể có 1 class public, và tên của class đó phải trùng với tên của file. Ví dụ, trong file QuanLySinhVien.java, ta chỉ có thể có 1 class public, và tên của class đó phải là QuanLySinhVien.
// QuanLySinhVien.java
public classQuanLySinhVien {
  ...
}


[Java Core] B3: Cách Java quản lý dữ liệu

This entry is part 3 of 8 in the series Java Core

Mục lục:

  • Cách Java lưu trữ dữ liệu (Kiểu dữ liệu nguyên thuỷ, Object, Wrapper class, Auto-boxing, Auto-unboxing)
  • Constructor, Super, This
  • Pass by value
  • Garbage Collector

I. Cách Java lưu trữ dữ liệu (Kiểu dữ liệu nguyên thuỷ, Object, Wrapper class, Auto-boxing, Auto-unboxing)

1. Kiểu dữ liệu nguyên thuỷ (Primitive data type)

  • Trong Java, có 8 kiểu dữ liệu nguyên thuỷ (Primitive data type): byteshortintlongfloatdoublebooleanchar.
  • Kiểu dữ liệu nguyên thuỷ là các kiểu dữ liệu mà không phải là đối tượng, không có các phương thức, thuộc tính, … như các đối tượng.
  • Tức là, bạn không thấy các phương thức toString()equals()hashCode(), … trong các kiểu dữ liệu nguyên thuỷ.
inta=1;

// a không có hàm hay biến như các object// a.toString() không tồn tại// a.equals() không tồn tại// a.(gì đó) không tồn tại
Primitive data type
  • Ví dụ, khi khai báo 1 biến int a = 1;, thì Java sẽ cấp phát 1 vùng nhớ có kích thước 4 byte để lưu giá trị 1 vào biến a. Thực chất trong bộ nhớ nó được lưu theo binary là 00000000 00000000 00000000 00000001. (1 byte = 8 bit)
  • Do có các 32 bit, nên int có thể lưu giá trị từ -2^31 đến 2^31 – 1. (1 bit để lưu dấu)
  • Tương tự, các kiểu dữ liệu khác cũng được lưu theo binary, và có kích thước khác nhau.

2. Object

2.1 Object là gì?

image 6 - quochung.cyou PTIT
  • Trong Java, mọi thứ đều là đối tượng (Object). Vậy nên các kiểu dữ liệu nguyên thuỷ cũng là đối tượng.
  • Hiện tại các em chưa học về kế thừa, nhưng cứ tạm hiểu, khi ta nói về 1 tập đối tượng, ví dụ: Xe Ô Tô, hay class Car, có các thuộc tính, phương thức gì đó. Xe máy, hay class Motorbike, cũng có các thuộc tính, phương thức gì đó. Vậy thì Xe Ô Tô và Xe Máy đều là đối tượng, và chúng có các thuộc tính, phương thức giống nhau.
  • Vậy ta gọi tập đối tượng có cả xe máy và xe ô tô, không chứa các phương thức đặc thù như chạydừng, … là gì? Đó là tập đối tượng Xe, hay class Vehicle. Vậy nên, Xe Ô Tô và Xe Máy đều là Xe, hay Vehicle. Tập hợp Vehicle có thể nằm trong tập hợp to hơn là Phương Tiện Giao Thông, hay class Transportation.
  • Trong Java cũng như vậy, mọi class các em tạo ra mặc định đều thuộc 1 tập hợp chung là Object. Và class này luôn cung cấp cho mọi object tạo ra từ các class khác các phương thức, thuộc tính như toString()equals()hashCode(), … như đã nói ở trên.

2.2 Object được lưu thế nào trong Java

image 7 - quochung.cyou PTIT
  • Hiểu đơn giản, giá trị cách lưu object thực sự rất phức tạp, điều đó đều do Java quản lý, và ta không cần quan tâm. Ta chỉ cần biết, khi tạo ra 1 object, ta chỉ lưu 1 địa chỉ tới object đó. Và từ địa chỉ này, ta có thể ra lệnh từ xa để lấy các thuộc tính, phương thức của object đó.
  • Vậy nên, khi ta gán 1 object cho 1 biến, thực chất ta đang gán 1 địa chỉ tới object đó cho biến đó.
Studentobj=newStudent();

// obj là 1 địa chỉ tới object Student
  • Vậy nên ta không thể so sánh 2 object với nhau bằng toán tử ==, mà phải dùng phương thức equals().
Studentobj1=newStudent();

Studentobj2=newStudent();

// obj1 != obj2// obj1.equals(obj2) == false

2.3 Wrapper class

  • Wrapper class là các class được tạo ra để bao bọc các kiểu dữ liệu nguyên thuỷ. Ví dụ: IntegerFloatDoubleBooleanCharacter, …
  • Wrapper class cung cấp các phương thức để thao tác với các kiểu dữ liệu nguyên thuỷ. Ví dụ: Integer.parseInt()Integer.toString()Integer.valueOf(), …
Primitive data typeWrapper class
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
booleanBoolean
charCharacter
  • Các kiểu dữ liệu lưu dạng object này tuân theo cách Java lưu trữ object, tức là không lưu trực tiếp object, mà chỉ lưu địa chỉ tới object đó.
Ưu điểmNhược điểm
Có thể sử dụng các phương thức của objectTốn bộ nhớ hơn
  • Ví dụ, ở Java ta có thể chuyển String thành số, từ các số hay từ 1 object bất kì sang String rất nhanh bằng toString(), ngoài ra khi nó là Object, ta có thể sử dụng các kĩ thuật OOP như kế thừa, đa hình, … (sẽ nói ở các buổi sau).
inta=1;

// a không có hàm hay biến như các object// a.toString() không tồn tạiIntegerb=1;

// b có hàm toString()// b.toString() tồn tại

2.4 Auto-boxing và Auto-unboxing

  • Auto-boxing là quá trình chuyển đổi từ kiểu dữ liệu nguyên thuỷ sang kiểu dữ liệu lưu dạng object.
  • Auto-unboxing là quá trình chuyển đổi từ kiểu dữ liệu lưu dạng object sang kiểu dữ liệu nguyên thuỷ.
  • Ví dụ:
inta=1;

Integerb=1;

// Auto-boxingIntegerc= a;

// Auto-unboxingintd= b;

  • Auto-boxing và Auto-unboxing là 2 quá trình tự động, do Java tự động thực hiện. Vậy nên các em không cần phải quan tâm đến nó.
  • Tuy nhiên, khi sử dụng Auto-boxing và Auto-unboxing, các em cần phải cẩn thận, vì nó có thể gây ra lỗi.
Integera=null;

intb= a; // NullPointerException
  • Vậy nên, khi sử dụng Auto-boxing và Auto-unboxing, các em cần phải kiểm tra giá trị null trước khi sử dụng.
Integera=null;

intb= a == null ? 0 : a;

  • Auto-boxing và Auto-unboxing có thể gây ra lỗi khi sử dụng toán tử ==.
Integera=1;

Integerb=1;

Integerc=128;

Integerd=128;

System.out.println(a == b); // true

System.out.println(c == d); // false
  • Autoboxing và Auto-unboxing giúp cho việc sử dụng các kiểu dữ liệu nguyên thuỷ và object trở nên dễ dàng hơn. Ví dụ, khi ta muốn sử dụng HashMap, thì ta không thể sử dụng kiểu dữ liệu nguyên thuỷ là int, mà phải sử dụng Integer.

HashMap<Integer, String> map = newHashMap<>();

map.put(1, "Hello");

map.put(2, "World");

System.out.println(map.get(1)); // Hello

System.out.println(map.get(2)); // World
  • Nếu không có Auto-boxing và Auto-unboxing, thì ta phải làm như sau:

HashMap<Integer, String> map = newHashMap<>();

map.put(Integer.valueOf(1), "Hello");

map.put(Integer.valueOf(2), "World");

System.out.println(map.get(Integer.valueOf(1))); // Hello

System.out.println(map.get(Integer.valueOf(2))); // World
  • Tuy nhiên, trong nhiều trường hợp nên hạn chế dùng các kiểu Object do chúng tốn bộ nhớ
for (Integeri=0; i < 1000000; i++) {

  // ...

}

// Tốn bộ nhớ hơn// Vì i là object, nên phải tạo ra 1 object mới mỗi lần tăng i// Còn nếu dùng int, thì chỉ cần tạo ra 1 biến int i, và tăng i mỗi lần

3. Các phương thức khởi tạo (Constructor) trong Java

  • Constructor là một phương thức đặc biệt, nó được gọi khi ta khởi tạo một đối tượng. Constructor có tên giống với tên class, và không có kiểu trả về.
  • Constructor được sử dụng để khởi tạo các giá trị ban đầu cho đối tượng. Ví dụ như ta có một class Student, trong đó có các thuộc tính nameageaddressmathliteratureenglish. Ta có thể tạo một constructor như sau:
classStudent {
  String name;
  int age;
  String address;
  float math;
  float literature;
  float english;

  Student(String name, int age, String address, float math, float literature, float english) {
    this.name = name;
    this.age = age;
    this.address = address;
    this.math = math;
    this.literature = literature;
    this.english = english;
  }
}

  • Constructor có thể có hoặc không có tham số. Nếu không có tham số, thì constructor đó được gọi là constructor mặc định. Nếu có tham số, thì constructor đó được gọi là constructor có tham số.
  • Constructor có thể có nhiều tham số, và các tham số đó có thể là bất kỳ kiểu dữ liệu nào.
  • Constructor có thể có nhiều hơn một, và các constructor đó có thể có số lượng tham số khác nhau.
  • Constructor có thể gọi constructor khác của cùng một class bằng từ khóa this(). Ví dụ:
classStudent {
  String name;
  int age;
  String address;
  float math;
  float literature;
  float english;

  Student(String name, int age, String address, float math, float literature, float english) {
    this.name = name;
    this.age = age;
    this.address = address;
    this.math = math;
    this.literature = literature;
    this.english = english;
  }

  Student(String name, int age, String address) {
    this(name, age, address, 0, 0, 0);
  }
}

4. Các khái niệm cơ bản về Garbage Collector (GC)

image 8 - quochung.cyou PTIT
image 9 - quochung.cyou PTIT
  • Trong quá trình thực thi chương trình, Java sẽ liên tục tìm các Object không được tham chiếu tới Heap Memory (mang giá trị null hoặc một vài trường hợp khác). Sau đó Garbage Collector sẽ tự động giải phóng bộ nhớ cho các ô nhớ chứa tham chiếu đó.
  • Điều này kiến lập trình viên không cần phải giải phóng bộ nhớ thủ công như C/C++. Thay vào đó, để giải phóng bộ nhớ, lập trình viên chỉ cần loại bỏ tham chiếu của Object.

5. Cách Java truyền tham số | Pass-by-value

Cách Java truyền tham số

5.1. Pass-by-value

  • Trong Java, khi ta truyền tham số vào một hàm, thì tham số đó sẽ được copy ra một vùng nhớ khác, và hàm sẽ thao tác với tham số ở vùng nhớ mới này.
  • Vậy nên, khi ta thay đổi giá trị của tham số trong hàm, thì giá trị của tham số bên ngoài hàm không bị thay đổi.
publicclassMain {
  publicstaticvoidmain(String[] args) {
    inta=1;
    System.out.println(a); // 1
    change(a);
    System.out.println(a); // 1
  }

  publicstaticvoidchange(int a) {
    a = 2;
  }
}

  • Tuy nhiên, nó sẽ lại thay đổi được các thuộc tính của object.
publicclassMain {
  publicstaticvoidmain(String[] args) {
    Studentstudent=newStudent();
    student.name = "A";
    System.out.println(student.name); // A
    change(student);
    System.out.println(student.name); // B
  }

  publicstaticvoidchange(Student student) {
    student.name = "B";
  }
}

classStudent {
  String name;
}

5.2. Tại sao pass-by-value mà String lại thay đổi được?

image 13 - quochung.cyou PTIT
  • Ta hãy quay trở lại khái niệm vừa nhắc tới Khi ta truyền một biến nguyên thuỷ, hay trong trường hợp trên là age, ta đang như tạo một bản sao của một tờ giấy bình thường, tờ giấy đó ghi số là 20, ta đưa cho người khác. Họ sửa chúng thành 90, 100 hay gì đi chăng nữa, thì tờ giấy ban đầu của ta vẫn là 20
  • Tuy nhiên, khi ta truyền một biến reference (tham chiếu) của Object person, ta đang truyền bản sao của “tham chiếu” của nó. Hay đơn giản là, ta đang tạo ra bản sao của một cái điều khiển, vậy thì cái điều khiển bản sao này khi đưa cho người khác, vẫn sẽ bật tắt được tivi ban đầu của mình.

[Java Core] B4: Tính chất đóng gói, kế thừa và đa hình trong Java

This entry is part 4 of 8 in the series Java Core

Đóng gói

  • Đóng gói là một trong những tính chất quan trọng của lập trình hướng đối tượng
  • Đóng gói giúp che dấu thông tin bên trong của một đối tượng, chỉ cho phép các đối tượng khác tương tác thông qua các phương thức được cung cấp
  • Đóng gói giúp giảm sự phức tạp của chương trình, giúp dễ dàng bảo trì và mở rộng chương trình
Encapsulation
  • Ví dụ, trong một chiếc xe ô tô, ta có rất nhiều cấu phận và thông số. Nó có động cơ, vô lăng, bình xăng, đánh lửa, …
  • Nhưng ta thường không tương tác trực tiếp được vào chúng, mà thông qua các phương thức như bấm ga, bấm còi, đạp phanh, …
  • Khi bấm ga, có thể cả một quy trình rất dài đang xảy ra, như động cơ chạy, bình xăng bơm xăng, đánh lửa đốt xăng, …
  • Nhưng ta không cần quan tâm đến những thứ đó, ta chỉ cần bấm ga, và xe sẽ chạy
  • Ngoài ra, đóng gói giúp ta đảm bảo việc dữ liệu sẽ được giấu khỏi các đối tượng khác, và chỉ có các phương thức được cung cấp mới có thể tương tác với dữ liệu đó. Giúp đảm bảo tính toàn vẹn của dữ liệu.

Ví dụ:

  • getter, setter là một ví dụ về đóng gói. Nhờ getter, setter, ta có thể đảm bảo dữ liệu được cập nhật từ đối tượng khác sẽ luôn theo ý của mình, hay dữ liệu được lấy ra từ đối tượng khác sẽ luôn đúng theo ý của mình
  • Ví dụ trong các thư viện của Java, ta có thể thấy rất nhiều ví dụ về đóng gói. Ví dụ như trong class Scanner, ta có thể thấy rằng các biến được khai báo là private, và ta không thể truy cập trực tiếp vào chúng. Nhưng ta có thể sử dụng các phương thức như nextInt(), nextDouble(), nextLine(), … để lấy dữ liệu từ bàn phím. (Có thể ctrl + click vào các phương thức đó để xem code, sẽ thấy Scanner sử dụng các biến private để lấy dữ liệu từ bàn phím)
image 26 - quochung.cyou PTIT
  • Đóng gói xuất hiện ở mọi nơi: Từ cả cái máy tính của bạn chỉ cần 1 nút bật, một game với cả trăm nghìn class chỉ cần 1 lần nhấn Play sẽ bắt đầu chạy, các thư viện Java với vô vàn chức năng đã được đóng gói và cho phép ta sử dụng qua những hàm rất đơn giản.
  • Trong lập trình phần mềm, yêu cầu của một chương trình thay đổi liên tục đáp ứng nhu cầu của khách hàng. Việc đóng gói và phân tách công việc của những bộ phận khác nhau là cực kì quan trọng, khi đó, ta sẽ cố gắng giới hạn những thay đổi chỉ bên trong 1 bộ phận nhỏ, độc lập nhất có thể và ít ảnh hưởng tới nhiều chỗ khác.
image 28 - quochung.cyou PTIT

Kế thừa

  • Kế thừa là một trong những tính chất quan trọng của lập trình hướng đối tượng
  • Kế thừa giúp ta có thể sử dụng lại các đặc tính của một đối tượng khác, giúp giảm sự lặp lại của code, giúp dễ dàng bảo trì và mở rộng chương trình
  • Kế thừa giúp ta có thể tạo ra các đối tượng mới từ các đối tượng đã có, và có thể thêm các đặc tính mới vào các đối tượng mới đó
Inheritance
image 27 - quochung.cyou PTIT
  • Ta nhận thấy kế thừa, hay sơ đồ cây xuất hiện ở mọi nơi trong cuộc sống. Như một chiếc ô tô có thể gọi là xe bốn bánh, xe bốn bánh có thể coi là phương tiện giao thông, …. Con người có thể coi là động vật. trong sinh học ta có các giống loài, trong vật lý có nhiều loại sóng khác nhau cùng là sóng,có chung tính chất là sóng, …
image 29 - quochung.cyou PTIT

Ví dụ:

  • Ví dụ ta có nhiều class cần tạo như Chó, Mèo, Gà, Vịt, … Ta nhận thấy chúng đều có chung 1 đặc điểm là chúng đều là động vật, và có chung 1 số đặc điểm như là có thể ăn, có thể ngủ, có thể chạy, có thể bơi, … Ta có thể tạo ra một class cha là Động vật, và các class trên sẽ kế thừa từ class Động vật. Như vậy, ta có thể sử dụng lại các đặc điểm của class Động vật cho các class con, và có thể thêm các đặc điểm mới vào các class con đó, ví dụ Chó thì có thêm đặc điểm là sủa, Mèo thì có thêm đặc điểm là kêu meo meo, …
  • Các phương thức, thuộc tính của class cha sẽ được các class con kế thừa, và có thể sử dụng lại. Ngoài ra, các class con có thể thêm các phương thức, thuộc tính mới vào các class con đó. Điều này giúp giảm bớt các code dư thừa, giúp dễ dàng bảo trì và mở rộng chương trình
  • Có thể ở các class nhỏ hiện tại các bạn chưa thấy kế thừa hữu ích. Tuy nhiên khi bài toán ngày càng lớn lên, đặc biệt thiên về domain chuyên ngành, 1 class sẽ có rất nhiều thuộc tính và phương thức. Ví dụ: một cái bánh bình thường ở tạp hoá, có thể có lên tới 10-15 thuộc tính như màu sắc, hình dạng, giá, hạn sử dụng, ngày sản xuất, thành phần sản phẩm, các chứng chỉ an toàn thực phẩm, mã barcode, hay có các biến như “có thể cho động vật ăn”, … sau đó lại có nhiều loại bánh khác nhau nữa
image 17 - quochung.cyou PTIT

Class Object

Class Object
  • Trong Java, mọi class đều kế thừa từ class Object
  • Class Object là class cha của tất cả các class khác
  • Class Object có các phương thức như equals()toString()hashCode()clone()finalize()wait()notify()notifyAll(), …
  • Các phương thức này được các class khác kế thừa, và có thể sử dụng lại
  • Có nhiều phương thức các bạn chưa cần đào sâu. Tuy nhiên tạm thời có thể để ý nó có các phương thức quan trọng như equals, toString, hashCode. Đây là các phương thức thường được dùng.

Đa hình

image 30 - quochung.cyou PTIT
  • Đa hình là một trong những tính chất quan trọng của lập trình hướng đối tượng
  • Đa hình giúp ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một kiểu dữ liệu, giúp giảm sự lặp lại của code, giúp dễ dàng bảo trì và mở rộng chương trình
Polymorphism

Override

  • Override là một kĩ thuật trong đa hình, giúp ta có thể thay đổi cách thức hoạt động của một phương thức đã có sẵn trong class cha
  • Override giúp ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một class cha, và khi gọi các phương thức, ta sẽ thấy mỗi đối tượng sẽ thực hiện theo cách của nó, chứ không phải cách của đối tượng khác

Cú pháp:

publicclassClassCha {
    @OverridepublicvoidmethodName() {
        // code
    }
}

classClassConextendsClassCon {
    @OverridepublicvoidmethodName() {
        // code
    }
}

Overload

  • Overload là một kĩ thuật trong đa hình, giúp ta có thể tạo ra nhiều phương thức cùng tên, nhưng khác nhau về tham số truyền vào
  • Overload giúp ta có thể sử dụng các phương thức khác nhau, nhưng có cùng một tên, và khi gọi các phương thức, ta sẽ thấy mỗi phương thức sẽ thực hiện theo cách của nó, chứ không phải cách của phương thức khác

Cú pháp:

publicclassClassCha {
    publicvoidmethodName(int a) {
        // code
    }

    publicvoidmethodName(int a, int b) {
        // code
    }
}

  • Dễ thấy overload trong các phương thức của class Math, như Math.max()Math.min()Math.abs()Math.pow()Math.sqrt()Math.round()Math.floor()Math.ceil()Math.random(), … Chúng đều có cùng một tên là Math, nhưng khác nhau về tham số truyền vào. Ví dụ nếu truyền vào số nguyên, thì xử lý khác, truyền số thực thì xử lý khác, …

Phân biệt Overload và Override

OverloadOverride
Cùng tên, khác tham sốCùng tên, cùng tham số
Cùng classKhác class
Overload giúp ta có thể tạo ra nhiều phương thức cùng tên, nhưng khác nhau về tham số truyền vàoOverride là một kĩ thuật trong đa hình, giúp ta có thể thay đổi cách thức hoạt động của một phương thức đã có sẵn trong class cha
Overload chạy trong compile timeOverride chạy trong run time

Đa hình compile time và runtime

  • Đa hình compile time là đa hình mà chúng ta có thể thấy được ngay trong quá trình code, ví dụ như overload. Đa hình compile time sẽ được thực hiện trong compile time, tức là khi chúng ta build chương trình, chúng ta sẽ thấy các phương thức được overload sẽ được thực hiện theo cách của nó, chứ không phải cách của phương thức khác
  • Đa hình runtime là đa hình mà chúng ta không thể thấy được ngay trong quá trình code, ví dụ như override. Đa hình runtime sẽ được thực hiện trong runtime, tức là khi chúng ta chạy chương trình, chúng ta sẽ thấy các phương thức được override sẽ được thực hiện theo cách của nó, chứ không phải cách của phương thức khác

Ví dụ: Khi viết code lúc compile ta đã thấy ngay phương thức nào được gọi, nhưng khi chạy chương trình, ta mới thấy phương thức nào được thực hiện của override

Ví dụ:

  • Ta có các class Chó, Mèo, Bò. Chúng đều kế thừa từ class Động vật. Ta có thể tạo ra một mảng các đối tượng Động vật, và có thể thêm vào đó các đối tượng Chó, Mèo, Bò. Như vậy, ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một kiểu dữ liệu, là Động vật.
  • Chúng có thể có chung 1 hàm là keu(), nhưng khi gọi hàm keu() trên mảng các đối tượng Động vật, ta sẽ thấy mỗi đối tượng sẽ keu() theo cách của nó, chứ không phải cách của đối tượng khác. Như vậy, ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một kiểu dữ liệu, và khi gọi các phương thức, ta sẽ thấy mỗi đối tượng sẽ thực hiện theo cách của nó, chứ không phải cách của đối tượng khác.
publicclassDongVat {
    publicvoidkeu() {
        System.out.println("Keu cua dong vat");
    }
}

publicclassChoextendsDongVat {
    @Overridepublicvoidkeu() {
        System.out.println("Gau gau");
    }
}

publicclassMeoextendsDongVat {
    @Overridepublicvoidkeu() {
        System.out.println("Meo meo");
    }
}

publicclassBoextendsDongVat {
    @Overridepublicvoidkeu() {
        System.out.println("Um bo");
    }
}

Bài tập kế thừa và đa hình

  • Tạo ra một class ConNguoi, có các thuộc tính là tên, tuổi, giới tính, …
  • Có các class con là SinhVien, NhanVien, HocSinhCap3 kế thừa từ class ConNguoi
  • Các class con có thể có thêm các thuộc tính riêng của nó, ví dụ SinhVien có thuộc tính là mã sinh viên, NhanVien có thuộc tính là mã nhân viên, …
  • Các class con có thể có thêm các phương thức riêng của nó, ví dụ SinhVien có phương thức là học, NhanVien có phương thức là làm việc, …
  • Có 1 phương thức chung của 3 class con là lamViecBuoiSang(), nhưng mỗi class con sẽ thực hiện theo cách của nó, chứ không phải cách của class con khác. Ví dụ SinhVien sẽ đi học đại học, NhanVien sẽ đi làm, HocSinhCap3 sẽ đi học ở trường cấp 3

[Java Core] B5: Tính chất trừu tượng, Interface và Abstract Class trong Java

This entry is part 5 of 8 in the series Java Core

Buổi 5: Interface và trừu tượng

  • Interface là gì?
  • Interface và Abstract class
  • Tính trừu tượng
  • Khi nào dùng interface, khi nào dùng abstract class?
  • Bài tập về interface và trừu tượng

Interface là gì?

  • Interface là một khái niệm trừu tượng, nó chỉ ra những gì mà một đối tượng có thể làm, nhưng không nói ra làm thế nào để đối tượng đó làm được điều đó.

Ví dụ cú pháp khai báo interface:

image 48 - quochung.cyou PTIT
  • Trong ví dụ trên, interface Animal chỉ ra rằng một Animal có thể làm được 2 việc là ăn và di chuyển, nhưng không nói ra làm thế nào để ăn và di chuyển.
  • Lúc này, ví dụ ta có 1 class Dog và Cat thì ta có thể implement interface Animal vào 2 class này như sau:
image 50 - quochung.cyou PTIT
  • Lúc này, ta có thể khởi tạo 2 đối tượng Dog và Cat và gọi các phương thức eat() và travel() mà không cần quan tâm đến cách thức của 2 phương thức này.
image 51 - quochung.cyou PTIT
  • Kết quả khi chạy chương trình:

Dog is eating

Dog is travelling

Cat is eating

Cat is travelling

Take away | Tổng hợp

  • Như vậy, interface có thể nhìn nhận như 1 bản hợp đồng, tôi quy định trước, tôi chỉ định nghĩa là, các class implement interface này phải làm được những gì tôi quy định.
  • Như vậy, khi bên khác implement interface này, bên đó phải làm theo những gì mà interface quy định, nếu không làm theo, bên đó sẽ bị lỗi compile.
  • Còn nếu bên nào sử dụng interface, ta có thể khởi tạo đối tượng của class implement interface đó, và gọi các phương thức, và biết chắc phương thức tồn tại và có thể gọi được ở class đó, vì class đó đã implement interface đó.

Abstract class

  • Abstract Class – Lớp trừu tượng, là 1 lớp trong Java, nhưng có thể chứa cả các phương thức trừu tượng (abstract method) và các phương thức thường (non-abstract method).
  • Abstract Class không thể khởi tạo đối tượng, nhưng có thể khai báo biến kiểu Abstract Class, và khởi tạo đối tượng của class con của Abstract Class.

Ví dụ:

image 52 - quochung.cyou PTIT
  • Kết quả khi chạy chương trình:

Dog is eating

Animal is travelling

Cat is eating

Animal is travelling

Take away | Tổng hợp

  • Như vậy, Abstract Class nhìn chung giống Class, nhưng mà có thể tạo thêm phương thức trừu tượng, các class kế thừa từ Abstract Class này phải implement các phương thức trừu tượng này, nếu không sẽ bị lỗi compile.
  • Các phương thức trừu tượng này, có thể hiểu là các phương thức mà Abstract Class này quy định, các class kế thừa từ Abstract Class này phải có, và phải implement các phương thức này.
  • Các phương thức thường (non-abstract method) thì không cần implement, vì các class kế thừa từ Abstract Class này đã có sẵn các phương thức này.
  • Abstract Class thì không thể khởi tạo đối tượng, nhưng có thể khai báo biến kiểu Abstract Class, và khởi tạo đối tượng của class con của Abstract Class.

Ví dụ

image 53 - quochung.cyou PTIT
  • Lí do, vì abstract class hay interface chứa những hàm trừu tượng, những hàm chỉ định nghĩa, chưa có logic và giao việc triển khai logic cho các nơi ghi đè nó, nên không thể khởi tạo đối tượng từ abstract class hay interface được. (Vì nó chưa có logic, chưa có gì để chạy cả)

So sánh Interface và Abstract Class

InterfaceAbstract Class
Interface chỉ có thể chứa các phương thức trừu tượngAbstract Class có thể chứa cả các phương thức trừu tượng và các phương thức thường
Interface không thể khởi tạo đối tượngAbstract Class không thể khởi tạo đối tượng
Biến trong interface mặc định là public static finalBiến trong abstract class không có mặc định gì cả
Interface không có constructorAbstract Class có constructor
Interface không có gì để ghi đèAbstract Class có thể có các phương thức để ghi đè
Interface có thể kế thừa nhiều interface khácAbstract Class chỉ có thể kế thừa 1 abstract class khác
Các class implement interface có thể implement nhiều interface khácCác class kế thừa từ abstract class chỉ có thể kế thừa 1 abstract class khác
  • Note: Sau này interface có thể có các phương thức thường, nhưng mà vẫn không thể khởi tạo đối tượng từ interface được.
  • Về biến trong interface, mặc định là public static final, nên khi khai báo biến trong interface, không cần khai báo public static final nữa. Tức là các biến trong interface là tồn tại duy nhất, không đổi được, ở dạng static.
  • Về biến trong abstract class, không có mặc định gì cả, nên có thể tạo các “biến thường”, tức là mỗi object tạo ra bởi 1 class chứa giá trị khác nhau.

Special Question: Diamond Problem

  • Diamond Problem là gì?
  • Diamond Problem là một vấn đề xảy ra khi đa kế thừa, tức là nếu 1 class kế thừa từ 2 nơi có trùng thứ gì đó, thì nó làm sao biết nên dùng cái nào ???
  • Mặc định thì, Java không cho phép kế thừa từ 2 class, nhưng nếu kế thừa từ 2 interface thì được. (Vì interface chỉ chứa các phương thức trừu tượng, không có gì để ghi đè)
  • Ví dụ:
image 55 - quochung.cyou PTIT

Khi nào dùng interface, khi nào dùng abstract class?

  • Khi nào dùng interface, khi nào dùng abstract class?
  • Câu trả lời đơn giản: Khi nào cần thì dùng, tuỳ vào mục đích của mình
  • Nhưng mà, để hiểu rõ hơn, ta cùng xem ví dụ sau:
image 56 - quochung.cyou PTIT
  • Như vậy, ta có thể thấy, khi dùng interface, ta phải implement lại cả phương thức travel(), trong khi khi dùng abstract class, ta không cần implement lại phương thức travel().
  • Tuy nhiên, interface lại cho phép ta kế thừa nhiều interface khác, trong khi abstract class thì không.

Tổng kết

  • Có thể thấy, abstract class cho phép ta có các khả năng trừu tượng, và có thể chứa các logic có sẵn ta muốn dùng cho nhiều class con. Lúc này ta có thể cân nhắc sử dụng abstract class
  • Thay vì điều đó, interface thường được dùng để chỉ behavior - tính cách, cách xử sự của 1 đối tượng. Tức là 1 cái ô tô thì có thể xử lý các interface là ChayXe, ChayDongCo, ChayDien, ChayDau… Như vậy, interface thường được dùng để chỉ ra tính chất của 1 đối tượng, còn logic thì để class implement interface đó tự triển khai.
image 47 - quochung.cyou PTIT
  • Với abstract class, ta dùng cho những case ta có logic tập trung, có thể reuse ở class con. Vừa có khả năng trừu tượng mà các class con sẽ có đặc tính riêng. Tuy nhiên nếu class đó quá lớn, ví dụ class PhuongTien có đủ thứ hàm như ChayXe, ChayDongCo, ChayDien, ChayDau… , thì ví dụ XeDap kế thừa lại có các hàm chạy động cơ, chạy điện => Như thế bị thừa, không cần thiết, làm confuse cho người đọc code => Ta nên tách ra thành interface.
  • Với interface, ta dùng cho những case ta chỉ cần chỉ ra tính chất của 1 đối tượng, còn logic thì để class implement interface đó tự triển khai.

Tính trừu tượng

  • Tính trừu tượng là gì?
  • Tính trừu tượng là một khái niệm trong lập trình hướng đối tượng, nó cho phép ta chỉ ra những gì mà một đối tượng có thể làm, nhưng không nói ra làm thế nào để đối tượng đó làm được điều đó.
  • Tức là, khi ta dùng xe máy, ta chỉ biết là nó có thể chạy, có thể dừng, có thể bật đèn, có thể bật còi, nhưng ta không cần biết làm thế nào để nó chạy, dừng, bật đèn, bật còi.
  • Tính trừu tượng là một khái niệm quan trọng trong lập trình hướng đối tượng, nó cho phép ta tách biệt được phần logic của đối tượng và phần giao tiếp với đối tượng. Ta chỉ quan tâm đến phần giao tiếp với đối tượng, còn phần logic của đối tượng thì ta không cần quan tâm. Điều này giúp ta có thể dễ dàng thay đổi phần logic của đối tượng mà không ảnh hưởng đến phần giao tiếp với đối tượng.

Tại sao cần sử dụng interface

  • Giả sử, ta có 1 interface là “Athlete” – Vận động viên với 2 hàm là play() – là chơi thể thao nào đó
image 45 - quochung.cyou PTIT
image 46 - quochung.cyou PTIT
  • Giả sử, ta có 1 class Team – 1 đội chơi, sẽ có thể thêm vận động viên vào, ta nhận thấy, nếu có thể, ta luôn lên sử dụng interface thay vì khai báo thẳng 1 class cụ thể, vì khi đó, ta có thể thêm bất cứ vận động viên nào vào đội chơi, miễn là vận động viên đó implement interface Athlete.
image 57 - quochung.cyou PTIT
  • Như code trên, bất kì vận động viên nào implement interface Athlete đều có thể thêm vào đội chơi, và có thể chơi được. Điều này giúp code dễ mở rộng, flexible hơn, sau này ta có thể thêm bất cứ vận động viên nào vào đội chơi, miễn là vận động viên đó implement interface Athlete.

Đây cũng được biết là quy tắc prefer interface over class hay prefer composition over inheritance

Interface làm đúng sự phụ thuộc của module level cao

  • Giả sử, ta có class DienThoai, khi này khi ta muốn sạc điện thoại, ta sẽ gọi 1 class bên trong DienThoai là sạc điện bằng củ sạc Samsung
image 58 - quochung.cyou PTIT
  • Ta thấy, với thiết kế hiện tại, ta sẽ truyền tham số là cường độ vào củ sạc Samsung để class đó thực hiện sạc.
  • Tuy nhiên, bất kì sự thay đổi nào ở class CuSac vào phương thức sac có thể yêu cầu class điện thoại truyền thêm tham số, hay phải dùng tên hàm khác, …
  • Điều này là không đúng, điện thoại rõ ràng là module cấp cao hơn, và nên có quyền quyết định dùng củ sạc nào, chứ không phải củ sạc quyết định điện thoại phải làm gì để dùng.
  • Vì vậy, ta nên tạo ra 1 interface là CuSac để điện thoại có thể sạc, và class CuSacSamSung implement interface CuSac này.
image 59 - quochung.cyou PTIT
  • Lúc này, sự phụ thuộc đã thay đổi. Giờ DienThoai chỉ sử dụng interface củ sạc, ví dụ như Iphone quy định là tôi dùng cổng lightning, máy Samsung bảo tôi dùng cổng sạc USB-C vậy. Các nhà sản xuất củ sạc phải đi theo họ.
  • Thì ở đây, DienThoai dùng interface để định nghĩa là tôi sẽ muốn sạc như thế nào. Còn củ sạc thì implement interface đó, và tự triển khai logic riêng của mình, nhưng phải tuân thủ theo hợp đồng interface mà DienThoai sử dụng

[Java Core] B6: Design Pattern Iterator. Iterable và Collection trong Java

This entry is part 6 of 8 in the series Java Core

Buổi 6: Một số trúc dữ liệu thường thấy trong Java

  • Cấu trúc dữ liệu là gì, sử dụng khi nào
  • Interface Iterable, Collection -> List, Set, Queue
  • Interface Map, SortedMap -> HashMap, TreeMap
  • Sử dụng một số hàm của cấu trúc dữ liệu như sort

Cấu trúc dữ liệu là gì, sử dụng khi nào

Cấu trúc dữ liệu là một cách tổ chức dữ liệu trong máy tính để có thể lưu trữ và xử lý dữ liệu một cách hiệu quả. Cấu trúc dữ liệu là một phần quan trọng trong việc thiết kế các chương trình và các thuật toán.

image 60 - quochung.cyou PTIT
  • Thông thường, sẽ có 2 kiểu cấu trúc dữ liệu chính là Linear Data Structure và Non-Linear Data Structure hay cấu trúc dữ liệu tuyến tính và cấu trúc dữ liệu phi tuyến tính.
  • Cấu trúc dữ liệu tuyến tính là các cấu trúc dữ liệu mà các phần tử dữ liệu được sắp xếp theo một thứ tự nhất định. Các cấu trúc dữ liệu tuyến tính thường được sử dụng để lưu trữ dữ liệu đơn giản như danh sách, mảng, hàng đợi, stack, …
  • Cấu trúc dữ liệu phi tuyến tính là các cấu trúc dữ liệu mà các phần tử dữ liệu được sắp xếp theo một thứ tự không nhất định. Các cấu trúc dữ liệu phi tuyến tính thường được sử dụng để lưu trữ dữ liệu phức tạp như cây, đồ thị, …
image 62 - quochung.cyou PTIT

=> Câu hỏi: Với các cấu trúc dữ liệu phức tạp như vậy, làm sao để ta có thể lưu trữ, và duyệt qua các phần tử của chúng?

  • Nghe như một công việc đơn giản với các cấu trúc dữ liệu tuyến tính như list. Ta chỉ cần duyệt qua toàn bộ phần tử khi duyệt. Nhưng làm sao để ta duyệt thứ tự một cấu trúc dữ liệu phức tạp, ví dụ như cây ? Có thể, ta cần nó duyệt theo DFS, hoặc BFS, hoặc duyệt theo thứ tự trước, sau, hoặc duyệt ngẫu nhiên, …
image 63 - quochung.cyou PTIT

Interface Iterable, Collection -> List, Set, Queue

  • Interface Iterable là một interface trong Java, nó có một phương thức là iterator() trả về một đối tượng Iterator để duyệt qua các phần tử của một Collection.  
image 64 - quochung.cyou PTIT
image 65 - quochung.cyou PTIT

Ví dụ:

image 66 - quochung.cyou PTIT


  • Như trong ví dụ trên, ta có thể thấy, ta có thể duyệt qua các phần tử của một Collection bằng cách sử dụng Iterator.
  • Ta lấy ra iterator của object list, thuộc class ArrayList. Do interface List đã define hàm iterator, nên class ArrayList phải implement hàm iterator này, và tự triển khai logic cách để duyệt qua bản thân như thế nào.
  • Các class khác như LinkedList, Vector, … cũng phải implement hàm iterator này, và tự triển khai logic cách để duyệt qua bản thân như thế nào.
  • Điều này cho phép khi duyệt qua các Map, Set, … ta không cần phải quan tâm đến cách duyệt của chúng, mà chỉ cần gọi hàm iterator() của chúng, và duyệt qua các phần tử của chúng.
  • Ví dụ về việc duyệt qua các phần tử của một Set:
image 67 - quochung.cyou PTIT
  • Hoặc là có thể duyệt bằng for-each, có 2 kiểu như sau:
image 68 - quochung.cyou PTIT

Định nghĩa một số cấu trúc dữ liệu trong Java

Set

  • Set: là một cấu trúc dữ liệu mà nó không chứa các phần tử trùng lặp. Các phần tử trong Set được sắp xếp theo thứ tự tùy vào cách implement của từng class. Các class implement Set có thể là HashSet, LinkedHashSet, TreeSet, …
    • HashSet: là một class implement Set, nó sử dụng một HashMap để lưu trữ các phần tử. Do đó, các phần tử trong HashSet sẽ không được sắp xếp theo thứ tự
    • LinkedHashSet: là một class implement Set, nó sử dụng một LinkedHashMap để lưu trữ các phần tử. Do đó, các phần tử trong LinkedHashSet sẽ được sắp xếp theo thứ tự.
    • TreeSet: là một class implement Set, nó sử dụng một TreeMap để lưu trữ các phần tử. Do đó, các phần tử trong TreeSet sẽ được sắp xếp theo thứ tự.

Độ phức tạp:

HashSetLinkedHashSetTreeSet
Cách thức làm việcHashSet sử dụng HashMap nội bộ để lưu trữ phần tử.LinkedHashSet sử dụng LinkedHashMap nội bộ để lưu trữ phần tử.TreeSet sử dụng TreeMap nội bộ để lưu trữ phần tử.
Thứ tự của các phần tửKhông duy trì thứ tự của các phần tử.Duy trì thứ tự chèn của các phần tử.Duy trì thứ tự theo bộ so sánh (hoặc tăng dần tự nhiên).
Hiệu suấtTốt hơn so với LinkedHashSet và TreeSet.Nằm giữa HashSet và TreeSet, hiệu suất gần như tương tự HashSet.Thấp hơn HashSet và LinkedHashSet do phải sắp xếp.
Thao tác thêm, xóa, truy xuấtO(1) cho chèn, loại bỏ, và truy xuất.O(1) cho chèn, loại bỏ, và truy xuất.O(log(n)) cho chèn, loại bỏ, và truy xuất.
So sánh các phần tửSử dụng equals() và hashCode() để so sánh và loại bỏ phần tử trùng lặp.Sử dụng equals() và hashCode() để so sánh và loại bỏ phần tử trùng lặp.Sử dụng compare() hoặc compareTo() để so sánh và loại bỏ phần tử trùng lặp. Không sử dụng equals() và hashCode().
Phần tử NullCho phép tối đa một phần tử null.Cho phép tối đa một phần tử null.Không cho phép phần tử null, nếu thử chèn sẽ ném NullPointerException.
Sử dụng bộ nhớĐòi hỏi ít bộ nhớ hơn so với LinkedHashSet và TreeSet.Yêu cầu bộ nhớ nhiều hơn HashSet do duy trì LinkedList cùng với HashMap.Yêu cầu bộ nhớ nhiều hơn HashSet do duy trì bộ so sánh và TreeMap.
Khi nào sử dụng?Muốn danh sách không chứa phần tử trùng và không cần duy trì thứ tự.Muốn danh sách không chứa phần tử trùng và muốn duy trì thứ tự chèn.Muốn danh sách không chứa phần tử trùng và muốn sắp xếp các phần tử.

Tóm gọn:

  • HashSet: không duy trì thứ tự, tốc độ nhanh
  • LinkedHashSet: duy trì thứ tự theo thứ tự chèn, tốc độ giữa
  • TreeSet: duy trì thứ tự theo bộ so sánh, tốc độ chậm hơn

Map

  • Là cấu trúc dữ liệu theo kiểu key-value, trong đó key là duy nhất, và value có thể trùng nhau. Các class implement Map có thể là HashMap, LinkedHashMap, TreeMap, …

Sơ đồ kế thừa của các cấu trúc dữ liệu trong Java

image 69 - quochung.cyou PTIT
  • Theo hình trên, ta có 3 interface chủ đạo gồm List, Queue, Set sẽ kế thừa từ interface Collection. Từ interface Collection, chúng sẽ có các hàm chung như add, remove, contains, … và các hàm khác.
  • Lúc này, ở các class implement List, Queue, Set, chúng sẽ phải implement các hàm này, và tự triển khai logic cách để add, remove, contains, … như thế nào. Ngoài ra là triển khai các hàm từ interface Iterable, để có thể duyệt qua các phần tử của chúng, và các hàm từ riêng các interface List, Queue, Set, để có thể thực hiện các thao tác riêng của chúng.
  • Map thì là một trường phái khác, do chúng không kế thừa từ interface Collection, vì Map nhận vào 2 value. Do đó, để sử dụng được các hàm Collection trong Map, ta có thể lấy ra keySet, valueSet, entrySet của Map, và sử dụng các hàm Collection trên chúng.
image 70 - quochung.cyou PTIT

Về Comparable và Comparator, cách sử dụng trong các cấu trúc dữ liệu để sắp xếp

  • Trong Java, có 2 cách để sắp xếp các phần tử của một cấu trúc dữ liệu, đó là sử dụng Comparable và Comparator.
  • Comparable là một interface, nó có một hàm là compareTo(), và nó được implement bởi các class muốn sắp xếp các phần tử của chúng. Hàm compareTo() sẽ trả về một số nguyên, và nó sẽ được sử dụng để so sánh 2 phần tử của cấu trúc dữ liệu đó. Nếu trả về số âm, thì phần tử đầu tiên sẽ được đặt trước phần tử thứ hai, nếu trả về số dương, thì phần tử thứ hai sẽ được đặt trước phần tử đầu tiên, nếu trả về 0, thì 2 phần tử sẽ được đặt ngang hàng với nhau.

Code ví dụ:

image 71 - quochung.cyou PTIT
  • Như code trên, class Student implement interface Comparable, và phải triển khai cách để so sánh giữa 2 Student với nhau
  • Để sử dụng so sánh, ta có thể sử dụng hàm sort() của class Collections để sắp xếp các phần tử của một List:
image 72 - quochung.cyou PTIT
  • Code thực tế trong Collection, có thể Ctrl+Click vào để xem
image 73 - quochung.cyou PTIT
  • Như code trên, ta có thể thấy, hàm sort của Collections sẽ nhận vào một List, và List này phải chứa T là một class implement Comparable, và hàm sort sẽ sử dụng hàm compareTo của T để so sánh các phần tử của List với nhau.
  • Comparator là một interface, nó có một hàm là compare(), và nó được implement bởi các class muốn sắp xếp các phần tử của chúng. Hàm compare() sẽ trả về một số nguyên, và nó sẽ được sử dụng để so sánh 2 phần tử của cấu trúc dữ liệu đó. Nếu trả về số âm, thì phần tử đầu tiên sẽ được đặt trước phần tử thứ hai, nếu trả về số dương, thì phần tử thứ hai sẽ được đặt trước phần tử đầu tiên, nếu trả về 0, thì 2 phần tử sẽ được đặt ngang hàng với nhau.

Code ví dụ:

image 74 - quochung.cyou PTIT
image 75 - quochung.cyou PTIT
  • Như code trên, class StudentAgeComparator implement interface Comparator, và phải triển khai cách để so sánh giữa 2 Student với nhau
  • Để sử dụng so sánh, ta có thể sử dụng hàm sort() của class Collections để sắp xếp các phần tử của một List:
image 76 - quochung.cyou PTIT
  • Code thực tế trong Collection, có thể Ctrl+Click vào để xem
image 77 - quochung.cyou PTIT
  • Như code trên, ta có thể thấy, hàm sort của Collections sẽ nhận vào một List, và 1 Comparator, và hàm sort sẽ sử dụng hàm compare của Comparator để so sánh các phần tử của List với nhau.
  • So sánh Comparable và Comparator
ComparableComparator
Comparable là một interface, nó có một hàm là compareTo(), và nó được implement bởi các class muốn sắp xếp các phần tử của chúng.Comparator là một interface, nó có một hàm là compare(), và nó được implement bởi các class muốn sắp xếp các phần tử của chúng.
Comparable bắt buộc triển khai trong class mà ta muốn sort class đó, và chỉ chỉ khai được một cách duy nhất.Comparator không bắt buộc triển khai trong class mà ta muốn sort class đó, và có thể khai báo nhiều Comparator khác nhau, tuỳ vào mục đích sử dụng.
  • Ví dụ về việc sử dụng nhiều loại Comparator khác nhau:
image 78 - quochung.cyou PTIT
image 79 - quochung.cyou PTIT
  • Như code trên, ta có thể thấy, ta có thể sử dụng nhiều Comparator khác nhau, để sắp xếp các phần tử của List theo nhiều cách khác nhau.

[Java Core] B7: Exception trong Java

This entry is part 7 of 8 in the series Java Core

1. Làm quen với Exception: Checked và Unchecked Exception, Error

1.1 Lời mở đầu

  • Trong cuộc sống, không phải lúc nào mọi việc cũng diễn ra suôn sẻ, không phải lúc nào mọi thứ cũng theo đúng kế hoạch. Đôi khi, có những tình huống không mong muốn xảy ra, và chúng ta cần phải xử lý chúng.
image - quochung.cyou PTIT
  • Khi một ứng dụng ngày càng lớn lên, chắc chắn ta không thể đảm bảo ứng dụng luôn hoạt động như cách ta muốn 100% mọi lúc được. Đến lúc nào đó, sẽ có lỗi xảy ra.

Ví dụ:

  • Khi một ứng dụng cần đọc một file từ ổ cứng, nhưng file đó không tồn tại.
  • Khi ứng dụng cần quyền chụp ảnh từ camera, nhưng người dùng không cho phép.
  • Khi ứng dụng kết nối tới một server, nhưng server đang bị mất điện và không kết nối được.

Khi đó, hệ thống cần có những cách xử lý riêng cho người dùng biết vấn đề đang xảy ra, và có những cách xử lý riêng cho hệ thống có thể tiếp tục hoạt động mà không bị ảnh hưởng quá nhiều.

Ví dụ:

  • Khi file không tồn tại, ứng dụng cần thông báo cho người dùng biết rằng file không tồn tại và yêu cầu người dùng chọn file khác, hoặc kiểm tra lại đường dẫn file.
  • Khi người dùng không cho phép ứng dụng chụp ảnh, ứng dụng cần thông báo cho người dùng biết rằng không thể chụp ảnh và yêu cầu người dùng cấp quyền.
  • Khi server bị mất điện, ứng dụng cần thông báo cho người dùng biết rằng không thể kết nối tới server và yêu cầu người dùng thử lại sau, hoặc kiểm tra xem đường truyền wifi, 3G, 4G có vấn đề gì không.

1.2 Exception là gì?

  • Exception dịch ra tiếng Việt có nghĩa là ngoại lệ. Exception là một sự kiện xảy ra trong quá trình thực thi chương trình, làm ảnh hưởng đến luồng thực thi của chương trình.

1.3 Checked và Unchecked Exception

1.3.1 Checked Exception

image 1 - quochung.cyou PTIT
  • Ảnh ví dụ: Khi ta thử đọc 1 file, IDE ngay lập tức thông báo có thể xảy ra FileNotFoundException (Ngoại lệ file không tồn tại) và yêu cầu ta phải xử lý nó.
  • Checked Exception thường là những lỗi phổ biến mà chúng ta có thể dự đoán được sẽ xảy ra cho một thao tác nào đó.
  • Ví dụ, khi đi dã ngoại, để phòng ngừa việc có mưa, ta mang theo áo mưa. Việc trời mưa là một ngoại lệ mà ta đã dự đoán trước và lên trước kế hoạch để xử lý nó.

1.3.2 Unchecked Exception

  • Đây là những Exception không được kiểm tra tại thời điểm biên dịch (compile time). Tức là chúng ta không nhận được thông báo lỗi từ trình biên dịch khi mà nó phát hiện ra rằng chúng ta chưa xử lý nó.
  • Đây là các exception khá nguy hiểm, khi mà chúng ta không thể dự đoán được chúng sẽ xảy ra khi nào. Bởi vì Exception này xảy ra khi ứng dụng đang thực thi, nên nó còn có thể gọi là Runtime Exception.
  • Ví dụ, khi một ứng dụng chia một số cho 0, nó sẽ ném ra một ngoại lệ ArithmeticException (Ngoại lệ toán học).
image 2 - quochung.cyou PTIT
  • Trong đoạn code trên, nếu ta nhập vào 1 số không tồn tại trong mảng, nó sẽ bị một Exception ArrayIndexOutOfBoundsException (Ngoại lệ vượt quá giới hạn của mảng)., đây là một ví dụ về Unchecked Exception. Vì ta không thể biết được trước khi chạy chương trình, nó sẽ xảy ra khi nào.

1.4 Error

  • Error là một dạng ngoại lệ mà chúng ta không thể xử lý được. Error thường xảy ra khi mà hệ thống gặp phải những vấn đề nghiêm trọng, không thể khắc phục được.
  • Ví dụ, khi một ứng dụng chạy hết bộ nhớ, nó sẽ ném ra một ngoại lệ OutOfMemoryError (Lỗi hết bộ nhớ). Đây là một ví dụ về Error. Đây là một ngoại lệ ta không thể xử lý được, và nó thường xảy ra khi mà hệ thống gặp phải những vấn đề nghiêm trọng, không thể khắc phục được.

2. Bắt Exception với try-catch

Bài tập

  • Hãy viết chương trình nhập vào 1 String, và sử dụng Integer.parseInt để chuyển đổi String đó sang kiểu int. Thử nếu nhập vào 1 chuỗi không phải là số, xem chương trình sẽ báo lỗi như thế nào.
image 3 - quochung.cyou PTIT
  • Khi chạy chương trình trên, ta sẽ nhận được một ngoại lệ NumberFormatException (Ngoại lệ không đúng định dạng số). Đây là một ví dụ về Unchecked Exception. Vì lúc compile, ta không nhận lỗi nào báo trước cả, do IDE không biết trước được String truyền vào khi chạy chương trình
image 5 - quochung.cyou PTIT

2.1 Cách bắt Exception

  • Để bắt Exception, ta sử dụng cấu trúc try-catch. Cấu trúc try-catch sẽ giúp chúng ta bắt ngoại lệ, và xử lý nó một cách an toàn.
  • Cấu trúc try-catch có dạng như sau:
image 6 - quochung.cyou PTIT
  • Trong đó:
    • try: Là một khối lệnh, chứa các câu lệnh có thể ném ra ngoại lệ.
    • catch: Là một khối lệnh, chứa các câu lệnh xử lý ngoại lệ. Trong đó, Exception e là một biến, chứa thông tin về ngoại lệ xảy ra.

2.2 Ví dụ

  • Ví dụ, ta sẽ sử dụng cấu trúc try-catch để bắt ngoại lệ NumberFormatException khi chuyển đổi một chuỗi không phải là số sang kiểu int.
image 7 - quochung.cyou PTIT
  • Khi chạy chương trình trên, ta sẽ nhận được thông báo “Chuỗi không phải là số” khi chuyển đổi chuỗi “4.5a” sang kiểu float.

2.3 catch nhiều Exception

  • Ta cũng có thể bắt nhiều ngoại lệ trong cùng một cấu trúc try-catch, bằng cách sử dụng nhiều khối catch.

Bài tập

  • Hãy viết chương trình nhập vào số n, sau đó nhập vào n string. Sau đó nhập vào 1 số m, và in ra string thứ m. Nếu truy cập ngoài mảng, hãy in ra thông báo “Truy cập ngoài mảng”, còn nếu không in ra số được chuyển đổi từ string đó sang kiểu int, nếu không thể chuyển đổi được, hãy in ra thông báo “Chuỗi không phải là số”.\
image 8 - quochung.cyou PTIT
  • Như ví dụ trên, ta thấy ta có thể catch nhiều Exception trong cùng một cấu trúc try-catch.

2.4 Catch Exception cha

  • Ta cũng có thể bắt Exception cha của một Exception con. Ví dụ, ta có thể bắt Exception cha của NumberFormatException là Exception.

Bài tập

  • Tương tự bài tập trên, nhưng chỉ dùng 1 catch, và có bất kì lỗi gì xảy ra ta đều in ra “Lỗi xảy ra + tên lỗi”.
image 9 - quochung.cyou PTIT
  • Như ví dụ trên, ta thấy ta có thể bắt Exception cha của NumberFormatException là Exception.

3. Sử dụng finally

3.1 finally là gì?

  • finally là một khối lệnh, chứa các câu lệnh sẽ được thực thi sau khi khối lệnh try-catch kết thúc.
  • finally sẽ được thực thi sau khi khối lệnh try-catch kết thúc, bất kể có ngoại lệ xảy ra hay không.

3.2 Ví dụ

  • Ví dụ, ta sẽ sử dụng finally để đóng Scanner sau khi sử dụng xong.
image 10 - quochung.cyou PTIT
  • Trong đoạn code trên, ta sẽ đóng Scanner sau khi sử dụng xong, bất kể có ngoại lệ xảy ra hay không.

Tác dụng

  • finally thường được sử dụng để giải phóng tài nguyên, như đóng file, đóng kết nối, đóng Scanner, … Đây là một practice nên làm đó là giải phóng tài nguyên sau khi sử dụng xong, tránh lãng phí tài nguyên.

3.3 Một số câu hỏi

3.3.1 Tại sao không sử dụng Exception là xong mà cần catch Exception cụ thể, có nhiều class Exception con

  • Khi sử dụng Exception là xong, ta sẽ bắt được tất cả các Exception, nhưng đôi khi ta cần xử lý một cách cụ thể cho từng Exception. Vì khi có lỗi xảy ra, cứ chỉ báo là đã có lỗi xảy ra thì thứ nhất người dùng rất khó chịu, ta khi sửa lỗi cũng không biết lỗi xảy ra là gì, ở đâu. Thứ hai, ta cũng không thể xử lý lỗi một cách cụ thể.
  • Việc catch đúng Exception cụ thể sẽ giúp ta xử lý lỗi một cách cụ thể. Ví dụ: Nếu lỗi là chia cho 0, ta báo cho người dùng rằng họ nhập sai để họ biết sai ở đâu và sửa lại, còn nếu lỗi là không tìm thấy file, ta báo cho người dùng biết rằng file không tồn tại để họ biết cần phải tạo file mới. Hoặc nếu là các lỗi người dùng không hiểu được, ta cần báo các lỗi cụ thể, có định danh rõ ràng để lập trình viên có thể biết và sửa lỗi
  • Ví dụ, trong window đôi lúc ta bị màn hình xanh, ta có các mã lỗi kiểu như 0x0000000A0x0000000B, … những mã lỗi này không phục vụ cho người dùng hiểu, nhưng người dùng có thể dùng nó để báo cáo, và lập trình viên có thể dùng nó để sửa lỗi.

3.3.2 Có thể Try không có Catch không?

  • Có thể, nhưng ta cần có một khối finally để giải phóng tài nguyên.
  • Ví dụ, ta có thể sử dụng try-finally để giải phóng tài nguyên, mà không cần catch Exception.
image 11 - quochung.cyou PTIT

4. Cây phân cấp Exception

  • Trong Java, Exception được phân cấp theo một cấu trúc cây, với Throwable là gốc của cây, Error và Exception là 2 nhánh con của Throwable.
image 12 - quochung.cyou PTIT
  • Interface Iterable nhằm ám chỉ những class bên dưới kế thừa có tính “có thể ném ra được”
  • Lí do tại sao cần 1 cây Exception sẽ được giải thích bên dưới, mục câu hỏi tại sao không catch mọi exception

4.1 Throw, tung ra ngoại lệ

  • Đến hiện tại, ta đã biết các Exception có sẵn trong Java, và cách bắt nó bằng cấu trúc try-catch.
  • Ta nhận thấy, các Exception đều triển khai từ Throwable, và có thể tung ra (throw) một Exception bằng cách sử dụng từ khóa throw.

Ví dụ: Ta cần nhập vào một số là tuổi của người dùng, ta sử dụng nextInt của Scanner để nhập vào, nhưng nếu người dùng nhập vào một chuỗi không phải là số, ta sẽ tung ra một Exception InputMismatchException (Ngoại lệ nhập không đúng).

image 13 - quochung.cyou PTIT
image 15 - quochung.cyou PTIT
  • Tuy nhiên, người dùng vẫn có thể nhập vào 1 số âm, lúc này ta có thể thêm các điều kiện của riêng mình và throw ra Exception nếu muốn
image 16 - quochung.cyou PTIT

4.2 Throws

  • throws là một từ khóa, dùng để khai báo một phương thức có thể ném ra một ngoại lệ.
  • Ví dụ: Ta chia 1 hàm riêng để nhập vào thông tin nhân viên, nếu nhập sai ta sẽ trả ra 1 số Exception như InputMismatchExceptionNumberFormatException, …. Tuy nhiên ta muốn nơi gọi đến hàm này tự xử lý ngoại lệ, ta có thể sử dụng throws để khai báo phương thức có thể ném ra ngoại lệ.
image 17 - quochung.cyou PTIT
  • Như code trên, ở hàm inputInfo ta sử dụng throws để khai báo phương thức có thể ném ra ngoại lệ. Lúc này ở nơi gọi đến hàm inputInfo sẽ bị yêu cầu xử lý ngoại lệ InputMismatchException mà ta đã khai báo.
image 19 - quochung.cyou PTIT
  • Như ảnh trên, ta thấy ở bên phải là luồng gọi hàm, và bên trái là luồng Exception được quay ngược lên trên để tìm nơi xử lý nó.

4.3 Tạo ra Exception của riêng mình

  • Đôi khi, ta cần tạo ra Exception của riêng mình, để phục vụ cho mục đích của riêng mình.
  • Tên Throwable có nghĩa là “có thể ném ra”, nghĩa là các Exception bên dưới của chúng ta có thể sử dụng các từ khoá throws, throw để ném ra.
  • Để tạo ra Exception của riêng mình, ta cần tạo ra một class kế thừa từ Exception hoặc RuntimeException. Để tạo 1 Checked Exception, ta kế thừa từ Exception, còn để tạo 1 Unchecked Exception, ta kế thừa từ RuntimeException.
  • Ví dụ, ta cần tạo ra 1 Exception AgeException (Ngoại lệ tuổi), nếu tuổi nhập vào nhỏ hơn 0, ta sẽ ném ra Exception này.
image 20 - quochung.cyou PTIT
  • Ví dụ, ta sẽ sử dụng AgeException để ném ra Exception khi tuổi nhập vào nhỏ hơn 0.
image 21 - quochung.cyou PTIT
  • Như code trên, ta sử dụng AgeException để ném ra Exception khi tuổi nhập vào nhỏ hơn 0.

[Java Core] B8: File, sơ lược về test trong Java

This entry is part 8 of 8 in the series Java Core

1. Xử lý File trong Java

Đọc và ghi file trong java là các hoạt động nhập/xuất dữ liệu (nhập dữ liệu từ bàn phím, đọc dữ liệu từ file, ghi dữ liệu lên màn hình, ghi ra file, ghi ra đĩa, ghi ra máy in…) đều được gọi là luồng (stream).

1.1 Một số class thường sử dụng

  • File: Là thực thể dẫn tới file, hoặc thư mục, cho các hàm để làm việc với File
  • FileInputStream/FileOutputStream: Đọc ghi file binary
  • FileReader/FileWriter: Đọc ghi file văn bản
  • BufferedReader/BufferedWriter: Đọc ghi file có Buffer
image - quochung.cyou PTIT
  • Hiểu đơn giản hiện tại thì, tất cả dữ liệu sẽ đều là các Input/OutputStream, là cấu phần byte, để đọc ghi dữ liệu
  • Thì ở tầng đầu tiên, các class như FileInputStream, FileOutputStream sẽ chỉ có các phương thức write các bytes.
  • Các tầng trên thực chất là wrap lại các cách viết, đọc byte này thành các phương thức dạng readIntwriteInt từ các class DataInputStream, DataOutputStream, …

1.2 Về Character Stream và Byte Stream

  • ByteStream của Java có thể thực hiện các thao tác vào ra mỗi 8-bit bytes (2^8 - 256) - ASCII
  • Còn luồng Character Stream, hay sử dụng các FileReader FileWriter thì đọc ghi 16-bit unicode, hỗ trợ được các ngôn ngữ đặc thù hơn (có dấu, gần thì tiếng Việt, các ngôn ngữ khác trên thế giới) do 16-bit unicode cho phép biểu diễn nhiều từ ngữ hơn
  • Thực chất thì cả 2 luồng này đều là wrap trên FileInputStream/FileOutputStream (cấu phần đọc byte thấp nhất ta nói ở trên), tuy nhiên FileReader/FileWriter sẽ đọc 2 bytes một lần
image 1 - quochung.cyou PTIT

1.4 BufferReader/Writer

  • Với BufferReader thì thực tế ta sẽ cung cấp một bộ đệm ở giữa ở Ram, giúp truy cập nhanh hơn. Hiểu đơn giản kiểu: Ví dụ trên mạng có một bài báo, bạn mỗi khi muốn đọc thì cần lên mạng, tải trang đó rồi đọc. Nhưng nếu lưu nó về máy thì khi truy cập sẽ nhanh hơn
image 3 - quochung.cyou PTIT

Câu hỏi: Buffer tốt thế sao không lúc nào cũng dùng ?

  • Tuỳ vào bài toán, thông thường thì BufferReader tốt cho mọi trường hợp vì việc sử dụng các bộ nhớ truy cập nhanh (cache, ram) để lưu tạm thời một vùng nhỏ bộ nhớ (rồi từ từ đẩy vào thêm, rồi lại đẩy ra) để đọc ghi là một practice chuẩn, thường được sử dụng trong nhiều lĩnh vực (buffer trong , database cache, front end cache, server cache, quy hoạch động chuẩn bị trước data …)
  • tuy nhiên việc vùng nhỏ đó là bao nhiêu cho chuẩn, lúc nào đẩy ra đẩy vào thì là 1 câu chuyện khác. -> Invalidation Cache, các chiến lược để nạp page trong hệ điều hành, …

1.5 Đường dẫn tương đối và tuyệt đối

1.5.1 Đường dẫn (Path) là gì?

Hầu hết các hệ thống tập tin được sử dụng ngày hôm nay lưu trữ các tập tin trong một cây (hoặc cấu trúc phân cấp). Ở đầu cây là một (hoặc nhiều hơn) các nút gốc. Dưới nút gốc, có các tệp và thư mục (thư mục trong Microsoft Windows). Mỗi thư mục có thể chứa các tệp tin và các thư mục con, do đó có thể chứa các tệp và thư mục con, v.v … có khả năng lưu trữ một chiều sâu gần như vô hạn.

image 4 - quochung.cyou PTIT

1.5.2 Đường dẫn tương đối/tuyệt đối

Một đường dẫn tuyệt đối luôn chứa các phần tử gốc và danh sách thư mục đầy đủ cần thiết để định vị tệp tin. Ví dụ, D:/file.txt là một đường dẫn tuyệt đối. Tất cả thông tin cần thiết để định vị tệp tin được chứa trong chuỗi đường dẫn.

Một đường dẫn tương đối cần phải được kết hợp với một đường dẫn khác để truy cập một tập tin. Ví dụ là đây là đường dẫn tới file xuất phát từ file dự án

image 5 - quochung.cyou PTIT
  • Ví dụ như ảnh trên, ta có đường dẫn tuyệt đối được trích từ ổ D, rồi vào thư mục Eclipse, …
  • Đường dẫn tương đối thì chỉ là từ thư mục dự án equinox/…/file

1.5.3 Ý nghĩa

  • Đường dẫn tuyệt đối thì ví dụ khi mang file sang nơi khác để chạy thì có thể sẽ không chạy được, vì có thể do máy người khác dùng một cách sắp thư mục khác (window vs linux), hoặc code thì dùng ổ D:/ mà máy người dùng không có ổ này, …
  • Đường dẫn tương đối sẽ được ưa dùng hơn, tức là ta sẽ lưu file nằm gọn trong cùng thư mục dự án, và khi đóng gói thì các thao tác chỉ nằm trong khu vực của dự án

2. Unit Test

2.1. Giới thiệu

Unit Test là một loại kiểm thử phần mềm trong đó các đơn vị hay thành phần riêng lẻ của phần mềm được kiểm thử.

Một Unit là một thành phần PM nhỏ nhất mà ta có thể kiểm tra được như các hàm (Function), thủ tục (Procedure), lớp (Class), hoặc các phương thức (Method).

Ở bài này cơ bản thì, ta sẽ viết các test cho các hàm/lớp trong Java, bằng cách kiểm tra với các input đầu vào định sẵn thì output có ra đúng với ta viết test hay không

  • Ví dụ, ta có phương thức tính tổng 2 số, thì ta dự đoán là khi truyền vào 10,20 thì kết quả phải là 30. Input là 10,20 – Output là 30, và ta kiểm tra 30 với kết quả thực tế khi chạy hàm
  • Việc kiểm tra sẽ được xử lý qua các test case, ta sẽ viết các test trước. Khi muốn kiểm tra tính năng, ta không phải ngồi thử bằng tay mà có thể chỉ cần ấn chạy test tự động

2.2 Cài đặt các thư viện để test trong Java

2.3 Assertions, viết Unit Test

Assertions là một thư viện cung cấp một số api phương thức hỗ trợ việc kiểm thử

image 6 - quochung.cyou PTIT

Ví dụ ảnh trên, ta có 1 hàm tính tổng. Ta viết ra 1 test để thử các input đầu vào để kiểm tra xem expectedSum là kết quả đúng với actualSum là kết quả trả ra của code có đúng không

-> Tưởng tượng như các test input output của codeforce để kiểm tra tính đúng đắn của code

image 7 - quochung.cyou PTIT

Chúng ta cùng bắt đầu viết thử 1 Unit Test cho đoạn code sau đây bằng thư viện JUnit:

Ví dụ ta có lớp Circle với phương thức sau:

public class Circle {

    public static double calculateArea(double radius) {
        return Math.PI * radius * radius;
    }
}

Sau đó, ta tạo lớp CalculatorTest trong đường dẫn src/main/test. Trong đó ta sử dụng annotation @Test để định nghĩa cho 1 test.

public class CircleTest {

    @Test
    public void testCalculateArea() {
        //...
    }
}

2.4 Một số quy tắc viết Test

Đối với JUnit, ta có một số quy tắc đặt tên sao cho chuẩn như sau:

  • Ta phải có từ “Test” ở cuối tên class, ví dụ: LoginControllerTest, StudentControllerTest,… -> Nhận biết file nào là test (Maven sẽ tự động thêm các lớp này vào Test scope)
  • Đặt tên các test rõ ràng, ta có thể theo cấu trúc sau: Given[ExplainYourInput]When[WhatIsDone]Then[ExpectedResult] ví dụ:
@Test
public void GivenNullUsernameWhenCreateStudentThenShouldThrowException()

public void Test_01_Khi_Nhap_Vao_So_Am_Thi_Thong_Bao_Sai()

public void Test_02_Khi_Nhap_Vao_So_Duong_Thi_Thong_Bao_Dung()
  • Tức là khi cho gì đó (Given) vào thời điểm nào đó (Create) thì nên làm gì (Then)
  • Ví dụ: Test cho 1 hàm nhập đầu vào gì đó, nếu người dùng nhập số âm thì chương trình nên trả ra là hãy nhập lại. File test sẽ kiểm tra có thật là chương trình bắt nhập lại hay không -> Yêu cầu làm thử bài toán trên
  • Ngoài ra, các test cần có tính độc lập. Tức là các test có thể thực hiện theo thứ tự ngẫu nhiên, và một test không nên phụ thuộc vào một test khác.
  • Ví dụ không nên có chuyện cần chạy test 1 rồi test 6 chạy mới pass

2.5 Chạy thử hàng loạt test và Test Coverage

image 8 - quochung.cyou PTIT
  • Chuột phải vào project, ví dụ như ảnh trên ta có các lựa chọn Run All Test là chạy toàn bộ test trong thư mục test của dự án

2.5.1 Test Coverage là gì

  • Test coverage (độ bao phủ kiểm thử) là một phép đo để đánh giá mức độ mà mã nguồn của chương trình đã được kiểm tra thông qua việc chạy các ca kiểm thử. Nó đo lường tỷ lệ phần trăm của mã nguồn được thực thi bởi các ca kiểm thử.
  • Ví dụ, test của ta đã đi hết 90/100 dòng của code thực tế. Tức là đã có 90 dòng đã được chạy trong quá trình test, và kết quả có vẻ đang đúng với expected trong test, ta gọi đó là Line Coverage (bao phủ theo số lượng dòng)
  • Ngoài ra còn có bao phủ theo số lượng class được chạy, số lượng hàm được chạy

=> Test Coverage là một chỉ số thể hiện tương đối dự án có đang ở trạng thái được test kiểm tra kĩ hay không. (nếu mà test viết chuẩn) Ví dụ, với những dự án được test coverage rất cao, khoảng 80-90%, thì dù sau này ta có sửa code, refactor, clean code hay gì đi nữa, ta có thể đảm bảo là khi chạy lại các test ta đã viết, ta sẽ biết chỗ nào có thể đang bị hỏng, chưa đúng để sửa lại

  • Mục tiêu của việc đạt test coverage là tăng độ tin cậy của chương trình bằng cách đảm bảo rằng các phần quan trọng của mã nguồn đã được kiểm tra. Tuy nhiên, test coverage không đảm bảo rằng tất cả các lỗi đều được phát hiện, và nó cũng không thể đo lường chất lượng của các ca kiểm thử.
image 9 - quochung.cyou PTIT
  • Như ảnh trên, ở More Run/Debug ta có thể có lựa chọn run thử toàn bộ test để đo coverage hiện tại của dự án
image 10 - quochung.cyou PTIT
  • Ta có hình ảnh Inteliji thể hiện bao nhiêu class đã được chạy qua, bao nhiêu method, bao nhiêu line đã được cover. Tuỳ vào cách đánh giá của công ty, hệ số, ... thì ta sẽ gộp thành 1 coverage tổng thể
image 11 - quochung.cyou PTIT
  • Đây được gọi là kim tự tháp test (testing pyramid), ở bên dưới là các test sẽ nhỏ, độc lập hơn, chạy nhanh hơn. Thường các test này khó móc nối với nhau (kiểu khó thử được 1 luồng là ấn nút giao diện rồi kiểm tra), nên để test ở đây, ta mặc định là mọi thứ đã móc nối chuẩn, rồi viết test sao cho từng module, cấu phần đều chuẩn
  • Ở các level cao hơn, test sẽ được móc nối giữa nhiều cấu phần (database, giao diện, nhiều service ngoài, …) sẽ yêu cầu thời gian chạy lâu hơn