+4

Học Collection Java Bằng Cách Độc Lạ Bình Dương (P1)

Vừa qua mình nhận được một vài câu hỏi về collection (Arraylist, LinkedList, Set, …) Và lúc đó mình chợt nhận ra rằng bản thân đã quên mất về các collection. Trong toàn bộ dự án dù lớn hay bé mình đều chỉ dùng ArrayList cả dù rằng biết nó sẽ không tối ưu. Cho nên hôm nay mình sẽ cùng các bạn tìm hiểu về collection trong java. Thú thật mình đã học qua về nó nhưng có lẽ chính việc học kiến thức một cách khô khan khiến mình quên mất đi tất cả. Cho nên trong bài viết này mình sẽ tìm hiểu theo một cách mới mẻ hơn. Đó chính là đi đọc mã nguồn của các collection. Oke let gâuuuuuu.

Sơ đồ sơ khai về các collection.

Screenshot from 2024-06-03 16-33-33.png ( Nguồn sưu tầm ) Ở đây mình cần một sơ đồ sơ khai để dễ hình dung về cấu trúc của các collection trước rồi mình sẽ tự vẽ lại nó ở phần sau.

I - ArrayList

Bây giờ ta sẽ đi đọc mã nguồn của ArrayList bằng cách:

Bước 1: Khởi tạo ArrayList Screenshot from 2024-06-03 14-06-11.png

Bước 2: Mình sẽ đưa trỏ chuột vào ArrayList và ấn ctrl + click (Trên intellij bạn có thể dùng ctrl + b) Screenshot from 2024-06-03 14-07-26.png Như ở đây ta thấy mã nguồn của ArrayList có: extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable. Tất nhiên ta sẽ không đọc tất cả mình sẽ tiếp tục vào AbstractList<E> bằng cách ctrl + click như trên. Screenshot from 2024-06-03 14-08-51.png Mình sẽ tiếp tục như thế cho tới khi tới tận cùng hay chính là gốc cha nhé. Và sau khi một hồi loay hoay thì mình cũng đã vẽ lại được 1 sơ đồ như sau đối với ArrayList. Screenshot from 2024-06-03 14-18-20.png Trên thực tế mọi thứ có thể phức tạp hơn nữa nhưng cốt lõi ta vẫn không cần quan tâm tới quá nhiều thứ như này. Ví dụ: RandomAccess, Cloneable, Serializable thực chất nó cũng chỉ cung cấp thêm tính năng cho ArrayList là chính. Thứ chúng ta quan tâm vẫn là cấu trúc cấu tạo của ArrayList mà thôi. Vậy nên ở đây mình sẽ tối giản lại sơ đồ này. Screenshot from 2024-06-03 14-21-51.png Oke như thế này là đủ.

Nhìn vào sơ đồ này ta sẽ đọc từ gốc tới ngọn nên mình sẽ vào Iterable để tìm hiểu.

Iterable

Screenshot from 2024-06-03 14-24-56.png

Đập vào mắt mình là dòng comment:

Implementing this interface allows an object to be the target of the enhanced for statement (sometimes called the "for-each loop" statement).

Hiểu nôm na thì nếu mình implement interface này mình sẽ có thể dùng được chức năng for each thần thánh. Ô vậy ra trước giờ mọi thứ mình làm thật đơn giản và nhờ thứ này. 10 điểm cho em nó 😂

Để cho mọi thứ thật tuyệt vời mình sẽ test luôn em nó:

public class SimpleIterable implements Iterable<Integer> {
 private final int[] numbers;


 public SimpleIterable(int[] numbers) {
   this.numbers = numbers;
 }


 @Override
 public Iterator<Integer> iterator() {
   return new Iterator<Integer>() {
     private int index = 0;


     @Override
     public boolean hasNext() {
       return index < numbers.length;
     }


     @Override
     public Integer next() {
       if (!hasNext()) {
         throw new NoSuchElementException();
       }
       return numbers[index++];
     }
   };
 }


 public static void main(String[] args) {
   int[] nums = {1, 2, 3, 4, 5};
   SimpleIterable iterable = new SimpleIterable(nums);


   for (int number : iterable) {
     System.out.println(number);
   }
 }
}

Ở đây mình khởi tạo 1 class có chứa 1 mảng và dùng constructor để khởi tạo. Vì đã implement nên mình cần override để triển khai các abstract method. Ở đây có 3 cái cần override y như vòng lặp for vậy:

  • Bắt đầu từ đâu ? private int index = 0;
  • Đến đâu là dừng ? return index < numbers.length;
  • Bước nhảy bao nhiêu ? return numbers[index++]

Oke vậy là ta đã hiểu được qua Iterable rồi đó. Và đọc trong ArrayList mình tìm được triển khai của Iterable trong ArrayList class Screenshot from 2024-06-03 14-52-54.png

