Thứ Bảy, 15 tháng 2, 2020

Tìm hiểu thuật toán Backtracking và ứng dụng giải Sudoku

 Mình để ý là trên blog mình viết khá nhiều thuật toán về AI, viết cả về máy học. Và như các bạn cũng thấy, những thuật toán này giải được những bài toán khá phức tạp, và do đó những thuật toán này cũng phức tạp không kém :)). Nhưng đã bao giờ bạn nghĩ chúng ta có thể giải những bài toán phức tạp bằng sức mạnh lưu trữ và tốc độ tính toán của máy tính chưa? Chúng ta sẽ không phải cất công suy nghĩ quá nhiều về thuật toán nữa. Máy tính sẽ lo hết cho chúng ta. ( Viết tới đây thì mình cũng thấy nó na ná như là máy học thật ^^)
 Hôm nay mình sẽ giới thiệu các bạn 1 thuật toán có tên là Backtracking, một thuật toán siêu đơn giản nhưng sức mạnh của nó thì vô cùng lớn.
Các ứng dụng mình viết trong bài này đều đã được host lên ở đây, các bạn có thể vào xem nhé.

Backtracking là gì?

Backtracking algorithm là kĩ thuật sử dụng các tổ hợp phép thử để giải quyết 1 vấn đề tính toán nào đó

Để dễ hiểu, ta đi vào ví dụ về mở khóa mã pin chẳng hạn. Ta có 1 mã pin có 4 chữ số từ 0-9. Để giải được mã pin này, thì có 1 cách đơn giản đó là phải thử nghiệm từng khả năng để xem có đúng không.

Đầu tiên ta có thể điền vào ô đầu là số 0.
Tiếp tục điền vào các ô tiếp theo.
Giả sử mã pin 0000 là mã pin sai. Vậy bước tiếp theo của backtracking là gì? Câu trả lời là thuật toán này sẽ thử khả năng tiếp theo của ô cuối cùng. Và ta sẽ có mã pin tiếp theo như sau
Và nếu mã pin 0001 cũng là mã pin sai. Chúng ta sẽ từ từ tăng giá trị hàng đơn vị lên 1 đơn vị. Ta sẽ có các mã pin từ 0002 đến 0009.
Vấn đề là các mã pin trên đều sai. Lúc này, backtracking sẽ quay lại ô trước hàng đơn vị. Đây cũng là lí do thuật toán này mang tên backtracking ( truy vết). Ta sẽ có mã pin được nhập như sau.
Lúc này, quá trình thử nghiệm lại tiếp tục với số hàng đơn vị, ta sẽ có các mã pin từ 0010 đến 0019. Và nếu những mã pin này sai, thì backtracking lại tiếp tục để cho ra các số 0020 đến 0029.
Cứ như thế, quá trình chạy sẽ kiểm thử các mã pin từ 0000 đến 9999. Và tất nhiên, khi mã pin đúng thì thuật toán sẽ kết thúc.

Ứng dụng giải sudoku bằng backtracking

Tương tự như việc giải mã pin, backtracking sẽ được chúng ta áp dụng để giải ma trận sudoku.
Chúng ta sẽ áp dụng kiểm thử từng khả năng của mỗi ô. Quá trình này sẽ kết thúc khi ta đạt được ma trận đúng của sudoku.

solve(maze){
        let currentCell = maze.getNextUnsolveCell();
        //already solved maze
        if(currentCell===undefined) {
            return true;
        }
        let availableValues = maze.getAvailableValues(currentCell);
        for(let value of availableValues){
            maze.setValueForCell(currentCell,value);
            if(this.solve(maze)){
                return true;
            }
        }
        maze.setValueForCell(currentCell,0);

        return false;
    }
