Vodič za BitSet na Javi

1. Pregled

U ovom uputstvu vidjet ćemo kako se možemo koristiti BitSets predstavlja vektor bitova.

Prvo ćemo započeti s obrazloženjem zašto se ne koristi boolean []. Zatim nakon upoznavanja s BitSet iznutra, pobliže ćemo pogledati njegov API.

2. Niz bitova

Da bismo pohranili i manipulirali nizima bitova, netko bi mogao tvrditi da bismo trebali koristiti boolean [] kao naša struktura podataka. Na prvi pogled to bi se moglo činiti razumnim prijedlogom.

Međutim, svaki boolean član u boolean [] obično troši jedan bajt umjesto samo jednog bita. Dakle, kada imamo uske memorijske zahtjeve ili samo težimo smanjenom memorijskom otisku, boolean [] daleko od toga da su idealni.

Da stvar bude konkretnija, pogledajmo koliko prostora ima a logička [] s 1024 elementa troši:

boolean [] bitovi = novo boolean [1024]; System.out.println (ClassLayout.parseInstance (bitovi) .toPrintable ());

U idealnom slučaju, očekujemo 1024-bitni memorijski otisak iz ovog polja. Međutim, Java Object Layout (JOL) otkriva posve drugačiju stvarnost:

[Unutarnji dijelovi Z: VRIJEDA ZAMJENE VELIČINE OPISA VRIJEDNOST 0 4 (zaglavlje objekta) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (zaglavlje objekta) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (zaglavlje objekta) 7b 12 07 00 (01111011 00010010 00000111 00000000) (463483) 12 4 (zaglavlje objekta) 00 04 00 00 (00000000 00000100 00000000 00000000) (1024) 16 1024 boolean [Z. N / A Veličina instance: 1040 bajtova

Ako zanemarimo režijske troškove zaglavlja objekta, elementi niza troše 1024 bajta, umjesto očekivanih 1024 bita. To je 700% više memorije od onoga što smo očekivali.

Theproblemi s adresiranjem i kidanje riječi glavni su razlozi zašto booleans više od samo jednog bita.

Da bismo riješili ovaj problem, možemo koristiti kombinaciju numeričkih tipova podataka (kao što je dugo) i bitne operacije. Tu je BitSet ulazi.

3. Kako BitSet Djela

Kao što smo ranije spomenuli, da bismo postigli jedan bit po upotrebi memorije zastavice, BitSet API koristi kombinaciju osnovnih numeričkih vrsta podataka i bitnih operacija.

Radi jednostavnosti, pretpostavimo da ćemo predstavljati osam zastavica s jednom bajt. Isprva inicijaliziramo sve bitove ovog singla bajt s nulom:

Sada ako želimo postaviti bit na položaj tri na pravi, prvo bismo trebali pomaknuti broj 1 za tri:

I onda ili njegov rezultat sa strujom bajt vrijednost:

Isti će se postupak dogoditi ako odlučite postaviti bit na indeks sedam:

Kao što je gore prikazano, izvodimo pomak ulijevo za sedam bitova i kombiniramo rezultat s prethodnim bajt vrijednost pomoću ili operater.

3.1. Dobivanje indeksa bitova

Da biste provjerili je li određeni bitni indeks postavljen na pravi ili ne, koristit ćemo i operater. Na primjer, evo kako provjeravamo je li postavljen indeks tri:

  1. Izvođenje pomicanja ulijevo za tri bita na vrijednosti jedan
  2. Anding rezultat s trenutnim bajt vrijednost
  3. Ako je rezultat veći od nule, pronašli smo podudaranje i taj je bitni indeks zapravo postavljen. Inače, traženi indeks je jasan ili je jednak lažno

Gornji dijagram prikazuje korake dobivanja za indeks tri. Međutim, ako se raspitamo o jasnom indeksu, rezultat će biti drugačiji:

Budući da je i rezultat je jednak nuli, indeks četiri je jasan.

3.2. Uzgoj skladišta

Trenutno možemo pohraniti samo vektor od 8 bitova. Da biste prešli ovo ograničenje, samo moramo koristiti niz bajts, umjesto jednog bajt, to je to!

Sad, svaki put kad trebamo postaviti, dobiti ili očistiti određeni indeks, prvo bismo trebali pronaći odgovarajući element niza. Na primjer, pretpostavimo da ćemo postaviti indeks 14:

Kao što je prikazano na gornjem dijagramu, nakon pronalaska pravog elementa niza postavili smo odgovarajući indeks.

Također, ako ovdje želimo postaviti indeks veći od 15, BitSet prvo će proširiti svoj unutarnji niz. Tek nakon proširenja niza i kopiranja elemenata postavit će traženi bit. Ovo je donekle slično kako ArrayList radi interno.

Do sada smo koristili bajt tip podataka radi jednostavnosti. The BitSet API, međutim, koristi niz dugo vrijednosti interno.

4. The BitSet API

Sad kad znamo dovoljno o teoriji, vrijeme je da vidimo što BitSet API izgleda.

Za početak, usporedimo memorijski otisak a BitSet primjer s 1024 bita s logička [] vidjeli smo ranije:

BitSet bitSet = novi BitSet (1024); System.out.println (GraphLayout.parseInstance (bitSet) .toPrintable ());

Ovo će ispisati i plitku veličinu BitSet instanca i veličina njenog unutarnjeg niza:

[e-pošta zaštićena] vanjski objekti: ADRESA VELIČINA TIP VRIJEDNOST PUTA 70f97d208 24 java.util.BitSet (objekt) 70f97d220 144 [J .words [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 , 0, 0, 0, 0, 0]

Kao što je gore prikazano, koristi a dugo [] sa 16 elemenata (16 * 64 bita = 1024 bita) iznutra. U svakom slučaju, ovaj primjerak koristi ukupno 168 bajtova, dok boolean [] koristili su 1024 bajta.

Što više bitova imamo, to se više povećava razlika u otisku. Na primjer, za pohranu 1024 * 1024 bita, boolean [] troši 1 MB, a BitSet instanci troši oko 130 KB.

4.1. Konstruiranje BitSets

Najjednostavniji način za stvaranje a BitSet instanca je koristiti konstruktor no-arg:

BitSet bitSet = novi BitSet ();

Ovo će stvoriti BitSet primjer s a dugo [] veličine jedan. Naravno, može automatski rasti ovaj niz ako je potrebno.

Također je moguće stvoriti BitSet s početnim brojem bitova:

BitSet bitSet = novi BitSet (100_000);

Ovdje će unutarnji niz imati dovoljno elemenata da stane 100 000 bitova. Ovaj konstruktor dobro dođe kada već imamo razumnu procjenu broja bitova za pohranu. U takvim slučajevima korištenja, može spriječiti ili smanjiti nepotrebno kopiranje elemenata niza dok ga raste.

Moguće je čak i stvoriti BitSet od postojećeg dugo [], bajt[], LongBuffer, i ByteBuffer. Na primjer, ovdje stvaramo BitSet primjer iz datog dugo []:

BitSet bitSet = BitSet.valueOf (novo dugo [] {42, 12});

Postoje još tri preopterećene verzije vrijednost() statička tvornička metoda za podršku ostalim spomenutim vrstama.

4.2. Postavljanje bitova

Vrijednost određenog indeksa možemo postaviti na pravi koristiti skup (indeks) metoda:

BitSet bitSet = novi BitSet (); bitSet.set (10); assertThat (bitSet.get (10)). isTrue ();

Kao i obično, indeksi se temelje na nuli. Moguće je čak postaviti niz bitova pravi koristiti set (odInclusive, doExclusive) metoda:

bitSet.set (20, 30); za (int i = 20; i <= 29; i ++) {assertThat (bitSet.get (i)). isTee (); } assertThat (bitSet.get (30)). isFalse ();

Kao što je vidljivo iz potpisa metode, početni indeks uključuje, a završni indeks je isključiv.

Kad kažemo postavljanje indeksa, obično mislimo na postavljanje indeksa pravi. Unatoč ovoj terminologiji, možemo postaviti određeni bitni indeks na lažno koristiti skup (indeks, logička vrijednost) metoda:

bitSet.set (10, false); assertThat (bitSet.get (10)). isFalse ();

Ova verzija također podržava postavljanje raspona vrijednosti:

bitSet.set (20, 30, false); za (int i = 20; i <= 29; i ++) {assertThat (bitSet.get (i)). isFalse (); }

4.3. Brisanje bitova

Umjesto postavljanja određenog indeksa bitova na lažno, možemo to jednostavno očistiti pomoću jasno (indeks) metoda:

bitSet.set (42); assertThat (bitSet.get (42)). isTee (); bitSet.clear (42); assertThat (bitSet.get (42)). isFalse ();

Štoviše, također možemo očistiti niz bitova pomoću jasno (odInclusive, doExclusive) preopterećena verzija:

bitSet.set (10, 20); za (int i = 10; i <20; i ++) {assertThat (bitSet.get (i)). isTee (); } bitSet.clear (10, 20); za (int i = 10; i <20; i ++) {assertThat (bitSet.get (i)). isFalse (); }

Zanimljivo, ako ovu metodu pozovemo bez prosljeđivanja argumenata, obrisat će sve postavljene bitove:

bitSet.set (10, 20); bitSet.clear (); za (int i = 0; i <100; i ++) {assertThat (bitSet.get (i)). isFalse (); }

Kao što je prikazano gore, nakon poziva na čisto() metoda, svi bitovi su postavljeni na nulu.

4.4. Dobivanje bitova

Do sada smo koristili dobiti (indeks) metoda prilično opsežno. Kad se postavi traženi indeks bitova, tada će se vratiti ova metoda pravi. U suprotnom, vratit će se lažno:

bitSet.set (42); assertThat (bitSet.get (42)). isTee (); assertThat (bitSet.get (43)). isFalse ();

Slično postavljen i čisto, možemo dobiti niz bitnih indeksa pomoću dobiti (fromInclusive, toExclusive) metoda:

bitSet.set (10, 20); BitSet newBitSet = bitSet.get (10, 20); za (int i = 0; i <10; i ++) {assertThat (newBitSet.get (i)). isTrue (); }

Kao što je gore prikazano, ova metoda vraća drugu BitSet u rasponu [20, 30) trenutnog. Odnosno, indeks 20 bitSet varijabla je ekvivalentna indeksu nula vrijednosti newBitSet varijabilna.

4.5. Preokretanje bitova

Da bismo poništili trenutnu vrijednost indeksa bitova, možemo koristiti okretanje (indeks) metoda. Odnosno, okrenuti će se pravi vrijednosti do lažno i obrnuto:

bitSet.set (42); bitSet.flip (42); assertThat (bitSet.get (42)). isFalse (); bitSet.flip (12); assertThat (bitSet.get (12)). isTee ();

Slično tome, možemo postići istu stvar za niz vrijednosti koristeći flip (fromInclusive, toExclusive) metoda:

bitSet.flip (30, 40); za (int i = 30; i <40; i ++) {assertThat (bitSet.get (i)). isTee (); }

4.6. Duljina

Postoje tri metode poput a dužini BitSet. The veličina() metoda vraća broj bitova koje unutarnji niz može predstavljati. Na primjer, budući da konstruktor no-arg dodjeljuje a dugo [] niz s jednim elementom, a zatim veličina() vratit će 64 za to:

BitSet defaultBitSet = novi BitSet (); assertThat (defaultBitSet.size ()). isEqualTo (64);

S jednim 64-bitnim brojem možemo predstavljati samo 64 bita. Naravno, to će se promijeniti ako izričito dodamo broj bitova:

BitSet bitSet = novi BitSet (1024); assertThat (bitSet.size ()). isEqualTo (1024);

Štoviše, kardinalnost () metoda predstavlja broj postavljenih bitova u a BitSet:

assertThat (bitSet.cardinality ()). isEqualTo (0); bitSet.set (10, 30); assertThat (bitSet.cardinality ()). isEqualTo (30 - 10);

Isprva ova metoda vraća nulu kao što su svi bitovi lažno. Nakon postavljanja raspona [10, 30) na pravi, onda kardinalnost () poziv metode vraća 20.

Također, duljina () metoda vraća jedan indeks nakon indeksa zadnjeg postavljenog bita:

assertThat (bitSet.length ()). isEqualTo (30); bitSet.set (100); assertThat (bitSet.length ()). isEqualTo (101);

Isprva je zadnji postavljeni indeks 29, pa ova metoda vraća 30. Kada indeks 100 postavimo na true, tada se duljina () metoda vraća 101. Također je vrijedno spomenuti da će ova metoda vratiti nulu ako su svi bitovi jasni.

Napokon, prazno je() metoda se vraća lažno kada postoji najmanje jedan postavljeni bit u BitSet. U suprotnom, vratit će se pravi:

assertThat (bitSet.isEmpty ()). isFalse (); bitSet.clear (); assertThat (bitSet.isEmpty ()). isTee ();

4.7. Kombinirajući s ostalim BitSets

The presijeca (BitSet) metoda uzima drugu BitSet i vraća se pravi kad dvoje BitSetimaju nešto zajedničko. Odnosno, oni imaju barem jedan postavljeni bit u istom indeksu:

BitSet prvi = novi BitSet (); first.set (5, 10); BitSet drugo = novi BitSet (); second.set (7, 15); assertThat (first.intersects (second)). isTee ();

Raspon [7, 9] postavljen je u obje BitSets, pa se ova metoda vraća pravi.

Također je moguće izvesti logičko i operacija na dvoje BitSets:

prvi.i (drugi); assertThat (first.get (7)). isTee (); assertThat (first.get (8)). isTee (); assertThat (first.get (9)). isTee (); assertThat (first.get (10)). isFalse ();

Ovo će izvesti logično i između to dvoje BitSets i modificira prvi varijabla s rezultatom. Slično tome, možemo izvesti logički xor na dva BitSets, također:

first.clear (); first.set (5, 10); prvi.xor (drugi); za (int i = 5; i <7; i ++) {assertThat (first.get (i)). isTrue (); } za (int i = 10; i <15; i ++) {assertThat (first.get (i)). isTee (); }

Postoje i druge metode poput iNije (BitSet) ili ili (BitSet),koji mogu izvoditi druge logičke operacije na dvoje BitSets.

4.8. Razno

Od Jave 8, tamo je stream () metoda za strujanje svih postavljenih bitova a BitSet. Na primjer:

BitSet bitSet = novi BitSet (); bitSet.set (15, 25); bitSet.stream (). forEach (System.out :: println);

Ovo će ispisati sve postavljene bitove na konzolu. Budući da će ovo vratiti IntStream, možemo izvoditi uobičajene numeričke operacije kao što su zbrajanje, prosjek, brojanje i tako dalje. Na primjer, ovdje računamo broj postavljenih bitova:

assertThat (bitSet.stream (). count ()). isEqualTo (10);

Također, the nextSetBit (fromIndex) metoda vratit će sljedeći indeks postavljenih bitova počevši od fromIndex:

assertThat (bitSet.nextSetBit (13)). isEqualTo (15);

The fromIndex sam je uključen u ovaj izračun. Kad ih nema pravi malo lijevo u BitSet, vratit će -1:

assertThat (bitSet.nextSetBit (25)). isEqualTo (-1);

Slično tome, the nextClearBit (fromIndex) vraća sljedeći jasni indeks počevši od fromIndex:

assertThat (bitSet.nextClearBit (23)). isEqualTo (25);

S druge strane, previousClearBit (fromIndex) vraća indeks najbližeg jasnog indeksa u suprotnom smjeru:

assertThat (bitSet.previousClearBit (24)). isEqualTo (14);

Isto vrijedi i za previousSetBit (fromIndex):

assertThat (bitSet.previousSetBit (29)). isEqualTo (24); assertThat (bitSet.previousSetBit (14)). isEqualTo (-1);

Štoviše, možemo pretvoriti a BitSet do a bajt[] ili a dugo [] koristiti toByteArray () ili toLongArray () metode, odnosno:

bajt [] bajtova = bitSet.toByteArray (); long [] longs = bitSet.toLongArray ();

5. Zaključak

U ovom uputstvu vidjeli smo kako se možemo koristiti BitSets predstavlja vektor bitova.

Isprva smo se upoznali s obrazloženjem neaktivnosti logička [] da predstavlja vektor bitova. Tada smo vidjeli kako a BitSet radi interno i kako izgleda njegov API.

Kao i obično, svi su primjeri dostupni na GitHubu.