Collection

Screenshot from 2024-06-03 14-54-01.png

Đọc qua Collection chúng ta cũng sẽ thấy các phương thức hay sử dụng nằm ở đây. Ta có thể hiểu Collection chính là bộ khung cơ bản chứa các phương thức mà các collection implement sử dụng. Qua đây ta cũng có cách để xem liệu 1 collection có những phương thức nào thông qua việc đọc mã nguồn của nó.

SequencedCollection

Screenshot from 2024-06-03 15-01-34.png Thực chất interface này cũng dùng để định nghĩa các phương thức cho việc cấu tạo collection. Bản chất ArrayList hay LinkedList hay collection nào cũng là cấu trúc dữ liệu liên kết. Nghĩa là 1 List gồm nhiều Node được nối với nhau thông qua con trỏ (Bạn có thể tìm hiểu thêm về nó). Vậy nên những phương thức trong interface này như getFirst, add, … khá là dễ hiểu đúng không nào.

List

Screenshot from 2024-06-03 15-01-34.png

Vẫn là cung cấp thêm 1 vài tính năng của List.

Việc chia ra làm nhiều interface implement nhau sẽ giúp module hóa các việc khác nhau qua đó nếu như một class nào chỉ cần dùng 1 nhánh vào đó có thể implement vào.

AbstractList

Screenshot from 2024-06-03 15-07-52.png

Dịch sang tiếng việt nôm na sẽ là: Đây là lớp cung cấp một triển khai cơ bản của giao diện List nhằm giảm thiểu công sức cần thiết để triển khai giao diện này dựa trên một kho dữ liệu "truy cập ngẫu nhiên" (chẳng hạn như một mảng). Đối với dữ liệu truy cập tuần tự (chẳng hạn như danh sách liên kết), nên sử dụng AbstractSequentialList thay cho lớp này. Để triển khai một danh sách không thể thay đổi, lập trình viên chỉ cần mở rộng lớp này và cung cấp các triển khai cho các phương thức get(int) và size().

Và hiểu nôm na sẽ là: Nó triển khai các interface ở trên ta đã nói :v

ArrayList (Chùm cúi)

Trong class này mình sẽ phải tìm hiểu về constructor xem các nó được khởi tạo. Qua đó mình sẽ hình dung được cấu trúc của nó. Và xem qua 1 vài method add, remove để xem nó thêm xóa giá trị trong mảng như thế nào. Screenshot from 2024-06-03 15-12-32.png Đây là constructor. Ta thấy có 3 kiểu constructor là:

  • Truyền số lượng mảng
  • Constructor trống
  • Truyền sẵn 1 mảng collection Screenshot from 2024-06-03 15-14-07.png Và chung quy chúng đều tạo ra thứ này mình sẽ gọi nó là một Node.

Ta đến với phương thức add để xem cách nó thêm 1 Node vào ArrayList như thế nào. Screenshot from 2024-06-03 15-49-11.png Qua đây ta hiểu cách hoạt động của add trong ArrayList. Nếu bạn chèn vào giữa 1 mảng ArrayList nó sẽ dịch chuyển toàn bộ phần sau của mảng và chèn giá trị bạn vào chỗ cần chèn.

Ban đầu mình nghĩa ArrayList sẽ là loại danh sách liên kết 1 chiều có con trỏ trỏ tới node next nhưng sau khi đọc mình hiểu rằng con trỏ ấy chính là index luôn. Thay vì phải có thêm 1 con trỏ thì ArrayList dùng index như cách con trỏ hoạt động. Nhờ việc làm này mà việc chèn cuối hay truy xuất tuần tự mảng hay việc truy xuất ngẫu nhiên 1 index sẽ cực kì là nhanh vì đơn giản nó chỉ cần lấy ngay index đã có nên thường các cái nói trên chỉ có độ phức tạp là O(1)

Tuy nhiên: Việc thêm sửa xóa sẽ chậm vì nó cần dịch chuyển lại toàn bộ index liên quan có thể lên tới O(n). Và để giải quyết vấn đề đó ta có LinkedList.

Tóm lại

**ArrayList là một cấu trúc mảng động sử dụng index để phân biệt cũng như truy cập các node. Do index không thay đổi nên khi ta cần chèn 1 phần tử hay xóa một phần tử giữa mảng thì đồng thời phần còn lại của mảng cũng phải tăng index hoặc giảm index. Do vậy việc thêm, xóa sẽ chậm (Nhanh O(1): chèn, xóa cuối dãy, chậm O(n): chèn, xóa đầu dãy ). Tuy vậy việc truy xuất ngẫu nhiên theo index hay truy xuất trình tự hoặc chèn cuối sẽ cực kì nhanh chỉ O(1) **

II - Linked List