Đoạn code trên chính là backtracking trong việc giải sudoku. Quá trình backtracking sẽ được thực hiện trong lập trình bởi đệ quy.
Các bước của thuật toán có thể được viết lại như sau:
Bước 1 : lấy ô chưa được giải tiếp theo để thử nghiệm
Bước 2: nếu không có ô nào để giải nữa thì ma trận đã giải xong
Bước 3: tìm những giá trị mà ô hiện tại có thể tồn tại. ( Thực ra bước này không cần thiết, bạn có thể cho các giá trị ứng thử từ 1 đến 9 cũng được. Mình thêm phần này  vào để tăng tốc độ tính toán lên thôi.)
Bước 4: với mỗi giá trị có thể ứng thử, ta ứng thử nó vào ô hiện tại.
Bước 5: thực hiện đệ quy lại từ Bước 1.
Bước 6: nếu đã thử hết tất cả giá trị ứng thử rồi mà không được thì reset lại ô hiện tại về 0
Các bạn để ý, thuật toán của chúng ta chẳng có gì là phức tạp cả. Nhưng nó đã giải quyết được 1 bài toán rất phức tạp là giải sudoku. Cái cốt lõi đằng sau việc này là chúng ta đã lợi dụng khả năng lưu trữ và tốc độ xử lý của máy tính để truy vết các phép thử.

Mình đã viết 1 ứng dụng để giải sudoku bằng javascript ( đoạn code demo ở trên đã thay đổi sau khi mình cập nhật hiệu ứng animation cho ứng dụng). Code các bạn có thể clone về ở github của mình.
Cám ơn các bạn đã ghé thăm và đọc bài trên blog của mình ^^
Share:

Chủ Nhật, 9 tháng 2, 2020

Thuật giải A* và ứng dụng tìm đường đi trong ma trận

 Hôm nay mình lại có nhã hứng làm 1 bài về AI. Và thứ mình muốn giới thiệu đó là thuật giải A*. Chúng ta sẽ đi tìm hiểu thuật giải A* này là gì và thử làm 1 cái app ứng dụng thuật giải này thử nhé.
Các ứng dụng mình viết trong bài này đều đã được host lên ở đây, các bạn có thể vào xem nhé.

Thuật giải A* là gì?

Thuật giải A* là thuật giải tìm đường đi ngắn nhất trong đồ thị. Đây là 1 giải thuật Heuristics, tức là đây là 1 giải thuật mang tính xấp xỉ, kết quả đầu ra có thể không hoàn toàn chính xác. Vậy câu hỏi đặt ra là: Tại sao không sử dụng một thuật giải tuyệt đối, kết quả đầu ra luôn luôn chính xác mà lại phải sử dụng thuật giải xấp xỉ như thế này?

Câu trả lời nằm ở chi phí tính toán. Để đưa ra được 1 kết quả chính xác, chúng ta phải vét cạn đồ thị ( nếu câu này sai thì mọi người hãy comment cho mình biết nhé), nhưng với thuật giải A*, nó không vét cạn, do đó, chi phí tính toán sẽ là thấp hơn.
Nói ra thì dài dòng, chốt lại là: Thuật giải A* là thuật giải tìm đường đi ngắn nhất trong đồ thị mà không vét cạn do đó mang tính xấp xỉ.

Thuật giải A* hoạt động như thế nào?

Trong thuật giải A*, mỗi 1 điểm trong đồ thị sẽ mang theo nó 3 giá trị cốt lõi:
   g: đây là chi phí cần phải bỏ ra để đi từ điểm bắt đầu đến điểm hiện tại.
   h: đây là chi phí ước lượng để đi từ điểm hiện tại đến điểm kết thúc.
   f : giá trị này thì dễ tính: f = g + h.
Dưới đây là thuật giải A*:
Input: 1 tập các điểm trong đồ thị với trọng số tương ứng. Node start và node end.
Output: Đường đi ngắn nhất từ node start đến node end.
Thuật giải gồm các bước sau đây:
B1: Khởi tạo 1 tập Open = rỗng . Khởi tạo tập Close = rỗng.
B2: Gán điểm Current = start để tính toán. Đưa điểm start vào tập Close.
B3: nếu current là end thì truy ngược lại đường đi dựa theo giá trị parent.
B4: Tìm tất cả những điểm lân cận (có thể đi được) từ điểm current và đưa vào 1 tập Neighbors. Duyệt qua từng điểm trong neighbors.
   B4.1: nếu điểm neighbor đang xét đã có trong tập Close => bỏ qua
             gán neighbor g = current g + cost(current, neighbor)                 
                    neighbor f = neighbor g + neighbor h.
                    neighbor parent = current
   B4.2: nếu điểm neighbor đang xet chưa có trong tập Open thì đưa neighbor vào tập Open
   B4.3: nếu neighbor đã có trong tập Open.
             Nếu neighbor trong open có g > neighbor g. Thì đưa neighbor vào thay thế cho neighbor trong open.
