Skip to Content

Coming in Java 17: HexFormat

A utility class for formatting and parsing hexadecimal strings

Posted on
Photo by CHUTTERSNAP on Unsplash
Photo by CHUTTERSNAP on Unsplash

I can’t tell you how many times I’ve written methods to handle various conversions between bytes and hexadecimal strings. Hopefully once Java 17 is out those days will be behind me. According to the Early Access builds of Java 17 it looks like we’re going to get a HexFormat class to assist with these conversions. While it might not be the flashiest feature ever added to Java, it certainly will come in handy and eliminate the need for a lot of cut and paste utility methods.

Refresher: Hexadecimal numbering uses the digits 0-9 and A-F, or base 16. In computing, bytes are commonly represented as hexadecimal characters to make them easier to read.

Why Do We Need This?

In some problem domains there is a frequent need to convert between hexadecimal strings and byte arrays. Until Java 17, the JDK did not ship with a fully featured and convenient to do this work, and a lot of developers ended up writing their own or adding a dependency to do this. It’s no surprise that after 25 years of having lived without this feature, Java developers have found many ways to solve this problem.

For example, one quick and easy way to do this is to get String.format() to do the work for us:

public String bytesToHexString(final byte[] encodedBytes) {
    final StringBuilder buffer = new StringBulder();
    for(byte theByte : encodedBytes) {
        buffer.append(String.format("%02x", theByte));
    }
    return buffer.toString();
}

This works. It is perhaps a bit slow because of the frequent calls to String.format(), and probably creates more work for the garbage collector than other implementations, but it’s simple and easily explained. The problem is now we have test it, and now we’re copying and pasting this code and the tests between projects.

And let’s not forget the code to convert from String to byte[] as well!

Another common way to solve this problem is to add Apache Commons Codec to the project. It is a high quality library and solves the problem. But if you only need this one feature, pulling in the rest of the library might not be worthwhile or practical.

Starting in Java 17, the standard library will have a new class called HexFormat that will handle conversions between String and byte[], and support a wide array of formatting options.

Creating a HexFormat

There are two main ways to create a new instance of the HexFormat class - one that supports a delimiter and one that does not. To create an instance that, for example, uses : as a delimiter, we would call HexFormat.ofDelimiter(':'). On the other hand, if we do not care for delimiter support (which is probably the most common use case), we would instead call HexFormat.of().

HexFormat ourHexFormat = HexFormat.of();

// Or...

HexFormat ourHexFormat = HexFormat.ofDelimiter(":");

In either case, according to the documentation, instances of HexFormat are thread-safe. This means we can create one instance and use it in more than one thread at a time.

Advice: Not all objects in the Java Standard Library are thread-safe (I’m looking at you, SimpleDateFormat)! It is a good habit to check for this every time you use a class for the first time if you plan on using it from multiple threads.

Using the HexFormat Instance

Once we have a configured HexFormat instance, we can use it to handle all of our hexadecimal conversions:

From String to byte[]:

byte[] asBytes = ourHexFormat.parseHex("0123456789ABCDEF");
// byte[8] { 1, 35, 69, 103, -119, -85, -51, -17 }

And back from byte[] to String:

byte[] someBytes = new byte[] { 1, 35, 69, 103, -119, -85, -51, -17 };
String asString = ourHexFormat.formatHex(someBytes);
// String "0123456789abcdef"

Our HexFormat instance also supports converting various primitive types to hexadecimal Strings:

String fromByte = ourHexFormat.toHexDigits((byte)42);    // "2a"
String fromInt  = ourHexFormat.toHexDigits(11_259_375);  // "00abcdef"
String fromLong = ourHexFormat.toHexDigits(11_259_375L); // "0000000000abcdef"

There are a few more methods around conversions and managing “nibbles” (the upper and lower half of a full byte), but I’ll leave that exploration to you, if you have those use cases.

Uppercase and Lowercase

If you look closely at the examples, you’ll notice that the default behavior in HexFormat is to return lowercase characters. Sometimes you might want to have these emitted in uppercase. Once we have an instance of HexFormat, we can request uppercase formatting:

var ourHexFormat = HexFormat.of().withUpperCase();

ourHexFormat.toHexDigits(11_259_375); // "00ABCDEF"

Or if we want to be explicit, for documentation purposes (which is nice sometimes), we can do that as well:

var ourHexFormat = HexFormat.of().withLowerCase();

ourHexFormat.toHexDigits(42); // "00abcdef"

Delimiters, Prefixes, and Suffixes

When converting between byte[] and String it is sometimes useful to have some control over the format of the String being parsed or the String being produced. Thankfully, the HexFormat class has some built in functions to help us.

We can add a prefix and a suffix to our HexFormat by applying withPrefix(String) and withSuffix(String) respectively. We already know about the ofDelimiter(String) builder, but in case we don’t want to use that, there’s withDelimiter(String) as well.

Combining all three into one:

var hf = HexFormat.of().withPrefix("[").withSuffix("]").withDelimiter(", ");

hf.formatHex(new byte[] {42, 55, 127, 19})  // String "[2a], [37], [7f], [13]"
hf.parseHex("[2a], [37], [7f], [13]")       // byte[4] { 42, 55, 127, 19 }

A common use case for delimiters would be to print out or parse a certificate signature:

var hf = HexFormat.ofDelimiter(":").withUpperCase()

hf.formatHex(someCertificateSignatureByteArray)  
// String "0F:57:0F:E2:BD:7B:48:62:24:D3:54:48:22:63:31:0F:6E:24:F0:9D"

hf.parseHex(""0F:57:0F:E2:BD:7B:48:62:24:D3:54:48:22:63:31:0F:6E:24:F0:9D"")
// byte[20] { 15, 87, 15, -30, -67, 123, 72, 98, 36, -45, 84, 72, 34, 99, 49, 15, 110, 36, -16, -99 }

In Conclusion…

I hope we will all find this new class useful when it lands in Java 17. Because Java 17 is not out yet and things are subject to change, I will keep an eye on the early releases and update this post to reflect any changes that have been made.