Java TreeMap versus HashMap

1. Inleiding

In dit artikel gaan we er twee vergelijken Kaart implementaties: TreeMap en Hash kaart.

Beide implementaties vormen een integraal onderdeel van de Java Collecties Framework en gegevens opslaan als sleutel waarde paren.

2. Verschillen

2.1. Implementatie

We zullen eerst praten over de Hash kaart wat een hashtabel-gebaseerde implementatie is. Het breidt de Abstracte kaart class en implementeert het Kaart koppel. EEN Hash kaart werkt volgens het principe van hashing.

Dit Kaart implementatie werkt meestal als een bucketed hash-tabel, maar wanneer emmers te groot worden, worden ze omgevormd tot knooppunten van TreeNodes, elk op dezelfde manier gestructureerd als die in java.util.TreeMap.

U kunt meer vinden op de HashMap's internals in het artikel erop gericht.

Aan de andere kant, TreeMap strekt zich uit Abstracte kaart klasse en werktuigen NavigableMap koppel. EEN TreeMap slaat kaartelementen op in een Rood Zwart boom, dat is een Zelfbalancerend Binaire zoekboom.

En u kunt ook meer vinden op de TreeMap's internals in het artikel concentreerden zich hier hier op.

2.2. Bestellen

Hash kaart biedt geen enkele garantie over de manier waarop de elementen in de Kaart.

Het betekent, we kunnen geen enkele volgorde aannemen tijdens het herhalen sleutels en waarden van een Hash kaart:

@Test openbare leegte whenInsertObjectsHashMap_thenRandomOrder () {Map hashmap = nieuwe HashMap (); hashmap.put (3, "TreeMap"); hashmap.put (2, "vs"); hashmap.put (1, "HashMap"); assertThat (hashmap.keySet (), containsInAnyOrder (1, 2, 3)); }

Items in een TreeMap zijn gesorteerd volgens hun natuurlijke volgorde.

Als TreeMap objecten kunnen niet op natuurlijke volgorde worden gesorteerd, dan kunnen we gebruik maken van een Comparator of Vergelijkbaar om de volgorde te bepalen waarin de elementen binnen het Kaart:

@ Test openbare ongeldig whenInsertObjectsTreeMap_thenNaturalOrder () {Map treemap = new TreeMap (); treemap.put (3, "TreeMap"); treemap.put (2, "vs"); treemap.put (1, "HashMap"); assertThat (treemap.keySet (), bevat (1, 2, 3)); }

2.3. Nul Waarden

Hash kaart staat het opslaan van maximaal één toe nulsleutel en veel nul waarden.

Laten we een voorbeeld bekijken:

@Test openbare leegte whenInsertNullInHashMap_thenInsertsNull () {Map hashmap = nieuwe HashMap (); hashmap.put (null, null); assertNull (hashmap.get (null)); }

Echter, TreeMap staat een nulsleutel maar kan er veel bevatten nul waarden.

EEN nul key is niet toegestaan ​​omdat de vergelijk met() of de vergelijken() methode gooit een NullPointerException:

@Test (verwacht = NullPointerException.class) public void whenInsertNullInTreeMap_thenException () {Map treemap = new TreeMap (); treemap.put (null, "NullPointerException"); }

Als we een TreeMap met een door de gebruiker gedefinieerd Comparator, dan hangt het af van de implementatie van de vergelijking() methode hoe nul waarden worden afgehandeld.

3. Prestatieanalyse

Prestaties zijn de meest kritische maatstaf die ons helpt de geschiktheid van een datastructuur te begrijpen bij een gegeven gebruiksscenario.

In dit gedeelte geven we een uitgebreide analyse van de prestaties voor Hash kaart en TreeMap.

3.1. Hash kaart

Hash kaart, omdat het een hashtabel-gebaseerde implementatie is, gebruikt het intern een array-gebaseerde datastructuur om de elementen te ordenen volgens de hash-functie.

Hash kaart biedt verwachte constante tijdprestaties O (1) voor de meeste bewerkingen zoals toevoegen(), verwijderen() en bevat (). Daarom is het aanzienlijk sneller dan een TreeMap.

De gemiddelde tijd om naar een element te zoeken onder de redelijke aanname, in een hashtabel is O (1). Maar een onjuiste implementatie van het hash-functie kan leiden tot een slechte verdeling van waarden in buckets, wat resulteert in:

  • Geheugenoverhead - veel bakken blijven ongebruikt
  • Prestatieverminderinghoe hoger het aantal botsingen, hoe lager de prestatie

Voordat Java 8, Afzonderlijke ketting was de enige manier om aanrijdingen het hoofd te bieden. Het wordt meestal geïmplementeerd met behulp van gekoppelde lijsten, d.w.z., als er een botsing is of als twee verschillende elementen dezelfde hash-waarde hebben, sla dan beide items op in dezelfde gekoppelde lijst.

Daarom zoeken naar een element in een Hash kaart, in het ergste geval had het zo lang kunnen duren als het zoeken naar een element in een gekoppelde lijst d.w.z.Aan) tijd.

Nu JEP 180 in beeld komt, is er echter een subtiele verandering in de implementatie van de manier waarop de elementen zijn gerangschikt in een Hash kaart.

Volgens de specificatie worden emmers die te groot worden en voldoende knooppunten bevatten, omgezet in modi van TreeNodes, elk op dezelfde manier gestructureerd als die in TreeMap.

Daarom zullen in het geval van hoge hash-botsingen de slechtste prestaties verbeteren van Aan) naar O (logboek n).

De code die deze transformatie uitvoert, wordt hieronder geïllustreerd:

if (binCount> = TREEIFY_THRESHOLD - 1) {treeifyBin (tabblad, hash); }

De waarde voor TREEIFY_THRESHOLD is acht, wat in feite de drempelwaarde aangeeft voor het gebruik van een boom in plaats van een gekoppelde lijst voor een bucket.

Het is duidelijk dat:

  • EEN Hash kaart vereist veel meer geheugen dan nodig is om de gegevens op te slaan
  • EEN Hash kaart mag niet meer dan 70% - 75% vol zijn. Als het in de buurt komt, wordt het formaat gewijzigd en worden vermeldingen opnieuw gehasht
  • Opnieuw hasj vereist n bewerkingen die kostbaar zijn, waarbij onze constante tijdinvoeging van orde wordt Aan)
  • Het is het hash-algoritme dat de volgorde bepaalt waarin de objecten in het Hash kaart

De uitvoering van een Hash kaart kan worden afgestemd door het instellen van de custom initiële capaciteit en de ladingsfactor, ten tijde van Hash kaart object creatie zelf.

We moeten echter een Hash kaart als:

  • we weten ongeveer hoeveel items we in onze collectie moeten houden
  • we willen items niet in een natuurlijke volgorde extraheren

Onder de bovenstaande omstandigheden, Hash kaart is onze beste keuze omdat het constant tijd invoegen, zoeken en verwijderen biedt.

3.2. TreeMap

EEN TreeMap slaat zijn gegevens op in een hiërarchische boom met de mogelijkheid om de elementen te sorteren met behulp van een aangepaste Comparator.

Een samenvatting van zijn prestaties:

  • TreeMap levert een prestatie van O (logboek (n)) voor de meeste bewerkingen zoals toevoegen(), verwijderen() en bevat ()
  • EEN Boomdiagram kan geheugen besparen (in vergelijking met Hash kaart) omdat het alleen de hoeveelheid geheugen gebruikt die nodig is om de items op te slaan, in tegenstelling tot een Hash kaart die een aaneengesloten geheugengebied gebruikt
  • Een boom moet zijn evenwicht bewaren om zijn beoogde prestatie te behouden, dit vereist een aanzienlijke inspanning en bemoeilijkt de implementatie

We moeten gaan voor een TreeMap wanneer:

  • Er moet rekening worden gehouden met geheugenbeperkingen
  • we weten niet hoeveel items er in het geheugen moeten worden opgeslagen
  • we willen objecten in een natuurlijke volgorde extraheren
  • als items consequent worden toegevoegd en verwijderd
  • we zijn bereid om te accepteren O (logboek n) zoektijd

4. Overeenkomsten

4.1. Unieke elementen

Beide TreeMap en Hash kaart geen ondersteuning voor dubbele sleutels. Indien toegevoegd, overschrijft het het vorige element (zonder fout of uitzondering):

@Test openbare leegte gegevenHashMapAndTreeMap_whenputDuplicates_thenOnlyUnique () {Map treeMap = nieuwe HashMap (); treeMap.put (1, "Baeldung"); treeMap.put (1, "Baeldung"); assertTrue (treeMap.size () == 1); Map treeMap2 = nieuwe TreeMap (); treeMap2.put (1, "Baeldung"); treeMap2.put (1, "Baeldung"); assertTrue (treeMap2.size () == 1); }

4.2. Gelijktijdige toegang

Beide Kaart implementaties zijn dat niet gesynchroniseerd en we moeten de gelijktijdige toegang zelf beheren.

Beide moeten extern worden gesynchroniseerd wanneer meerdere threads er gelijktijdig toegang toe hebben en ten minste één van de threads ze wijzigt.

We moeten expliciet gebruiken Collections.synchronizedMap (mapName) om een ​​gesynchroniseerde weergave van een verstrekte kaart te verkrijgen.

4.3. Fail-Fast Iterators

De Iterator gooit een ConcurrentModificationException als het Kaart wordt op elke manier en op elk moment gewijzigd zodra de iterator is gemaakt.

Bovendien kunnen we de verwijderingsmethode van de iterator gebruiken om de Kaart tijdens iteratie.

Laten we een voorbeeld bekijken:

@Test openbare leegte whenModifyMapDuringIteration_thenThrowExecption () {Map hashmap = nieuwe HashMap (); hashmap.put (1, "Een"); hashmap.put (2; "Twee"); Uitvoerbaar uitvoerbaar bestand = () -> hashmap .forEach ((sleutel, waarde) -> hashmap.remove (1)); assertThrows (ConcurrentModificationException.class, uitvoerbaar); }

5. Welke implementatie te gebruiken?

Over het algemeen hebben beide implementaties echter hun respectieve voor- en nadelen, het gaat om het begrijpen van de onderliggende verwachting en vereiste die onze keuze met betrekking tot hetzelfde moeten bepalen.

Samenvatten:

  • We zouden een TreeMap als we onze inzendingen gesorteerd willen houden
  • We zouden een Hash kaart als we voorrang geven aan prestaties boven geheugengebruik
  • Sinds een TreeMap een grotere plaats heeft, zouden we het kunnen overwegen als we toegang willen hebben tot objecten die relatief dicht bij elkaar liggen volgens hun natuurlijke ordening
  • Hash kaart kan worden afgestemd met de initialCapacity en ladingsfactor, wat niet mogelijk is voor de TreeMap
  • We kunnen de LinkedHashMap als we de invoegvolgorde willen behouden terwijl we profiteren van constante tijdtoegang

6. Conclusie

In dit artikel hebben we de verschillen en overeenkomsten tussen TreeMap en Hash kaart.

Zoals altijd zijn de codevoorbeelden voor dit artikel beschikbaar op GitHub.