B5: Nếu Open còn phần tử thì lấy phần tử có f nhỏ nhất để làm current, đưa current vào tập close và quay lại bước 3.
B6: nếu Open rỗng thì không tìm được đường đi.

Chắc chắn đọc xong đoạn thuật giải trên các bạn chẳng hiểu được gì đâu. Vì vậy chúng ta  sẽ đi vào ví dụ cho dễ hiểu nhé:
Ta có đồ thị sau:
Điểm bắt đầu là A, điểm kết thúc là L. Với các giá trị h (ước lượng) của từng node tới L như sau:

POINT
h value
A
9
B
8
C
8
D
7
E
6
F
5
G
4
H
4
K
3
L
0
Bây giờ thì bắt đầu chạy thử thuật toán với ví dụ nhé:
Step
Open Set
Close Set
Current
Neighbors
Description
1
{}
{}
A(g=0, h=9, f=9)


2
{}
A(g=0, h=9, f=9)
A(g=0, h=9, f=9)
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
G(g=6,h=4,f=10,parent=A)

3
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
G(g=6,h=4,f=10,parent=A)
A(g=0, h=9, f=9)


Vì trong tập Open, G có f bé nhất nên lấy làm current.
4
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
G(g=6,h=4,f=10,parent=A)
C(g=8,h=8,f=16,parent=G)
K(g=9,h=3,f=12,parent=G)

5
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
K(g=9,h=3,f=12,parent=G)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)


Vì trong tập open, B có f nhỏ nhất nên lất làm current
6
C(g=7,h=8,f=15, parent=A)
K(g=9,h=3,f=12,parent=G)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)

B(g=3,h=8,f=11, parent=A)

D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)

7
C(g=7,h=8,f=15, parent=A)
K(g=9,h=3,f=12,parent=G)
D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)



Vì trong tập open, K có f nhỏ nhất nên lất làm current
8
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
K(g=9,h=3,f=12,parent=G)
D(g=12,h=7,f=19,parent=K)
L(g=16,h=0,f=16,parent=K)

9
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)
L(g=16,h=0,f=16,parent=K)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)


Vì trong tập open, E có f nhỏ nhất nên lất làm current
10
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=16,h=0,f=16,parent=K)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)

E(g=7,h=6,f=13,parent=B)

F(g=8,h=5,f=13,parent=E)
H(g=15,h=4,f=19,parent=E)

11
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=16,h=0,f=16,parent=K)
F(g=8,h=5,f=13,parent=E)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)



Vì trong tập open, F có f nhỏ nhất nên lất làm current
12
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=16,h=0,f=16,parent=K)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)
F(g=8,h=5,f=13,parent=E)
F(g=8,h=5,f=13,parent=E)

L(g=10,h=0,f=10,parent=F)
Vì L trong neighbor có g < L trong open, nên thay L trong open bằng L trong neighbor
13
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=10,h=0,f=10,parent=F)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)
F(g=8,h=5,f=13,parent=E)


Vì trong tập open, L có f nhỏ nhất nên lất làm current
14
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)
F(g=8,h=5,f=13,parent=E)
L(g=10,h=0,f=10,parent=F)
L(g=10,h=0,f=10,parent=F)

Vì L là điểm end. Nên thuật toán kết thúc. Truy ngược đường đi theo parent ta có:
A -> B -> E -> F -> L

Nhìn vào cách chạy thuật toán trong bảng trên, mình mong là các bạn đã hiểu được thuật giải A* hoạt động như thế nào.
Và cũng trong một khoảng thời gian rảnh rỗi và có hứng thú, mình cũng đã viết 1 đoạn chương trình bằng javascript hiện thực hóa thuật giải A* này trong việc tìm đường đi trong ma trận.

