Panduan untuk BitSet di Java

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melihat bagaimana kita dapat menggunakan BitSet untuk mewakili vektor bit.

Pertama, kita akan mulakan dengan rasional di belakang untuk tidak menggunakan boolean [] . Kemudian setelah membiasakan diri dengan BitSet dalaman, kami akan melihat APInya dengan lebih dekat.

2. Array Bits

Untuk menyimpan dan memanipulasi susunan bit, seseorang mungkin berpendapat bahawa kita harus menggunakan boolean [] sebagai struktur data kita. Pada pandangan pertama, itu mungkin merupakan cadangan yang munasabah.

Walau bagaimanapun, setiap anggota boolean dalam boolean [] biasanya menggunakan satu bait dan bukan hanya satu bit . Oleh itu, apabila kita mempunyai keperluan memori yang ketat, atau hanya bertujuan untuk mengurangkan jejak memori, boolean [] jauh dari ideal.

Untuk menjadikan perkara lebih konkrit, mari kita lihat berapa banyak ruang boolean [] dengan 1024 elemen yang digunakan:

boolean[] bits = new boolean[1024]; System.out.println(ClassLayout.parseInstance(bits).toPrintable());

Sebaik-baiknya, kami menjangkakan jejak memori 1024-bit dari array ini. Walau bagaimanapun, Java Object Layout (JOL) mengungkapkan realiti yang sama sekali berbeza:

