Eine Anleitung zu TreeSet in Java

1. Übersicht

In diesem Artikel werfen wir einen Blick auf einen integralen Bestandteil des Java Collections Framework und eine der beliebtesten Set- Implementierungen - das TreeSet .

2. Einführung in TreeSet

Einfach ausgedrückt ist das TreeSet eine sortierte Sammlung, die die AbstractSet- Klasse erweitert und die NavigableSet- Schnittstelle implementiert .

Hier ist eine kurze Zusammenfassung der wichtigsten Aspekte dieser Implementierung:

  • Es speichert einzigartige Elemente
  • Die Einfügereihenfolge der Elemente wird nicht beibehalten
  • Es sortiert die Elemente in aufsteigender Reihenfolge
  • Es ist nicht threadsicher

In dieser Implementierung werden Objekte in aufsteigender Reihenfolge gemäß ihrer natürlichen Reihenfolge sortiert und gespeichert . Das TreeSet verwendet einen selbstausgleichenden binären Suchbaum, insbesondere einen Rot-Schwarz- Baum.

Einfach ausgedrückt besteht jeder Knoten des Binärbaums als selbstausgleichender binärer Suchbaum aus einem zusätzlichen Bit, mit dem die Farbe des Knotens identifiziert wird, der entweder rot oder schwarz ist. Bei nachfolgenden Einfügungen und Löschungen tragen diese „Farbbits“ dazu bei, dass der Baum mehr oder weniger ausgeglichen bleibt.

Erstellen wir also eine Instanz eines TreeSet :

Set treeSet = new TreeSet();

2.1. TreeSet mit einem Konstruktor-Komparator Param

Optional können wir ein TreeSet mit einem Konstruktor erstellen, mit dem wir die Reihenfolge definieren können, in der die Elemente mithilfe eines Vergleichs- oder Komparators sortiert werden :

Set treeSet = new TreeSet(Comparator.comparing(String::length));

Obwohl TreeSet nicht threadsicher ist, kann es mit dem Wrapper Collections.synchronizedSet () extern synchronisiert werden :

Set syncTreeSet = Collections.synchronizedSet(treeSet);

Okay, jetzt, da wir eine klare Vorstellung davon haben, wie eine TreeSet- Instanz erstellt wird, werfen wir einen Blick auf die allgemeinen Operationen, die uns zur Verfügung stehen.

3. TreeSet add ()

Die add () -Methode kann erwartungsgemäß zum Hinzufügen von Elementen zu einem TreeSet verwendet werden . Wenn ein Element hinzugefügt wurde, gibt die Methode true zurück, andernfalls false.

Der Vertrag der Methode besagt, dass ein Element nur hinzugefügt wird, wenn es nicht bereits im Set vorhanden ist .

Fügen wir einem TreeSet ein Element hinzu :

@Test public void whenAddingElement_shouldAddElement() { Set treeSet = new TreeSet(); assertTrue(treeSet.add("String Added")); }

Die add- Methode ist äußerst wichtig, da die Implementierungsdetails der Methode veranschaulichen, wie das TreeSet intern funktioniert und wie die put- Methode der TreeMap zum Speichern der Elemente genutzt wird:

public boolean add(E e) { return m.put(e, PRESENT) == null; }

Die Variable m bezieht sich auf eine interne Backing- TreeMap (beachten Sie, dass TreeMap NavigateableMap implementiert ):

private transient NavigableMap m;

Daher hängt das TreeSet intern von einer NavigableMap ab, die beim Erstellen einer Instanz des TreeSet mit einer Instanz von TreeMap initialisiert wird:

public TreeSet() { this(new TreeMap()); }

Mehr dazu finden Sie in diesem Artikel.

4. TreeSet enthält ()

Die Methode includes () wird verwendet, um zu überprüfen, ob ein bestimmtes Element in einem bestimmten TreeSet vorhanden ist . Wenn das Element gefunden wird, gibt es true zurück, andernfalls false.

Mal sehen, enthält () in Aktion:

@Test public void whenCheckingForElement_shouldSearchForElement() { Set treeSetContains = new TreeSet(); treeSetContains.add("String Added"); assertTrue(treeSetContains.contains("String Added")); }

5. TreeSet remove ()

Die Methode remove () wird verwendet, um das angegebene Element aus der Menge zu entfernen, falls es vorhanden ist.

Wenn eine Menge das angegebene Element enthielt, gibt diese Methode true zurück.

Lassen Sie es uns in Aktion sehen:

@Test public void whenRemovingElement_shouldRemoveElement() { Set removeFromTreeSet = new TreeSet(); removeFromTreeSet.add("String Added"); assertTrue(removeFromTreeSet.remove("String Added")); }

6. TreeSet clear ()

Wenn wir alle Elemente aus einem Satz entfernen möchten, können wir die clear () -Methode verwenden:

@Test public void whenClearingTreeSet_shouldClearTreeSet() { Set clearTreeSet = new TreeSet(); clearTreeSet.add("String Added"); clearTreeSet.clear(); assertTrue(clearTreeSet.isEmpty()); }

7. TreeSet-Größe ()

Die size () -Methode wird verwendet, um die Anzahl der im TreeSet vorhandenen Elemente zu identifizieren . Dies ist eine der grundlegenden Methoden in der API:

@Test public void whenCheckingTheSizeOfTreeSet_shouldReturnThesize() { Set treeSetSize = new TreeSet(); treeSetSize.add("String Added"); assertEquals(1, treeSetSize.size()); }

8. TreeSet isEmpty ()

Mit der Methode isEmpty () kann ermittelt werden, ob eine bestimmte TreeSet- Instanz leer ist oder nicht:

@Test public void whenCheckingForEmptyTreeSet_shouldCheckForEmpty() { Set emptyTreeSet = new TreeSet(); assertTrue(emptyTreeSet.isEmpty()); }

9. TreeSet-Iterator ()

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.

Wenn Daten von der Festplatte gelesen werden müssen (die eine höhere Latenz aufweist als Daten, die aus dem Cache oder Speicher gelesen werden), bevorzugen Sie TreeSet, da es eine größere Lokalität aufweist

17. Fazit

In diesem Artikel konzentrieren wir uns auf das Verständnis der Verwendung der Standard- TreeSet- Implementierung in Java. Wir haben den Zweck und die Effizienz der Benutzerfreundlichkeit erkannt, da Duplikate vermieden und Elemente sortiert werden können.

Wie immer finden Sie Code-Schnipsel auf GitHub.