Code mình đã up sẵn lên github ở đây. Bạn chỉ cần clone về và chạy thôi nhé.
Cám ơn các bạn đã xem bài viết này của mình. Nếu có thắc mắc gì thì cứ comment vào blog. Nếu được mình sẽ giải đáp.
Share:

Thứ Ba, 11 tháng 6, 2019

Tìm hiểu về Spring Boot

Bàn về Spring Framework, ông bụt framework này giúp đỡ chúng ta rất nhiều thứ, nhưng mà nhiều thứ quá thì lại không tốt. Spring framework là một framework rất lớn với hệ sinh thái khủng khiếp mà nó mang lại, nhưng vì thế nên độ tập trung về một mảng nào đó của nó vẫn cần phải có sự config của người lập trình khá nhiều. Để build và deploy application với spring vẫn cần config nhiều bước, vậy nên nhưng đứa con lập trình bắt đầu thấy bụt nhà không thiêng. Và bắt đầu than khóc để chờ một ông bụt khác xuất hiện, ổng có tên gọi: sờ pờ rin bụt, tên quốc tế của ông là: Spring boot.
Spring boot quan sát những sự config từ trước, và rút ra được những kinh nghiệm riêng. Từ đó nó biết tự động config hết cho chúng ta, vậy nên lượng config còn lại cho lập trình viên là ít hơn trước rất nhiều. Và cái tuyệt vời của Spring boot application là nó "Stand alone". Điều này có nghĩa là sao? Nếu như bạn build 1 Application với Spring framework, bạn sẽ có 1 war file, từ file này bạn có thể deploy nó lên tomcat hay bất cứ container nào bạn muốn để cho nó chạy. Còn với spring boot, bạn có nguyên cho mình 1 application có thể chạy thẳng ngay luôn, nó chạy 1 cách độc lập chỉ với dòng command đơn giản.
Nhìn nhận lại vấn đề thì như sau: từ lúc bạn chập chững code java ở trường, những dòng code của bạn là những dòng code tự tay bạn viết ra. Sau đó, bạn được làm những bài tập lớn, quá nhiều tác vụ phải giải quyết, thế là bạn nhờ vào sự trợ giúp của các java library. Đến khi đi làm, bạn được giao cho 1 dự án nhỏ, nhưng để tránh khởi tạo từ những dòng code, những thư mục đầu tiên, bạn dựa lại dựa vào spring framework. Sau dự án này, bạn lại cảm thấy nên chỉ tập trung vào phần bussiness của dự án thôi, thế là bạn sử dụng Spring boot.
 Quá trình đi lên như thế, bạn sẽ thấy công việc của bạn nhàn hạ hơn, tốn ít công sức hơn. Nhưng cái gì cũng có lợi và hại, mọi thứ chuẩn bị sẳn hết cho bạn, thì sự tùy biến của bạn có thể thực hiện sẽ càng giảm. Vậy tự hỏi, sau Spring boot, còn có thể có thêm thứ gì có thể giúp chúng ta code ít hơn nữa không. Có rồi đấy: JHipster là 1 trong số đó. Ông nội này hỗ trợ cả về client side, server side, deployment và cả CI/CD nữa.
Share:

Tìm hiểu về Spring framework

 Bạn là một lập trình viên đang viết một ứng dụng java, ứng dụng của bạn có rất nhiều class, đó là điều chắc chắn. Những class này có sự tương tác với nhau, class này sử dụng class kia, class kia sử dụng class nọ. Điều này tạo ra mớ hỗn độn như hình dưới đây.
Và thông thường khi lập trình, để Class B sử dụng được class A, bạn có thể code như sau:

