When developing applications, there are situations where we need to represent and manipulate large collections of binary values – typically true or false. Storing these values in a conventional data structure like a boolean[] or a HashSet<Integer> can be inefficient in terms of memory and speed.
Java provides the BitSet class in the java.util package to address this problem. BitSet represents a sequence of bits that can grow dynamically and provides built-in methods for bit-level manipulation.
In this article, we will cover:
- Introduction to
BitSet - Internal representation and advantages
- Common operations
- Basic usage example
- Real-world example: Attendance tracking
- Bitwise operations (AND, OR, XOR, ANDNOT)
- Performance comparison with
boolean[]andHashSet - Conclusion
1. Introduction to BitSet
A BitSet is essentially a vector of bits that can grow as needed. Each bit in the set can be either true (1) or false (0). Unlike arrays of booleans, which require at least one byte per element, BitSet stores bits in a much more compact form, using an internal array of long values.
Characteristics:
- Index-based: Each bit is referenced by its index (starting from 0).
- Resizable: The size is dynamic and increases automatically when higher indexes are set.
- Memory efficient: Stores data as bits instead of full booleans.
- Supports set operations: Provides methods for union, intersection, difference, and complement.
2. Internal Representation and Advantages
Internally, a BitSet uses an array of long values, with each long containing 64 bits. This allows efficient storage and manipulation of large sequences of bits.
For example, if we need to represent 1,000 flags:
- Using a
boolean[], memory usage is at least 1,000 bytes (1 byte per boolean). - Using a
BitSet, memory usage is around 125 bytes (1,000 bits ≈ 125 bytes).
This significant reduction makes BitSet highly suitable for scenarios where memory is critical.
3. Common Operations in BitSet
Here are the most commonly used methods of the BitSet class:
| Method | Description |
|---|---|
set(int index) | Sets the bit at the specified index to true. |
clear(int index) | Clears (sets to false) the bit at the specified index. |
flip(int index) | Toggles the bit at the specified index. |
get(int index) | Returns the value of the bit at the given index. |
cardinality() | Returns the number of bits set to true. |
nextSetBit(int fromIndex) | Returns the index of the first set bit on or after fromIndex. |
nextClearBit(int fromIndex) | Returns the index of the first clear bit on or after fromIndex. |
and(BitSet bs) | Performs logical AND with another BitSet. |
or(BitSet bs) | Performs logical OR with another BitSet. |
xor(BitSet bs) | Performs logical XOR with another BitSet. |
andNot(BitSet bs) | Clears all bits in this BitSet that are set in the given BitSet. |
4. Example 1: Basic BitSet Usage
import java.util.BitSet;
public class BitSetExample {
public static void main(String[] args) {
BitSet bitSet = new BitSet();
// Set bits at index 1, 3, and 5
bitSet.set(1);
bitSet.set(3);
bitSet.set(5);
// Print the BitSet
System.out.println("BitSet: " + bitSet);
// Check if a bit is set
System.out.println("Is bit at index 3 set? " + bitSet.get(3));
System.out.println("Is bit at index 2 set? " + bitSet.get(2));
// Flip bit at index 3
bitSet.flip(3);
System.out.println("After flipping index 3: " + bitSet);
}
}
Output:
BitSet: {1, 3, 5}
Is bit at index 3 set? true
Is bit at index 2 set? false
After flipping index 3: {1, 5}
Explanation: Bits at positions 1, 3, and 5 are initially set. Checking index 3 returns true, while index 2 returns false. After flipping index 3, it is cleared, leaving only {1, 5}.
5. Example 2: Real-World Example – Attendance Tracking
import java.util.BitSet;
public class AttendanceTracker {
public static void main(String[] args) {
BitSet attendance = new BitSet(10);
// Students present: 0, 2, 4, 7
attendance.set(0);
attendance.set(2);
attendance.set(4);
attendance.set(7);
System.out.println("Attendance BitSet: " + attendance);
System.out.println("Number of students present: " + attendance.cardinality());
System.out.println("Is student 5 present? " + attendance.get(5));
System.out.println("First absent student index: " + attendance.nextClearBit(0));
}
}
Output:
Attendance BitSet: {0, 2, 4, 7}
Number of students present: 4
Is student 5 present? false
First absent student index: 1
Explanation: The BitSet efficiently stores attendance. The cardinality() method counts present students, get() checks if a specific student is present, and nextClearBit() identifies the first absent student.
6. Example 3: Bitwise Operations
import java.util.BitSet;
public class ClubMembership {
public static void main(String[] args) {
BitSet mathClub = new BitSet();
BitSet scienceClub = new BitSet();
mathClub.set(1);
mathClub.set(2);
mathClub.set(3);
mathClub.set(5);
scienceClub.set(2);
scienceClub.set(3);
scienceClub.set(4);
scienceClub.set(6);
BitSet bothClubs = (BitSet) mathClub.clone();
bothClubs.and(scienceClub);
System.out.println("Students in both clubs: " + bothClubs);
BitSet eitherClub = (BitSet) mathClub.clone();
eitherClub.or(scienceClub);
System.out.println("Students in either club: " + eitherClub);
BitSet exactlyOne = (BitSet) mathClub.clone();
exactlyOne.xor(scienceClub);
System.out.println("Students in exactly one club: " + exactlyOne);
BitSet onlyMath = (BitSet) mathClub.clone();
onlyMath.andNot(scienceClub);
System.out.println("Students only in Math club: " + onlyMath);
}
}
Output:
Students in both clubs: {2, 3}
Students in either club: {1, 2, 3, 4, 5, 6}
Students in exactly one club: {1, 4, 5, 6}
Students only in Math club: {1, 5}
Explanation: This example demonstrates how BitSet can perform mathematical set operations directly on bits. This makes it highly efficient for problems involving set theory.
7. Performance Comparison
Let us compare three approaches for representing 1,000 boolean flags:
- boolean[] – Each boolean consumes 1 byte → ~1,000 bytes.
- HashSet<Integer> – Each entry stores an object reference plus overhead → multiple kilobytes.
- BitSet – 1,000 bits ≈ 125 bytes.
Benchmark Considerations:
- Memory Usage: BitSet is the most compact, followed by boolean[], and HashSet consumes the most.
- Lookup Speed:
- BitSet provides
O(1)lookup using bitwise operations. - boolean[] also provides
O(1)lookup but uses more memory. - HashSet provides
O(1)average lookup but with significant overhead.
- BitSet provides
- Bulk Operations:
- BitSet can perform operations like AND, OR, XOR across thousands of elements in a single CPU instruction, which is far more efficient than iterating through arrays or sets.
Thus, BitSet is the best choice when memory efficiency and bulk operations are required, especially when dealing with large sets of boolean values.
8. Conclusion
The BitSet class in Java provides an efficient way to store and manipulate large collections of bits. Compared to other data structures like boolean arrays or HashSets, it is both memory-efficient and computationally powerful.
Practical applications include:
- Attendance or availability tracking
- Feature flags in software systems
- Graph algorithms (storing adjacency matrices compactly)
- Filtering and searching in large datasets
- Mathematical set operations
If your application requires handling a large number of boolean states, BitSet should be strongly considered as a solution.