☕ Java

Base64

Base64 is a binary-to-text encoding scheme that represents arbitrary byte data using only 64 printable ASCII characters (A–Z, a–z, 0–9, plus two symbols), making binary data safe to embed in contexts that only support text — email bodies (MIME), URLs, JSON payloads, HTTP headers (Basic Authentication), and embedded data URIs. Java exposed Base64 encoding only through ad-hoc, inconsistent mechanisms before Java 8 (e.g. the internal, unsupported sun.misc.BASE64Encoder, or pulling in javax.xml.bind.DatatypeConverter, or third-party libraries like Apache Commons Codec); java.util.Base64, introduced in Java 8, provides a standard, supported, and notably fast encoder/decoder directly in the JDK. This entry covers why Base64 encoding exists and what problem it solves, the three Base64 variants Java supports (Basic, URL-safe, MIME), the streaming encoder/decoder APIs, and common pitfalls like confusing encoding with encryption.

Motivation — Why Binary Data Needs Text-Safe Encoding

Many transport mechanisms and storage formats were designed to carry text, not arbitrary binary data, and either reject raw binary bytes outright or silently corrupt them. Email systems built on protocols designed for 7-bit ASCII text can mangle bytes with the high bit set; many text-based formats like JSON and XML have no defined way to embed an arbitrary byte sequence inside a string value; URLs reserve and disallow certain byte values entirely; and HTTP headers expect printable, mostly-ASCII content. Base64 solves this by mapping every possible sequence of input bytes onto a sequence using only 64 universally-safe printable characters, at the cost of approximately a 33% increase in size (every 3 input bytes become 4 output characters, since 4 characters of 6 bits each represent the same 24 bits as 3 bytes of 8 bits each). Before Java 8, there was no single standard, public, supported way to do Base64 encoding in the JDK. Developers commonly reached for sun.misc.BASE64Encoder/Decoder, internal classes in the sun.* package tree that were never part of the public API, could be removed or changed without notice, and triggered compiler warnings; or javax.xml.bind.DatatypeConverter, part of the JAXB API and tied to XML-binding semantics rather than general-purpose encoding (and later removed from the default JDK module path in Java 11 as JAXB was decoupled); or third-party dependencies like Apache Commons Codec purely for this one utility. java.util.Base64 consolidated this into one official, fast, dependency-free API.
Java
// ── The problem: raw bytes don't survive text-only transports/formats ──
byte[] imageBytes = readImageFile();   // arbitrary binary data
// Cannot safely embed imageBytes directly inside a JSON string field,
// an HTTP header, or a URL query parameter — many byte values are illegal
// or will be altered/stripped by intermediate systems.

// ── Pre-Java 8: inconsistent, unsupported approaches ────────────────────
// sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();   // internal, unsupported API
// String encoded = encoder.encode(imageBytes);                     // triggers warnings

// javax.xml.bind.DatatypeConverter.printBase64Binary(imageBytes);  // tied to JAXB, removed by default in Java 11+

// ── Java 8+: standard, supported API ────────────────────────────────────
String encoded = Base64.getEncoder().encodeToString(imageBytes);
byte[] decoded = Base64.getDecoder().decode(encoded);
// encoded is now plain ASCII text — safe to put in JSON, XML, or most text protocols

Basic, URL-Safe, and MIME Variants

java.util.Base64 provides three distinct encoder/decoder pairs, because the standard Base64 alphabet uses two characters — + and / — that are problematic in certain contexts, and because some contexts require line-wrapped output. Basic (Base64.getEncoder() / getDecoder()) implements the standard RFC 4648 Base64 alphabet, producing output on a single line (no line breaks), and is the right default for general-purpose use such as embedding data in JSON fields, HTTP Basic Authentication headers, or binary file encoding where no line-length constraint applies. URL-safe (Base64.getUrlEncoder() / getUrlDecoder()) replaces + with - and / with _ in the output alphabet, because + and / both have special meaning inside URLs and query strings (+ can mean a space in some encodings, / is a path separator) and would otherwise require additional percent-encoding. This variant is the right choice whenever the Base64 output itself will be placed directly into a URL path segment or query parameter, such as encoding identifiers or tokens for use in links. MIME (Base64.getMimeEncoder() / getMimeDecoder()) implements RFC 2045's MIME variant, which inserts a CRLF line break every 76 characters of output, matching the line-length conventions email transport historically required. This is primarily relevant when generating content for actual email/MIME messages or formats that explicitly expect MIME-style line wrapping; using it where Basic encoding is expected will produce output with unwanted embedded line breaks. Both Basic and URL encoders also offer a withoutPadding() variant, which omits the trailing = padding characters that Base64 normally uses to indicate the input length wasn't a clean multiple of 3 bytes — useful in contexts (like some token formats) where padding characters are undesirable but the decoder is able to infer or doesn't require the padding.
Java
byte[] data = "Hello, World! /+special?".getBytes(StandardCharsets.UTF_8);

// ── Basic — general purpose, single line, standard alphabet ────────────
String basic = Base64.getEncoder().encodeToString(data);
System.out.println(basic);   // contains possible '+' and '/' characters

// ── URL-safe — for embedding directly in URLs/query params ─────────────
String urlSafe = Base64.getUrlEncoder().encodeToString(data);
System.out.println(urlSafe); // '+' -> '-', '/' -> '_' — safe in URL paths/queries

String url = "https://example.com/download?token=" + urlSafe;

// ── Without padding ─────────────────────────────────────────────────────
String noPad = Base64.getUrlEncoder().withoutPadding().encodeToString(data);
// omits trailing '=' characters

// ── MIME — line-wrapped at 76 chars, CRLF separated, for email content ─
byte[] largeData = new byte[200];
new Random().nextBytes(largeData);
String mime = Base64.getMimeEncoder().encodeToString(largeData);
System.out.println(mime);   // contains embedded \r\n every 76 chars

// ── Decoding must match the encoding variant used ───────────────────────
byte[] decodedBasic = Base64.getDecoder().decode(basic);
byte[] decodedUrl = Base64.getUrlDecoder().decode(urlSafe);
// Base64.getDecoder().decode(urlSafe) would FAIL or produce wrong bytes —
// the Basic decoder doesn't understand '-' and '_' substitutions

Streaming API and the Common Pitfall: Encoding Is Not Encryption

For large inputs that shouldn't be loaded entirely into memory as a single byte array, Base64 provides wrapping stream classes: Base64.getEncoder().wrap(OutputStream) returns an OutputStream that Base64-encodes everything written to it before passing it to the underlying stream, and Base64.getDecoder().wrap(InputStream) returns an InputStream that decodes Base64 data as it's read. This allows encoding/decoding to be composed with file I/O or network streams without buffering the whole payload, and integrates naturally with try-with-resources and other stream-based idioms. The most important conceptual pitfall with Base64 is mistaking it for a security mechanism. Base64 is an encoding, not encryption: it has no key, provides no confidentiality, and is fully and trivially reversible by anyone, since the alphabet and algorithm are public and fixed. Encoding a password or secret in Base64 does not protect it in any meaningful sense — it is exactly as readable as the original plaintext to anyone who decodes it, which requires no secret information at all. This matters in practice because HTTP Basic Authentication transmits credentials as user:password Base64-encoded in a header — this only obscures the credentials from accidental casual viewing in transit logs, and provides zero protection without TLS/HTTPS layered underneath to actually encrypt the connection. Any system that uses Base64 encoding alone as a substitute for actual encryption or hashing (e.g. storing "encoded" passwords in a database) has not achieved any real security property.
Java
// ── Streaming encode while writing to a file ────────────────────────────
try (OutputStream fileOut = Files.newOutputStream(Path.of("output.b64"));
     OutputStream encodedOut = Base64.getEncoder().wrap(fileOut)) {
    encodedOut.write(largeBinaryData);   // encodes on the fly, no full buffering required
}

// ── Streaming decode while reading from a file ───────────────────────────
try (InputStream fileIn = Files.newInputStream(Path.of("output.b64"));
     InputStream decodedIn = Base64.getDecoder().wrap(fileIn)) {
    byte[] original = decodedIn.readAllBytes();   // decodes on the fly
}

// ── PITFALL: Base64 is NOT encryption — fully reversible, no secret ────
String password = "mySecretPassword123";
String encoded = Base64.getEncoder().encodeToString(password.getBytes());
// Anyone can reverse this with zero knowledge of any key:
String revealed = new String(Base64.getDecoder().decode(encoded));
System.out.println(revealed);   // "mySecretPassword123" — fully recovered, no secret needed

// HTTP Basic Auth — Base64 only obscures, TLS provides the actual protection:
String credentials = "user:password";
String authHeader = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
// Without HTTPS, this header is as exposed on the wire as plaintext "user:password"

// Correct approach for actually protecting a password — hash with a salt, e.g.:
// String hashed = someStrongPasswordHasher.hash(password);   // NOT Base64 — irreversible by design

Related Topics in Java 8 Features

Stream API
The Stream API, introduced in Java 8, provides a functional, declarative model for processing sequences of elements. A Stream<T> is not a data structure — it carries no storage. It is a pipeline specification: a source that provides elements, zero or more intermediate operations that transform or filter the stream, and exactly one terminal operation that consumes the stream and produces a result or side effect. Streams are lazy: intermediate operations do not execute until the terminal operation is invoked, and execution is fused — elements flow through the entire pipeline one at a time (or in batches for parallel streams), avoiding intermediate collection. Streams are single-use: once a terminal operation has been invoked, the stream is consumed and cannot be reused. Java provides both reference streams (Stream<T>) and primitive streams (IntStream, LongStream, DoubleStream) that avoid boxing overhead. The Stream API covers sources (collections, arrays, files, generators), intermediate operations (filter, map, flatMap, sorted, distinct, limit, skip, peek, mapToInt, mapToObj), terminal operations (forEach, collect, reduce, count, findFirst, findAny, anyMatch, allMatch, noneMatch, toList, min, max), and collectors (toList, toSet, toMap, groupingBy, partitioningBy, joining, counting, summarizing). This entry covers the full lifecycle, every operation class, the short-circuit evaluation, the Spliterator model, collector design, and performance guidance.
Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.