Java 17 API Additions: HexFormat, InstantSource and More
Introduction
Besides sealed classes and the new random number generators, Java 17Click to read tutorial: Overview of New Features in Java 17 added several smaller classes and methods to the core libraries. This tutorial explains the Java 17 API additions with examples. We will convert bytes to and from hexadecimal with
OUTPUT of the above code
A
InstantSource
Code which calls
OUTPUT of the above code
Because
OUTPUT of the above code
The factory is called for every new
Other additions in Java 17
HexFormat, make time-dependent code testable with the InstantSource interface, and protect deserialization with the context-specific filters of JEP 415. Every example is compiled for Java 17.
HexFormat
Converting bytes to a hexadecimal string - for hashes, keys, binary protocols or debugging - used to need a hand-written loop or a library, or the removed javax.xml.bind.DatatypeConverterClick to read: javax.xml.bind.DatatypeConverter. java.util.HexFormat does it in one call, in both directions, with optional delimiters, prefixes, suffixes and upper case -
Formatting and parsing hexadecimal
//HexFormatDemo.java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HexFormat;
public class HexFormatDemo {
public static void main(String[] args) throws Exception {
HexFormat hex = HexFormat.of();
byte[] data = "Java 17".getBytes(StandardCharsets.UTF_8);
String encoded = hex.formatHex(data);
System.out.println("formatHex : " + encoded);
System.out.println("parseHex : " + new String(hex.parseHex(encoded), StandardCharsets.UTF_8));
byte[] sha = MessageDigest.getInstance("SHA-256").digest(data);
System.out.println("SHA-256 : " + hex.formatHex(sha));
HexFormat mac = HexFormat.ofDelimiter(":").withUpperCase(); // like a MAC address
System.out.println("MAC style : " + mac.formatHex(new byte[] {0x3c, 0x22, (byte) 0xfb, 0x1a, 0x00, 0x7e}));
HexFormat cArray = HexFormat.ofDelimiter(", ").withPrefix("0x"); // like a C array
System.out.println("C style : {" + cArray.formatHex(new byte[] {1, 2, (byte) 255}) + "}");
System.out.println("toHexDigits(255) : " + hex.toHexDigits((byte) 255) + ", " + hex.toHexDigits(1025));
System.out.println("fromHexDigits : " + HexFormat.fromHexDigits("7e1"));
System.out.println("Round trip equal : " + Arrays.equals(sha, hex.parseHex(hex.formatHex(sha))));
}
}
formatHex : 4a617661203137
parseHex : Java 17
SHA-256 : ee8852e3c233dc78273829b0506ecdd43ad216e28db6888836a78dd54d797ae1
MAC style : 3C:22:FB:1A:00:7E
C style : {0x01, 0x02, 0xff}
toHexDigits(255) : ff, 00000401
fromHexDigits : 2017
Round trip equal : true
HexFormat is immutable and thread-safe; methods like withUpperCase() return a new formatter, so formatters can be created once and shared as constants.
Instant.now() directly is hard to test: the result changes on every run. The usual fix was to pass in a java.time.Clock, but a Clock also carries a time zone, which much code does not need. Java 17 extracted the essential part into a new interface, java.time.InstantSource, with a single abstract method, instant(). Clock now implements it, and InstantSource.system() and InstantSource.fixed() provide the real and a fixed source -
Testable time with InstantSource
//InstantSourceDemo.java
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
public class InstantSourceDemo {
static class Session {
private final InstantSource time;
private final Instant startedAt;
Session(InstantSource time) { this.time = time; this.startedAt = time.instant(); }
boolean isExpired() { return Duration.between(startedAt, time.instant()).toMinutes() >= 30; }
}
public static void main(String[] args) {
// Production code would pass InstantSource.system()
Session live = new Session(InstantSource.system());
System.out.println("Live session expired right away? " + live.isExpired());
// A test controls time with a mutable source - InstantSource is a functional interface
Instant[] now = {Instant.parse("2021-09-14T10:00:00Z")};
Session tested = new Session(() -> now[0]);
now[0] = now[0].plus(Duration.ofMinutes(29));
System.out.println("After 29 minutes expired? " + tested.isExpired());
now[0] = now[0].plus(Duration.ofMinutes(1));
System.out.println("After 30 minutes expired? " + tested.isExpired());
System.out.println("Fixed source: " + InstantSource.fixed(Instant.EPOCH).instant());
}
}
Live session expired right away? false After 29 minutes expired? false After 30 minutes expired? true Fixed source: 1970-01-01T00:00:00Z
InstantSource has one abstract method, a test can supply time with a lambda, as above, and move it forward without sleeping.
Context-specific deserialization filters
Deserializing untrusted data with ObjectInputStream is a well-known source of security vulnerabilities: a malicious stream can make the application instantiate unexpected classes. Java 9 added deserialization filters, which accept or reject classes as they are read. Java 17 (JEP 415) added a filter factory, set once for the whole JVM with ObjectInputFilter.Config.setSerialFilterFactory(), which chooses the right filter for each stream, and convenience methods such as ObjectInputFilter.allowFilter() -
Allowing only expected classes during deserialization
//DeserializationFilterDemo.java
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class DeserializationFilterDemo {
static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) { out.writeObject(o); }
return bytes.toByteArray();
}
public static void main(String[] args) throws Exception {
// JVM-wide factory: combine whatever filter a stream already has with an allow-list
ObjectInputFilter allowList = ObjectInputFilter.allowFilter(
c -> c.getName().startsWith("java.lang.") || c == ArrayList.class || c == Object[].class,
ObjectInputFilter.Status.REJECTED);
ObjectInputFilter.Config.setSerialFilterFactory((current, requested) ->
requested == null ? allowList : ObjectInputFilter.merge(requested, allowList));
List<String> safe = new ArrayList<>(List.of("Tom", "Harry"));
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(serialize(safe)))) {
System.out.println("Allowed: " + in.readObject());
}
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(serialize(new java.util.HashMap<String, String>())))) {
in.readObject();
} catch (InvalidClassException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}
Allowed: [Tom, Harry] Rejected: filter status: REJECTED
ObjectInputStream, so a library can decide filtering for streams it does not create itself. Its second argument is the filter requested for that stream, which is null when the stream has none - so the factory must handle null, since merge() does not accept it. Rejected classes cause an InvalidClassException before the object is constructed. Note that the allow-list includes Object[]: an ArrayList asks the filter to check the array it creates for its elements while it is being deserialized, so leaving it out rejects every ArrayList. Building a tight allow-list means testing it with real data, which is exactly what makes it effective.
- The RandomGenerator APIClick to read tutorial: Java 17 Random Number Generators: RandomGenerator API for random numbers.
Map.Entry.copyOf(), which returns an unmodifiable copy of a map entry that is not connected to its map.Class.isSealed()andClass.getPermittedSubclasses(), for sealed classesClick to read tutorial: Java 17 Sealed Classes Tutorial with Examples.strictfpbecame redundant: all floating-point arithmetic is now strict (JEP 306), as it was before Java 1.2, so results are identical on every platform.
HexFormat, including delimiters, prefixes and upper case, made time-dependent code testable with InstantSource, and allowed only expected classes during deserialization with a JVM-wide filter factory. Lastly, we listed the other additions in Java 17. To see everything new from Java 12 to 17 in one place, read the overview of new features in Java 17Click to read tutorial: Overview of New Features in Java 17.Continue Reading Java 12 to 17 Tutorials
Overview of New Features in Java 17Click to read tutorial: Overview of New Features in Java 17 Java 17 Sealed Classes Tutorial with ExamplesClick to read tutorial: Java 17 Sealed Classes Tutorial with Examples Java 17 Random Number Generators: RandomGenerator APIClick to read tutorial: Java 17 Random Number Generators: RandomGenerator API Overview of New Features in Java 12Click to read tutorial: Overview of New Features in Java 12 Overview of New Features in Java 13Click to read tutorial: Overview of New Features in Java 13 Overview of New Features in Java 14Click to read tutorial: Overview of New Features in Java 14 Overview of New Features in Java 15Click to read tutorial: Overview of New Features in Java 15 Overview of New Features in Java 16Click to read tutorial: Overview of New Features in Java 16 Java 12 API Additions: String.indent, teeing, mismatchClick to read tutorial: Java 12 API Additions: String.indent, teeing, mismatch
Overview of New Features in Java 17Click to read tutorial: Overview of New Features in Java 17 Java 17 Sealed Classes Tutorial with ExamplesClick to read tutorial: Java 17 Sealed Classes Tutorial with Examples Java 17 Random Number Generators: RandomGenerator APIClick to read tutorial: Java 17 Random Number Generators: RandomGenerator API Overview of New Features in Java 12Click to read tutorial: Overview of New Features in Java 12 Overview of New Features in Java 13Click to read tutorial: Overview of New Features in Java 13 Overview of New Features in Java 14Click to read tutorial: Overview of New Features in Java 14 Overview of New Features in Java 15Click to read tutorial: Overview of New Features in Java 15 Overview of New Features in Java 16Click to read tutorial: Overview of New Features in Java 16 Java 12 API Additions: String.indent, teeing, mismatchClick to read tutorial: Java 12 API Additions: String.indent, teeing, mismatch