public class B {
   A a = new A();
}
Nhưng với cách khởi tạo như trên, chúng ta sẽ không thể sử dụng được những class chứa thông tin chung cho nhiều class khác nhau. Ví dụ như class G chứa thông tin chung cho class F và class E. Nếu như ta khởi tạo theo cách trên, F và E sẽ có những instance khác nhau của G, do vậy dữ liệu mà 2 class này nhận được là khác nhau.
Để giải quyết vấn đề này, chúng ta có thể sử dụng Singleton Pattern. Nhưng nếu số lượng các class dạng này nhiều lên thì sao? Làm sao để chúng ta có thể xử lý được đống singleton class này và những quan hệ rối rắm giữa chúng với các class khác. Đó là lúc Spring framework xuất hiện như vị bụt trong tấm cám để giúp những người lương thiện như chúng ta.
Spring framework sẽ giúp ta quản lý những instance này bằng application context. Nơi đây sẽ chứa tất cả những instance mà chúng ta muốn, những cái instance này trong spring được gọi là "Bean". Từ những bean này, nếu như bạn muốn 1 class A nào đó sử dụng bean B, spring sẽ giúp bạn truyền bean B vào class A. Việc truyền bean này còn có thể thực hiện giữa các bean với nhau. Ví dụ bean C muốn dùng bean E và bean F, spring cũng hỗ trợ bạn truyền 2 bean này luôn. Và cái quá trình truyền bean này, được gọi là Dependency injection.
Application của bạn muốn hoành tráng, thì ngoài việc chỉ giải quyết vấn đề trên, còn phải quan tâm tới việc tương tác với cơ sở dữ liệu nữa. Với việc này, bạn chắc hẳn đã nghe đến JDBC, bạn có thể sử dụng nó để tương tác với database. Nhưng thực sự, cái công nghệ này đã quá cũ rồi, chắc cũng chẳng còn mấy ai xài nữa. Vậy nên, Spring lại tiếp tục giúp bạn việc này bằng cách cung cấp những api giúp bạn tương tác với database dễ dàng hơn.
 Spring framework hỗ trợ chúng ta rất nhiều việc khác, như hỗ trợ trong việc xây dựng 1 java web application bằng spring mvc. Đến hiện tại, spring đã trở thành 1 cái gì đó như 1 hệ sinh thái ấy, nó có rất nhiều spring project hỗ trợ bạn trong nhiều lĩnh vực: như về bảo mật có spring security, về việc làm việc với mongodb ta có spring mongodb.... rất rất nhiều thứ khác nữa mà bạn có thể khai thác với framework này.
Share:

Sự khác nhau giữa Library và Framework

 Tiêu đề của bài viết này là "sự khác nhau" giữa library và framework, nhưng có lẽ mình sẽ vẽ một bức tranh khác để mọi người dễ hiểu hơn về 2 khái niệm này. Từ đó mọi người có thể tự suy luận ra cho bản thân sự khác nhau giữa 2 loại trên.
 Chắc hẳn mọi người đã từng nghe tới nghề mộc nhỉ? Đó là nghề người ta làm những món đồ liên quan đến gỗ, mình rất may mắn vì đã được tới 1 xưởng chế tác tượng gỗ thủ công. Ở đó, người ta biến đổi 1 khúc gỗ vô tri thành những tác phẩm rất đẹp mắt bằng những dụng cụ như búa, đục, sơn mài... Và mình cũng đã từng được đến xưởng sản xuất bàn gỗ, từ những khúc gỗ công nghiệp, cũng với những cái búa, những đinh tán, những chiếc đục, những hộp sơn... người ta cũng có thể chế tạo ra được những chiếc bàn rất đẹp. Cả 2 xưởng tạo ra những sản phẩm khác nhau, nhưng họ lại dùng chung những dụng cụ như búa, đục và sơn. Những dụng cụ này đã có sẵn ở cửa tiệm, những người thợ mộc chỉ cần ra tiệm lấy về và chế tác, việc của họ là chỉ quan tâm đến việc đục đẽo và lắp ráp gỗ, còn những dụng cụ kia thì không cần phải chế lại từ đầu. Những dụng cụ này, bạn có thể hiểu nó là Library.

 Trong lập trình cũng vậy, khi bạn lập trình một ứng dụng, có rất nhiều thứ bạn phải quan tâm để xây dựng, nhưng rất nhiều trong những thứ ấy đã được những người khác làm sẵn, bạn có thể lấy về và xài. Việc này giúp bạn giảm thiểu được lượng công việc. Những phần code có sẵn ấy, người ta gọi nó là Library. 
 Quay lại với việc chế tạo bàn gỗ. Có rất nhiều xưởng bàn gỗ khác nhau, họ chế tạo ra những mẫu bàn khác nhau, nhưng lại dùng chung một mớ dụng cụ như : búa, sơn bóng, đinh, bào gỗ... và họ cũng thực hiện chung những công đoạn như chế tác chân bàn, mài nhẵn mặt bàn, sơn bóng cho bàn... Như vậy, những dụ cụ và công việc này đều theo 1 quy chuẩn nào đó, và ta có thể chế tạo ra 1 cái máy thần kì, chiếc máy này chỉ cần ta bấm chọn loại bàn cần tạo. Nó sẽ tự động sử dụng gỗ, búa, đục, sơn để chế tạo ra chiếc bàn đó cho ta. Mặc dù đây là chiếc bàn giả tưởng nhưng thật sự là nó quá ngon lành đúng không? Nó đảm nhận hầu hết mọi việc, chỉ cần ta cho nó 1 ít thông tin cần làm mà thôi. Và cái này, ta có thể gọi nó là Framework.

 Như vậy, Framework là tập hợp những Library có sự liên quan với nhau trong 1 phạm vi nào đó, nó định nghĩa việc tương tác giữa các library với nhau để giúp ta dễ dàng trong việc xây dựng ứng dụng. Tất nhiên Framework được tạo ra để hỗ trợ bạn trong một phạm vi nào đó thôi. Giống như chiếc máy thần kì phía trên kia, nó có thể giúp bạn tạo ra rất nhiều loại bàn khác nhau, nhưng nó không thể giúp bạn tạo ra 1 tác phẩm điêu khắc được.
 Mình mong qua bài viết này, bạn có thể tự rút ra cho bản thân sự khác nhau giữa 2 khái niệm trên.
