☕ 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

Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Predicate
Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.
Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.