[Z object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 7b 12 07 00 (01111011 00010010 00000111 00000000) (463483) 12 4 (object header) 00 04 00 00 (00000000 00000100 00000000 00000000) (1024) 16 1024 boolean [Z. N/A Instance size: 1040 bytes

Sekiranya kita mengabaikan overhead header objek, elemen array memakan 1024 byte, bukannya 1024 bit yang diharapkan. Itu 700% lebih banyak memori daripada apa yang kita jangkakan.

Masalah kebolehtanganan dan pemecahan kata adalah sebab utama mengapa boolean lebih daripada satu bit sahaja.

Untuk menyelesaikan masalah ini, kita dapat menggunakan kombinasi jenis data berangka (seperti panjang ) dan operasi bit-bijaksana. Di situlah BitSet masuk.

3. Bagaimana BitSet Berfungsi

Seperti yang telah kami sebutkan sebelumnya, untuk mencapai penggunaan memori satu bit per bendera, API BitSet menggunakan kombinasi jenis data angka asas dan operasi bit-bijaksana.

Demi kesederhanaan, anggaplah kita akan mewakili lapan bendera dengan satu bait . Pada mulanya, kami memulakan semua bit bait tunggal ini dengan sifar:

Sekarang jika kita mahu menetapkan bit pada kedudukan tiga menjadi benar , pertama kita harus menggeser kiri nombor 1 dengan tiga:

Dan kemudian atau hasilnya dengan semasa bait nilai :

Proses yang sama akan berlaku sekiranya memutuskan untuk menetapkan bit pada indeks tujuh:

Seperti yang ditunjukkan di atas, kami melakukan pergeseran kiri sebanyak tujuh bit dan menggabungkan hasilnya dengan nilai bait sebelumnya menggunakan operator atau .

3.1. Mendapatkan Indeks Bit

Untuk memeriksa sama ada indeks bit tertentu ditetapkan ke benar atau tidak, kami akan menggunakan dan operator . Sebagai contoh, inilah cara kita memeriksa apakah indeks tiga ditetapkan:

  1. Melakukan pergeseran kiri sebanyak tiga bit pada nilai satu
  2. Anding hasilnya dengan semasa bait nilai
  3. Sekiranya hasilnya lebih besar daripada sifar, maka kami menjumpai padanan, dan indeks bit itu sebenarnya telah ditetapkan. Jika tidak, indeks yang diminta jelas atau sama dengan false

Gambar rajah di atas menunjukkan langkah-langkah operasi get untuk indeks tiga. Sekiranya kita menanyakan indeks yang jelas, hasilnya akan berbeza:

Sejak dan keputusan adalah sama dengan sifar, indeks empat jelas.

3.2. Menambah Penyimpanan

Pada masa ini, kita hanya dapat menyimpan vektor 8 bit. Untuk melampaui batasan ini, kita hanya perlu menggunakan pelbagai bait , bukannya satu bait , itu sahaja!

Sekarang, setiap kali kita perlu menetapkan, mendapatkan, atau membersihkan indeks tertentu, kita harus mencari elemen array yang sesuai, pertama. Sebagai contoh, anggaplah kita akan menetapkan indeks 14:

Seperti yang ditunjukkan dalam rajah di atas, setelah menemukan elemen susunan yang tepat, kami menetapkan indeks yang sesuai.

Juga, jika kita ingin menetapkan indeks melebihi 15 di sini, BitSet akan memperluas susunan dalamannya, terlebih dahulu. Hanya setelah mengembangkan array dan menyalin elemen-elemen itu, ia akan menetapkan bit yang diminta. Ini agak serupa dengan cara ArrayList berfungsi secara dalaman.

Setakat ini, kami menggunakan jenis data bait demi kesederhanaan. The BitSet API, bagaimanapun, adalah dengan menggunakan pelbagai panjang nilai-nilai dalaman .

4. API BitSet

Sekarang kita sudah cukup mengetahui tentang teori, sudah waktunya untuk melihat seperti apa BitSet API.

Sebagai permulaan, mari kita bandingkan jejak memori contoh BitSet dengan 1024 bit dengan boolean [] yang kita lihat sebelumnya:

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

Ini akan mencetak ukuran cetek contoh BitSet dan ukuran array dalamannya:

[email protected] object externals: ADDRESS SIZE TYPE PATH VALUE 70f97d208 24 java.util.BitSet (object) 70f97d220 144 [J .words [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Seperti yang ditunjukkan di atas, ia menggunakan panjang [] dengan 16 elemen (16 * 64 bit = 1024 bit) secara dalaman. Bagaimanapun, contoh ini menggunakan total 168 bait, sementara boolean [] menggunakan 1024 bait .

The more bits we have, the more the footprint difference increases. For example, to store 1024 * 1024 bits, the boolean[] consumes 1 MB, and the BitSet instance consumes around 130 KB.

4.1. Constructing BitSets

The simplest way to create a BitSet instance is to use the no-arg constructor:

BitSet bitSet = new BitSet();

This will create a BitSet instance with a long[] of size one. Of course, it can automatically grow this array if needed.

It's also possible to create a BitSet with an initial number of bits:

BitSet bitSet = new BitSet(100_000);

Here, the internal array will have enough elements to hold 100,000 bits. This constructor comes in handy when we already have a reasonable estimate on the number of bits to store. In such use cases, it can prevent or decrease the unnecessary copying of array elements while growing it.

It's even possible to create a BitSet from an existing long[], byte[], LongBuffer, and ByteBuffer. For instance, here we're creating a BitSet instance from a given long[]:

BitSet bitSet = BitSet.valueOf(new long[] { 42, 12 });

There are three more overloaded versions of the valueOf() static factory method to support the other mentioned types.

4.2. Setting Bits

We can set the value of a particular index to true using the set(index) method:

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

As usual, the indices are zero-based. It's even possible to set a range of bits to true using the set(fromInclusive, toExclusive) method:

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

As is evident from the method signature, the beginning index is inclusive, and the ending one is exclusive.

When we say setting an index, we usually mean setting it to true. Despite this terminology, we can set a particular bit index to false using the set(index, boolean) method:

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

This version also supports setting a range of values:

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

4.3. Clearing Bits

Instead of setting a specific bit index to false, we can simply clear it using the clear(index) method:

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

Moreover, we can also clear a range of bits with the clear(fromInclusive, toExclusive) overloaded version:

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

Interestingly, if we call this method without passing any arguments, it'll clear all the set bits:

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

As shown above, after calling the clear() method, all bits are set to zero.

4.4. Getting Bits

So far, we used the get(index) method quite extensively. When the requested bit index is set, then this method will return true. Otherwise, it'll return false:

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

Similar to set and clear, we can get a range of bit indices using the get(fromInclusive, toExclusive) method:

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

As shown above, this method returns another BitSet in the [20, 30) range of the current one. That is, index 20 of the bitSet variable is equivalent to index zero of the newBitSet variable.

4.5. Flipping Bits

To negate the current bit index value, we can use the flip(index) method. That is, it'll turn true values to false and vice versa:

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

Similarly, we can achieve the same thing for a range of values using the flip(fromInclusive, toExclusive) method:

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

4.6. Length

There are three length-like methods for a BitSet. The size() method returns the number of bits the internal array can represent. For instance, since the no-arg constructor allocates a long[] array with one element, then the size() will return 64 for it:

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

With one 64-bit number, we can only represent 64 bits. Of course, this will change if we pass the number of bits explicitly:

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

Moreover, the cardinality() method represents the number of set bits in a BitSet:

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

At first, this method returns zero as all bits are false. After setting the [10, 30) range to true, then the cardinality() method call returns 20.

Also, the length() method returns the one index after the index of the last set bit:

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

At first, the last set index is 29, so this method returns 30. When we set the index 100 to true, then the length() method returns 101. It's also worth mentioning that this method will return zero if all bits are clear.

Finally, the isEmpty() method returns false when there is at least one set bit in the BitSet. Otherwise, it'll return true:

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

4.7. Combining With Other BitSets

The intersects(BitSet) method takes another BitSet and returns true when two BitSets have something in common. That is, they have at least one set bit in the same index:

BitSet first = new BitSet(); first.set(5, 10); BitSet second = new BitSet(); second.set(7, 15); assertThat(first.intersects(second)).isTrue();

The [7, 9] range is set in both BitSets, so this method returns true.

It's also possible to perform the logical and operation on two BitSets:

first.and(second); assertThat(first.get(7)).isTrue(); assertThat(first.get(8)).isTrue(); assertThat(first.get(9)).isTrue(); assertThat(first.get(10)).isFalse();

This will perform a logical and between the two BitSets and modifies the first variable with the result. Similarly, we can perform a logical xor on two BitSets, too:

first.clear(); first.set(5, 10); first.xor(second); for (int i = 5; i < 7; i++) { assertThat(first.get(i)).isTrue(); } for (int i = 10; i < 15; i++) { assertThat(first.get(i)).isTrue(); }

There are other methods such as the andNot(BitSet) or the or(BitSet),which can perform other logical operations on two BitSets.

4.8. Miscellaneous

As of Java 8, there is a stream() method to stream all set bits of a BitSet. For instance:

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

This will print all set bits to the console. Since this will return an IntStream, we can perform common numerical operations such as summation, average, counting, and so on. For instance, here we're counting the number of set bits:

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

Also, the nextSetBit(fromIndex) method will return the next set bit index starting from the fromIndex:

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

The fromIndex itself is included in this calculation. When there isn't any true bit left in the BitSet, it'll return -1:

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

Similarly, the nextClearBit(fromIndex) returns the next clear index starting from the fromIndex:

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

On the other hand, the previousClearBit(fromIndex) returns the index of the nearest clear index in the opposite direction:

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

Same is true for previousSetBit(fromIndex):

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

Moreover, we can convert a BitSet to a byte[] or a long[] using the toByteArray() or toLongArray() methods, respectively:

byte[] bytes = bitSet.toByteArray(); long[] longs = bitSet.toLongArray();

5. Conclusion

Dalam tutorial ini, kita melihat bagaimana kita dapat menggunakan BitSet untuk mewakili vektor bit.

Pada mulanya, kami sudah biasa dengan alasan di sebalik tidak menggunakan boolean [] untuk mewakili vektor bit. Kemudian kami melihat bagaimana BitSet berfungsi secara dalaman dan bagaimana APInya.

Seperti biasa, semua contoh boleh didapati di GitHub.