1. Gambaran keseluruhan
Dalam artikel ini, kita akan melihat bahagian tidak terpisahkan dari Java Collections Framework dan salah satu implementasi Set yang paling popular - TreeSet .
2. Pengenalan kepada TreeSet
Ringkasnya, TreeSet adalah koleksi tersusun yang memperluas kelas AbstractSet dan menerapkan antara muka NavigableSet .
Berikut adalah ringkasan ringkas aspek terpenting dalam pelaksanaan ini:
- Ia menyimpan elemen unik
- Ia tidak mengekalkan susunan kemasukan elemen
- Ia menyusun elemen mengikut urutan menaik
- Ia tidak selamat untuk benang
Dalam pelaksanaan ini, objek disusun dan disimpan dalam urutan menaik mengikut urutan semula jadi . The TreeSet menggunakan pokok carian binari mengimbang sendiri, lebih khusus yang merah-hitam pokok.
Ringkasnya, sebagai pohon carian binari yang mengimbangkan sendiri, setiap simpul pokok binari terdiri daripada bit tambahan, yang digunakan untuk mengenal pasti warna nod yang berwarna merah atau hitam. Semasa penyisipan dan penghapusan berikutnya, bit "warna" ini membantu memastikan bahawa pokok kekal seimbang atau tidak seimbang.
Oleh itu, mari buat contoh TreeSet :
Set treeSet = new TreeSet();
2.1. TreeSet Dengan Param Banding Pembina
Sebagai pilihan, kita dapat membina TreeSet dengan konstruktor yang membolehkan kita menentukan urutan elemen diurutkan dengan menggunakan Comparable atau Comparator:
Set treeSet = new TreeSet(Comparator.comparing(String::length));
Walaupun TreeSet tidak selamat di utas , ia dapat diselaraskan secara luaran menggunakan pembungkus Collections.synchronizedSet () :
Set syncTreeSet = Collections.synchronizedSet(treeSet);
Baiklah, sekarang kita mempunyai idea yang jelas tentang cara membuat contoh TreeSet , mari kita lihat operasi biasa yang kita ada.
3. TreeSet tambah ()
Kaedah add () , seperti yang diharapkan, dapat digunakan untuk menambahkan elemen ke TreeSet . Sekiranya elemen ditambahkan, kaedah akan kembali benar, jika tidak - salah.
Kontrak kaedah menyatakan bahawa elemen akan ditambahkan hanya apabila yang sama belum ada dalam Set .
Mari tambahkan elemen ke TreeSet :
@Test public void whenAddingElement_shouldAddElement() { Set treeSet = new TreeSet(); assertTrue(treeSet.add("String Added")); }
The add kaedah adalah amat penting kerana butiran pelaksanaan kaedah yang menunjukkan bagaimana TreeSet berfungsi secara dalaman , bagaimana ia memanfaatkan TreeMap yang meletakkan kaedah untuk menyimpan unsur:
public boolean add(E e) { return m.put(e, PRESENT) == null; }
Pemboleh ubah m merujuk kepada TreeMap penyokong dalaman (perhatikan bahawa TreeMap menerapkan NavigateableMap ):
private transient NavigableMap m;
Oleh itu, TreeSet dalaman bergantung pada sokongan NavigableMap yang mendapat dimulakan dengan satu contoh TreeMap apabila suatu unsur yang TreeSet dicipta:
public TreeSet() { this(new TreeMap()); }
Lebih banyak mengenai perkara ini boleh didapati dalam artikel ini.
4. TreeSet mengandungi ()
Kaedah mengandung () digunakan untuk memeriksa apakah elemen tertentu ada di TreeSet tertentu . Sekiranya unsur itu dijumpai, ia akan kembali benar, sebaliknya tidak benar.
Mari kita lihat mengandungi () dalam tindakan:
@Test public void whenCheckingForElement_shouldSearchForElement() { Set treeSetContains = new TreeSet(); treeSetContains.add("String Added"); assertTrue(treeSetContains.contains("String Added")); }
5. TreeSet keluarkan ()
Kaedah remove () digunakan untuk membuang elemen yang ditentukan dari set jika ada.
Sekiranya satu set mengandungi elemen yang ditentukan, kaedah ini akan kembali benar.
Mari kita lihat dalam tindakan:
@Test public void whenRemovingElement_shouldRemoveElement() { Set removeFromTreeSet = new TreeSet(); removeFromTreeSet.add("String Added"); assertTrue(removeFromTreeSet.remove("String Added")); }
6. TreeSet jelas ()
Sekiranya kita ingin membuang semua item dari satu set, kita boleh menggunakan kaedah jelas () :
@Test public void whenClearingTreeSet_shouldClearTreeSet() { Set clearTreeSet = new TreeSet(); clearTreeSet.add("String Added"); clearTreeSet.clear(); assertTrue(clearTreeSet.isEmpty()); }
7. Saiz TreeSet ()
Kaedah size () digunakan untuk mengenal pasti bilangan elemen yang terdapat di TreeSet . Ini adalah salah satu kaedah asas dalam API:
@Test public void whenCheckingTheSizeOfTreeSet_shouldReturnThesize() { Set treeSetSize = new TreeSet(); treeSetSize.add("String Added"); assertEquals(1, treeSetSize.size()); }
8. TreeSet kosong ()
Kaedah isEmpty () dapat digunakan untuk mengetahui apakah contoh TreeSet yang diberikan kosong atau tidak:
@Test public void whenCheckingForEmptyTreeSet_shouldCheckForEmpty() { Set emptyTreeSet = new TreeSet(); assertTrue(emptyTreeSet.isEmpty()); }
9. Iterator TreeSet ()
The iterator() method returns an iterator iterating in the ascending order over the elements in the Set. Those iterators are fail-fast.
We can observe the ascending iteration order here:
@Test public void whenIteratingTreeSet_shouldIterateTreeSetInAscendingOrder() { Set treeSet = new TreeSet(); treeSet.add("First"); treeSet.add("Second"); treeSet.add("Third"); Iterator itr = treeSet.iterator(); while (itr.hasNext()) { System.out.println(itr.next()); } }
Additionally, TreeSet enables us to iterate through the Set in descending order.
Let's see that in action:
@Test public void whenIteratingTreeSet_shouldIterateTreeSetInDescendingOrder() { TreeSet treeSet = new TreeSet(); treeSet.add("First"); treeSet.add("Second"); treeSet.add("Third"); Iterator itr = treeSet.descendingIterator(); while (itr.hasNext()) { System.out.println(itr.next()); } }
The Iterator throws a ConcurrentModificationException if the set is modified at any time after the iterator is created in any way except through the iterator's remove() method.
Let's create a test for this:
@Test(expected = ConcurrentModificationException.class) public void whenModifyingTreeSetWhileIterating_shouldThrowException() { Set treeSet = new TreeSet(); treeSet.add("First"); treeSet.add("Second"); treeSet.add("Third"); Iterator itr = treeSet.iterator(); while (itr.hasNext()) { itr.next(); treeSet.remove("Second"); } }
Alternatively, if we had used the iterator's remove method, then we wouldn't have encountered the exception:
@Test public void whenRemovingElementUsingIterator_shouldRemoveElement() { Set treeSet = new TreeSet(); treeSet.add("First"); treeSet.add("Second"); treeSet.add("Third"); Iterator itr = treeSet.iterator(); while (itr.hasNext()) { String element = itr.next(); if (element.equals("Second")) itr.remove(); } assertEquals(2, treeSet.size()); }
There's no guarantee on the fail-fast behavior of an iterator as it's impossible to make any hard guarantees in the presence of unsynchronized concurrent modification.
More about this can be found here.
10. TreeSet first()
This method returns the first element from a TreeSet if it's not empty. Otherwise, it throws a NoSuchElementException.
Let's see an example:
@Test public void whenCheckingFirstElement_shouldReturnFirstElement() { TreeSet treeSet = new TreeSet(); treeSet.add("First"); assertEquals("First", treeSet.first()); }
11. TreeSet last()
Analogously to the above example, this method will return the last element if the set is not empty:
@Test public void whenCheckingLastElement_shouldReturnLastElement() { TreeSet treeSet = new TreeSet(); treeSet.add("First"); treeSet.add("Last"); assertEquals("Last", treeSet.last()); }
12. TreeSet subSet()
This method will return the elements ranging from fromElement to toElement. Note that fromElement is inclusive and toElement is exclusive:
@Test public void whenUsingSubSet_shouldReturnSubSetElements() { SortedSet treeSet = new TreeSet(); treeSet.add(1); treeSet.add(2); treeSet.add(3); treeSet.add(4); treeSet.add(5); treeSet.add(6); Set expectedSet = new TreeSet(); expectedSet.add(2); expectedSet.add(3); expectedSet.add(4); expectedSet.add(5); Set subSet = treeSet.subSet(2, 6); assertEquals(expectedSet, subSet); }
13. TreeSet headSet()
This method will return elements of TreeSet which are smaller than the specified element:
@Test public void whenUsingHeadSet_shouldReturnHeadSetElements() { SortedSet treeSet = new TreeSet(); treeSet.add(1); treeSet.add(2); treeSet.add(3); treeSet.add(4); treeSet.add(5); treeSet.add(6); Set subSet = treeSet.headSet(6); assertEquals(subSet, treeSet.subSet(1, 6)); }
14. TreeSet tailSet()
This method will return the elements of a TreeSet which are greater than or equal to the specified element:
@Test public void whenUsingTailSet_shouldReturnTailSetElements() { NavigableSet treeSet = new TreeSet(); treeSet.add(1); treeSet.add(2); treeSet.add(3); treeSet.add(4); treeSet.add(5); treeSet.add(6); Set subSet = treeSet.tailSet(3); assertEquals(subSet, treeSet.subSet(3, true, 6, true)); }
15. Storing Null Elements
Before Java 7, it was possible to add null elements to an empty TreeSet.
However, that was considered a bug. Therefore, TreeSet no longer supports the addition of null.
When we add elements to the TreeSet, the elements get sorted according to their natural order or as specified by the comparator. Hence adding a null, when compared to existing elements, results in a NullPointerException since null cannot be compared to any value:
@Test(expected = NullPointerException.class) public void whenAddingNullToNonEmptyTreeSet_shouldThrowException() { Set treeSet = new TreeSet(); treeSet.add("First"); treeSet.add(null); }
Elements inserted into the TreeSet must either implement the Comparable interface or at least be accepted by the specified comparator. All such elements must be mutually comparable,i.e.e1.compareTo(e2) or comparator.compare(e1, e2)mustn't throw a ClassCastException.
Let's see an example:
class Element { private Integer id; // Other methods... } Comparator comparator = (ele1, ele2) -> { return ele1.getId().compareTo(ele2.getId()); }; @Test public void whenUsingComparator_shouldSortAndInsertElements() { Set treeSet = new TreeSet(comparator); Element ele1 = new Element(); ele1.setId(100); Element ele2 = new Element(); ele2.setId(200); treeSet.add(ele1); treeSet.add(ele2); System.out.println(treeSet); }
16. Performance of TreeSet
When compared to a HashSet the performance of a TreeSet is on the lower side. Operations like add, remove and search take O(log n) time while operations like printing n elements in sorted order require O(n) time.
A TreeSet should be our primary choice if we want to keep our entries sorted as a TreeSet may be accessed and traversed in either ascending or descending order, and the performance of ascending operations and views is likely to be faster than that of descending ones.
The Principle of Locality – is a term for the phenomenon in which the same values, or related storage locations, are frequently accessed, depending on the memory access pattern.
When we say locality:
- Similar data is often accessed by an application with similar frequency
- If two entries are nearby given an ordering, a TreeSet places them near each other in the data structure, and hence in memory
A TreeSet being a data-structure with greater locality we can, therefore, conclude in accordance to the Principle of Locality, that we should give preference to a TreeSet if we're short on memory and if we want to access elements that are relatively close to each other according to their natural ordering.
Sekiranya data perlu dibaca dari cakera keras (yang mempunyai latensi lebih besar daripada data yang dibaca dari cache atau memori) maka lebih suka TreeSet kerana mempunyai lokasi yang lebih besar
17. Kesimpulannya
Dalam artikel ini, kami fokus untuk memahami bagaimana menggunakan implementasi TreeSet standard di Java. Kami melihat tujuannya dan sejauh mana keberkesanannya mengenai kebolehgunaan kerana kemampuannya untuk mengelakkan pendua dan menyusun elemen.
Seperti biasa, coretan kod boleh didapati di GitHub.