Bài này mình sẽ giới thiệu lại các kiến thức về Collection trong ngôn ngữ Java.
Hệ thống lại các thành phần sau đây : Arrays, Iterator, Collections Interfaces(gọi là bộ chứa đi) như Set, List, Map. Bên trong các collections interfaces này sẽ implements các nhiều classes.
1. Arrays : Là 1 cách hiệu quả để tham chiếu đến các đối tượng có cùng kiểu dữ liệu hay kiểu đối tượng .
Xử lý Array nhờ vào giá trị index của array đó. Điều này hầu như ai cũng biết khi đã làm việc với Array. Chỉ số index bắt đầu từ 0.
Ưu điểm :
- Array cho biết kiểu đối tượng mà nó đang nắm giữ, được kiểm tra tại thời điểm biên dịch.
- Array cho biết kích thước của đối tượng mà nó tạo ra.
- Array có thể mà 1 kiểu nguyên thủy trong 8 kiểu dữ liệu nguyên thủy trong ngôn ngữ Java.
Nhược điểm :
- 1 array chỉ giữ được duy nhất 1 kiểu đối tượng.
- Array có kích thước cố định.
Ví dụ : Các kiểu khởi tạo mảng đối tượng.
class Car{}; // minimal dummy class
Car[] cars1; // null reference
Car[] cars2 = new Car[10]; // null references
for (int i = 0; i < cars2.length; i++)
cars2[i] = new Car();
// Aggregated initialization
Car[] cars3 = {new Car(), new Car(), new Car(), new Car()};
cars1 = {new Car(), new Car(), new Car()};
Ngôn ngữ Java cũng định nghĩa sẵn 1 lớp java.util.Arrays với các mục đích sau :
- Tìm kiếm và sắp xếp : binarySearch() and sort().
- So sánh : equals()
- Instantiation: fill()
- Chuyển đổi thành đối tượng kiểu List qua : asList()
2. Iterator : ý tưởng là liệt kê mỗi phần tử của một collection.
Iterator interface định nghĩa 3 phương thức sau :
- boolean hasNext() ;
- Object next() ;
- void remove() ;
Câu hỏi đặt ra là khi nào chúng ta dùng đến Iterator ?
Khi chúng ta muốn dấu đi đối tượng collection mà chúng ta đang thực hiện. Nghĩa là thay vì thao tác với đối tượng collection thì chúng ta thao tác với đối tương Iterator khi muốn lấy ra các phần tử của đối tương collection . Tuy nhiên điều này khiến cho vòng lặp thực hiện chậm hơn. Vì sao lại chậm hơn ? fail-fast :)
Ngoại lệ được ném ra nếu đối tượng collection được thay đổi từ bên ngoài.
Iterator Interface :
// the interface definition
Interface Iterator {
boolean hasNext();
Object next(); // note "one-way" traffic
void remove();
}
Implements iterator interface :
// an example
public static void main (String[] args){
ArrayList cars = new ArrayList();
for (int i = 0; i < 12; i++)
cars.add (new Car());
Iterator it = cats.iterator();
while (it.hasNext())
System.out.println ((Car)it.next());
}
Cuối cùng là các phương thức tồn tại trong Collection Interfaces. với mỗi Interfaces được thừa kế từ Collection Interfaces sẽ có những phương thức khác đúng với mục đích của Interface đó.
public interface Collection {
// Basic Operations
int size();
boolean isEmpty();
boolean contains(Object element);
boolean add(Object element); // Optional
boolean remove(Object element); // Optional
Iterator iterator();
// Bulk Operations
boolean containsAll(Collection c);
boolean addAll(Collection c); // Optional
boolean removeAll(Collection c); // Optional
boolean retainAll(Collection c); // Optional
void clear(); // Optional
// Array Operations
Object[] toArray();
Object[] toArray(Object a[]);
}
Hệ thống lại các thành phần sau đây : Arrays, Iterator, Collections Interfaces(gọi là bộ chứa đi) như Set, List, Map. Bên trong các collections interfaces này sẽ implements các nhiều classes.
1. Arrays : Là 1 cách hiệu quả để tham chiếu đến các đối tượng có cùng kiểu dữ liệu hay kiểu đối tượng .
Xử lý Array nhờ vào giá trị index của array đó. Điều này hầu như ai cũng biết khi đã làm việc với Array. Chỉ số index bắt đầu từ 0.
Ưu điểm :
- Array cho biết kiểu đối tượng mà nó đang nắm giữ, được kiểm tra tại thời điểm biên dịch.
- Array cho biết kích thước của đối tượng mà nó tạo ra.
- Array có thể mà 1 kiểu nguyên thủy trong 8 kiểu dữ liệu nguyên thủy trong ngôn ngữ Java.
Nhược điểm :
- 1 array chỉ giữ được duy nhất 1 kiểu đối tượng.
- Array có kích thước cố định.
Ví dụ : Các kiểu khởi tạo mảng đối tượng.
class Car{}; // minimal dummy class
Car[] cars1; // null reference
Car[] cars2 = new Car[10]; // null references
for (int i = 0; i < cars2.length; i++)
cars2[i] = new Car();
// Aggregated initialization
Car[] cars3 = {new Car(), new Car(), new Car(), new Car()};
cars1 = {new Car(), new Car(), new Car()};
Ngôn ngữ Java cũng định nghĩa sẵn 1 lớp java.util.Arrays với các mục đích sau :
- Tìm kiếm và sắp xếp : binarySearch() and sort().
- So sánh : equals()
- Instantiation: fill()
- Chuyển đổi thành đối tượng kiểu List qua : asList()
2. Iterator : ý tưởng là liệt kê mỗi phần tử của một collection.
Iterator interface định nghĩa 3 phương thức sau :
- boolean hasNext() ;
- Object next() ;
- void remove() ;
Câu hỏi đặt ra là khi nào chúng ta dùng đến Iterator ?
Khi chúng ta muốn dấu đi đối tượng collection mà chúng ta đang thực hiện. Nghĩa là thay vì thao tác với đối tượng collection thì chúng ta thao tác với đối tương Iterator khi muốn lấy ra các phần tử của đối tương collection . Tuy nhiên điều này khiến cho vòng lặp thực hiện chậm hơn. Vì sao lại chậm hơn ? fail-fast :)
Ngoại lệ được ném ra nếu đối tượng collection được thay đổi từ bên ngoài.
Iterator Interface :
// the interface definition
Interface Iterator {
boolean hasNext();
Object next(); // note "one-way" traffic
void remove();
}
Implements iterator interface :
// an example
public static void main (String[] args){
ArrayList cars = new ArrayList();
for (int i = 0; i < 12; i++)
cars.add (new Car());
Iterator it = cats.iterator();
while (it.hasNext())
System.out.println ((Car)it.next());
}
Cuối cùng là các phương thức tồn tại trong Collection Interfaces. với mỗi Interfaces được thừa kế từ Collection Interfaces sẽ có những phương thức khác đúng với mục đích của Interface đó.
public interface Collection {
// Basic Operations
int size();
boolean isEmpty();
boolean contains(Object element);
boolean add(Object element); // Optional
boolean remove(Object element); // Optional
Iterator iterator();
// Bulk Operations
boolean containsAll(Collection c);
boolean addAll(Collection c); // Optional
boolean removeAll(Collection c); // Optional
boolean retainAll(Collection c); // Optional
void clear(); // Optional
// Array Operations
Object[] toArray();
Object[] toArray(Object a[]);
}
Tiếp tục :
Set interface được hiện thực bởi 2 classes là HashSet và TreeSet.
HashSet :
- Hiện thực bằng cách dùng hash table (Bảng băm).
- không có sự sắp xếp giữa các phần tử.
-Các phương thức add, remove and contains có thời gian tính toán khoảng O(c);
TreeSet :
- Hiện thực bằng cách sử dụng cấu trúc cây.
- Có sự sắp xếp giữa các phần tử.
- Các phương thức add, remove, and contains có thời gian tính toán khoảng O(log(n)) với n la số phần tử trong Set.
List interface được hiện thực bởi 2 classes là ArrrayList và LinkedList.
ArrayList : truy cập trực tiếp các phần tử qua set và get methods.
LinkedList : hiệu quả tốt hơn là sử dụng phương thức add và remove của ArrayList. nhưng hiệu quả kém hơn khi so với phương thức set và get của ArrayList.
Thường sử dụng ArrayList.
Cần quan tâm đến ListIterator.
Map interface : là đối tượng ánh xạ 1 khóa cho 1 giá trị (key/value) thông qua 2 phương thức put() và remove() và truy cập thông qua get(). Được hiện thực với HashMap và TreeMap.
Xem thêm phần Set interface.
Xem thêm phần Set interface.
Ưu điểm của Collection :
- Có thể giữ nhiều kiểu khác nhau của đối tượng.
- Thay đổi được kích thước.
Nhược điểm :
- Không ép kiểu chính xác được. - Không thể kiểm tra kiểu tại thời điểm biên dịch.
Bây giờ ta sẽ đi vào chi tiết hơn, Collections bao gồm 3 thành phần :
Set : HashSet, TreeSet
Map :HashMap và TreeMap
Collections có một số hành vi:
Các phiên bản cũ hơn của JDK chứa một lớp được gọi là
Một
Như chúng ta đã thảo luận ở trên, bạn phải ép kiểu đúng khi bạn trích ra các phần tử từ sưu tập khi sử dụng
Tuy nhiên,
Phương thức này gọi
Chúng ta nhận được một
Có thể bạn sẽ không thường xuyên gọi
Lúc này, chúng ta có thể loại bỏ chỉ một hóa đơn riêng lẻ mỗi lần khỏi
Chúng ta cần phải thêm
Đây là việc triển khai thực hiện dễ dàng nhất mà chúng ta có thể sử dụng. Chúng ta gọi
Các kết quả không phải là những gì mà chúng ta muốn. Chúng ta đã kết thúc mà không còn hóa đơn nào trong ví cả. Tại sao? Bởi vì
Mã này chỉ loại bỏ một kết quả khớp riêng rẽ, chứ không phải là tất cả các kết quả khớp. Nhớ cẩn thận với
Giả sử rằng mỗi
Sau đó chúng ta thêm một phương thức để thêm biệt hiệu vào
Bây giờ hãy thử chạy mã này:
Bạn sẽ thấy chỉ có một
Chúng ta sẽ cung cấp cho mỗi
Sau đó chung ta thêm một phương thức để bổ sung thêm một thẻ tín dụng (CreditCard)tới
Giao diện của
Tất cả những gì còn lại là thêm phương thức
Bây giờ hãy thử chạy mã dưới đây, nó sẽ hiển thị cho bạn
Một thẻ tín dụng điển hình có một tên, một số tài khoản, một hạn mức tín dụng và một số dư. Mỗi mục trong một
Có một số khía cạnh thú vị khác về giao diện
Một số trong các phương thức này, chẳng hạn như
Bây giờ ta sẽ đi vào chi tiết hơn, Collections bao gồm 3 thành phần :
- Giao diện List định nghĩa một sưu tập các phần tử Object có thể dẫn hướng.
- Giao diện Set định nghĩa một sưu tập không có các phần tử trùng lặp.
- Giao diện Map định nghĩa một sưu tập các cặp khóa - giá trị.
Set : HashSet, TreeSet
Map :HashMap và TreeMap
Collections có một số hành vi:
- Các phương thức để mô tả kích thước của sưu tập (như size() và isEmpty()).
- Các phương thức để mô tả nội dung của sưu tập (như contains() và containsAll()).
- Các phương thức để hỗ trợ thao tác về nội dung của sưu tập (như add(), remove() và clear()).
- Các phương thức để cho phép bạn chuyển đổi một sưu tập thành một mảng (như toArray()).
- Một phương thức để cho phép bạn nhận được một trình vòng lặp (iterator) trên mảng các phần tử (iterator()).
Các phiên bản cũ hơn của JDK chứa một lớp được gọi là
Vector
. Nó vẫn còn có trong các phiên bản mới hơn, nhưng bạn chỉ nên sử dụng nó khi bạn cần có một Collections đồng bộ hoá -- đó là, một trong những yếu tố là an toàn phân luồng. Trong các trường hợp khác, bạn nên sử dụng lớp ArrayList
. Bạn vẫn có thể sử dụng Vector
, nhưng nó áp đặt một số chi phí thêm mà bạn thường không cần.Một
ArrayList
là cái như tên của nó gợi ý: danh sách các phần tử theo thứ tự. Chúng ta đã thấy làm thế nào để tạo ra một danh sách và làm thế nào để thêm các phần tử vào nó, trong bài hướng dẫn giới thiệu trước. Khi chúng ta tạo ra một lớpWallet
lồng trong trong hướng dẫn này, chúng ta đã tích hợp vào đó một ArrayList
để giữ các hoá đơn thanh toán của Adult
:protected class Wallet {Phương thức
protected ArrayList bills = new ArrayList();
protected void addBill(int aBill) {
bills.add(new Integer(aBill));
}
protected int getMoneyTotal() {
int total = 0;
for (Iterator i = bills.iterator(); i.hasNext(); ) {
Integer wrappedBill = (Integer) i.next();
int bill = wrappedBill.intValue();
total += bill;
}
return total;
}
}
getMoneyTotal()
sử dụng một trình vòng lặp (iterator) để duyệt qua danh sách các hoá đơn thanh toán và tính tổng giá trị của chúng. Một Iterator
tương tự như một Enumeration
trong các phiên bản cũ hơn của ngôn ngữ Java. Khi bạn nhận được một trình vòng lặp trên sưu tập (bằng cách gọi iterator()
), trình vòng lặp cho phép bạn duyệt qua (traverse) toàn bộ sưu tập bằng cách sử dụng một số phương thức quan trọng, được minh họa trong mã lệnh ở trên:hasNext()
cho bạn biết còn có một phần tử tiếp theo khác trong sưu tập không.next()
cho bạn phần tử tiếp theo đó.
Như chúng ta đã thảo luận ở trên, bạn phải ép kiểu đúng khi bạn trích ra các phần tử từ sưu tập khi sử dụng
next()
.Tuy nhiên,
Iterator
còn cho chúng ta một số khả năng bổ sung thêm. Chúng ta có thể loại bỏ các phần tử khỏi lớp ArrayList
bằng cách gọi remove()
(hay removeAll()
, hay clear()
), nhưng chúng ta cũng có thể sử dụngIterator
để làm điều đó. Hãy thêm một phương thức rất đơn giản được gọi là spendMoney()
tới Adult
:public void spendMoney(int aBill) { |
Phương thức này gọi
removeBill()
trên Wallet
:protected void removeBill(int aBill) { |
Chúng ta nhận được một
Iterator
trên các hoá đơn thanh toán
ArrayList
, và duyệt qua danh sách để tìm một kết quả khớp với giá trị hóa đơn được chuyển qua (aBill
). Nếu chúng ta tìm thấy một kết quả khớp, chúng ta gọiremove()
trên trình vòng lặp để loại bỏ hóa đơn đó. Cũng đơn giản, nhưng còn chưa phải là đơn giản hết mức. Mã dưới đây thực hiện cùng một công việc và dễ đọc hơn nhiều:protected void removeBill(int aBill) { |
Có thể bạn sẽ không thường xuyên gọi
remove()
trên một Iterator
nhưng sẽ rất tốt nếu có công cụ đó khi bạn cần nó.Lúc này, chúng ta có thể loại bỏ chỉ một hóa đơn riêng lẻ mỗi lần khỏi
Wallet
. Sẽ là tốt hơn nếu sử dụng sức mạnh của một List
để giúp chúng ta loại bỏ nhiều hóa đơn cùng một lúc, như sau:public void spendMoney(List bills) { |
Chúng ta cần phải thêm
removeBills()
vào wallet
của chúng ta để thực hiện việc này. Hãy thử mã dưới đây:protected void removeBills(List billsToRemove) { |
Đây là việc triển khai thực hiện dễ dàng nhất mà chúng ta có thể sử dụng. Chúng ta gọi
removeAll()
trên List
các hoá đơn của chúng ta, chuyển qua một Collection
. Sau đó phương thức này loại bỏ tất cả các phần tử khỏi danh sách có trong Collection
. Hãy thử chạy mã dưới đây:List someBills = new ArrayList(); |
Các kết quả không phải là những gì mà chúng ta muốn. Chúng ta đã kết thúc mà không còn hóa đơn nào trong ví cả. Tại sao? Bởi vì
removeAll()
loại bỏ tất cả các kết quả khớp. Nói cách khác, bất kỳ và tất cả các kết quả khớp với một mục trongList
mà chúng ta chuyển cho phương thức đều bị loại bỏ. Các hoá đơn thanh toán mà chúng ta đã chuyển cho phương thức có chứa 1 và 2. Ví của chúng ta có chứa hai số 1 và một số 2. Khi removeAll()
tìm kiếm kết quả khớp với phần tử số 1, nó tìm thấy hai kết quả khớp và loại bỏ chúng cả hai. Đó không phải là những gì mà chúng ta muốn! Chúng ta cần thay đổi mã của chúng ta trong removeBills()
để sửa lại điều này:protected void removeBills(List billsToRemove) { |
Mã này chỉ loại bỏ một kết quả khớp riêng rẽ, chứ không phải là tất cả các kết quả khớp. Nhớ cẩn thận với
removeAll()
.SETCó hai triển khai thực hiện
Tập hợp
(Set) thường được sử dụng phổ biến:HashSet
, không đảm bảo thứ tự vòng lặp.TreeSet
, bảo đảm thứ tự vòng lặp.
Set
của bạn xếp theo một thứ tự nhất định nào đó khi bạn duyệt qua nó bằng một trình vòng lặp, thì hãy sử dụng triển khai thực hiện thứ hai. Nếu không, sử dụng cách thứ nhất. Thứ tự của các phần tử trong một TreeSet
(có thực hiện giao diện SortedSet
) được gọi là thứ tự tự nhiên(natural ordering); điều này có nghĩa là, hầu hết mọi trường hợp, bạn sẽ có khả năng sắp xếp các phần tử dựa trên phép so sánh equals()
.Giả sử rằng mỗi
Adult
có một tập hợp các biệt hiệu. Chúng ta thực sự không quan tâm đến chúng được sắp đặt thế nào, nhưng các bản sao sẽ không có ý nghĩa. Chúng ta có thể sử dụng một HashSet
để lưu giữ chúng. Trước tiên, chúng ta thêm một biến cá thể:protected Set nicknames = new HashSet(); |
Sau đó chúng ta thêm một phương thức để thêm biệt hiệu vào
Set
:public void addNickname(String aNickname) { |
Bây giờ hãy thử chạy mã này:
Adult anAdult = new Adult(); |
Bạn sẽ thấy chỉ có một
Bobby
đơn lẻ xuất hiện trên màn hình.MAP
Map
(Ánh xạ) là một tập hợp các cặp khóa - giá trị. Nó không thể chứa các khóa giống hệt nhau. Mỗi khóa phải ánh xạ tới một giá trị đơn lẻ, nhưng giá trị đó có thể là bất kỳ kiểu gì. Bạn có thể nghĩ về một ánh xạ như là List
có đặt tên. Hãy tưởng tượng một List
trong đó mỗi phần tử có một tên mà bạn có thể sử dụng để trích ra phần tử đó trực tiếp. Khóa có thể là bất cứ cái gì kiểu Object
, giống như giá trị. Một lần nữa, điều đó có nghĩa là bạn không thể lưu trữ các giá trị kiểu nguyên thủy (primitive) trực tiếp vào trong một Map
. Thay vào đó, bạn phải sử dụng các lớp bao gói kiểu nguyên thủy để lưu giữ các giá trị đó.Chúng ta sẽ cung cấp cho mỗi
Adult
một tập hợp các thẻ tín dụng đơn giản nhất có thể chấp nhận được. Mỗi thẻ sẽ có một tên và một số dư (ban đầu là 0). Trước tiên, chúng ta thêm một biến cá thể:protected Map creditCards = new HashMap(); |
Sau đó chung ta thêm một phương thức để bổ sung thêm một thẻ tín dụng (CreditCard)tới
Map
:public void addCreditCard(String aCardName) { |
Giao diện của
Map
khác với các giao diện của các sưu tập khác. Bạn gọi put()
với một khóa và một giá trị để thêm một mục vào ánh xạ. Bạn gọi get()
với khóa để trích ra một giá trị. Chúng ta sẽ làm việc này trong một phương thức để hiển thị số dư của một thẻ:public double getBalanceFor(String cardName) { |
Tất cả những gì còn lại là thêm phương thức
charge()
để cho phép cộng thêm vào số dư của chúng ta:public void charge(String cardName, double amount) { |
Bây giờ hãy thử chạy mã dưới đây, nó sẽ hiển thị cho bạn
19.95
trên màn hình.Adult anAdult = new Adult(); |
Một thẻ tín dụng điển hình có một tên, một số tài khoản, một hạn mức tín dụng và một số dư. Mỗi mục trong một
Map
chỉ có thể có một khóa và một giá trị. Các thẻ tín dụng rất đơn giản của chúng ta rất phù hợp, bởi vì chúng chỉ có một tên và một số dư hiện tại. Chúng ta có thể làm cho phức tạp hơn bằng cách tạo ra một lớp được gọi là CreditCard
, với các biến cá thể dành cho tất cả các đặc tính của một thẻ tín dụng, sau đó lưu trữ các cá thể của lớp này như các giá trị cho các mục trongMap
của chúng ta.Có một số khía cạnh thú vị khác về giao diện
Map
để trình bày trước khi chúng ta đi tiếp (đây không phải là một danh sách đầy đủ):Phương thức | Hành vi |
containsKey() | Trả lời Map có chứa khóa đã cho hay không. |
containsValue() | Trả lời Map có chứa giá trị đã cho hay không. |
keySet() | Trả về một Set tập hợp các khóa. |
values() | Trả về một Set tập hợp các giá trị. |
entrySet() | Trả về một Set tập hợp các cặp khóa - giá trị, được định nghĩa như là các cá thể của các Map.Entry . |
remove() | Cho phép bạn loại bỏ giá trị cho một khóa đã cho. |
isEmpty() | Trả lời Map có rỗng không (rỗng có nghĩa là, không chứa khóa nào). |
isEmpty()
chỉ là để cho tiện thôi, nhưng một số là rất quan trọng. Ví dụ, cách duy nhất để thực hiện vòng lặp qua các phần tử trong một Map
là thông qua một trong các tập hợp có liên quan (tập hợp các khóa, các giá trị, hoặc các cặp khóa-giá trị).
Nhận xét
Đăng nhận xét