Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

UUID (Universally Unique Identifier) represents a 128-bit number that is designed to be globally unique. In practice, UUIDs are suitable for use in situations that require unique identification, such as creating the primary key for a database table.

In Java, the primitive long is a data type that is easy to read and understand by humans. In many cases, the uniqueness of a 64-bit long is often sufficient, and the probability of a collision is quite low. Furthermore, databases like MySQL, PostgreSQL, and many others have been optimized to work efficiently with numeric data types.

In this article, we’ll discuss various ways to generate unique positive long values using the Java UUID class.

2. Generate Unique Positive long

This is an interesting challenge because UUIDs are represented as 128-bit values, but long is only 64 bits. This means that long will not be as unique as UUID. However, we will try various approaches to overcome this.

In this case, we’ll first create a class, UUIDPositiveLongGenerator, to accommodate each of these and make testing easier later:

public class UUIDPositiveLongGenerator {
    // methods to generate a unique positive long using a UUID
}

2.1. Using getLeastSignificantBits()

The getLeastSignificantBits() is a built-in method of the UUID class that returns the lowest 64 bits of the UUID. This means it only provides one-half (to be exact) of the 128-bit UUID value.

Let’s call it in Java after calling the randomUUID() method:

public long getLeastSignificantBits(){
    return Math.abs(UUID.randomUUID().getLeastSignificantBits());
}

We’ll continue to use Math.abs() in each of the following methods to ensure positive values.

2.2. Using getMostSignificantBits()

Similarly, the getMostSignificantBits() method in the UUID class also returns a 64-bit long. The difference is that it takes the bits that are located in the highest position in the 128-bit UUID value.

Again, we’ll chain it after the randomUUID() method:

public long getMostSignificantBits(){
    return Math.abs(UUID.randomUUID().getMostSignificantBits());
}

As we can see, if simplicity and speed are our priorities, then choosing getLeastSignificantBits() or getMostSignificantBits() is an excellent option.

2.3. Combine Using ByteBuffer

Alternatively, we can enhance the uniqueness and distribution of the resulting values by combining getMostSignificantBits() and getLeastSignificantBits() from UUIDs.

This time, we will use ByteBuffer to combine them:

public long combineByteBuffer(){
    UUID uuid = UUID.randomUUID();
    ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
    bb.putLong(uuid.getMostSignificantBits());
    bb.putLong(uuid.getLeastSignificantBits());
    bb.rewind(); 
    return Math.abs(bb.getLong());
}

Here, we use a ByteBuffer of 16 bytes or 128 bits, since this size is sufficient to hold two long values. Then we call rewind() to set the buffer position back to the beginning. Next, we call getLong() to get the combined value.

This way, we hope to generate more unique and random values, although we still cannot guarantee complete uniqueness.

Because it uses ByteBuffer, this method may incur more memory allocation than just using getMostSignificantBits() or getLeastSignificantBits().

2.4. Combine Using Bitwise Operations

We can also combine getLeastSignificantBits() and getMostSignificantBits() using the bitwise operation:

public long combineBitwise() {
    UUID uniqueUUID = UUID.randomUUID();
    long mostSignificantBits = uniqueUUID.getMostSignificantBits();
    long leastSignificantBits = uniqueUUID.getLeastSignificantBits();
    return Math.abs((mostSignificantBits << 32) | (leastSignificantBits & 0xFFFFFFFFL));
}

First, we take the last 32 bits of the most significant bits and shift them to the left side. Then, we take the last 32 bits of the least significant bits and keep them on the right side. The result is a combination of both, with all 64 bits of the long populated.

This way outperforms ByteBuffer in terms of performance by solely using bitwise operations, despite both still having the potential for collision.

The additional memory required for this approach is not as extensive as that of ByteBuffer.

2.5. Combine Bits Directly

We can also do this by combining the bits directly:

public long combineDirect(){
    UUID uniqueUUID = UUID.randomUUID();
    long mostSignificantBits = uniqueUUID.getMostSignificantBits();
    long leastSignificantBits = uniqueUUID.getLeastSignificantBits();
    return Math.abs(mostSignificantBits ^ (leastSignificantBits >> 1));
}

We shift the leastSignificantBits to the right by one bit so that the right-most bit will be removed, and a zero bit will be added on the left side.

Then, we use the XOR operator (^) to combine the result of the mostSignificantBits with the result of the leastSignificantBits shift.

In this approach, memory allocation is more efficient than in previous methods because the operation is simpler, although the possibility of collisions still exists.

2.6. Combine Using Simple Permutation

We can also perform simple permutations on the UUID bytes to create greater variation and convert them to long values:

public long combinePermutation(){
    UUID uuid = UUID.randomUUID();
    long mostSigBits = uuid.getMostSignificantBits();
    long leastSigBits = uuid.getLeastSignificantBits();
    byte[] uuidBytes = new byte[16];

    for (int i = 0; i < 8; i++) {
        uuidBytes[i] = (byte) (mostSigBits >>> (8 * (7 - i)));
        uuidBytes[i + 8] = (byte) (leastSigBits >>> (8 * (7 - i)));
    }

    long result = 0;
    for (byte b : uuidBytes) {
        result = (result << 8) | (b & 0xFF);
    }
    return Math.abs(result);
}

We convert the UUID into a byte array and then convert the byte array into a long value.

This approach requires additional memory allocation for byte array iteration and requires more instructions because it needs to move bits into bytes.

This is somewhat lengthier than the previous approaches, but with this approach, we can provide greater control over the UUID formation process.

3. Comparison

Let’s compare the approaches we’ve presented based on four main criteria: simplicity, execution speed, memory usage efficiency, and the uniqueness level of the generated values:

Approach Simplicity Execution Speed Memory Usage Efficiency Uniqueness Level
getLeastSignificantBits() 4 4 5 3
getMostSignificantBits() 4 4 5 3
combineByteBuffer() 3 2 2 4
combineBitwise() 3 3 3 4
combineDirect() 3 3 3 4
combinePermutation() 2 3 3 4

In this context, scores are given on a scale of 1 to 5, where higher values indicate better performance or characteristics in the given criteria.

4. Conclusion

In this article, we explored various methods for generating unique positive long values using UUID. The choice of approach ultimately depends on specific use-case requirements. If simplicity and speed are crucial, one may prefer direct access methods. On the other hand, if prioritizing uniqueness and variability, bitwise and permutation-based methods offer more diverse outcomes.

In real projects, such as databases, maintaining uniqueness and preventing collisions becomes more accessible by using primary keys, implementing unique constraints, creating indexes on frequently accessed columns, and employing transactions and locking techniques. Therefore, we must ensure that we choose a unique strategy that aligns with our specific needs.

As always, the full source code is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!