Ta vẫn sẽ làm y hệt các bước lúc nãy. Để tránh mất thời gian đọc mình đã lấy được ra sơ đồ của em nó. Screenshot from 2024-06-03 15-42-33.png

Ta thấy có sự giống nhau giữa ArrayList và LinkedList phải không nào. Điều này cũng giải thích vì sao 2 cái lại có cách sự dụng giống nhau đến thế. Ta chỉ cần tìm hiểu thẳng vào class LinkedList là xong. Screenshot from 2024-06-03 15-43-44.png

Đập vào mắt mình chính là việc khai báo của Linked: Size: Lưu độ dài mảng first: Con trỏ cho node đầu last: Con trỏ cho node cuối Và dĩ nhiên ai đã học qua cấu trúc dữ liệu liên kết sẽ thấy rất quen đúng không nào. Và đúng thì LinkedList chính là 1 cấu trúc dữ liệu liên kết 2 chiều. Sau đó mình vẫn vào constructor xem nó làm như nào thì thấy nó gọi tới addAll. Mình cũng thử vào xem xem nó hoạt động ra sao nhé. Screenshot from 2024-06-03 15-43-44.png

Ở đây ta cũng sẽ thấy rõ nó tạo node và truyền vào 3 tham số:

  • pred: Địa chỉ node trước
  • e: giá trị truyền
  • null: địa chỉ node sau (Do khi ta chèn node này luôn là node cuối nên để địa chỉ sau là null)

Ở đây mình cũng tìm thấy được cấu hình của Node trong class. Screenshot from 2024-06-03 15-51-46.png

Sau đó nó sẽ xem có node đầu chưa nếu chưa nó sẽ cho node vừa tạo làm node đầu bằng cách cho node = first. Nếu có rồi nó sẽ gắn prev cho nốt cuối và cho last = node. Thật ra đoạn này mình sẽ nói khá khó hiểu nếu bạn chưa học qua về cấu trúc dữ liệu liên kết. Screenshot from 2024-06-03 16-27-54.png Đây là hình ảnh của linkedList. Vì linkedList được nối với nhau bằng địa chỉ chứ không phải index vậy nên nó là một liên kết vô cùng lỏng lẻo. Do vậy ta có thể dễ dàng chèn thêm 1 node vào giữa mảng bằng cách thay đổi địa chỉ các node liên quan. Screenshot from 2024-06-03 16-28-16.png Như ở đây ta thấy 2 -> 7 nhưng ta chỉ cần thay đổi địa chỉ cho 2 -> 4 và 4 -> 7 là được. Tương tự với xóa cũng thế.

Tóm lại

**LinkedList là một cấu trúc dữ liệu liên kết 2 chiều và được liên kết với nhau bằng con trỏ. Do vậy liên kết nó sẽ rất lỏng lẻo cho phép ta chèn, xóa một cách cực dễ dàng và nhanh chóng O(1). Tuy vậy việc duyệt qua nó hay truy xuất nó lại chậm do phải thông qua con trỏ tìm địa chỉ tới node tiếp theo O(n). Và việc cần thêm 2 con trỏ trong 1 node cũng khiến cho nó trở nên nặng và tốn tài nguyên hơn so với ArrayList. **

So sánh ArrayList và LinkedList

Screenshot from 2024-06-03 16-01-13.png Chat Gpt viết khá hay nên mình copy luôn :v

III - Vector

Và không thể không nhắc tới vector được ta cũng xem thử cấu trúc nó nhé. Screenshot from 2024-06-03 16-02-31.png

Khá giống 2 cái trên đúng không nào. Ta có thể ví chúng là anh em của nhau :v. Đọc qua mình thấy khá giống ArrayList. Tuy vậy điểm khác lớn nhất của Vector so với ArrayList là đồng bộ hóa. Screenshot from 2024-06-03 16-03-12.png Mọi method trong vector điều được đồng bộ hóa. Cho nên nó có thể đồng bộ hóa giữa nhiều luồng khác nhau. Do vậy nếu có 2 luồng đang chạy mà dùng cùng 1 ArrayList nó sẽ lỗi còn Vector thì không. Screenshot from 2024-06-03 16-06-43.png Rõ ràng kết quả ArrayList đã sai. Screenshot from 2024-06-03 16-07-26.png Còn với Vector lại đúng.

Đồng bộ hóa ArrayList

Tuy vậy ta vẫn có thể đồng bộ hóa ArrayList bằng cách sử dụng Collections.synchronizedList.

Syntax: List<Integer> list = Collections.synchronizedList(new ArrayList<>()); Screenshot from 2024-06-03 16-09-11.png

IV - Tổng kết:

Vậy là qua bài đọc khá dài này chúng ta đã có một các tiếp cận công nghệ bằng một phương pháp khác. Bài viết vẫn còn nhiều lỗi mình mong được sự góp ý của tất cả mọi người. Chúc mọi người một ngày thật vui vẻ.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí