Java 8 Base64 Encoding and StringJoiner Tutorial

Introduction

This quick tutorial covers two small Java 8 additions to the standard library that replace a lot of hand-written code: java.util.Base64, for encoding binary data as text, and java.util.StringJoiner, for building delimited strings such as [Tom, Harry, Nancy]. We will start with how Base64 encoding works and the three encoders Java 8 provides - basic, URL-safe and MIME. We will then look at StringJoiner's delimiter, prefix, suffix and empty value, and its relationship with String.join() and Collectors.joining(). Lastly, we will run one example covering both.

How Base64 encoding works

Base64 represents arbitrary bytes using only 64 safe, printable characters - A-Z, a-z, 0-9, + and / - so that binary data such as images, keys or credentials can travel through channels built for text: JSON, XML, HTTP headers, email. Every 3 bytes (24 bits) are split into four 6-bit groups, and each group picks one character of the alphabet. When the input length is not a multiple of 3, the output is padded with =.
How Base64 encoding works in Java 8 - three bytes split into four 6-bit groups mapped to four characters, with padding
Base64 is an encoding, not encryption: anyone can decode it. Before Java 8, there was no public Base64 API in the JDK, and code used the internal sun.misc.BASE64Encoder - which was removed in Java 9 - or a library. java.util.Base64 replaces both.

The Base64 encoders and decoders

  • Base64.getEncoder() / getDecoder(): the basic alphabet from RFC 4648, no line breaks. The decoder rejects any character outside the alphabet with an IllegalArgumentException.
  • Base64.getUrlEncoder() / getUrlDecoder(): the URL- and file-name-safe variant, which uses - and _ instead of + and /. Used in JSON Web Tokens and URL parameters.
  • Base64.getMimeEncoder() / getMimeDecoder(): for email, breaks the output into lines of at most 76 characters separated by \r\n; the MIME decoder ignores characters outside the alphabet, such as those line breaks.
  • encoder.withoutPadding(): an encoder that leaves out the trailing = characters.
  • Methods: encodeToString(byte[]) and encode(byte[]) on encoders, decode(String) and decode(byte[]) on decoders, plus wrap() for encoding and decoding streams.

StringJoiner

StringJoiner builds a string from parts separated by a delimiter, optionally surrounded by a prefix and a suffix. It solves the classic problem of the loop that adds ", " after every element and then has to remove the last one.
The parts of a Java 8 StringJoiner - prefix, elements, delimiter between elements, and suffix
  • new StringJoiner(delimiter) and new StringJoiner(delimiter, prefix, suffix).
  • add(CharSequence): adds an element and returns the joiner, so calls can be chained.
  • setEmptyValue(CharSequence): the text to return from toString() when no element has been added. By default it is prefix + suffix.
  • merge(StringJoiner other): adds the other joiner's content as one element, using this joiner's delimiter between the two, but the other joiner's delimiter inside its content and without its prefix and suffix.
  • length(): the length of what toString() would return.
Java 8's String.join(delimiter, elements...) (see Essential Java String methodsClick to read: Essential Java String methods) and Collectors.joining()Click to read: Collectors.joining() are both built on StringJoiner. Use them for one-line joins, and StringJoiner itself when elements are added in a loop or need an empty value.

Java 8 code example of Base64 and StringJoiner

Java 8 code showing Base64 encoders and decoders and StringJoiner
//Base64AndStringJoiner.java
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.StringJoiner;
import java.util.stream.Collectors;

public class Base64AndStringJoiner {

  public static void main(String[] args) {
    // 1. Basic Base64: encode bytes to text, and back
    byte[] original = "Tom:secret".getBytes(StandardCharsets.UTF_8);
    String encoded = Base64.getEncoder().encodeToString(original);
    String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
    System.out.println("1. encoded: " + encoded + "  decoded: " + decoded);

    // 2. Padding: 3 bytes -> 4 characters; shorter input is padded with '='
    for (String s : Arrays.asList("Man", "Ma", "M")) {
      System.out.print((s.equals("Man") ? "2. " : "   ") + s + " -> " + encode(s) + "  ");
    }
    System.out.println();
    byte[] ma = "Ma".getBytes(StandardCharsets.UTF_8);
    System.out.println("   without padding: "
        + Base64.getEncoder().withoutPadding().encodeToString(ma));

    // 3. URL-safe Base64 uses - and _ instead of + and /
    byte[] bytes = {(byte) 0xfb, (byte) 0xff, (byte) 0xbf};
    System.out.println("3. basic: " + Base64.getEncoder().encodeToString(bytes)
        + "  URL-safe: " + Base64.getUrlEncoder().encodeToString(bytes));

    // 4. MIME Base64 breaks long output into lines of at most 76 characters
    byte[] longInput = new byte[60];
    Arrays.fill(longInput, (byte) 'x');
    String mime = Base64.getMimeEncoder().encodeToString(longInput);
    System.out.println("4. MIME output lines: " + mime.split("\r\n").length
        + ", first line length: " + mime.split("\r\n")[0].length());

    // 5. Invalid input is rejected when decoding
    try {
      Base64.getDecoder().decode("not*base64");
    } catch (IllegalArgumentException e) {
      System.out.println("5. decode failed: IllegalArgumentException");
    }

    // 6. StringJoiner: a delimiter, plus an optional prefix and suffix
    StringJoiner team = new StringJoiner(", ", "[", "]");
    team.add("Tom Jones").add("Harry Major").add("Nancy Smith");
    System.out.println("6. " + team + " (length " + team.length() + ")");

    // 7. setEmptyValue: what to print when nothing was added
    StringJoiner absent = new StringJoiner(", ", "Absent today: ", ".");
    absent.setEmptyValue("Everyone is in.");
    System.out.println("7. " + absent);
    absent.add("Ethan Hardy");
    System.out.println("   " + absent);

    // 8. merge: add another joiner's content (without its prefix and suffix)
    StringJoiner finance = new StringJoiner(", ", "{", "}").add("Tom").add("Harry");
    StringJoiner sales = new StringJoiner(" | ", "<", ">").add("Ethan").add("Nancy");
    System.out.println("8. merged: " + finance.merge(sales));

    // 9. String.join and Collectors.joining use StringJoiner internally
    System.out.println("9. " + String.join(" / ", "Finance", "Sales", "IT") + "   "
        + Arrays.asList("a", "b", "c").stream().collect(Collectors.joining(",", "(", ")")));
  }

  static String encode(String s) {
    return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8));
  }
}
 OUTPUT of the above code
1. encoded: VG9tOnNlY3JldA==  decoded: Tom:secret
2. Man -> TWFu     Ma -> TWE=     M -> TQ==
   without padding: TWE
3. basic: +/+/  URL-safe: -_-_
4. MIME output lines: 2, first line length: 76
5. decode failed: IllegalArgumentException
6. [Tom Jones, Harry Major, Nancy Smith] (length 37)
7. Everyone is in.
   Absent today: Ethan Hardy.
8. merged: {Tom, Harry, Ethan | Nancy}
9. Finance / Sales / IT   (a,b,c)

Explanation of the code

  • Part 1: Base64 works on bytes, so text is converted with an explicit charset - StandardCharsets.UTF_8 - both when encoding and when decoding. Relying on the platform's default charset can give different bytes on different machines.
  • Part 2: "Man" (3 bytes) becomes exactly 4 characters, as in the diagram. "Ma" and "M" are padded with one and two = signs; withoutPadding() drops them.
  • Part 3: these three bytes map to the characters at positions 62 and 63 of the alphabet, which are + and / in basic Base64 - both special in URLs - and - and _ in URL-safe Base64.
  • Part 4: 60 bytes encode to 80 characters, which the MIME encoder splits into a 76-character line and a short second line.
  • Part 5: * is not in the alphabet, so the basic decoder throws IllegalArgumentException.
  • Part 6: the joiner adds the delimiter only between names, and the brackets once around the whole.
  • Part 7: with nobody absent, setEmptyValue() supplies a whole sentence; as soon as a name is added, the prefix, the name and the suffix are used instead.
  • Part 8: merge() adds Sales' content "Ethan | Nancy" as one element of Finance's joiner - with Sales' own delimiter inside it and without its angle brackets.
  • Part 9: String.join() and Collectors.joining() give the same results in one line.

Summary

In this tutorial we first saw how Base64 turns every 3 bytes into 4 printable characters, with = padding, and that it is an encoding rather than encryption. Next we covered Java 8's basic, URL-safe and MIME encoders and decoders, and withoutPadding(). We then looked at StringJoiner's delimiter, prefix, suffix, empty value and merge(), and its relationship with String.join() and Collectors.joining(). Lastly, we ran an example covering both classes.