Een gids voor Java HashMap

1. Overzicht

In dit artikel zullen we zien hoe u Hash kaart in Java, en we zullen kijken hoe het intern werkt.

Een klasse die erg lijkt op Hash kaart is Hashtable. Raadpleeg een aantal van onze andere artikelen voor meer informatie over het java.util.Hashtable klasse zelf en de verschillen tussen Hash kaart en Hashtable.

2. Basisgebruik

Laten we eerst kijken wat het betekent Hash kaart is een kaart. Een map is een sleutelwaarde-mapping, wat betekent dat elke sleutel is toegewezen aan exact één waarde en dat we de sleutel kunnen gebruiken om de bijbehorende waarde van een kaart op te halen.

Je zou je kunnen afvragen waarom je de waarde niet gewoon aan een lijst toevoegt. Waarom hebben we een Hash kaart? De simpele reden is prestatie. Als we een specifiek element in een lijst willen vinden, is de tijdcomplexiteit Aan) en als de lijst is gesorteerd, zal het zijn O (logboek n) met behulp van bijvoorbeeld een binaire zoekopdracht.

Het voordeel van een Hash kaart is dat de tijdcomplexiteit om een ​​waarde in te voegen en op te halen is O (1) gemiddeld. We zullen later bekijken hoe dat kan worden bereikt. Laten we eerst kijken hoe u Hash kaart.

2.1. Opstelling

Laten we een eenvoudige klasse maken die we in het hele artikel zullen gebruiken:

public class Product {private String naam; private String beschrijving; privélijsttags; // standard getters / setters / constructors public Product addTagsOfOtherProdcut (Productproduct) {this.tags.addAll (product.getTags ()); dit teruggeven; }}

2.2. Leggen

We kunnen nu een Hash kaart met de sleutel van het type Draad en elementen van type Product:

Map productsByName = nieuwe HashMap (); 

En voeg producten toe aan onze Hash kaart:

Product eBike = nieuw product ("E-bike", "Een fiets met accu"); Product roadBike = nieuw product ("Racefiets", "Een fiets voor competitie"); productsByName.put (eBike.getName (), eBike); productsByName.put (roadBike.getName (), roadBike); 

2.3. Krijgen

We kunnen een waarde van de kaart ophalen met de sleutel:

Product nextPurchase = productsByName.get ("E-Bike"); assertEquals ("Een fiets met een batterij", nextPurchase.getDescription ());

Als we een waarde proberen te vinden voor een sleutel die niet op de kaart bestaat, krijgen we een nul waarde:

Product nextPurchase = productsByName.get ("Auto"); assertNull (nextPurchase);

En als we een tweede waarde met dezelfde sleutel invoegen, krijgen we alleen de laatst ingevoegde waarde voor die sleutel:

Product newEBike = nieuw product ("E-Bike", "Een fiets met een betere accu"); productsByName.put (newEBike.getName (), newEBike); assertEquals ("Een fiets met een betere batterij", productsByName.get ("E-Bike"). getDescription ());

2.4. Null als de sleutel

Hash kaart stelt ons ook in staat om te hebben nul als sleutel:

Product defaultProduct = nieuw product ("Chocolade", "Koop tenminste chocolade"); productsByName.put (null, defaultProduct); Product nextPurchase = productsByName.get (null); assertEquals ("Koop tenminste chocolade", nextPurchase.getDescription ());

2.5. Waarden met dezelfde sleutel

Bovendien kunnen we hetzelfde object twee keer invoegen met een andere sleutel:

productsByName.put (defaultProduct.getName (), defaultProduct); assertSame (productsByName.get (null), productsByName.get ("Chocolade"));

2.6. Verwijder een waarde

We kunnen een sleutel / waardetoewijzing verwijderen uit het Hash kaart:

productsByName.remove ("E-Bike"); assertNull (productsByName.get ("E-Bike"));

2.7. Controleer of er een sleutel of waarde op de kaart staat

Om te controleren of er een sleutel op de kaart aanwezig is, kunnen we de bevatKey () methode:

productsByName.containsKey ("E-Bike");

Of om te controleren of een waarde op de kaart aanwezig is, kunnen we de bevatValue () methode:

productsByName.containsValue (eBike);

Beide methodeaanroepen zullen terugkeren waar in ons voorbeeld. Hoewel ze erg op elkaar lijken, is er een belangrijk verschil in prestatie tussen deze twee methodeaanroepen. De complexiteit om te controleren of er een sleutel bestaat is O (1), terwijl de complexiteit om te controleren op een element is Aan), omdat het nodig is om alle elementen op de kaart te herhalen.