Share:

Serialization trong java

 Trước h mình đã gặp khái niệm này rất nhiều lần rồi, từ đồng nghiệp cho tới code, Serialization xuất hiện cực kì nhiều, mà mình thì lại không tìm hiểu kĩ về khái niệm này lắm. Cho tới dạo vừa rồi, mình có làm 1 task liên quan đến logging management, và khi in ra dữ liệu thì mình thấy một dãy dài các mã hexa. Điều này làm mình tò mò và tìm hiểu sâu, cuối cùng mình nhận ra cái mã hexa đó là từ thằng cha serialization mà ra. Vậy Serialization là gì, và trong java nó hoạt động ra làm sao? Hôm nay mình sẽ vén bức tấm áo của anh chàng này ra cùng các bạn nhá.

Serialization là gì? Deserialization là gì?


Serialization là chuyển một đối tượng object sang dạng bytes. Còn deserialization là chuyển dạng bytes thành một đối tượng object.
Thực sự khái niệm này rất đơn giản, có thể hiểu rằng đây giống như là quá trình mã hóa và giải mã mà thôi. Nhưng tại sao lại phải cần serialization và deserialization? Điều này chúng ta cần phải nhìn vào thực tế, hiện nay các ứng dụng lớn thường gồm nhiều module được kết nối với nhau, khi các module này muốn trao đổi dữ liệu thì bắt buộc chúng phải trao đổi qua 1 phương thức nào đó. Mà để phương thức trao đổi này hoạt động ấy, thì nó ít nhất phải biểu diễn được thông tin mà các module đang muốn trao đổi. Mà tính ra khi lập trình, các module có cả trăm loại object, vậy thì làm sao mà những phương thức trao đổi có thể biểu diễn được hết? Vậy nên người ta mới nghĩ ra cái vụ serialization và deserialization.
 Giả sử module A muốn trao đổi thông tin với module B dựa trên object x đi. Chỉ cần A serialize x thành bytes, sau đó B sẽ deserialize bytes thành x. Thế là được rồi chứ gì nữa. Và trong java, việc này được hỗ trợ khá tốt, người code chỉ cần thêm vài kí tự vào là có thể làm được.

Serialization và Deserialization trong java


