Collections in JAVA
19:50
Tóm tắt lại chỉ cần hiểu nhứng điều sau đây:
- Hiểu 3 collection interfaces cơ bản nhất: List, Set, Map khác nhau chỗ nào, dùng để làm gì. Đối với một thằng coder bình thường thì cũng chả cần hiểu low-level implementations của mỗi Interface khác nhau như thế nào:
LinkedList vs. ArrayList, (Bình thường thí cứ dùng ArrayList)
HashSet vs. SortedSet vs. TreeSet, (Bình thường thì cứ dùng HashSet)
TreeMap vs. HashMap vs. SortedMap. (Bình thường thì cứ dùng HashMap)
Còn muốn hiểu sâu hiểu xa thì kiếm quyển "Thinking in Java" đọc Chapter "Container in Depth" thì nó phân tích cho mấy cái implementations này khác nhau như thế nào, lợi hại ra sao, khác nhau chỗ nào và khi nào thì dùng cái nào.
- Hiểu những methods chunng có thể dùng cho tất cả các loại List hoặc Set: add, addAll, remove, removeAll, isEmpty, containsAll etc.
- Dùng Queue nếu muốn quản lý dữ liệu theo kiểu FIFO - First In First Out = Nhết vào trước thì sẽ lấy ra đc trước, nhét sau thì phải dợi thằng đc nhét vào trước ra đã rồi mới lấy ra đc.
- Dùng Stack nếu muốn quản lý dữ liệu theo kiểu LIFO - Last In First Out = Nhét vào sau cùng nhưng mà lại có thể lấy ra đầu tiên
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ị.
List : ArrayList, Vector
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 { 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; } }Phương thức
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) { this.wallet.removeBill(aBill); } |
Phương thức này gọi
removeBill()
trên Wallet
:protected void removeBill(int aBill) { Iterator iterator = bills.iterator(); while (iterator.hasNext()) { Integer bill = (Integer) iterator.next(); if (bill.intValue() == aBill) iterator.remove(); } } |
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) { bills.remove(new Integer(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) { this.wallet.removeBills(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) { this.bills.removeAll(bills); } |
Đâ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(); someBills.add(new Integer(1)); someBills.add(new Integer(2)); Adult anAdult = new Adult(); anAdult.acceptMoney(1); anAdult.acceptMoney(1); anAdult.acceptMoney(2); List billsToRemove = new ArrayList(); billsToRemove.add(new Integer(1)); billsToRemove.add(new Integer(2)); anAdult.spendMoney(someBills); System.out.println(anAdult.wallet.bills); |
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) { Iterator iterator = billsToRemove.iterator(); while (iterator.hasNext()) { this.bills.remove(iterator.next()); } } |
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) { nicknames.add(aNickname); } |
Bây giờ hãy thử chạy mã này:
Adult anAdult = new Adult(); anAdult.addNickname("Bobby"); anAdult.addNickname("Bob"); anAdult.addNickname("Bobby"); System.out.println(anAdult.nicknames); |
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) { creditCards.put(aCardName, new Double(0)); } |
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) { Double balance = (Double) creditCards.get(cardName); return balance.doubleValue(); } |
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) { Double balance = (Double) creditCards.get(cardName); double primitiveBalance = balance.doubleValue(); primitiveBalance += amount; balance = new Double(primitiveBalance); creditCards.put(cardName, balance); } |
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(); anAdult.addCreditCard("Visa"); anAdult.addCreditCard("MasterCard"); anAdult.charge("Visa", 19.95); adAdult.showBalanceFor("Visa"); |
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ị).Nguồn: IBM
===========================================================================================
1) List:
Ở phần trước mình đã được làm quen với 2 loại List là
ArrayList với LinkedList rồi. 1 cái là tổ chức theo dạng mảng (các phần tử xếp
liền nhau), 1 cái tổ chức theo kiểu móc nối (các phần tử có thể nằm bất cứ vị
trí nào, được nối với nhau bằng con trỏ địa chỉ).
Ngoài ra List còn có 1 kiểu nữa là Vector.
Nó cùng với LinkedList có cùng bản chất với nhau, nhưng mà Vectorsynchronized
(đồng bộ hóa) còn LinkedList thì không synchronized. Synchronized sẽ làm chương
trình chạy chậm hơn, nhưng nó giúp tránh các lỗi gặp phải khi sử dụng chung
nguồn tài nguyên. Nói chung vấn đề “dùng chung tài nguyên” cũng hiếm khi gặp,
trường hợp hay gặp nhất chính là vừa duyệt list vừa xóa phần tử mà mình đã nêu
ví dụ ở phần 6 – cái trường hợp này có thể tránh được đối với LinkedList bằng
việc sử dụng Iterator.
2) Set:
Bản chất giống như List, nhưng List thì thích có bao
nhiêu phần tử có giá trị giống nhau cũng được, còn Set thì chỉ có 1. Nếu trong
set đã có 1 phần tử giá trị là A rồi mà mình vẫn cố tình thêm phần tử mới cũng
có giá trị là A, thì nó sẽ tự động xóa phần tử cũ đi.
Điểm khác tiếp theo: List có thứ tự biết trước, phần tử
nào thêm vào trước thì có thứ tự trước, còn Set chưa chắc như vậy
- HashSet:
thứ tự không xác định. Iterator được tạo ra từ Set cũng không biết trước thứ
tự. HashSet được tổ chức theo dạng mảng tương tự như bảng băm (Hashtable). Đối
với những ai chưa biết bảng băm thì mình sẽ nói qua như sau:
Đặt f(x) = x mod 10. Khi đó phần tử có giá trị 11 sẽ nằm ở vị trí số 1 (11 chia 10 dư 1), phần tử có giá trị 20 nằm ở vị trí số 0 (20 chia 10 dư 0), phần tử có giá trị 41 đáng ra phải nằm ở vị trí số 1 nhưng vị trí này đã có 11 chiếm mất rồi, nên nó sẽ tự động dịch sang 1 vị trí là số 2, … cứ tiếp tục như vậy cho đến khi đầy 10 ô rồi thì nó sẽ tự mở rộng thêm giống như cơ chế của ArrayList. Đó là lí do tại sao HashSet không thể đoán trước được thứ tự - LinkedHashSet: cũng tổ chức theo dạng bảng băm nhưng kết hợp với LinkedList, 41 và 11 sẽ không phải tranh nhau ô số 1 nữa mà lúc này 41 sẽ móc vào đuôi 11, nên thứ tự của LinkedHashSet có thể đoán trước được
- SortedSet: tổ chức Set theo thứ tự tăng dần hoặc giảm dần tùy mình cài đặt, loại này cũng đoán được thứ tự. NavigableSet với TreeSet chỉ là các loại Set cài đặt khác nhau giúp giảm bớt thời gian trong các thao tác duyệt Set, thêm, sửa, xóa tùy từng trường hợp thôi, chúng là con của class SortedSet
Cách duyệt Set:
Cách 1:
class IterateHashSet{ public static void main(String[] args) { // Create a HashSet HashSet<String> hset = new HashSet<String>(); //add elements to HashSet hset.add("Chaitanya"); hset.add("Rahul"); hset.add("Tim"); hset.add("Rick"); hset.add("Harry"); Iterator<String> it = hset.iterator(); while(it.hasNext()){ System.out.println(it.next()); } } }
Cách 2:
class IterateHashSet{ public static void main(String[] args) { // Create a HashSet Set<String> hset = new HashSet<String>(); //add elements to HashSet hset.add("Chaitanya"); hset.add("Rahul"); hset.add("Tim"); hset.add("Rick"); hset.add("Harry"); for (String temp : hset) { System.out.println(temp); } } }
3) Map:
Khác với List, các phần tử có khóa là 1, 2, 3, …. thì Map
có khóa là 1 đối tượng (Object). Giá trị của các phần tử trong Map có thể trùng
nhau, nhưng khóa thì không được giống nhau (vì thế có thể dễ dàng tạo ra 1 Set
có các phần tử là khóa của Map, sử dụng method keySet()).
Map cũng không biết trước thứ tự như Set.
Chắc cũng không cần giải thích thêm nữa, ngay cái tên của
các loại Map nó cũng đã nói lên bản chất của nó, LinkedHashMap thì tương tự như
LinkedHashSet, Hastable tương tự HashSet, SortedMap thì tương tự
SortedSet.
Cách duyệt Map: 3 cách:
public class LoopMap {
public static void main(String[] args) {
// initial a Map
Map<String, String> map = new HashMap<String, String>();
map.put("1", "Jan");
map.put("2", "Feb");
map.put("3", "Mar");
map.put("4", "Apr");
map.put("5", "May");
map.put("6", "Jun");
System.out.println("Example 1...");
// Map -> Set -> Iterator -> Map.Entry -> troublesome
Iterator iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry mapEntry = (Map.Entry) iterator.next();
System.out.println("The key is: " + mapEntry.getKey()
+ ",value is :" + mapEntry.getValue());
}
System.out.println("Example 2...");
// more elegant way
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("Key : " + entry.getKey() + " Value : "
+ entry.getValue());
}
System.out.println("Example 3...");
// weired way, but works anyway
for (Object key : map.keySet()) {
System.out.println("Key : " + key.toString() + " Value : "
+ map.get(key));
}
}
}
0 nhận xét