2.8. Itereren over een Hash kaart

Er zijn drie basismanieren om alle sleutel / waarde-paren in een Hash kaart.

We kunnen de reeks van alle sleutels herhalen:

for (String key: productsByName.keySet ()) {Product product = productsByName.get (key); }

Of we kunnen de verzameling van alle vermeldingen herhalen:

voor (Map.Entry entry: productsByName.entrySet ()) {Product product = entry.getValue (); String key = entry.getKey (); // doe iets met de sleutel en waarde}

Ten slotte kunnen we alle waarden herhalen:

List products = new ArrayList (productsByName.values ​​());

3. De sleutel

We kunnen elke klasse gebruiken als de sleutel in onze Hash kaart. Om de kaart echter goed te laten werken, moeten we zorgen voor een implementatie voor is gelijk aan () en hashCode ().Laten we zeggen dat we een kaart willen hebben met het product als de sleutel en de prijs als de waarde:

HashMap priceByProduct = nieuwe HashMap (); priceByProduct.put (eBike, 900);

Laten we het is gelijk aan () en hashCode () methoden:

@Override public boolean equals (Object o) {if (this == o) {return true; } if (o == null || getClass ()! = o.getClass ()) {return false; } Product product = (Product) o; return Objects.equals (name, product.name) && Objects.equals (description, product.description); } @Override public int hashCode () {return Objects.hash (naam, beschrijving); }

Let daar op hashCode () en is gelijk aan () hoeven alleen te worden overschreven voor klassen die we als kaartsleutels willen gebruiken, niet voor klassen die alleen als waarden in een kaart worden gebruikt. In sectie 5 van dit artikel zullen we zien waarom dit nodig is.

4. Aanvullende methoden vanaf Java 8

Java 8 heeft verschillende functionele methoden toegevoegd aan Hash kaart. In deze sectie zullen we enkele van deze methoden bekijken.

Voor elke methode bekijken we twee voorbeelden. Het eerste voorbeeld laat zien hoe u de nieuwe methode kunt gebruiken, en het tweede voorbeeld laat zien hoe u hetzelfde kunt bereiken in eerdere versies van Java.

Omdat deze methoden vrij eenvoudig zijn, zullen we niet naar meer gedetailleerde voorbeelden kijken.

4.1. voor elke ()

De voor elk methode is de functionele manier om alle elementen in de kaart te herhalen:

productsByName.forEach ((key, product) -> {System.out.println ("Key:" + key + "Product:" + product.getDescription ()); // doe iets met de sleutel en waarde}); 

Voorafgaand aan Java 8:

voor (Map.Entry entry: productsByName.entrySet ()) {Product product = entry.getValue (); String key = entry.getKey (); // doe iets met de sleutel en waarde}

Ons artikel Gids voor de Java 8 voor elk dekt de voor elk loop in meer detail.

4.2. getOrDefault ()

De ... gebruiken getOrDefault () methode, kunnen we een waarde van de kaart halen of een standaardelement retourneren voor het geval er geen mapping is voor de gegeven sleutel:

Product chocolade = nieuw product ("chocolade", "iets zoets"); Product defaultProduct = productsByName.getOrDefault ("paardenkoets", chocolade); Product bike = productsByName.getOrDefault ("E-Bike", chocolade);

Voorafgaand aan Java 8:

Product bike2 = productsByName.containsKey ("E-Bike")? productsByName.get ("E-Bike"): chocolade; Product defaultProduct2 = productsByName.containsKey ("paardenkoets")? productsByName.get ("paardenkoets"): chocolade; 

4.3. putIfAbsent ()

Met deze methode kunnen we een nieuwe mapping toevoegen, maar alleen als er nog geen mapping is voor de gegeven sleutel:

productsByName.putIfAbsent ("E-Bike", chocolade); 

Voorafgaand aan Java 8:

if (productsByName.containsKey ("E-Bike")) {productsByName.put ("E-Bike", chocolade); }

In ons artikel Twee kaarten samenvoegen met Java 8 wordt deze methode nader bekeken.

4.4. samenvoegen()

En met samenvoegen(), we kunnen de waarde voor een bepaalde sleutel wijzigen als er een mapping bestaat, of anders een nieuwe waarde toevoegen:

Product eBike2 = nieuw product ("E-bike", "Een fiets met accu"); eBike2.getTags (). add ("sport"); productsByName.merge ("E-Bike", eBike2, Product :: addTagsOfOtherProdcut);

Voorafgaand aan Java 8:

if (productsByName.containsKey ("E-Bike")) {productsByName.get ("E-Bike"). addTagsOfOtherProdcut (eBike2); } else {productsByName.put ("E-Bike", eBike2); } 

4.5. berekenen()

Met de berekenen() methode, kunnen we de waarde voor een bepaalde sleutel berekenen:

productsByName.compute ("E-Bike", (k, v) -> {if (v! = null) {return v.addTagsOfOtherProdcut (eBike2);} else {return eBike2;}});

Voorafgaand aan Java 8:

if (productsByName.containsKey ("E-Bike")) {productsByName.get ("E-Bike"). addTagsOfOtherProdcut (eBike2); } else {productsByName.put ("E-Bike", eBike2); } 

Het is vermeldenswaard dat de methoden samenvoegen() en berekenen() zijn vrij gelijkaardig. De compute () methode accepteert twee argumenten: de sleutel en een BiFunction voor het opnieuw toewijzen. En samenvoegen() accepteert drie parameters: de sleutel, een standaardwaarde om toe te voegen aan de kaart als de sleutel nog niet bestaat, en een BiFunction voor het opnieuw toewijzen.

5. Hash kaart Internals

In deze sectie zullen we bekijken hoe Hash kaart werkt intern en wat zijn de voordelen van gebruik Hash kaart in plaats van bijvoorbeeld een simpele lijst.

Zoals we hebben gezien, kunnen we een element ophalen uit een Hash kaart door zijn sleutel. Een benadering zou zijn om een ​​lijst te gebruiken, alle elementen te herhalen en terug te keren wanneer we een element vinden waarvoor de sleutel overeenkomt. Zowel de tijd- als de ruimte-complexiteit van deze benadering zou zijn Aan).

Met Hash kaartkunnen we een gemiddelde tijdcomplexiteit bereiken van O (1) voor de leggen en krijgen operaties en ruimtecomplexiteit van Aan). Laten we eens kijken hoe dat werkt.

5.1. De hashcode en gelijken

In plaats van al zijn elementen te herhalen, Hash kaart probeert de positie van een waarde te berekenen op basis van zijn sleutel.

De naïeve benadering zou zijn om een ​​lijst te hebben die zoveel mogelijk elementen kan bevatten als er sleutels mogelijk zijn. Laten we als voorbeeld zeggen dat onze sleutel een kleine letter is. Dan is het voldoende om een ​​lijst van maat 26 te hebben, en als we toegang willen hebben tot het element met de sleutel ‘c ', dan weten we dat dit het element is op positie 3 en kunnen we het direct ophalen.

Deze benadering zou echter niet erg effectief zijn als we een veel grotere sleutelruimte hebben. Laten we bijvoorbeeld zeggen dat onze sleutel een geheel getal was. In dat geval zou de omvang van de lijst 2.147.483.647 moeten zijn. In de meeste gevallen zouden we ook veel minder elementen hebben, dus een groot deel van het toegewezen geheugen zou ongebruikt blijven.

Hash kaart slaat elementen op in zogenaamde buckets en het aantal buckets wordt genoemd capaciteit.

Als we een waarde in de kaart plaatsen, is de sleutel hashCode () methode wordt gebruikt om de bucket te bepalen waarin de waarde wordt opgeslagen.

Om de waarde op te halen, Hash kaart berekent de bucket op dezelfde manier - met hashCode (). Vervolgens doorloopt het de objecten die in die emmer zijn gevonden en gebruikt het de sleutels is gelijk aan () methode om de exacte overeenkomst te vinden.

5.2. De onveranderlijkheid van Keys

In de meeste gevallen moeten we onveranderlijke sleutels gebruiken. Of we moeten ons in ieder geval bewust zijn van de gevolgen van het gebruik van veranderlijke sleutels.

Laten we eens kijken wat er gebeurt als onze sleutel verandert nadat we deze hebben gebruikt om een ​​waarde op een kaart op te slaan.

Voor dit voorbeeld maken we de MutableKey:

openbare klasse MutableKey {private String-naam; // standard constructor, getter en setter @Override public boolean equals (Object o) {if (this == o) {return true; } if (o == null || getClass ()! = o.getClass ()) {return false; } MutableKey dat = (MutableKey) o; retourneer Objects.equals (name, that.name); } @Override public int hashCode () {return Objects.hash (naam); }}

En hier gaat de test:

MutableKey-sleutel = nieuwe MutableKey ("initieel"); Kaartitems = nieuwe HashMap (); items.put (sleutel, "succes"); key.setName ("gewijzigd"); assertNull (items.get (key));

Zoals we kunnen zien, kunnen we de overeenkomstige waarde niet meer krijgen als de sleutel eenmaal is gewijzigd, in plaats daarvan: nul wordt geretourneerd. Dit is zo omdat Hash kaart zoekt in de verkeerde bucket.

De bovenstaande testcase kan verrassend zijn als we niet goed begrijpen hoe Hash kaart werkt intern.

5.3. Botsingen

Om dit correct te laten werken, moeten gelijke sleutels dezelfde hash hebben, maar verschillende sleutels kunnen dezelfde hash hebben. Als twee verschillende sleutels dezelfde hash hebben, worden de twee bijbehorende waarden in dezelfde bucket opgeslagen. Binnen een bucket worden waarden opgeslagen in een lijst en opgehaald door alle elementen te herhalen. De kosten hiervan zijn Aan).

Vanaf Java 8 (zie JEP 180), wordt de datastructuur waarin de waarden in één bucket worden opgeslagen, gewijzigd van een lijst in een gebalanceerde boom als een bucket 8 of meer waarden bevat, en wordt deze teruggezet in een lijst als, op op een bepaald moment zijn er nog maar 6 waarden in de bucket. Dit verbetert de prestatie om te zijn O (logboek n).

5.4. Capaciteit en belastingsfactor

Om te voorkomen dat er veel bakken met meerdere waarden zijn, wordt de capaciteit verdubbeld als 75% (de beladingsgraad) van de bakken niet leeg raakt. De standaardwaarde voor de belastingsfactor is 75% en de standaard initiële capaciteit is 16. Beide kunnen in de constructor worden ingesteld.

5.5. Samenvatting van leggen en krijgen Operaties

Laten we samenvatten hoe de leggen en krijgen operaties werken.

Als we een element aan de kaart toevoegen,Hash kaart berekent de emmer. Als de bucket al een waarde bevat, wordt de waarde toegevoegd aan de lijst (of boom) die bij die bucket hoort. Als de bezettingsgraad groter wordt dan de maximale bezettingsgraad van de kaart, wordt de capaciteit verdubbeld.

Als we een waarde van de kaart willen halen,Hash kaart berekent de bucket en haalt de waarde met dezelfde sleutel uit de lijst (of boomstructuur).

6. Conclusie

In dit artikel hebben we gezien hoe u een Hash kaart en hoe het intern werkt. Samen met ArrayList, Hash kaart is een van de meest gebruikte datastructuren in Java, dus het is erg handig om een ​​goede kennis te hebben van hoe je het moet gebruiken en hoe het werkt onder de motorkap. Ons artikel The Java HashMap Under the Hood behandelt de interne onderdelen van Hash kaart meer gedetailleerd.

Zoals gewoonlijk is de volledige broncode beschikbaar op Github.