Encode and decode a string
java.util.Base64 works with bytes. Convert strings with an explicit charset, because getBytes() without arguments uses the platform default, which was not always UTF-8 before Java 18.
import java.nio.charset.StandardCharsets;
import java.util.Base64;
String enc = Base64.getEncoder()
.encodeToString("héllo wörld".getBytes(StandardCharsets.UTF_8));
System.out.println(enc); // aMOpbGxvIHfDtnJsZA==
String dec = new String(Base64.getDecoder().decode(enc), StandardCharsets.UTF_8);
System.out.println(dec); // héllo wörld
Base64.Encoder and Base64.Decoder instances are thread-safe, so you can keep them in a static final field.
URL-safe Base64 with and without padding
getUrlEncoder() uses - and _. Padding is on by default; withoutPadding() returns a new encoder that omits it:
byte[] d = "<<???>>".getBytes(StandardCharsets.UTF_8);
Base64.getUrlEncoder().encodeToString(d); // PDw_Pz8-Pg==
Base64.getUrlEncoder().withoutPadding().encodeToString(d); // PDw_Pz8-Pg
new String(Base64.getUrlDecoder().decode("PDw_Pz8-Pg"), StandardCharsets.UTF_8);
// <<???>>
The decoders accept input with or without padding, so getDecoder().decode("aGk") also works and returns hi. JWT segments are unpadded Base64URL; see Base64 vs Base64URL for when to use which.
MIME encoder and decoder
getMimeEncoder() writes lines of 76 characters separated by \r\n, as RFC 2045 requires. It does not add a line break after the last line.
String mime = Base64.getMimeEncoder().encodeToString("x".repeat(60).getBytes());
// eHh4eHh4...eHh4 (76 chars) + "\r\n" + "eHh4"
Base64.getMimeEncoder(64, "\n".getBytes()); // PEM-style 64-char lines
The line length is rounded down to a multiple of 4. getMimeDecoder() is the lenient one: it ignores line separators and any other character outside the alphabet.
Base64.getMimeDecoder().decode("aGVs\r\nbG8K!"); // bytes of "hello\n"
The MIME Base64 encoder produces the same format in the browser.
Encode and decode a file
Small files:
import java.nio.file.*;
byte[] file = Files.readAllBytes(Path.of("hello.txt"));
System.out.println(Base64.getEncoder().encodeToString(file)); // aGVsbG8K
Files.write(Path.of("copy.txt"), Base64.getDecoder().decode("aGVsbG8K"));
Streaming large files with wrap()
Encoder.wrap(OutputStream) and Decoder.wrap(InputStream) encode and decode on the fly, so memory use stays flat no matter how large the file is:
try (InputStream in = new FileInputStream("big.bin");
OutputStream out = Base64.getEncoder().wrap(new FileOutputStream("big.b64"))) {
in.transferTo(out);
}
try (InputStream in = Base64.getDecoder().wrap(new FileInputStream("big.b64"));
OutputStream out = new FileOutputStream("big.out")) {
in.transferTo(out);
}
Closing the wrapping stream matters: the encoder writes the final group and padding on close(). Forget it and the final group of up to four characters, padding included, is missing. On a 300 KB test file the output matched encodeToString exactly and the round trip was byte-identical. InputStream.transferTo needs Java 9; on Java 8, copy with a buffer loop.
Validation and exceptions
The basic and URL decoders are strict and throw java.lang.IllegalArgumentException. The message tells you what went wrong, with characters printed in hex:
Input to getDecoder() | Message |
|---|---|
PDw_Pz8-Pg== | Illegal base64 character 5f |
"aGVs\nbG8K" | Illegal base64 character a |
aGk=!! | Input byte array has incorrect ending byte at 4 |
aGk== | Input byte array has incorrect ending byte at 4 |
a | Input byte[] should at least have 2 bytes for base64 bytes |
So validation is just a try/catch:
static boolean isBase64(String s) {
try {
Base64.getDecoder().decode(s);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
One gap: the decoder accepts non-zero padding bits, so aGl= decodes to hi without complaint. If you need canonical input only, re-encode and compare. The Base64 validator shows which character is the problem, and common Base64 errors explains each class of failure.
Common pitfalls
- Platform charset.
"text".getBytes()andnew String(bytes)use the default charset. On Windows with Java 17 or older that was usually Windows-1252, which breaks non-ASCII text. Java 18 made UTF-8 the default (JEP 400), but passingStandardCharsets.UTF_8costs nothing. - Wrong decoder for the input. URL-safe input in the basic decoder gives
Illegal base64 character 5for2d. MIME input givesIllegal base64 character aord. encode()vsencodeToString().encode()returns abyte[]of ASCII characters. Printing that array shows something like[B@1b6d3586, not Base64.- Not closing the wrapped stream. The encoder output is truncated without
close(). - Assuming Base64 hides data. It does not; see encoding vs encryption.
Version notes
- Java 8:
java.util.Base64introduced with basic, URL and MIME variants,withoutPadding()andwrap(). - Java 9:
InputStream.transferTo, used above for stream copies. - Java 11:
javax.xml.bind.DatatypeConverter.printBase64Binaryis gone with the removal of JAXB from the JDK. - Java 18: UTF-8 became the default charset.
Kotlin can use all of the above; it also has its own multiplatform API, covered in Base64 in Kotlin.
FAQ
What does 'Illegal base64 character 5f' mean?
The number is the offending character in hex. 5f is _ and 2d is -, which means you are decoding URL-safe Base64 with getDecoder(). Use getUrlDecoder() instead. a is a newline and 20 a space; use getMimeDecoder() or strip whitespace.
Should I still use sun.misc.BASE64Encoder or javax.xml.bind.DatatypeConverter?
No. sun.misc.BASE64Encoder is an internal API that is gone from modern JDKs, and javax.xml.bind was removed from the JDK in Java 11. java.util.Base64 replaces both.
Does the Java decoder accept Base64 without padding?
Yes. Base64.getDecoder().decode("aGk") returns the bytes of hi. The decoders accept missing padding but reject wrong padding.
How do I Base64 encode on Android?
On API level 26+ java.util.Base64 is available. Older code uses android.util.Base64.encodeToString(bytes, Base64.NO_WRAP); without NO_WRAP it adds line breaks.