Để 1 object trong java có thể được serialized hay deserialized, thì chỉ cần để object đó là implements ông nội java.io.Serializable interface. Mà cái interface này cũng khỏe cái nữa là nó chỉ là 1 marker interface, nó chả có phương thức nào hết nên mình cũng không cần bận tâm phải hiện thực gì hết.
Ví dụ ta có 1 object Student như dưới đây:
   public class Student {
  public String name = "cuong";
  public Integer age = 24;
 }
Muốn object này có thể được serialized và deserialized thì chỉ cần cho nó implements Serializable như sau:
   public class Student implements Serializable{
  public String name = "cuong";
  public Integer age = 24;
 }
Thế là xong, ta thử viết vài dòng code nữa xem thử nó có hoạt động không nhá. Đầu tiên là ta serialize nó thử nè:
   public static void main(String args[]) throws IOException {
  FileOutputStream fos = new FileOutputStream("temp.out");
  ObjectOutputStream oos = new ObjectOutputStream(fos);
  Student ts = new Student();
  oos.writeObject(ts);
  oos.flush();
  oos.close();
 }
Ở đây, java cung cấp cho chúng ta hàm writeObject(). Hàm này có khả năng serialize, nên ta chỉ việc bóc ra mà xài thôi. Đoạn code trên có nhiệm vụ serialize class student của chúng ta vào 1 file tên là temp.out. Sau khi mở file temp.out ra bằng hex editor, chúng ta có thể thấy mớ dữ liệu sau:
Đây chính xác là biến ts đã được serialize. Bây giờ, hãy thử viết code để thực hiện deserialize xem nào:

   public static void main(String[] args) throws Exception {
  FileInputStream fis = new FileInputStream("temp.out");
  ObjectInputStream oin = new ObjectInputStream(fis);
  Student ts = (Student) oin.readObject();
  System.out.println("name =" +ts.name + ",age = " + ts.age);
 }
Kết quả sẽ trả về cho bạn name=cuong, age = 24. Để kết thúc bài viết ngắn này thì mình cũng có chút lưu ý về serialization và deserialization trong java:
- các thuộc tính static và transient sẽ không được serialize. Ví dụ bạn đổi code thành: public transient Integer age = 24; , thì age sẽ field này sẽ không được serialize.
- Lớp cha implements Serializable rồi thì lớp con khỏi phải implements
- Một đối tượng được Serialized khi tất cả các đối tượng con trong nó phải được implements Serializable. Với ví dụ trên thì bạn thấy ta có 2 đối tượng con là name (String) và age(Integer). Sở dĩ 2 đối tượng này serialized được là nhờ chúng đã implements Serializable.
Share:

Thứ Năm, 25 tháng 4, 2019

Lambda expression trong Java 8

Nếu là 1 java developer, và đã xài qua java 8, chắc chắn bạn sẽ nghe nói tới lambda expression. Đây là 1 phần mới đc thêm vào trong java 8, và nó thực sự rất hữu ích nếu chúng ta biết tận dụng để viết code. Nhưng trước khi đi vào tìm hiểu lambda expression, bạn phải tìm hiểu một khái niệm khác gọi là : Functional interface.

Functional interface là gì?

Functional interface là một interface chỉ có chứa 1 abstract method duy nhất.
Ví dụ, interface dưới đây là functional interface.
public interface MyInterface{
    double getValue();
}
Còn interface dưới đây không phải là functional interface, vì có đến 2 abstract method.
public interface MyInterface{
    double getValue();
    double getUnit();
}
Trước khi có lambda expression, để sử dụng MyInterface chúng ta phải code như sau:
MyInterface myInterface = new MyInterface() {
   @Override
   public double getValue() {
    return 1;
   }
  };
Đoạn code trên chúng ta đã hiện thực hàm getValue để trả về 1. Nhưng vấn đề nằm ở chỗ, MyInterface của chúng ta chỉ có duy nhất 1 method nên việc ta phải viết lại cả tên method khi hiện thực là thừa thải. Đó là lí do ông nội lambda expression ra đời.

Lambda expression

