Tổng hợp kiến thức
Bài 1¶
Cài đặt môi trường¶
Java
hoạt động như vậy, nó chỉ nói 1 ngôn ngữ duy nhất thôi, tuy nhiên nó có một thằng anh bá đạo, tên ông ý nôm na là môi trường ảo hay tên chuẩn là Java virtual machine (JVM)
. Nhiệm vụ của JVM
là nó phụ đề (thuyết minh) cho từng loại OS khác nhau rằng thằng Java
đang làm gì, nói gì, làm gì.
Vì chúng ta là Developer nên sẽ cài gói JDK
(Java Development Kit
), nó chứa các công cụ giúp lập trình Java
. Ngoài ra trong quá trình cài, nó sẽ cài môi trường JRE
(Java Runtime Enviroment
, bao gồm cả thằng JVM
ở trên) luôn.
Hello World¶
Tạo Project trong Intellij chẳng hạn, xong rồi thì cùng nhìn vào cấu trúc của project thì sẽ thấy có 3 thư mục:
.idea
: Thằng này là thư mục doIntellij
tự tạo ra để chứa các file config của phần mềm này, bạn sẽ k cần quan tâm đến.src
: Đây là thư mục chính bạn sẽ làm việc, tất cảcode
bạn để trong này{project-name}.iml
: File này cũng doIntellj
tạo ra và quản lý module, bạn không cần quan tâm nó.
Một số thông tin khác¶
public static void main(String[] args)
: (Gọi tắt làpsvm
nhé) Cái thằng này sẽ là nơiJava
tìm tới đầu tiên, và đọc toàn bộ các đoạn code trong cái thằng tên làpsvm
này. Dù nó ở bất cứ đâu, nó sẽ được tìm tới.- 2 cái dấu
{``}
: Đánh dấu đoạn bắt đầu và kết thúc của cáipublic static void main(String[] args)
kia.
Vậy là thằng Java
sẽ đi lùng tìm, xem cái thằng psvm
xem nó ở đâu. Rồi đọc hết tất cả những thứ nằm trong cái 2 dấu {``}
của thằng này.
Biến, phạm vi, kiểu dữ liệu, toán tử trong Java¶
Biến & Kiểu dữ liệu¶
public class Calculation{
public static void main(String[] args){
// khai bao so nguyen
int a = 5;
int b = 10;
int x = 10 + 5;
System.out.println(x);
}
}
Thứ nhất là cái // khai bao so nguyen
, cái này gọi là Comment
, tức các bạn viết gì sau 2 cái dấu //
thì nó sẽ không ảnh hưởng tới code
của chương trình, nó chỉ mang ý nghĩa chú thích thôi.
Thứ hai là cái này:
int a = 5;
Nói về Biến
(Variable
) các bạn có liên tưởng tới liên tưởng tới biến x
trong đồ thị hàm số ax + b = 0
không. Thì chính là nó đấy.
Biến sẽ giúp chúng ta lưu trữ và quản lý các giá trị trong chương trình.
Trong Java
, Biến
cũng là đại diện cho một đối tượng và đối tượng này phải được xác định là thuộc Kiểu dữ liệu
nào. Có các kiểu dữ liệu nguyên thuỷ
(primitive
) như sau:
boolean
: là kiểu logic, chỉ có 2 giá trịtrue
hoặcfalse
char
: kiểu ký tự, chỉ chứa đc được một ký tự, được định nghĩa trong dấu ngoặc đơn'
int
: số nguyên (1,2,3, ..
)long
: số nguyên, lớn hơnint
. (sẽ giải thích ở dưới)float
: số thực (1.5, 2.5, ..
).double
: số thực, lớn hơnfloat
.
Ngoài ra còn 2 kiểu dữ liệu nhỏ hơn int
là byte
và short
.
Kiểu dữ liệu cao cấp hơn gọi là Object
mà đặc trưng nhất là String
.
String
: Một chuỗi các ký tự, được định nghĩa trong dấu ngoặc kép""
. vdString a = "Hellooo world~~~"
Cách khai báo¶
Để khai báo biến, bắt buộc trước đó bạn phải chỉ cho nó kiểu dữ liệu
mà nó sẽ nhận, ngoài ra có thể có giá trị hoặc không.
- Cách 1:
[kiểu_dữ_liệu][tên_biến];
- Cách 2:
[kiểu_dữ_liệu][tên_biến] = [giá_trị];
int a, b, c; // Khai báo 3 biến có kiểu dữ liệu int
float b = 4.5f, c = 4f; // Khai báo 2 biến có kiểu dữ liệu float với giá trị ban đầu. ở đây biến `c` sẽ được hiểu là c = 4.0
double c = 4444.3;
char t = 'c';
String e = "Hello";
Cách đặt tên¶
Tên biến phải tuân theo quy tắc lạc đà (Camel Case)
: đó là chữ cái đầu tiên của từ đầu tiên phải viết thường và chữ cái đầu tiên của các từ tiếp theo phải viết hoa, ví dụ: listStudent
, minScore
.
Phạm vi sử dụng¶
Một khi bạn đã khai báo biến, thì bạn có thể sử dụng nó trong những Phạm vi
mà nó khả dụng. ?? 😀?? Cùng nhìn ví dụ ở dưới nhé.
Ví dụ:
public static void main(String[] args){
// khai bao so nguyen `a`
int a;
// Gán giá trị cho a, bạn sử dụng toán tử `=`
// Sử dụng biến a bình thường
a = 124214;
// lấy a và cộng thêm 1,, rồi gán ngược lại giá trị đó vào a :D
// Sử dụng biến a bình thường
a = a + 1;
}
// Gán lại giá trị cho a = 100 - 10;
// Chương trình lỗi
a = 100 - 10;
Phạm vi
(Scope
) là đây các bạn ạ, chính là 2 cái dấu {}
, khi bạn khai báo một biến a
trong 2 cái dấu {``}
thì bạn chỉ có thể sử dụng ở trong nó thôi, ra ngoài nó sẽ không hiểu a
là thằng nào và từ đâu chui ra.
Biến không thể sử dụng ngoài, nhưng nó có thể được sử dụng ở bên trong những scope mà nó chứa hoặc cùng cấp với nó.
public class Calculation{
// Khai báo a ở ngoài main, cái `public static` là cần thiết nhé, còn chi tiết thì chúng ta sẽ học ở các bài sau.
public static int a = 5;
public static void main(String[] args){
// thay đổi a, ở trong, vẫn okie.
a = 10;
// Biến a có thể sử dụng trong các `scope` con của nó
// Làm gì biến a ở đây cũng được, biến đổi nó.
// gán giá trị biến a vào b;
int b = a;
System.out.println(b);
}
}
Toán tử¶
Khi đã xác định các Biến
trong chương trình, bạn có thể sử dụng toán tử
để thay đổi các giá trị. Các toán tử
thì khá đơn giản, giống môn toán bình thường thôi. Với các kiểu nguyên thuỷ (primitive)
ta có:
public class Calculation{
public static void main(String[] args){
int a;
int b = 5;
int c = a + b; // c = 0 + 5 cộng
int d = a - b; // d = 0 - 5 trừ
int f = a * 5; // f = 0 x 5 nhân
int g = a / 5; // g = 0 : 5; chia
}
}
Còn với String
thì bạn có thể sử dụng +
để ghép 2 chuỗi mà thôi. Còn các toàn tử còn lại không được sử dụng với String
public class Calculation{
public static void main(String[] args){
String a = "Hello"
String b = "World"
// Mình đã nối 3 xâu là "Hello" + " " (Khoảng trắng) + "World" lại với nhau
System.out.println(a + " " + b);
String c = a + 5; // String cộng với một số nguyên?
System.out.println(c); // Kết quả sẽ là: "Hello 5" :V
// Bạn sẽ hiểu là khi cộng String với một số, số đó sẽ bị chuyển thành String và nối vào sau.
}
}
Ép kiểu dữ liệu¶
Nhìn vào ví dụ sau, bạn sẽ rõ.
public class Calculation{
public static void main(String[] args){
int a = 2;
float b = 3.5f; // dùng chữ f để nó hiểu đây là 3,5 float chứ k phải 3,5 double
float c = a + b; // c = 5.5
int d = a + b; // báo lỗi. Vì sao?
// vì java đang hiểu 2 + 3.5 nó sẽ ép thành 5.5 là float. Bây giờ gán nó vào số nguyên thì sẽ như này int = float?
// Để gán được bạn cần sử dụng ép kiểu
int d = (int) a + b; // d = 5
// a + b = 5.5 => ép thành (int) => 5 (lấy phần nguyên thôi)
char character = '5';
int number = (int) character; // number = 53. Why?
// Vì ép `char` thành `int` thì nó sẽ không chuyển chữ thành số, mà nó sẽ kiếm tra '5' là ký tự ASCII thứ bao nhiêu trong máy tính, và trả lại số thứ tự đó.
float = (float) 5; // => 5.0
}
}
Bản chất của biến (Nói thêm)¶
Khi các bạn khai báo một biến int
trong chương trình của mình và sử dụng lung tung khắp mọi nơi, thì bạn có biết cái biến int
ý ở đâu lòi ra không :))
Về bản chất, Biến
sẽ là một vùng nhớ trong thiết bị vật lý mà dễ nhất là để trong ram
. và khi bạn cho nó một giá trị, nó sẽ lưu trữ số đó vào ram
, và cần thì lấy lên.
Vậy để ram
biết bạn muốn lưu cái gì thì bạn phải khai báo cho nó. Ví dụ bạn bảo tôi cần một số nguyên int
. Thì máy tính hiểu là mình cần lưu trữ một số nguyên bình thường, không quá lớn, nó sẽ cho bạn 4 byte
trong Ram
thích lưu gì thì lưu. nhưng không được vượt quá 4 byte
.
4 byte = 32 bit, bỏ đi 1 bit đầu tiên để đánh dấu là số âm hay dương, thì còn 31 bit => số lớn nhất mà biến int lưu trữ được là 2^31 - 1 = 2147483647
Từ đây, bạn sẽ hiểu vì sao có số long
, vì nhu cầu lưu số lớn hơn thì long
được cấp tận 8 byte
.
Còn trường hợp đặc biệt như String
thì tuỳ giá trị của nó có bao nhiêu ký tự, mà Ram
sẽ cấp tương ứng bấy nhiêu byte
Bài 3: Hàm và câu lệnh điều kiện¶
#1 Câu lệnh rẽ nhánh¶
if¶
Các bạn nhìn qua ví dụ này:
public static void main(String[] args){
// khai bao so nguyen
int a = 9;
// Kiểm tra xem a có bằng 9 không
if (a == 9) {
// nếu bằng 9, in ra màn hình "Hello"
System.out.println("Hello");
}
// Kết quả trên màn hình:
// Hello
}
Thì các cần biết như sau, câu lệnh if
là một câu lệnh điều kiện, và nhận vào là một điều kiện true
hoặc false
. Có cú pháp như sau:
if ([điều kiện]){
// Thực hiện đoạn code nếu [điều kiện] là `true`. Nếu `false` bỏ qa đi xuống dưới.
}
// Tiếp tục thực hiện đoạn code phía dưới
Vậy đấy, nên để so sánh bạn cần dùng toán tử quan hệ
mình liệt kê dưới đây:
==
: Kiểm tra 2 toán hạng cóbằng nhau
không? (if(a==b)
)!=
: Kiểm tra 2 toán hạng cókhác nhau
không? (if(a!=b)
)>
: Kiểm tra toàn hạng A cólớn hơn
B không? (if(a>b)
)<
: Kiểm tra toàn hạng A cónhỏ hơn
B không? (if(a<b)
)>=
: Kiểm tra toàn hạng A cólớn hơn hoặc bằng
B không? (if(a>=b)
)<=
: Kiểm tra toàn hạng A cónhỏ hơn hoặc bằng
B không? (if(a<=b)
)
Tất cả toán tử quan hệ
ở trên, khi thực hiện xong nó sẽ trả về là kiểu boolean
, nên bạn có thể gán nó vào một biến bất kỳ, như lày:
int a = 5;
int b = 6;
boolean result = a == b; // false
System.out.println("Result: " + result);
// Kết quả in ra trên màn hình:
// "Result: false"
if(result){ // viết tắt của if(result == true)
System.out.println("Result is true");
}
Đến đây, có thể nói câu lệnh if
thực chất nhận vào một giá trị boolean
.
else¶
Tiếp theo, chúng ta tới với dạng đầy đủ của if
chính là cấu trúc if else
.
if ([điều kiện]){
// Thực hiện đoạn code nếu [điều kiện] là `true`.
} else {
// Thực hiện đoạn code trong này nếu [điều kiện] là `false`
}
//Các đoạn code ở dưới thực hiện bình thường sau khi if hoặc else diễn ra
Ví dụ:
int a = 5;
if ( (a + 2) == 7 ){
System.out.println("Bằng 7");
// Sử dụng biến `a` ở ngay trong scope {} của `if`,, như bài #2 mình có nói, biến được sử dụng trong các scope con hoặc bằng cấp
System.out.println("Giá trị lúc này của a = " + a);
}else{
System.out.println("Khác 7");
System.out.println("Giá trị lúc này của a = " + a);
int b = 7; // Tạo ra 1 biến b trong else
}
b = 50; // Lỗi, không biết b là gì, vì b ở scope nhỏ hơn, bên ngoài không hiểu.
Toán tử logic¶
Toán tử logic là những toán tử giúp chúng ta kết hợp nhiều [điều kiện] lại với nhau.
Ví dụ mình nói: "Nếu ab = 3 VÀ ac = 4 VÀ bc = 5 thì abc là tam giác vuông"
Thì trong code cần viết chương trình như thế nào?
Cách 1: Sử dụng if
thông thường.
int ab = 3;
int ac = 4;
int bc = 5;
if(ab == 3){
if(ac == 4){
if(bc == 5){
System.out.println("abc là tam giác cực vuông");
}
}
}
Cách 2: Sử dụng if
và toán tử logic
int ab = 3;
int ac = 4;
int bc = 5;
// Nếu ab = 3 VÀ ac = 4 VÀ bc = 5
if(ab == 3 && ac == 4 && bc==5){
// thì abc là tam giác vuông
System.out.println("abc là tam giác cực vuông");
}
Các bạn nhìn ví dụ cũng đoán ra &&
chính là toán tử logic
đại diện cho khái niệm AND
. Chúng ta có tất cả các loại toán tử logic
như sau:
&&
: AND||
: OR!
: NOT
Mục tiêu của các toán tử logic
là tác động lên các biểu thức boolean
để cho ra mộ biến boolean
mới.
Phép AND (&&)¶
Phép &&
hoạt động theo nguyên tắc, chỉ cần có 1 cái sai, thì tất cả đều sai
hay Tất cả đều phải đúng, mới là đúng
Nếu "A đúng và B đúng và C sai thì kết quả vẫn là sai"
// Bạn chạy thử xem nó đi vào phần nào nhé
if(true && true && true && false){
System.out.println("true");
}else{
System.out.println("false");
}
Phép OR (||)¶
Phép ||
thì rất dễ dãi, Chỉ 1 cái đúng là đủ
// Bạn chạy thử xem nó đi vào phần nào nhé
if(false || false || true || false){
System.out.println("true");
}else{
System.out.println("false");
}
Phép NOT (!)¶
Phép !
làm phủ định giá trị của biểu thức, nếu nó đang true
thì biến nó thành false
và ngược lại.
int a = 7;
if(!(a == 7)){ // (a==7) => true gặp thằng ! lại bị chuyển thành false. => vào vế else
System.out.println("Đáng nhẽ ra nên vào đây");
}else{
System.out.println("But nope, nó lại vào đây");
}
Hàm (Function)¶
public class Calculation {
public static void main(String[] args){
f(5,6);
f(2,3);
f(1,10);
}
public static void f(int x, int y){
int a = x + y;
System.out.println("In a ra màn hình: " + a);
}
}
// Kết quả khi chạy:
// In a ra màn hình: 11
// In a ra màn hình: 5
// In a ra màn hình: 11
Cách khai báo¶
Cách khai báo một phương thức như sau:
[kiểu_truy_cập] [kiểu_trả_về] [tên_phương_thức] ([danh_sách_tham_số]){}
ví dụ:
public static void f(int x, int y){
// Code của bạn
}
public static void main(String[] args){
}
Và khai báo ở ngoài hàm main()
. Tới đây, bạn hiểu main()
cũng là một hàm (function)
. Tuy nhiên nó đặc biệt vì cú pháp của nó là cố định và được Java
tìm tới để đọc đầu tiên.
1 - [kiểu_truy_cập]
:
Trong ví dụ trên [kiểu_truy_cập]
chính là vế public static
. Nó định nghĩa phạm vi hàm
được sử dụng. chúng ta sẽ tìm hiểu ở các bài sau nhé các bạn, bây giờ bạn hãy mặc định sử dụng public static
ở trước mỗi hàm khai báo để có thể sử dụng được nhé. Ở bài này, chúng ta tạm hiểu với nhau: public static
là "truy cập ở bất cứ đâu"
tức có thể gọi hàm này ở bất kì chỗ nào.
2 - [kiểu_trả_về]
:
Tương đương với phần void
ở ví dụ trên, kiểu trả về là giá trị chúng ta nhận được sau khi gọi hàm.
Bạn hãy nhớ lại, khi truyền x
vào f(x)
chúng ta sẽ nhận lại là y
. Thì hàm cũng vậy, chúng ta có thể trả lại một giá trị gì đó. ví dụ:
// [kiểu trả về]: int
public static int tong(int x, int y){
int t = x + y; // Tính tổng 2 só x, y
return t; // trả số đó ra sử dụng câu lệnh `return {biến}`
}
public static void main(String[] args){
int t = tong(5,6); // Lấy giá trị trả ra, gán nó vào t;
}
Tôi định nghĩa một hàm tính tổng tong(x,y)
nhận vào 2 số nguyên, và yêu cầu nó trả ra một số int
.
Các kiểu trả về:
primitive
:int
,boolean
,char
, ...Object
:String
, (còn rất nhiều, sẽ học ở bài tiếp theo)void
: Không trả về gì cả
Ở ví dụ đầu tiên mình đã sử dụng void
để định nghĩa hàm.
public static void f(int x, int y){
int a = x + y;
System.out.println("In a ra màn hình: " + a);
}
Điều này nói là hàm của chúng ta thực hiện một hoạt động khép kín, và không có nhu cầu trả ra ngoài cái gì cả. Mình chỉ tính tổng rồi in luôn ra màn hình thôi, không cần đưa gì ra ngoài cả.
3 - [danh_sách_tham_số]
Tham số đầu vào, là những thứ chúng ta đưa vào hàm, định nghĩa tham số đầu vào bao gồm [kiểu_dữ_liệu] [tên_biến]
. Chúng ta có truyền nhiều tham số vào hàm
bằng cách đặt dầu phẩy ,
giữa mỗi tham số.
public static int f(int x, int y, int z, ... ){
// code
}
Ở đây lưu ý phần [tên_biến]
bạn có thể đặt tên bất kỳ. chẳng hạn:
// Hàm nhận vào 2 biến `x`, `y` và trả ra kết quả `boolean` xem nó có bằng nhau hay không
public static boolean bangnhau(int x, int y){
return x == y;
}
public static void main(String[] args){
int a = 5; // tên biến là `a`
int b = 6; // tên biến là `b`
boolean ketqua = bangnhau(a,b); // đưa `a` , `b` vào hàm.
// bản chất khi gọi hàm `bangnhau`:
// int x = a;
// int y = b;
// return x == y;
//
System.out.println("Kết quả: " + ketqua);
}
Bạn định nghĩa tham số đầu vào
là x
và y
thì nó chỉ hiểu trong ở hàm đó thôi, và những giá trị truyền vào sẽ gán vào các biến x
và y
.
Nhập xuất dữ liệu trong Java¶
Nhập xuất từ bàn phím¶
public class Calculation {
public static void main(String[] args) {
// Chúng ta khai báo 3 biến a,b,c không có giá trị.
int a, b, c;
//Khai báo đối tượng Scanner, giúp chúng ta nhận thông tin từ keyboard
Scanner sc = new Scanner(System.in);
System.out.print("Nhập a: "); //print thay vì println, nó sẽ in ra, nhưng không xuống dòng
a = sc.nextInt(); // sc.nextInt() là cách để lấy giá trị từ bàn phím, nó sẽ chờ tới khi chúng ta nhập một số.
System.out.print("Nhập b: ");
b = sc.nextInt();
System.out.print("Nhập c: ");
c = sc.nextInt();
// In các giá trị ra màn hình
System.out.println("a = " + a + ", b = " + b + ", c = " + c);
// Đây là phép cộng String mình đã nói trong Bài #1.
}
Cái dòng lệnh này a = sc.nextInt()
. Nó sẽ chờ cho tới khi bạn nhập 1 số nguyên và gõ Enter
thì thôi. Giả sử mình nhập 5
Chương trình lại tiếp tục chạy cho tới khi gặp câu lệnh sc.nextInt()
tiếp theo. Và cứ tiếp tục như vậy cho tới dòng lệnh cuối cùng.
Từ đây, các bạn có thể hiểu là đối tượng Scanner
đã làm nhiệm vụ là nhận dữ liệu người dùng nhập từ bàn phím, và gán nó vào biến, bằng câu lệnh nextInt
.
Bây giờ quay trở ngược lên trên 1 chút, ở câu lệnh:
Scanner sc = new Scanner(System.in);
các bạn sẽ thấy một khái niệm là new
. cái này thì [Bài #5][link-bai5] mình sẽ nói chi tiết, còn ở đây thì bạn hiểu nó được sử dụng để tạo ra 1 đối tượng Scanner
.
Các phương thức nhập xuất¶
Scanner
có một loạt các hàm hỗ trợ như sau:
next()
: Nhận vào mộtString token
(nhận vào 1 từ đầu tiên thay cả câu)nextInt()
: Nhận vào một sốint
nextLong()
: Nhận vào một sốlong
nextFloat()
: Nhận vào một sốfloat
nextDouble()
: Nhận vào một sốdouble
sc.nextLine()
: Nhận vào mộtchuỗi String
(Cả 1 câu)nextByte()
: Nhận vào mộtbyte
nextBoolean()
: Nhận vào mộtboolean
Các hàm trên bạn hiểu nguyên lý là nó đều sẽ chờ
cho tới khi bạn nhập kiểu dữ liệu nó muốn vào.
Có next()
và nextLine()
khá đặc biệt, mình sẽ ví dụ:
Scanner sc = new Scanner(System.in); //Tạo đối tượng Scanner
System.out.print("Nhập gì đó: ");
String a = sc.nextLine(); // nhận vào 1 string
System.out.println("Bạn vừa nhập: "+a);
System.out.print("Nhập thêm gì đi: ");
String b = sc.next(); // cũng nhận vào 1 String
System.out.println("Bạn vừa nhập: "+b);
nextLine
thì nhận vào cả 1 chuỗi dài String
, cho tới khi bạn nhấn Enter
. Còn next
dù bạn có nhập dài như nào, nó cũng nhận 1 từ đầu tiên thôi.
Bản chất củanext
¶
Bạn để ý là các hàm lấy giá trị từ bàn phím đều có chữ next
. Bây giờ bạn chạy cho mình ví dụ này, bạn sẽ hiểu:
public static void main(String[] args) {
int a,b,c;
Scanner sc = new Scanner(System.in); // Tạo đối tượng Scanner
System.out.print("Nhập a: ");
a = sc.nextInt();
b = sc.nextInt();
c = sc.nextInt();
System.out.println("a = "+a);
System.out.println("b = "+b);
System.out.println("c = "+c);
}
Bạn sẽ thấy là, nó đưa tuần tự các giá trị hiện có trên bàn phím vào các biến. bản chất của chữ next
chính là tuần tự. Nó sẽ chờ bạn nhập nếu không có giá trị gì trên màn hình, nhưng nếu đã có sẵn giá trị rồi, nó sẽ ghi nhớ trong bộ đệm
và khi gặp hàm nextInt()
nó không chờ nữa, mà nó lấy luôn cái giá trị còn thừa ra, chưa sử dụng đến để gắn luôn vào biến 😂
Nhìn như như này cho dễ hiểu:
public static void main(String[] args) {
int a,b,c;
Scanner sc = new Scanner(System.in); // Tạo đối tượng Scanner
System.out.print("Nhập a: ");
a = sc.nextInt(); // Chờ bạn nhập.
// bạn nhập: 5 6 7 8 9 10
// bộ đệm = 5 6 7 8 9 10
// lấy 5 ra, gắn vào a
// bộ đệm còn: 6 7 8 9 10
b = sc.nextInt(); // gặp lệnh nextInt()
// thấy bộ đệm còn, lấy 6 ra, gắn vào b
// bộ đệm còn: 7 8 9 10
c = sc.nextInt(); // gặp lệnh nextInt()
// thấy bộ đệm còn thừa, lấy 7 ra, gắn vào b
// bộ đệm còn: 8 9 10
System.out.println("a = "+a); // in a
System.out.println("b = "+b); // in b
System.out.println("c = "+c); // in c
}
Inpụt/ outpụt từ File¶
Để cho thuận tiện trong việc đọc ghi, thì ngoài bàn phím, một trong những yêu cầu quan trọng khi lập trình đó là nhập xuất dữ liệu từ File, phần này sẽ không khác nhiều với từ bàn phím đâu các bạn, mình sẽ hướng dẫn.
Tại thư mục gốc của project, bạn click New
> File
. Tạo 1 tệp tên là input.txt
. Như hình:
public static void main(String[] args) throws FileNotFoundException { // Thêm cái này vào đây
int a,b,c;
Scanner sc = new Scanner(new File("input.txt")); // Tạo đối tượng Scanner đọc tới cái file vừa tạo
System.out.print("Nhập a: ");
a = sc.nextInt();
b = sc.nextInt();
c = sc.nextInt();
System.out.println("a = "+a); // in a
System.out.println("b = "+b); // in b
System.out.println("c = "+c); // in c
}
// Kết quả chạy:
// Nhập a: a = 5
// b = 7
// c = 8
Đoạn throws FileNotFoundException
. Ở đây thì bạn hiểu nó là lỗi có thể xảy ra, nếu nó không tìm thấy file input.txt
thì nó sẽ xảy ra cái lỗi kia. Chúng ta sẽ xử lý lỗi đó sau, hiện tại thì nếu bạn nhập đúng tên File thì không thể lỗi được.# Vì sao nên sử dụng StringBuffer
Cùng xem ví dụ này nhé:
long start = System.nanoTime();
String s = "Hello";
for (int i = 0; i < 1000; i++) {
s += " world";
}
long end = System.nanoTime();
System.out.println("Total time: "+(end-start));
// Kết quả:
// Total time: 17495917 ns
// = 17.4 ms (Milliseconds)
Bây giờ, vẫn là chương trình tương tự, mình sử sụng String Buffer
long start = System.nanoTime();
StringBuffer sb = new StringBuffer("Hello");
for (int i = 0; i < 1000; i++) {
sb.append(" world");
}
String s = sb.toString();
long end = System.nanoTime();
System.out.println("Total time: "+(end-start));
// Kết quả:
// Total time: 461198 ns
// = 0.46 ms
String Buffer
nhanh hơn gấp 38 lần.
Hiệu năng được chạy trên Mac Pro 2017, tại máy bạn có thể sẽ khác, nhưng chắc chắn rằng StringBuffer
luôn nhanh hơn!
Góc giải thích¶
Có một điều ít bạn học lập trình Java
để ý, đó là String
là immutable
. Tức nội dung trong String
là không được quyền thay đổi.
Nhiều bạn lầm tưởng rằng việc nối xâu là bạn thay đổi nội dung của String
, nhưng thực chất bạn đang tạo ra một đối tượng hoàn toàn mới:
String s = "A";
s += "B";
// Complier sẽ tạo ra một đối tượng mới là "AB"
// Và gán vào `s`
// Bản chất `s` bây giờ là một đối tượng mới chứ bạn không hề thay đổi nội dung ban đầu của `s`.
// Đây là những gì ở dưới Compiler sẽ làm:
StringBuffer sb = new StringBuffer("A"); // Compiler Vẫn phải xài tới StringBuffer
sb.append("B");
s = sb.toString();
Vì vậy khi nối xâu trong Java
, việc bạn thực hiện nó liên tục, sẽ tương đương với việc khởi tạo liên tục và nối 2 xâu lại rồi trả về đối tượng String
mới dẫn tới chi phí lớn.
StringBuffer
cho phép chúng ta thao tác trên một đối tượng duy nhất và thay đổi được nội dung trong nó. Nếu ban đầu nội dung là "A"
, bạn muốn nối thêm "B"
vào. Thì nó chỉ cần gắn chuỗi bytes
của "B"
vào liền kề ngay sau "A"
là xong. (Vì nó có thể thay đổi, khác với String
là immutable
).
Hướng dẫn Java Reflection¶
Giới thiệu¶
Java Relection
là một core package trong thư viện chuẩn của Java
. Mục đích của nó là cho phép chúng ta truy cập vào gần như mọi thứ bên trong đối tượng. "Dưới một góc độ khác"!
Chúng ta thường biết tới Java
thông qua khái niệm hướng đối tượng như sau:
String str = "Hello Loda";
str.toUpperCase(); // Chúng ta gọi hàm toUpperCase() thông qua toán tử "."
// Mọi thứ trong đối tượng là khép kín, chúng ta phải gọi thông qua hàm public
Hoặc
public class Girl {
String name;
int age;
int atk;
int agi;
int def;
// ... Và 1000 thuộc tính khác
public static void main(String[] args) {
Girl girl = new Girl();
// Chúng ta thường phải nhớ tên thuộc tính để gọi nó ra
girl.name = "Ngoc Trinh";
// Giá sử class này có 100 thuộc tính là String.
// Bạn muốn set giá trị của tất cả trường String là "Ngoc Trinh"
// Bạn sẽ rất bối rối vs việc gọi từng thuộc tính bằng việc ".{tên thuộc tính}" như này.
// Có cách nào cho code duyệt tìm toàn bộ thuộc tính, cái nào là String thì đổi nó thành "Ngoc Trinh"?
}
}
Đúng vậy, khi chúng ta muốn gọi tên thuộc tính, mà lại không muốn gõ .
và nhớ ra tên thuộc tính, thì làm như nào?
Bây giờ, chúng ta phải tiếp cận từ góc nhìn khác. Chúng ta sẽ ước mình có thể duyệt hết tất cả các thuộc tính của 1 class bằng vòng lặp. Rồi check xem thuộc tính có là String
không? nếu có thì gán giá trị mới là "Ngoc Trinh"!
Để làm được điều này, chúng ta cần đào sâu vào Class
và phá vỡ giới hạn của java truyền thống. Đây là lúc Java Reflection
(Sự phản chiếu) vào trận.
Java Reflection¶
Java Reflecion
cho phép bạn đánh giá, sửa đổi cấu trúc và hành vi của một đối tượng tại thời gian chạy (runtime) của chương trình. Đồng thời nó cho phép bạn truy cập vào các thành viên private (private member) tại mọi nơi trong ứng dụng, điều này không được phép với cách tiếp cận truyền thống.
Lấy ra Thuộc tính (Field)¶
Quay trở lại ví dụ trên, Chúng ta sẽ lấy ra toàn bộ thuộc tính của Girl
. Tìm xem cái nào tên name
và bổ sung giá trị mới cho nó.
public class Girl {
private String name;
public Girl() {
}
public Girl(String name) {
this.name = name;
}
public void setName(String name){
this.name = name;
}
@Override
public String toString() {
return "Girl{" +
"name='" + name + '\'' +
'}';
}
public static void main(String[] args) throws Exception {
Girl girl = new Girl(); // KHởi tạo đối tượng Girl
girl.setName("Ngoc trinh");
// Lay ra tat ca field cua object
// Chỉ để bạn xem ví dụ thôi, bỏ qua phần này nhé!
for(Field field : girl.getClass().getDeclaredFields()){
System.out.println();
System.out.println("Field: " +field.getName());
System.out.println("Type: " +field.getType());
}
// PHẦN CHÍNH
Field nameField = girl.getClass().getDeclaredField("name"); // Lấy ra field có tên "name" (nếu không tìm thấy, nó sẽ bắn NoSuchFieldException)
nameField.setAccessible(true); // Cho phép truy cập tạm thời. (Vì nó đang là Private mà)
// Bây giờ cái "nameField" đại diện cho thuộc tính "name" của mọi object có class Girl.
nameField.set(girl, "Bella"); // thay giá trị mới của `girl` bằng nameField.
System.out.println(girl);
}
}
// Output:
// Field: name
// Type: class java.lang.String
// Girl{name='Bella'}
Lấy ra Hàm (Method)¶
Vấn đề đặt ra, giống với field
. Chúng ta cũng sẽ có nhu cầu duyệt tìm một method
nào đó và sử dụng nó:
public static void main(String[] args) throws Exception {
Class<Girl> girlClass = Girl.class;
// Su dung getDeclaredMethods de lay ra nhung method cua class va cha no.
Method[] methods = girlClass.getDeclaredMethods();
for(Method method : methods){
System.out.println();
System.out.println("Method: " + method.getName());
System.out.println("Parameters: " + Arrays.toString(method.getParameters()));
}
// Lay ra method ten la setName va co 1 tham so truyen vao ->
// => chính là: setName(String name)
Method methodSetName = girlClass.getMethod("setName", String.class);
// Bây giờ methodSetName sẽ đại diện cho method setName(String name) của mọi object có class là Girl
Girl girl = new Girl(); // Tạo ra đối tượng Girl
// Thực hiện hàm setName() trên đối tượng girl, giá trị truyền vào là "Ngoc Trinh"
methodSetName.invoke(girl, "Ngoc Trinh");
System.out.println(girl);
}
Lấy ra Constructor¶
Lấy ra hàm khởi tạo của một class. Từ đó cho phép chúng ta cách tạo ra đối tượng từ theo một cách khác, thay vì new Class()
như bình thường
public static void main(String[] args) {
Class<Girl> girlClass = Girl.class;
System.out.println("Class: " + girlClass.getSimpleName());
System.out.println("Constructors: " + Arrays.toString(girlClass.getConstructors())); // Lấy ra toàn bộ Constructor của class này
try {
// Tạo ra một object Girl từ class. (Khởi tạo không tham số)
Girl girl1 = girlClass.newInstance();
System.out.println("Girl1: " + girl1);
// Lấy ra hàm constructor với tham số là 1 string
// Chính là -> public Girl(String name) {}
Constructor<Girl> girlConstructor = girlClass.getConstructor(String.class);
Girl girl2 = girlConstructor.newInstance("Hello");
System.out.println("Girl2: " + girl2);
} catch (Exception e) {
// Exception xay ra khi constructor khong ton tai hoac tham so truyen vao khong dung
e.printStackTrace();
}
}
Lấy ra Annotation trên Field, Method, Class¶
Đúng vậy, đây cũng chính là một trong những phần quan trọng bậc nhất của Java Reflection
. Cho phép chúng ta kiểm tra Class
hiện tại đang được chú thích bởi những Annotation
nào.
@SuppressWarnings("deprecation")
@Deprecated
public class Girl {
private String name;
public Girl() {
}
public Girl(String name) {
this.name = name;
}
@Nullable
public void setName(String name){
this.name = name;
}
@Override
public String toString() {
return "Girl{" +
"name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
Class<Girl> girlClass = Girl.class;
System.out.println("Class: "+girlClass.getSimpleName()); // Lấy ra tên Class
for(Annotation annotation : girlClass.getDeclaredAnnotations()){
System.out.println("Annotation: " + annotation.annotationType()); // Lấy ra tên các Annatation trên class này
}
for(Method method: girlClass.getDeclaredMethods()){ // Lấy ra các method của class
System.out.println("\nMethod: " + method.getName()); //Tên method
for(Annotation annotation : method.getAnnotations()){
System.out.println("Annotation: " + annotation.annotationType()); // Lấy ra tên các Annatation trên method này
}
}
}
}
Hướng dẫn tự tạo một Annotations¶
Khái niệm¶
Annotation
(Chú thích) được sử dụng để chú thích trên một class
, một trường (field
) hoặc một method
để cung cấp hoặc bổ sung các thông tin. Nó hoàn toàn không ảnh hưởng tới code của bạn.
Trong bài có sử dụng các kiến thức:
- Optional
- Functional Interface & Lambda
- Java Reflection
Annotation
được sử dụng ở 3 dạng:
- Chú thích cho trình biên dịch (Compiler)
- Chú thích cho quá trình build
- Chú thích trong quá trình chạy chương trình (Runtime)
Hẳn bạn đã 1 lần từng thấy cái @Override
phải không? nó là một Annotation chú thích cho trình biên dịch, để cho trình biên dịch biết hàm đó đã bị ghi đè.
Còn chú thích cho quá trình build thì không hẳn có ví dụ cụ thể, nhưng bạn hãy nghĩ tới Maven
, Gradle
những công cụ build này sẽ có thêm thông tin khi build ứng dụng của bạn khi gặp một số Annotation
đặc biệt, và sẽ bổ sung thêm code vào đó.
Chú thích trong quá trình chạy chương trình sẽ là nội dung chính của chúng ta hôm nay. Đây là những Annotation
mà chỉ khi bạn chạy chương trình rồi thì nó mới tác động tới code. Cùng vào ví dụ để dễ hiểu nhé!
Khai báo Annotation¶
Cách khai báo Annotation
là sử dụng @interface
vậy là bạn đã có 1 Annotation
. Giờ gọi nó ra và sử dụng:
Khai báo phạm vi cho Annotation¶
Chúng ta có thể quy định phạm vi sử dụng của Annotation
bằng cách:
@Retention
: Dùng để chú thích mức độ tồn tại của một annotation nào đó. Cụ thể có 3 mức nhận thức tồn tại của vật được chú thích:
RetentionPolicy.SOURCE
: Tồn tại trên code nguồn, và không được bộ dịch (compiler) nhận ra.RetentionPolicy.CLASS
: Mức tồn tại được bộ dịch nhận ra, nhưng không được nhận biết bởi máy ảo tại thời điểm chạy (Runtime).RetentionPolicy.RUNTIME
: Mức tồn tại lớn nhất, được bộ dịch (compiler) nhận biết, và máy ảo (jvm) cũng nhận ra khi chạy chương trình.
@Target
: Dùng để chú thích phạm vi sử dụng của một Annotation
ElementType.TYPE
- Cho phép chú thích trên Class, interface, enum, annotation.ElementType.FIELD
- Cho phép chú thích trường (field), bao gồm cả các hằng số enum.ElementType.METHOD
- Cho phép chú thích trên method.ElementType.PARAMETER
- Cho phép chú thích trên parameterElementType.CONSTRUCTOR
- Cho phép chú thích trên constructorElementType.LOCAL_VARIABLE
- Cho phép chú thích trên biến địa phương.ElementType.ANNOTATION_TYPE
- Cho phép chú thích trên Annotation khácElementType.PACKAGE
- Cho phép chú thích trên package.
Xử lý Annotation¶
Bước 1: Chú thích bất kì chỗ nào bạn muốn.
Bước 2: Viết class xử lý @JsonName
Bước 3: Chạy thử:
public @interface JsonName {
String value(); // các giá trị trong @interface đều dạng hàm abstract, không tham số
}
@JsonName(value = "super_man")
public class SuperMan extends Person {
private String name;
}
@Retention(RetentionPolicy.RUNTIME) // Tồn tại trong lúc chạy chương trình
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) // Được sử dụng trên class, interface, method, biến
public @interface JsonName {
String value();
}
@JsonName(value = "super_man")
public class SuperMan {
// Không chú thích, thì chúng ta sẽ coi như lấy tên field là `name` luôn
private String name;
@JsonName("date_of_birth")
private LocalDateTime dateOfBirth;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDateTime getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(LocalDateTime dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
}
public class JsonNameProcessor {
public static String toJson(Object object) throws IllegalAccessException {
StringBuilder sb = new StringBuilder(); // Dùng StringBuilder de tao json tu class
Class<?> clazz = object.getClass();
JsonName jsonClassName = clazz.getDeclaredAnnotation(JsonName.class); // Lay ra annotation @JsonName tren Class
sb.append("{\n")
.append("\t\"")
// Lay gia tri cua Annotation, neu annotation la null thi lay ten Class de thay the
.append(Optional.ofNullable(jsonClassName).map(JsonName::value).orElse(clazz.getSimpleName()))
.append("\": {\n"); //
Field fields[] = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true); // Set setAccessible = true. De co the truy cap vao private field
JsonName jsonFieldName = fields[i].getDeclaredAnnotation(JsonName.class); // get annotation tren field
sb.append("\t\t\"")
// Lay gia tri cua Annotation, neu annotation la null thi lay ten field thay the
.append(Optional.ofNullable(jsonFieldName).map(JsonName::value).orElse(fields[i].getName())) // L
.append("\": ")
// Neu field la String hoac Object. thi append dau ngoac kep vao
.append(fields[i].getType() == String.class || !fields[i].getType().isPrimitive() ? "\"" : "")
// Lay gia tri cua field
.append(fields[i].get(object))
// Neu field la String hoac Object. thi append dau ngoac kep vao
.append(fields[i].getType() == String.class || !fields[i].getType().isPrimitive()? "\"" : "")
// Nếu là field cuối cùng, thì không append dấu ","
.append(i != fields.length -1 ? ",\n" : "\n");
}
sb.append("\t}\n");
sb.append("}");
return sb.toString();
}
}
public static void main(String[] args) throws IllegalAccessException {
SuperMan superMan = new SuperMan(); // Tao doi tuong super man
superMan.setDateOfBirth(LocalDateTime.now());
superMan.setName("loda");
String json =JsonNameProcessor.toJson(superMan);
System.out.println(json);
}
// OUTPUT:
/*
{
"super_man": {
"name": "loda",
"date_of_birth": "2019-04-03T21:07:23.983"
}
}
*/
「Java 8」Functional Interfaces & Lambda Expressions cực dễ hiểu¶
Giới thiệu¶
Khái niệm Functional Interfaces
được Java
đưa ra cùng với phiên bản Java 8
. về cơ bản, có thể hiểu:
Functional Interfaces là interface nhưng chỉ có một 1 abstract function duy nhất.
Ví dụ:
interface Runable {
public void run(); // Chỉ có duy nhất một abstract function.
}
Functional Programming¶
Trước khi đi vào chi tiết, chúng ta cùng tìm hiểu khái niệm Lập trình hướng hàm
.
Cùng xem ví dụ dưới đây:
public static void main(String[] args) {
// Mình muốn xử lý dữ liệu trước khi ỉn ra màn hình.
System.out.println(process("Hey Loda!!!"));
}
public static String process(String input){
// Cho tất cả viết hoa lên.
return input.toUpperCase();
}
// Output:
HEY LODA!!!
Tuy nhiên bạn sẽ thấy cách làm này không flexible
, vì các bạn chỉ có thể xử lý cho chữ thành UPPER CASE
. Muốn làm gì đó khác, như toLowerCase
chẳng hạn, mình sẽ phải viết một function
mới.
Chúng ta giải quyết cách cách này bằng Anonymous function (Hàm ẩn danh)
Sửa code chút:
public interface StringProcessor{
public String process(String input);
}
// StringProcessor ở đây là một Interface, hay Functional Interface
public static String getStr(String input, StringProcessor processor){
return processor.process(input);
}
public static void main(String[] args) {
// In ra chữ hoa
System.out.println(getStr("Hello Loda!", new StringProcessor() {
@Override
public String process(String input) {
return input.toUpperCase();
}
}));
// In ra chữ thường
System.out.println(getStr("Hey Loda!", new StringProcessor() {
@Override
public String process(String input) {
return input.toLowerCase();
}
}));
}
// Output:
// HELLO LODA!
// hey loda!
Lambda Expressions¶
Quay lại ví dụ ở trên, chúng ta thấy là StringProcessor
chỉ có duy nhất một function process(x)
. Nên mọi đoạn code đều sẽ giống hệt nhau ở việc implement function
này.
new StringProcessor() {
@Override
public String process(String input) {
// Do something here
// Chỉ khác nhau đoạn code ở giữa
return x;
}
}
Thực ra cái chúng ta quan tâm là: Input -> Process -> Output
. Hãy thử nhìn ở ví dụ dưới cho Lambda Expressions:
// (input) -> input.toUpperCase()
// đầu vào -> đầu ra
System.out.println(getStr("Hello Loda!", input -> input.toUpperCase()));
// Cấu trúc của một lambda như sau:
// parameter -> expression body
Trong đó:
parameter
là những tham số đầu vào của hàm (một hoặc nhiều)expression body
là phần xử lýparameter
, bạn cần trả ra đúng kiểu dữ liệu đã khai báo trongFunctional Interface
Nếu code
bạn chỉ cần 1 thao tác, thì không cần return
giống ví dụ ở trên. Còn nếu code
yêu cầu xử lý nhiều, thì dạng đầy đủ của nó như sau:
parameter -> {
expression body
[return] // (không trả về nếu là void)
}
ví dụ:
System.out.println(getStr("Hello Loda!", input -> {
String temp = input + " Oke!!!";
return temp.toLowerCase();
}));
Functional Interface¶
Tới đây, bạn đã hiểu ý nghĩa của việc cho ra đời khái niệm Functional Interface
, nó là một quy định chung phải có để có thể viết code dưới dạng biểu thức Lambda
. Một số điều cần lưu ý với Functional Interface
như sau:
@FunctionalInterface¶
Annotation
này chỉ để bổ sung, nó đánh dấu một interface
là Functional Interface
. Lúc này bạn khai báo 2 abtract function
bên trong interface
thì sẽ báo lỗi.
@FunctionalInterface // Gắn cái này lên interface, nó đánh dấu interface chỉ được phép có 1 funtion thôi
public interface StringProcessor{
public String process(String input);
public String preProcess(String input); // lỗi
}
default function & static funtion¶
Java 8
cải tiến cho phép interface
được khai báo code
bên trong nó, với điều kiện code
phải nằm trong default
hoặc static
. default
và static
không phá vỡ quy luật của @FunctionInterfaces
@FunctionalInterface // Gắn cái này lên interface, nó đánh dấu interface chỉ được phép có 1 funtion thôi
public interface StringProcessor{
public String process(String input);
// Mọi class implement StringProcessor đều có thể gọi hàm này để sử dụng luôn
public default void printf(Object t){
System.out.println(t);
}
// Là hàm static, gọi từ class cũng được.
// StringProcessor.concat(a,b)
public static String concat(String a, String b){
return a + b;
}
}
Method reference¶
Phần này chỉ để bổ sung, không có nó, bạn vẫn có thể sử dụng Lambda Expressions
bình thường. Nhưng với Method reference
, code của bạn sẽ còn sạch sẽ hơn nữa.
Ví dụ:
System.out.println(getStr("Hello Loda!", input -> input.toUpperCase()));
// Tương đương với việc viết như này:
System.out.println(getStr("Hello Loda!", String::toUpperCase));
hoặc
System.out.println(getStr("Hello Loda!", input -> new String(input));
// Tương đương với việc viết như này:
System.out.println(getStr("Hello Loda!", String::new));
Method reference
là cách viết ngắn gọn, sẽ bỏ qua luôn cả phần parameter
vì bản thân tên hàm đã biết nó sẽ nhận vào gì và trả ra cái gì rồi. Việc còn lại để Compiler
lo thôi kakaka. Có các cách để gọi Method reference
như sau:
[Tên Class]::[Tên method]
: Giống với ví dụ ở trênString::toUpperCase
.[Tên Class]::new
: Tạo ra một đối tượng mới, từ tham số được truyền vào
Hướng dẫn Stream API¶
Khái quát¶
Stream
là một abtract layer cho phép bạn xử lý một dòng dữ liệu dựa trên các thao tác đã định nghĩa trước. Bạn có thể tạo Stream
từ các nguồn dữ liệu như Collections
, Arrays
hoặc I/O resources
. Mặc định các lớp kế thừa của Collection
đều có hàm .stream()
:
Collection<String> collection = Arrays.asList("hello", "loda", "kaka");
Stream<String> streamOfCollection = collection.stream(); // Tạo ra một stream từ collection
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream(); // tạo ra 1 luồng
Stream<String> parallelStream = list.parallelStream(); // luồng dữ liệu song song (xử lý trên nhiều thread cùng lúc)
Cách sử dụng¶
Chức năng của Stream
là cực kì đa dạng giúp bạn thao tác dữ liệu dễ dàng hơn.
forEach()
: Duyệt qua toàn bộ dữ liệu của bạn¶
list.stream().forEach(s -> System.out.println(s));
map()
: Tạo ra các giá trị mới từ dữ liệu hiện có¶
Arrays.asList(3, 5, 7)
.stream() // tạo ra Stream từ List<Integer>
.map(i -> "loda-"+i) // biến đổi từng phần tử thành String
.map(String::toUpperCase) // biến đổi từng phần tử thành Upper case
.forEach(System.out::println); // in ra xem thử
filter()
gíup chúng ta thao tác với những dữ liệu mong muốn.¶
Arrays.asList(2, 3, 5, 7)
.stream()
.filter(i -> i % 2 != 0) //từ đây trở đi, chúng ta chỉ muốn làm việc với số lẻ
.map(i -> "loda-" + i)
.map(String::toUpperCase)
.forEach(System.out::println);
limit()
: Giới hạn số lượng dữ liệu cần xử lý¶
IntStream.range(1, 1000).boxed() // Tạo ra Stream có dữ liệu từ 1->999
.filter(i -> i % 2 != 0)
.map(i -> "loda-" + i)
.map(String::toUpperCase)
.limit(10) // Chúng ta giới hạn lấy 10 cái rồi in ra
.forEach(System.out::println);
sorted()
: sắp xếp Stream
. Bạn có thể tự định nghĩa cách sort bằng cách thêm Comparator vào¶
sorted((o1, o2) -> o1.compareTo(o2))
List<String> result = IntStream.range(1, 1000).boxed()
.filter(i -> i % 2 != 0)
.map(i -> "loda-" + i)
.map(String::toUpperCase)
.limit(10)
.sorted(Comparator.naturalOrder()) // một cách khác để sort
.collect(Collectors.toList());
collect()
giúp chúng ta lấy toàn bộ dữ liệu đã biến đổi trong Stream
thành đối tượng mình mong muốn.¶
List<String> result = Stream.of("bạn", "hãy", "like", "Fanpage", "loda","dể","cập","nhật","nhiều","hơn")
.filter(s -> {
System.out.println("[filtering] " + s);
return s.length()>=4;
})
.map(s -> {
System.out.println("[mapping] " + s);
return s.toUpperCase();
})
.limit(3)
.collect(Collectors.toList());
System.out.println("----------------------");
System.out.println("Result:");
result.forEach(System.out::println);
[filtering] bạn // không thoả mãn
[filtering] hãy // tiếp tục tìm, cũng k thoả mãn
[filtering] like // thoả mãn
[mapping] like // mapping nó luôn
[filtering] Fanpage // lại quay lại filter tìm tiếp, thoả mãn
[mapping] Fanpage // mapping
[filtering] loda // thoả mãn
[mapping] loda // mapping
// Đủ 3 trường hợp thoả mãn, dừng.
----------------------
Result:
LIKE
FANPAGE
LODA
Bản chất của Stream¶
(Có ví dụ trong bài gốc nữa)
Stream
là Lazy evaluation
. Hiểu đơn giản là nó sẽ không xử lý dữ liệu trực tiếp qua từng bước, mà chờ bạn khai báo xong tất cả các thao tác operation
như map
, filter
,v.v.. cho tới khi gặp lệnh .collect()
thì nó thực hiện toàn bộ trong một vòng lặp duy nhất.
Hàm .collect()
và một số hàm như min()
, max()
, count()
được gọi là terminal operation
. Khi gọi những function có dạng terminal
thì Stream
mới chính thức hoạt động.
Một lưu ý khi sử dụng là Stream không được tái sử dụng. Vì Stream
được tạo ra để xử lý dữ liệu chứ không phải để lưu trữ! Nên muốn sử dụng, mỗi lần bạn sẽ cần tạo ra 1 Stream
mới.
Stream<String> stream =
Stream.of("loda", ".", "me","like").filter(element -> element.contains("e"));
Optional<String> anyElement = stream.findAny(); //Lấy ra một phần tử bất kỳ trong Stream, nó sẽ trả ra Optional
// Thực hiện dòng lệnh tiếp theo sẽ bắn ra IllegalStateException
Optional<String> firstElement = stream.findFirst();
Khái niệm ThreadPool và Executor trong Java¶
Một ví dụ đơn giản nhé (Trong thực tế sẽ khác, hãy coi đây là ví dụ nha):
Bây giờ, giả sử bạn có một Server Web. Nếu chúng ta nhận 1 request từ client, chúng ta sẽ xử lý mất 0.5s và trả về kết quả cho người dùng.
Thế nếu có 2 người request cùng lúc? => giải quyết bằng cách mỗi một request sẽ xử lý ở 1 thread, đơn giản.
Thế nếu có 100 người request cùng lúc? => mỗi người tạo một thre... wait a minute.... (nếu 1 tháng có 10M lượt request => tạo ra 10M thread)
Nếu bạn tạo 1-2 thread mới, chả ai trách gì bạn cả. Nhưng nếu bạn tạo liên tục và tới hàng trăm cái mới mỗi lần nhưng lại giải quyết cùng 1 vấn đề thì có lỗ hổng đấy. Vì chi phí của việc tạo 1 thread là tương đối lớn, thường dẫn tới các vấn đề về hiệu năng và cấp phát dữ liệu.
Với việc xử lý các tác vụ liên tục như vậy, có một giải pháp là sử dụng Thread Pool
.
Ở ví dụ trên, Bây giờ tôi sẽ chỉ sử dụng 30 thread thôi! Và đặt 30 thread này ở trạng thái không làm gì và vứt vào 1 cái Pool
(1 cái bể chứa, kiểu vậy). Với mỗi request đến, tôi sẽ lấy trong Pool
ra 1 thread và xử lý công việc, xử lý xong, thì cất thread vào ngược trở lại pool. Đơn giản vậy thôi, như thế chúng ta sẽ không phải tạo mới Thread nữa. Tránh tình tốn chi phí và hiệu năng.
Vấn đề là giả sử có hơn 31 request tới cùng lúc thì sao? rất đúng, trường hợp này là chắc chắn có. Lúc này Pool
sẽ không còn thread nào sẵn có nữa. Nên 1 request còn lại sẽ bị đẩy vào 1 hàng đợi BlockingQueue
. Nó sẽ đợi ở đó, bao giờ Pool
có 1 thread rảnh rỗi thì sẽ quay lại xử lý nốt.
Cách tạo ThreadPool trong Java¶
Java Concurrency API hỗ trợ một vài loại ThreadPool
sau:
- Cached thread pool: Mỗi nhiệm vụ sẽ tạo ra thread mới nếu cần, nhưng sẽ tái sử dụng lại các thread cũ. (Cái này vẫn nguy hiểm nhé, nên áp dụng với các task nhỏ, tốn ít tính toán)
- Fixed thread pool: giới hạn số lượng tối đa của các Thread được tạo ra. Các task khác đến sau phải chờ trong hàng đợi (BlockingQueue). (Ví dụ đầu bài)
- Single-threaded pool: chỉ giữ một Thread thực thi một nhiệm vụ một lúc.
- Fork/Join pool: một Thread đặc biệt sử dụng Fork/ Join Framework bằng cách tự động chia nhỏ công việc tính toán cho các core xử lý. (Tính toán song song)
Executor¶
Executor
là một class đi kèm trong gói java.util.concurrent
, là một đối tượng chịu trách nhiệm quản lý các luồng và thực hiện các tác vụ Runnable được yêu cầu xử lý. Nó tách riêng các chi tiết của việc tạo Thread, lập kế hoạch (scheduling), … để chúng ta có thể tập trung phát triển logic của tác vụ mà không quan tâm đến các chi tiết quản lý Thread.
Nói chung nó là thằng wrapper các các bước mình nói ở trên, và quản lý hộ chúng ta.
Chúng có thể tạo một Executor bằng cách sử dụng một trong các phương thức được cung cấp bởi lớp tiện ích Executors
như sau:
- newSingleThreadExecutor(): trong ThreadPool chỉ có 1 Thread và các task (nhiệm vụ) sẽ được xử lý một cách tuần tự.
- newCachedThreadPool(): như giải thích ở trên, nó sẽ có 1 số lượng nhất định thread để sử dụng lại, nhưng vẫn sẽ tạo mới thread nếu cần. Mặc định nếu một Thread không được sử dụng trong vòng 60 giây thì Thread đó sẽ bị tắt.
- newFixedThreadPool(int n): trong Pool chỉ có n Thread để xử lý nhiệm vụ, các yêu cầu tới sau bị đẩy vào hàng đợi
- newScheduledThreadPool(int corePoolSize): tương tự như
newCachedThreadPool()
nhưng sẽ có thời gian delay giữa các Thread. - newSingleThreadScheduledExecutor(): tương tự như
newSingleThreadExecutor()
nhưng sẽ có khoảng thời gian delay giữa các Thread.
Code chạy thử¶
Chúng ta sẽ lấy ví dụ đầu bài để code luôn nhé.
Tạo một class implement Runnable
để xử lý request đến. (phân biệt Runnable
và Thread
nhé các bạn)
public class RequestHandler implements Runnable {
String name;
public RequestHandler(String name){
this.name = name;
}
@Override
public void run() {
try {
// Bắt đầu xử lý request đến
System.out.println(Thread.currentThread().getName() + " Starting process " + name);
// cho ngủ 500 milis để ví dụ là quá trình xử lý mất 0,5 s
Thread.sleep(500);
// Kết thúc xử lý request
System.out.println(Thread.currentThread().getName() + " Finished process " + name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
newSingleThreadExecutor¶
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
// Có 100 request tới cùng lúc
for (int i = 0; i < 100; i++) {
executor.execute(new RequestHandler("request-" + i));
}
executor.shutdown(); // Không cho threadpool nhận thêm nhiệm vụ nào nữa
while (!executor.isTerminated()) {
// Chờ xử lý hết các request còn chờ trong Queue ...
}
}
}
// OUTPUT:
/*
..
..
pool-1-thread-1 Starting process request-98
pool-1-thread-1 Finished process request-98
pool-1-thread-1 Starting process request-99
pool-1-thread-1 Finished process request-99
*/
Cả chương trình chỉ có 1 pool, 1 thread duy nhất, xử lý toàn bộ request đến. Cái nào đến sau thì đợi thôi.
newFixedThreadPool()¶
public class FixedThreadPoolExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(5);
// Có 100 request tới cùng lúc
for (int i = 0; i < 100; i++) {
executor.execute(new RequestHandler("request-" + i));
}
executor.shutdown(); // Không cho threadpool nhận thêm nhiệm vụ nào nữa
while (!executor.isTerminated()) {
// Chờ xử lý hết các request còn chờ trong Queue ...
}
}
}
// OUTPUT:
/*
..
..
pool-1-thread-2 Finished process request-96
pool-1-thread-5 Starting process request-99
pool-1-thread-3 Finished process request-97
pool-1-thread-4 Finished process request-98
pool-1-thread-5 Finished process request-99
*/
Loại này thì chúng ta cố định 5 thread, và nó cử mặc định như vậy mà xài thôi, thiếu thread thì phải chờ tới khi có
newCachedThreadPool()¶
public class CachedThreadPoolExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
// Có 100 request tới cùng lúc
for (int i = 0; i < 100; i++) {
executor.execute(new RequestHandler("request-" + i));
Thread.sleep(200);
}
executor.shutdown(); // Không cho threadpool nhận thêm nhiệm vụ nào nữa
while (!executor.isTerminated()) {
// Chờ xử lý hết các request còn chờ trong Queue ...
}
}
}
//OUTPUT:
/*
..
..
pool-1-thread-3 Starting process request-98
pool-1-thread-1 Finished process request-96
pool-1-thread-1 Starting process request-99
pool-1-thread-2 Finished process request-97
pool-1-thread-3 Finished process request-98
pool-1-thread-1 Finished process request-99
*/
Có chút khởi sắc, chương trình chạy nhanh hơn hẳn. Vì nó được tạo số thread thoải mái nếu cần :)))) Rất nguy hiểm. Nhưng bạn sẽ thấy là có chỗ nó sử dụng lại các thread đã xong trước đó.# ThreadPoolExecutor và nguyên tắc quản lý pool size
- Khái niệm
- Nguyên tắc vận hành
- Code ví dụ
Giới thiệu¶
ThreadPoolExecutor
là một class nâng cao hơn của các ThreadPool
cơ bản trong gói java concurrent. Cụ thể các thể loại ThreadPool khác bạn xem ở đây:
- Khái niệm ThreadPool và Executor trong Java
Đặc điểm của các loại ThreadPool
thông thường được cung cấp trong ExecutorService
là không đủ linh động theo tình huống. điển hình là bị fix số lượng thread, hoặc cho phép tạo quá nhiều thread. Nó thực sự chưa phải phương án tối ưu.
ThreadPoolExecutor
thì khác, một phiên bản nâng cấp hơn, cho phép chúng ta tùy biến số lượng Thread theo kịch bản. Giúp nó thông minh hơn mấy cái kia một chút.
Ngoài ra còn có ThreadPoolTaskExecutor
do Spring Framework
cung cấp cũng hoạt động tương tự
Khái niệm¶
ThreadPoolExecutor
và ThreadPoolTaskExecutor
cũng là Executor
nhưng nó có thêm các tham số như sau:
corePoolSize
: Số lượng Thread mặc định trongPool
maxPoolSize
: Số lượng tối đa Thread trongPool
queueCapacity
: Số lượng tối da củaBlockingQueue
Nguyên tắc vận hành¶
Ví dụ với ThreadPoolExecutor
có:
corePoolSize
: 5maxPoolSize
: 15-
queueCapacity
: 100 -
Khi có request, nó sẽ tạo trong Pool tối đa 5 thread (
corePoolSize
). - Khi số lượng thread vượt quá 5 thread. Nó sẽ cho vào hàng đợi.
- Khi số lượng hàng đợi full 100 (
queueCapacity
). Lúc này mới bắt đầu tạo thêm Thread mới. - Số thread mới được tạo tối đa là 15 (
maxPoolSize
). - Khi Request vượt quá số lượng 15 thread. Request sẽ bị từ chối!
Với kịch bản như thế này, bạn sẽ luôn tiết kiệm được số lượng thread sử dụng là 5 trong trường hợp bình thường. Nhưng vẫn có thể handle lên tới 15 thread nếu server quá tải.
Điểm chúng ta hay nhầm lẫn là điều kiện để tạo thêm thread đó là khi hàng đợi phải full. Đúng vậy, nếu hàng đợi chưa full, thì có nghĩa chúng ta chưa quá tải.
Code ví dụ¶
Tạo ra một Runnable để xử lý các nhiệm vụ.
public class RequestHandler implements Runnable {
String name;
public RequestHandler(String name){
this.name = name;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " Starting process " + name);
// Giả sử nhiệm vụ xử lý hết 0.5s
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " Finished process " + name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Tạo ra ThreadPoolExecutor
để xử lý 1000 request tới dồn dập.
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
int queueCapacity = 100;
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, // Số corePoolSize
maximumPoolSize, // số maximumPoolSize
10, // thời gian một thread được sống nếu không làm gì
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueCapacity)); // Blocking queue để cho request đợi
// 1000 request đến dồn dập, liền 1 phát, không nghỉ
for (int i = 0; i < 1000; i++) {
executor.execute(new RequestHandler("request-" + i));
}
executor.shutdown(); // Không cho threadpool nhận thêm nhiệm vụ nào nữa
while (!executor.isTerminated()) {
// Chờ xử lý hết các request còn chờ trong Queue ...
}
}
}
// OUTPUT
/*
..
..
pool-1-thread-3 Finished process request-96
pool-1-thread-5 Finished process request-97
pool-1-thread-4 Finished process request-98
pool-1-thread-8 Finished process request-100
pool-1-thread-2 Finished process request-99
pool-1-thread-6 Finished process request-102
pool-1-thread-7 Finished process request-101
pool-1-thread-9 Finished process request-104
pool-1-thread-10 Finished process request-103
*/
Bạn sẽ thấy là chương trình đã phải sử dụng tới 10 thread để xử lý hết 1000 request cùng 1 lúc. Nhớ là cùng 1 lúc nhé các bạn, thế là nhiều rồi đó. Và theo nguyên tắc. Nó đã tận dụng hết maxPoolSize
rồi. Mà queue
vẫn full. Nên các request không ở trong queue
sẽ bị reject. Dẫn tới chỉ sử lý được 104 request
mà thôi.
Bây giờ, vẫn là ví dụ này, nhưng mỗi request cách nhau 50 milliseconds
thì sẽ như nào, dễ thở hơn k? chỉ 0.05s thôi.
for (int i = 0; i < 1000; i++) {
executor.execute(new RequestHandler("request-" + i));
Thread.sleep(50);
}
// OUTPUT:
/*
..
..
pool-1-thread-2 Finished process request-993
pool-1-thread-1 Finished process request-994
pool-1-thread-3 Finished process request-995
pool-1-thread-4 Finished process request-996
pool-1-thread-5 Finished process request-997
pool-1-thread-9 Finished process request-998
pool-1-thread-10 Finished process request-999
*/
Xử lý gọn gàng, sạch sẽ các bạn ạ. Sức mạnh của ThreadPoolExecutor
phát huy rõ rệt hơn. Tận dụng được 10 thread và queue vẫn còn chỗ nên rất nhanh, khác biệt trong một hệ thống có thể đc tính bằng milliseconds
như vậy đó. nếu mỗi request cách nhau 100 milliseconds
thì nó chỉ cần sử dụng 5 thread thôi.
toàn bộ code mình để tại Github: CODE
Chúc các bạn học tập tốt! ohoho
Giới thiệu Reactive Programming với Reactor¶
Giới thiệu¶
Các ứng dụng hiện nay yêu cầu một tốc độ phản hồi cao để nâng cao trải nghiệm người dùng, giúp hệ thống mượt mà, linh hoạt, không bị đóng băng luồng. Các yêu cầu này cũng là kết quả hướng tới khi chúng ta sử dụng mô hình lập trình theo Reactive Programming.
Trong bài viết này, chúng ta sẽ cố gắng làm sáng tỏ mô hình lập trình này thông qua một số khái niệm Synchronous
và Asynchronous
, Blocking
và Non-Blocking
trước.
Synchronous và Asynchronous¶
Synchronous
(Xử lý đồng bộ): là xử lý mà chương trình sẽ chạy theo từng bước, nghĩa là thực hiện xong đoạn code trên mới tới đoạn code kế tiếp và sẽ theo thứ tự từ trên xuống dưới, từ trái qua phải. Đây cũng là nguyên tắc cơ bản mà các bạn đã được học.
Asynchronous
(Xử lý bất đồng bộ): Ngược lại với xử lý đồng bộ, nghĩa là chương trình có thể hoạt động nhảy cóc, function phía dưới có thể hoạt động mà không cần phải chờ function hay một đoạn code nào đó phía trên thực hiện xong. Dưới đây là minh họa cho việc làm việc với dữ liệu đồng bộ và bất đồng bộ :
!image
Như ta thấy nếu các công việc không liên quan đến nhau thì bất đồng bộ giúp ta tiết kiệm thời gian xử lý hơn và mang lại cho người dùng trải nghiệm tốt hơn.
Blocking và Non-Blocking¶
Chúng ta có thể hiểu một cách đơn giản khi chúng ta muốn dấy một danh sách Student
.
Lập trình theo mô hình Blocking
thì phải chờ đợi chương trình thực hiện lấy tất cả Student
rồi mới thực hiện các thao tác tiếp theo, hay được gọi là bị đóng băng luồng chờ quá trình đóng gói tất cả Student
hoàn tất. Do đó sẽ dẫn tốn thời gian chờ đợi nếu số lượng danh sách rất lớn.
Lập trình theo mô hình Non-Blocking
thì hoạt động ngược lại, không cần phải chờ đợi hoàn thiện cả danh sách Student
mà với mỗi Student
nào được đưa ra thì thực hiện thao tác luôn với nó. Điều này dẫn tới không bị đóng băng luồng, kể cả số lượng danh sách lớn.
Reactive Programming¶
Nói một cách ngắn gọn, Reactive Programming là mô hình lập trình mà ở đó dữ liệu được truyền tải dưới dạng luồng ( stream). Mô hình này dưa trên nguyên tắc Asynchronous
và Non-Blocking
để làm việc với dữ liệu.
Dưới đây là một số khái niệm mà bạn cần phải biết khi làm việc với mô hình này:
Publisher: Là nhà cung cấp dữ liệu, hoặc là nơi phát ra nguồn dữ liệu.
Subscriber: Lắng nghe Publisher, yêu cầu dữ liệu mới. Hay được gọi Là người tiêu thụ dữ liệu.
Backpressure: Là khả năng mà Subscriber cho phép Publisher có thể xử lý bao nhiêu yêu cầu tại thời điểm đó. Bởi vì Subscriber chịu trách nhiệm về luồng dữ liệu, không phải Publisher vì nó chỉ cung cấp dữ liệu.
Stream: Luồng dữ liệu bao gồm các dữ liệu trả về , các lỗi xảy ra và luồng này phải là luồng bất đồng bộ.
Như vậy dữ liệu của chúng ra sẽ được chuyển thành một dòng (data stream) do đó tránh được việc bị blocking và các dữ liệu phát ra thì đều được subcriber lắng nghe dẫn đến quá trình xử lý và báo lỗi diễn ra một cách đơn giản hơn.
Reactor¶
Reactor là một nền tảng để ta triển khai việc lập trình theo phong cách reactive programming. Nó được tích hợp trực tiếp với Java 8 funcion APIs như CompletableFuture
, Stream
, Duration
.
Reactor cung cấp 2 loại về Publisher :
Flux
: là một steam phát ra từ 0...n phần tử.
!image
Mono
: là một steam phát ra từ 0...1 phần tử.
!image
Vậy là các bạn có thể hiểu được Reactive Programming phải không nào :D. Các bài viết tới chúng ta sẽ đi sâu hơn về các thực thi cũng như các function
mà Reactor hỗ trợ. Hãy chú ý theo dõi và đừng quên nhận xét để chúng tôi có thể cải thiện các bài viết tốt hơn.
Giới thiệu Reactor Core¶
- Maven Dependencies
- Tạo ra một luồng dữ liệu
- Subscribe()
- So sánh với Streams Java 8
- Backpressure
- Concurrency
- Kết luận
Tổng Quan¶
Reactor Core là một thự viện Java 8 implement mô hình Reactive Programming. Nó được xây dựng dựa trên Reactive Streams Specification - một tiêu chuẩn để xây dựng ứng dụng Reactive
.
Trong bài viết này, chúng ta sẽ đi từng bước nhỏ thông qua Reactor
cho đến khi có cái nhìn toàn cảnh cũng như cách thực thi của Reactor core.
Maven Dependencies¶
Đây là thư viện của Reactor
, chúng ta có thể lấy thư viện mới nhất tại đây
<dependency><groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.2.8.RELEASE</version>
</dependency>
Tạo ra một luồng dữ liệu¶
Để có một ứng dựng phản ứng (reactive), điều đầu tiên chúng ta cần phải làm là tạo ra một luồng dữ liệu. Không có dữ liệu này chúng ta sẽ không có bất cứ điều gì để phản ứng, đó là lý do tại sao đây là bước đầu tiên.
Reactor core
cung cấp 2 loại dữ liệu cho phép chúng ta thực hiện điều này.
Flux
Cách đầu tiên đó là dùng Flux
. Flux
là một luồng có thể phát ra 0..n phần tử. Ví dụ tạo đơn giản:
Flux<Integer> just = Flux.just(1,2,3,4);
Mono
Cách thứ hai đó là Mono
. Mono
là một luồng có thể phát ra 0..1 phần tử. Nó hoạt động gần giống hệ như Flux
, chỉ là bị giới hạn không quá một phần tử. Ví dụ:
Mono<String> just = Mono.just("atomPtit");
Điều lưu ý rằng cả Flux
và Mono
đề được triển khai từ interface Publisher
. Cả hai đều tuần thủ tiêu chuẩn Reactive
, chúng ta có thể sử dụng interface như sau:
Publisher<String> just = Mono.just("foo");
Subscribe()¶
Hãy luôn ghi nhớ rằng: Không có gì xảy ra cho đến khi subscribe() .
Trong reactor
, khi bạn viết một Publisher
, dữ liệu không bắt đầu được bơm vào theo mặc định. Thay vào đó, bạn tạo một mô tả trừu tượng về quy định không đồng bộ của bạn(hỗ trợ tái sử dụng).
Để hiểu rõ luồng hoạt động hãy theo dõi qua ví dụ đơn giản sau.
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>
Chúng ta thêm thư viện logback
. Điều này sẽ giúp chúng ta ghi nhật ký đầu ra của quá trình hoạt động reactor
từ đó hiểu rõ hơn về luồng dữ liệu.
public class ReactorCode {
public static void main(String[] args) {
List<Integer> elements = new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.subscribe(elements::add);
}
}
// OUTPUT:
/*
23:02:16.996 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
23:02:17.014 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
23:02:17.017 [main] INFO reactor.Flux.Array.1 - | request(unbounded)
23:02:17.018 [main] INFO reactor.Flux.Array.1 - | onNext(1)
23:02:17.018 [main] INFO reactor.Flux.Array.1 - | onNext(2)
23:02:17.018 [main] INFO reactor.Flux.Array.1 - | onNext(3)
23:02:17.018 [main] INFO reactor.Flux.Array.1 - | onNext(4)
23:02:17.019 [main] INFO reactor.Flux.Array.1 - | onComplete()
*/
Hãy nhìn vào phần output, mọi thứ đều chạy trên main thread. Bây giờ chugn ta đi xem rõ từng dòng thực thi: 1. onSubscribe()
- Điều này được gọi thi chúng ra đăng ký (subscriber()) luồng
request(unbounded)
- Khi chúng ta gọi đăng ký, thì hàm này được chạy ngầm nhằm ý nghĩa tạo đăng ký. Trong trường hợp này chạy mặc định là unbounded (không giới hạn), nghĩa là nó yêu cầu mọi phần tử có sẵn.onNext()
- Hàm này được gọi cho mọi phần tử đơn.onComplete()
- Hàm này được gọi sau cùng sau khi nhận được phần tử cuối cùng. Trong thực có thể xảy ra các hàm khác nhưonError()
, cái mà có thể được gọi khi xảy ra một exception.
So sánh với Streams Java 8¶
Có vẻ nhiều người vẫn đang nghĩ sự tương đồng với Stream trong Java 8:
List<Integer> collected = Stream.of(1, 2, 3, 4)
.collect(toList());
Sự khác biết cốt lõi là Reactive
là một hình push (đẩy) , trong khi Stream Java 8 là mô hình pull (kéo)
Streams Java 8 là terminal
- kéo tất cả dữ liệu và trả về một kết quả. Với Reactive
, chúng ta có một luồng vô hạn đến từ một nguồi tài nguyên bên ngoài, với nhiều người subscribe(). Chúng ta cũng có thể làm những việc như kết hợp các luồng, tiều tiết luồng và backpressure
.
Backpressure¶
Trong ví dụ trên, người đăng ký nói với Publisher
đẩy từng phần tử một. Điều này có thể trở nên quá tải cho người đăng ký phải tiêu thụ hết tất cả tài nguyên của nó.
Backpressure đơn giản chỉ là bảo với Publisher
gửi cho nó ít dữ liệu hơn để ngăn chặn nó bị quá tải.
Ví dụ dưới đây, chúng ta sẽ yêu cầu chỉ gửi 2 phần từ cùng một lúc bằng cách sử dụng request ()
:
Flux.just(1, 2, 3, 4)
.log()
.subscribe(new Subscriber<Integer>() {
private Subscription s;
int onNextAmount;
@Override
public void onSubscribe(Subscription s) {
this.s = s;
s.request(2);
}
@Override
public void onNext(Integer integer) {
elements.add(integer);
onNextAmount++;
if (onNextAmount % 2 == 0) {
s.request(2);
}
}
@Override
public void onError(Throwable t) {}
@Override
public void onComplete() {}
});
//OUTPUT
/*
23:31:15.395 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
23:31:15.397 [main] INFO reactor.Flux.Array.1 - | request(2)
23:31:15.397 [main] INFO reactor.Flux.Array.1 - | onNext(1)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onNext(2)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | request(2)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onNext(3)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onNext(4)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | request(2)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onComplete()
*/
Bây giờ chúng ta nhìn thấy hàm request()
được gọi trước, tiếp theo đó là 2 hàm onNext()
thực hiện, sau đó lại là request()
.
Concurrency¶
Tất cả các ví dụ trên chúng ta đều đang chạy trên một luồng chính. Tuy nhiên, chúng ta có thể kiểm soát luồng nào mà code của chúng ta chạy nếu chúng ta muốn. Các inteface Scheduler
cung cấp một sự trừu tượng với asynchronous
.
public class ReactorCode {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
Flux.just(1, 2, 3, 4)
.log()
.subscribeOn(Schedulers.fromExecutorService(service))
.subscribe();
Flux.just(5, 6, 7, 8)
.log()
.subscribeOn(Schedulers.fromExecutorService(service))
.subscribe();
}
}
//OUTPUT
/*
23:48:02.972 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
23:48:02.996 [pool-1-thread-2] INFO reactor.Flux.Array.2 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
23:48:02.996 [pool-1-thread-1] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
23:48:03.000 [pool-1-thread-2] INFO reactor.Flux.Array.2 - | request(unbounded)
23:48:03.000 [pool-1-thread-1] INFO reactor.Flux.Array.1 - | request(unbounded)
23:48:03.001 [pool-1-thread-1] INFO reactor.Flux.Array.1 - | onNext(1)
23:48:03.001 [pool-1-thread-2] INFO reactor.Flux.Array.2 - | onNext(5)
23:48:03.001 [pool-1-thread-1] INFO reactor.Flux.Array.1 - | onNext(2)
23:48:03.001 [pool-1-thread-2] INFO reactor.Flux.Array.2 - | onNext(6)
23:48:03.001 [pool-1-thread-1] INFO reactor.Flux.Array.1 - | onNext(3)
23:48:03.001 [pool-1-thread-2] INFO reactor.Flux.Array.2 - | onNext(7)
23:48:03.001 [pool-1-thread-1] INFO reactor.Flux.Array.1 - | onNext(4)
23:48:03.001 [pool-1-thread-2] INFO reactor.Flux.Array.2 - | onNext(8)
23:48:03.002 [pool-1-thread-1] INFO reactor.Flux.Array.1 - | onComplete()
23:48:03.002 [pool-1-thread-2] INFO reactor.Flux.Array.2 - | onComplete()
*/
Ở đây chúng ta dùng ExecutorService, 2 luồng code thực hiện song song trên 2 thread khác nhau, điều mà đã chứng minh bằng output.
Kết luận¶
Sau bài viết này, chúng tôi đã có cái nhìn tổng quan về Reactor Core
. Từ các tạo một Publisher
, các đăng ký, backpressure cũng như xử lý không đồng bộ. Đây cũng là nền tảng để cho chúng tôi viết cái bài viết khác liên quan về Reactor Core
.
Bạn thực sự đã biết khi nào dùng Interface khi nào dùng Abstract?¶
Tổng quan¶
Trong java, chúng ta có class
abstract
và một Interface
, ai cũng biết một class có thể impements
nhiều Interface
và chỉ kế thừa được một class
abstract
. Nhưng bạn thực sự đã biết khi nào thì ta dùng Interface
, khi nào dùng Abstract
. Chưa kể bắt đầu từ Java 8 có sự thay đổi về Interface
càng làm khó phân biệt giữa hai loại này.
Trong bài viết này chúng tôi sẽ đi so sánh một số tính chất của 2 loại này, sau đó là đưa ra ví dụ đơn giải để các bạn hình dung rõ nhất. Cuối cùng là hiểu khi nào thì dùng chúng.
Sự khác nhau giữa Interface và Abstract¶
- Methods: Class
abstract
có các phương thức abstract và non-abstract. Trong khiInterface
chỉ có phương thức abstract, từ Java 8, thì Interface có thêm 2 loại phương thức làdefault
vàstatic
. - Variables: Class
abstract
có thể có các biến final, non-final, static và non-static. Trong khiInterface
chỉ có các biến static và final. - Implementation: Class
abstract
có thể implement các Interface. Trong khiInterface
thì không thể implement class abstract. - Inheritance: Class
abstract
có thể kế thừa được một class khác. Trong khiInterface
có thể kế thừa được nhiều Interface khác. - Accessibility: các thành viên trong
Interface
kiếu mặc định làpublic
. Trong khi classabstract
thì lại có thể là private, protected,..
Nguồn: https://loda.me - còn nhiều cái hay ho lắm!
Abstract là gì?¶
Abstract(trừu tượng) nghĩa là một cái gì đó không hoàn toàn cụ thể, nó chỉ là một ý tưởng hoặc ý chính của một cái gì đó mà không có bản triển khai cụ thể. Vì vậy Class abstract chỉ là một cấu trúc hoặc hướng dẫn được tạo cho các class cụ thể khác.
Chúng ta có thể nói rằng một class abstract là linh hồn của một class cụ thể, và rõ ràng một cơ thể (class) không thể có hai linh hồn. Đây cũng là lý do Java không hỗ trợ nhiều kế thừa cho các class abstract.
Hãy nhìn vào class abstract sau: Xe.class
public abstract class Xe {
private String dongCo;
abstract void khoiDongDongCo();
abstract void dungDongco();
}
Chúng tôi tạo một class abstract Xe
có thuộc tính là động cơ
, và các phương thức khởi động/ dừng động cơ. Xe
là một cái gì đó không cụ thể, nó có thể là ô tô, xe máy, ... và rõ ràng không có Xe
nào mà không tồn tại động cơ và cơ chế khởi động/dừng động cơ cả.
Oto.class
public class Oto extends Xe {
@Override
void khoiDongDongCo() {
System.out.println("Khởi động động cơ của ôtô");
}
@Override
void dungDongco() {
System.out.println("Dừng động cơ của ôtô");
}
}
Interface là gì?¶
Interface (Giao diện) là một hình thức, giống như một hợp đồng, nó không thể tự làm bất cứ điều gì. Nhưng khi có một class ký kết hợp đồng (implement Interface) này, thì class đó phải tuân theo hợp đồng này.
Trong Interface, chúng tôi định nghĩa các hành vi của một class sẽ thực hiện. Một class có thể có một số cách hành vi khác nhau, cũng giống như nó có thể ký kết được với nhiều hợp đồng khác nhau. Đó cũng là lý do tại sao Java cho phép implement nhiều Interface.
Tiếp nối ví dụ trên, Xe
có thể di chuyển, vì vậy chúng tôi tạo một Interface Hành động di chuyển và class Oto
implement nó.
HanhDongDiChuyen.class
public interface HanhDongDiChuyen {
void diChuyen();
}
Đây là những hành vi của Oto
, chứ không thuộc tính sẵn có của nó: Ôtô là xe hơi, ngay cả khi nó không thể di chuyển được!
Oto.class
public class Oto extends Xe implements HanhDongDiChuyen{
@Override
void khoiDongDongCo() {
System.out.println("Khởi động động cơ của ôtô");
}
@Override
void dungDongco() {
System.out.println("Dừng động cơ của ôtô");
}
@Override
public void diChuyen() {
System.out.println("Ôtô đang di chuyển");
}
}
Khi nào nên dùng?¶
- Class abstract đại diện cho mối quan hệ "IS - A" (Ôtô là Xe)
- Interface đại diện cho mối quan hệ "like - A" (Ô tô có thể chuyển động).
- Tạo một class abstract khi bạn đang cung cấp các hướng dẫn cho một class cụ thể.
- Tạo Interface khi chúng ta cung cấp các hành vi bổ sung cho class cụ thể và những hành vì này không bắt buộc đối với clas đó.
Kết luận¶
Mục đích của bài viết này là để giúp bạn hiểu và nắm vững class abstract, Interface và kịch bản sử dụng. Thông qua nổ lực toàn bộ bài viết của chúng tôi, chúng tôi tin chắc rằng bạn đã hiểu được điều gì đó. Cuối cùng, cảm ơn bạn đã đọc bài viết.
Java Concurrency (Phần 1): Thread¶
Source: Java Concurrency (Phần 1): Thread
1. Giới thiệu¶
Lập trình đồng thời (concurrency) trong Java đề cập đến khả năng của một chương trình Java thực thi nhiều tác vụ đồng thời hoặc song song, tận dụng tối đa các bộ xử lý (CPU) đa lõi (core) hiện đại. Khi các ứng dụng ngày càng trở nên phức tạp và đòi hỏi hiệu suất cao hơn, lập trình đồng thời trở thành yếu tố thiết yếu để cải thiện hiệu năng, khả năng phản hồi và khả năng mở rộng. Java cung cấp một bộ công cụ và các thư viện phong phú giúp các nhà phát triển tạo ra các ứng dụng đồng thời, quản lý nhiều luồng (threads) và điều phối các tác vụ một cách hiệu quả. Trong bài viết này, chúng sẽ khám phá các khái niệm cơ bản về lập trình đồng thời trong Java.
2. Định nghĩa Thread¶
Một thread là một đơn vị thực thi nhỏ hơn một process. Một process có thể tạo ra nhiều thread trong quá trình thực thi. Tất cả các thread trong cùng một process sẽ chia sẻ, dùng chung một số vùng nhớ với nhau (heap memory, static variables, metaspace, … phần này mình sẽ chia sẻ cụ thể hơn ở một bài viết khác). Vì vậy, việc giao tiếp giữa các thread khá đơn giản và dễ dàng hơn so với giao tiếp giữa các process. Ngoài ra, việc tạo mới/hủy thread đơn giản và tốn ít công hơn so với việc tạo mới/hủy một process. Vì các lý do này, thread còn được gọi là lightweight process.
3. Cách khởi tạo thread¶
Đây là một câu hỏi thường hay gặp trong phỏng vấn. Bạn có thể tham khảo hoặc trả lời như sau: Ta có thể phân loại các cách khởi tạo thread như sau:
3.1. Tạo trực tiếp thread¶
sử dụng new Thread().start()
.
new Thread(() -> resource.counter++).start();
3.2. Khai báo Thread execution method¶
3.2.1. Kế thừa class Thread¶
Đây là một cách phổ biến. Chúng ta tạo ra một class mới kế thừa class Thread và ghi đè method run như sau:
public class ExtendsThread extends Thread {
@Override
public void run() {
System.out.println("Do something");
}
public static void main(String[] args) {
new ExtendsThread().start();
}
}
3.2.2. Triển khai interface Runnable¶
Đây cũng là một cách phổ biến, implement Runnable interface và override method run, như sau:
public class ImplementsRunnable implements Runnable {
@Override
public void run() {
System.out.println("Do something");
}
public static void main(String[] args) {
ImplementsRunnable runnable = new ImplementsRunnable();
new Thread(runnable).start();
}
}
3.2.3. Triển khai interface Callable¶
Tương tự như method trước, ngoại trừ method này có thể nhận giá trị trả về sau khi Thread được thực thi, như sau:
public class ImplementsCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Do something");
return "test";
}
public static void main(String[] args) throws Exception {
ImplementsCallable callable = new ImplementsCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
3.2.4. Sử dụng class ẩn danh hoặc biểu thức Lambda¶
public class UseAnonymousClass {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("AnonymousClass");
}
}).start();
new Thread(() ->
System.out.println("Lambda")
).start();
}
}
3.3. Tạo gián tiếp thread¶
3.3.1. Sử dụng thread pool của ExecutorService¶
public class UseExecutorService {
public static void main(String[] args) {
ExecutorService poolA = Executors.newFixedThreadPool(2);
poolA.execute(() -> {
System.out.println("do something");
});
}
3.3.2. Sử dụng thread pool hoặc Stream song song (parallel stream)¶
public class UseForkJoinPool {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.execute( () -> {
System.out.println("Do something");
});
List<String> list = Arrays.asList("e1");
list.parallelStream().forEach(System.out::println);
}
}
3.3.3. Sử dụng CompletableFuture¶
public class UseCompletableFuture {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
System.out.println("5......");
return "test";
});
Thread.sleep(1000);
}
}
3.3.4. Sử dụng class Timer¶
public class UseTimer {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("9......");
}
}, 0, 1000);
}
}
Java chỉ có một cách để tạo thread một cách trực tiếp, đó là thông qua việc tạo new Thread().start(). Do đó, cho dù sử dụng phương thức nào thì cuối cùng nó cũng phụ thuộc vào new Thread().start(). Các đối tượng Runnable, Callable, … chỉ là phần thân của Thread, tức là tác vụ được cung cấp cho Thread để thực thi.
4. Trạng thái của thread¶
Tại một thời điểm, một thread trong Java chỉ có thể ở một trong sáu trạng thái trong vòng đời của nó:
NEW
: Khi đối tượng thread được tạo, nó sẽ chuyển sang trạng thái NEW, chẳng hạn như: Thread t = new MyThread();RUNNABLE
: Trạng thái sẵn sàng để chạy. Ta có thể hiểu, nó sẽ được chia thành 2 trường hợp nhỏ hơn: đang chạy hoặc đang chờ để chạy. Ví dụ, khi sau, ta gọi method start(), thread đó có thể chưa chạy được ngay mà phải đợi CPU schedule để chạy.BLOCKED
: Trạng thái bị chặn, thread A đang cố giành khóa (lock) nhưng khoá đang giữa bởi thread B, thread A phải đợi, bị blocked cho đến khi khoá được giải phóng.TIME_WAITING
: Trạng thái chờ có thời gian chờ, có thể tự động quay trở lại trạng thái RUNNABLE sau khoảng thời gian xác định.WAITING
: Trạng thái chờ, biểu thị rằng thread A đang chờ các thread khác thực hiện một số hành động cụ thể, như (notification) thông báo cho thread A hoặc (interruption) ngắt thread A. Khác với TIME_WAITING, trạng thái WAITING không có thời gian timeout, chỉ được wakeup khi có thông báo từ thread khác.TERMINATED
: Trạng thái kết thúc, biểu thị rằng thread đã hoàn thành công việc hoặc dừng lai do gặp exception.
5. Các method cơ bản của thread¶
5.1. start()¶
Method start() khởi tạo việc thực thi một thread. Nó gọi phương thức run() được xác định trong class thread hoặc runnable object. Thread sẽ chuyển từ trạng thái NEW sang trạng thái RUNNABLE sau khi method này được gọi.
public class Main {
public static void main(String[] args) {
Thread myThread = new Thread(new MyRunnable());
myThread.start();
}
}
5.2. run()¶
Method run() chứa mã sẽ được thực thi trong luồng.
class MyRunnable implements Runnable {
public void run() {
System.out.println("This is a runnable.");
}
}
5.3. sleep() và wait()¶
Method sleep()
làm cho thread hiện đang thực thi ở chế độ ngủ (TIMED_WAITING) trong 1 khoảng thời gian được chỉ định (tính bằng milliseconds).
Method wait() khiến thread hiện tại đợi cho đến khi một thread khác gọi notify() hoặc notifyAll() trên cùng một object. Thread sẽ chuyển từ trạng thái RUNNABLE sang trạng thái WAITING nếu dùng wait() không truyền thêm thời gian timeout, còn nếu truyền thêm thời gian timeout - wait(timeout) thì thread sẽ ở trạng thái TIMED_WAITING
.
Sự khác biệt giữa 2 method:
- Method wait() cần được đặt trong synchronized code, còn sleep() thì không.
- Method sleep() không giải phóng khóa, trong khi method wait() sẽ giải phóng khóa.
- Method wait() thường được sử dụng cho tương tác/giao tiếp giữa các thread, còn sleep() thường được sử dụng để tạm dừng thực thi.
- Sau khi method wait() được gọi, thread sẽ không tự động thức dậy; cần một luồng khác gọi method notify() hoặc notifyAll() trên cùng một đối tượng để đánh thức luồng đó. Sau khi method sleep() được thực thi, thread sẽ tự động thức dậy (RUNNABLE).
- sleep() là một method static của class Thread, còn wait() là một method của class Object.
5.4. notify() và notifyAll()¶
notify(): đối với tất cả các thread đang chờ object monitor bằng cách sử dụng bất kỳ method wait() nào, method notify() thông báo cho một trong số các thread đó thức dậy. Việc lựa chọn chính xác thread nào được đánh thức là mẫu nhiên và chúng ta không thể kiểm soát được thread được đánh thức. notifyAll(): Phương pháp này chỉ đơn giản đánh thức tất cả các thread đang chờ trên object monitor. Mình sẽ nói chi tiết hơn về các method này trong bài giao tiếp giữa các threads.
5.5. yield()¶
Method yield() làm cho thread hiện đang thực thi tạm dừng và cho phép các thread khác thực thi. Mọi người lưu ý, đây chỉ là hint cho scheduler tạm dừng thread, scheduler có thể bỏ qua cái hint này. Method này có thể dùng để tái hiện bug do race condition. Tuy nhiên, method này hiếm khi được sử dụng và mình recommend không dùng method này trong production code.
5.6. join()¶
Method join()
cho phép một thread chờ đợi một thread khác hoàn thành. Điều này có thể hữu ích khi bạn cần đảm bảo hoàn thành một số nhiệm vụ nhất định trước khi tiếp tục. Khi thread A gọi method join()
của thread B, thread A sẽ chuyển sang trạng thái chờ ( RUNNABLE → WAITING
). Nó vẫn ở trạng thái chờ cho đến khi thread B kết thúc.
Giả sử bạn cần thực hiện một số lệnh gọi API đến các endpoints khác nhau lấy dữ liệu đồng thời. Mỗi lệnh gọi API được thực hiện trong một thread riêng biệt và bạn muốn đợi cho đến khi tất cả các thread hoàn thành yêu cầu API của chúng trước khi tổng hợp (aggregate) kết quả.
String[] apiEndpoints = {
"https://api.example.com/data1",
"https://api.example.com/data2",
"https://api.example.com/data3"
};
List<Thread> threads = new ArrayList<>();
List<String> results = new ArrayList<>();
for (String endpoint : apiEndpoints) {
Thread thread = new Thread(() -> {
String response = makeApiCall(endpoint);
synchronized (results) {
results.add(response);
}
});
threads.add(thread);
thread.start();
}
// Wait for all threads to complete
try {
for (Thread thread : threads) {
thread.join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// Process and aggregate results
results.forEach(response -> System.out.println("API response: " + response));
Nếu mọi người thấy bài viết hữu ích thì nhờ mọi người share để nội dung của Ronin được nhiều người biết hơn.
Cám ơn mọi người. 🙏
🧑💻 150+ Ronin Engineers: https://roninhub.com/
✏️ System Design VN: https://fb.com/groups/systemdesign.vn
📚 Tài liệu khác: https://roninhub.com/tai-lieu
🎬 Youtube: https://youtube.com/@ronin-engineer
「Java 8」Optional¶
Java 8
ra đời cùng với một class mới tên là Optional
. Nhiệm vụ của nó là kiểm soát null
hộ chúng ta.
Khái niệm Optional¶
Optional<T>
là một đối tượng Generic
, nhiệm vụ chính của nó là bọc hay wrapper lấy một object khác. Nó chỉ chứa được một object duy nhất bên trong.
Việc bạn lấy giá trị của object bây giờ sẽ thông qua Optional
và nếu object đó null
cũng không sao, vì thằng Optional
kiểm soát nó chặt chẽ hơn là if else
.
Ví dụ bạn có một đối tượng bất kỳ:
Khi chúng ta thực hiện các thao tác, chúng ta có thể kiểm tra như thế này:
Hmmm..... trông thế này thì khác đếch gì if (str != null)
=))) Nhiều bạn sẽ tự nghĩ. Đúng là như vậy, nếu nó chỉ làm được đến đây, thì thôi.. nghỉ mịa đee huhu :((
Bây giờ mình sẽ giới thiệu từng tính năng lần lượt của Optional
để bạn thấy nó kì diệu như nào.
ifPresent¶
ifPresent
nhận vào một Consumer
, nó cũng chỉ là Functional Interface
thôi các bạn. Nhận vào một đối tượng và thao tác trên nó, không return gì cả.
Nếu bạn chưa rõ Functional Interface
và Lambda Expression
thì bạn có thể xem ngay đây, dễ hiểu lém:
Functional Interfaces & Lambda Expressions cực dễ hiểu
orElse() và orElseGet()¶
orElse()
lấy ra object trong Optional
. Nếu null
, trả về giá trị mặc định do bạn quy định
orElseGet()
Tương tự orElse()
nhưng trả ra bằng Supplier interface
map()¶
map()
giúp chúng ta biến đổi đối tượng bên trong Optional
.
mình sẽ ví dụ bằng code dễ hiểu hơn.
code
trông sáng sủa hơn nhiều phải không bạn :3
Trong code ở trên sử dụng Method reference
, khái niệm này mình đã nói chi tiết tại đây:
Hướng dẫn Method Reference và Lambda Expressions
Khái niệm map()
mình có nói chi tiết tại đây:
Stream Trong Java 8 cực dễ hiểu!
filter()¶
filter()
giúp chúng ta kiểm tra giá trị trong Optional
nếu không thỏa mãn điều kiện, trả về empty()
Tới đây mình đã giới thiệu xong với các bạn các tính năng khá hay ho của Optional
. Ngoài việc giúp chúng ta kiểm soát NullException
thì còn giúp code
của chúng ta sáng sủa hơn rất nhiều và thuận tiện hơn trong nhiều trường hợp yêu cầu điều kiện phức tạp
Chúc các bạn học tập thành công. Và chớ quên like và share ủng hộ nhá ahihi :3
String str = null;
// Tạo ra một đối tượng Optional
Optional<String> optional = Optional.ofNullable(str);
// Bây giờ Optional đã wrap lấy cái str.
if (optional.isPresent()) {
System.out.println(opt.get()); // lấy ra cái str mình đã wrapper
}
optional.ifPresent(s -> System.out.println(s));
String b = optional.orElse("Giá trị mặc định");
String b = optional.orElseGet(() -> {
StringBuilder sb = new StringBuilder();
// Thao tác phức tạp
return sb.toString();
});
class Outfit{
public String type;
public String getType() { return type; }
}
class Girl{
private Outfit outfit;
public Outfit getOutfit() { return outfit; }
}
public String getOutfitType(Girl girl){
return Optional.ofNullable(girl) // Tạo ra Optional wrap lấy girl
.map(Girl::getOutfit) // nếu girl != null thì lấy outfit ra xem kakaka :3 ngược lại trả ra Optional.empty()
.map(Outfit::getType) // nếu outfit != null thì lấy ra xem type của nó
.orElse("Không mặc gì"); // Nếu cuối cùng là Optional.empty() thì trả ra ngoài Không mặc gì.
}
public String getOutfitType(Girl girl){
return Optional.ofNullable(girl) // Tạo ra Optional wrap lấy girl
.map(Girl::getOutfit)
.map(Outfit::getType)
.filter(s -> s.contains("bikini")) // Nó chỉ chấp nhận giá trị bikini, còn lại dù khác null thì vẫn trả ra ngoài là Optiional.empty()
.orElse("Không mặc gì"); // Nếu cuối cùng là Optional.empty() thì trả ra ngoài "Không mặc gì".