Lambda expression trong java là dấu "->", nó giúp ta định nghĩa 1 method bằng dòng code siêu ngắn gọn vì ta không cần phải viết lại tên method đang hiện thực nữa. Ví dụ ta có hàm getValue() khi viết bình thường sẽ như sau:
double getValue(){ return 1;}
Nhưng với lambda expression ta sẽ viết được như sau:
() -> 1
Bạn thấy đấy, code của chúng ta ngắn hơn 1 cách đáng kinh ngạc. Minh sẽ đưa ra một số ví dụ cho những trường hợp khác khi ta dùng lambda expression. Ví dụ ta có 1 method void như sau:
void sayHello(){
    System.out.println("Hello");
}
Method trên có chức năng viết ra "hello". Với lambda expression, ta có thể viết như sau:
() -> System.out.println("Hello");
Cũng với method trên, nếu ta muốn in ra hai dòng là "hello" và "world" thì ta có:
void sayHello(){
    System.out.println("Hello");
    System.out.println("World");
}
Với lambda expression:
() -> {
    System.out.println("Hello");
    System.out.println("World");
}
Với method có parameter như method sau:
void sayHelloToPerson(String name){
    System.out.println("Hello" + name);
}
Ta cũng viết lại được dưới dạng lambda expression:
(name) -> System.out.println("Hello" + name);
Vậy ta dùng lambda expression như thế nào? Lamdba expression ko phải cứ muốn tùy tiện viết là viết đâu nhé. Nó phải viết dựa trên 1 functional interface đã có từ trước. Ví dụ với "MyInterface" của chúng ta, bây giờ ta có thể sử dụng nó bằng cách:
MyInterface myInterface = () -> 1;
myInterface.getValue(); //trả về 1
Với lamdba expression, mọi thứ đều trở nên ngắn hơn, vậy nên hãy tin dùng lambda expression.!!! Ta đi tiếp 1 ví dụ nữa nhé! Giả sử ta có 1 functional interface thứ 2.
public interface MyInterface2{
    double sum(double a, double b);
}
Ta có thể sử dụng interface này với lambda expression như sau:
MyInterface2 myInterface2 = (a,b) -> a+b;
System.out.println(myInterface2.sum(2, 3)); //in ra 5.0
Chúng ta cũng có thể tạo ra 1 interface generic hơn như sau:
public interface MyInterface {
 public T func(T t);
}
Từ đó ta có thể định nghĩa interface đó với đầu vào là String
MyInterface myInterface1 = (str) -> str + " is my friend";
System.out.println(myInterface1.func("cuong")); //cuong is my friend
Hoặc có thể định nghĩa parameter là Double
MyInterface myInterface2 = (number) -> number + 1;
System.out.println(myInterface2.func(1.0)); //2.0

Lambda expression kết hợp Stream trong java 8

Java 8 không chỉ show hàng mỗi ông nội lambda, mà còn cho chúng ta thêm 1 thứ nữa gọi là stream. 2 cái mới này nếu ta biết sử dụng nó thì đúng là code ngắn 1 cách kinh khủng khiếp. Ví dụ ta có đối tượng:
public static class Name {
  public Name(String first, String last){
   this.firstName = first;
   this.lastName = last;
  }
  private String firstName;
  private String lastName;
  public String getFirstName(){
   return firstName;
  }
  public String getLasttName(){
   return lastName;
  }
 }
Và ta có danh sách 3 người như sau:
List names = Arrays.asList(new Name("le","cuong"),new Name("nguyen","hung"),new Name("le","nam"));
Nhiệm vụ bây h là ta sẽ lọc ra những người họ "le", và sau đó in ra tên (lastname) của người đó. Nếu như không sử dụng stream và lambda expression, thực sự lượng code của bạn phải viết ra khó có thể viết được trong 1 hay 2 dòng. Nhưng với stream + lambda expession, code của bạn siêu đẹp và siêu ngắn như sau:
System.out.println(names.stream().filter(name -> name.getFirstName().equals("le")).map(name -> name.getLasttName()).collect(Collectors.toList())); //in ra [cuong, nam]
Quá ngắn đúng không? Và đó là lambda expression. Bài viết được viết dựa trên bài viết của programiz. Cám ơn mọi người đã ghé đọc.
Share:
Được tạo bởi Blogger.