JavaのBase64変換は、標準ライブラリの java.util.Base64 クラスだけで完結します。エンコードは Base64.getEncoder().encodeToString(バイト列)、デコードは Base64.getDecoder().decode(文字列) の1行です。 追加のライブラリは要りません。用途に応じて「基本」「URL用」「MIME」の3種類を選び、必要なら withoutPadding で末尾の = を省きます。

この記事では、同じバイト列を4つの方法で変換した出力を並べ、+ / = - _ と改行の違いを表にしました。 さらに、画像ファイルとBase64文字列の相互変換、data URIの生成、wrap を使った大きなファイルの変換、デコードで出る IllegalArgumentException の原因と直し方、Linuxの base64 コマンドとの一致確認まで、そのまま貼って動くコードと出力をセットで載せています。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(java ファイル名.java の単一ファイル実行)/ GNU coreutils 9.4 の base64・basenc。data URI の表示確認は Chromium 141 です。

Java Base64の早見表

やりたいこと 書き方 出力の特徴
普通にエンコード Base64.getEncoder().encodeToString(bytes) + / を使う。末尾に =。改行なし
普通にデコード Base64.getDecoder().decode(str) = はあってもなくても読める
URLやファイル名に入れる Base64.getUrlEncoder().encodeToString(bytes) + が - に、/ が _ になる
URL用をデコード Base64.getUrlDecoder().decode(str) - _ を読める
末尾の = を省く Base64.getUrlEncoder().withoutPadding() = が付かない
76文字ごとに改行する Base64.getMimeEncoder().encodeToString(bytes) 76文字ごとに CRLF(\r\n)
行の長さと改行コードを指定 Base64.getMimeEncoder(64, new byte[] {'\n'}) 64文字ごとに LF
改行入りの文字列をデコード Base64.getMimeDecoder().decode(str) 改行や空白を読み飛ばす
画像ファイルを文字列に encodeToString(Files.readAllBytes(path)) 元の約1.33倍の文字数
文字列を画像ファイルに Files.write(path, getDecoder().decode(str)) バイト列が完全に一致
data URI を作る "data:image/png;base64," + base64 HTMLやCSSに埋め込める
大きなファイルを変換 Base64.getEncoder().wrap(outputStream) 少ないメモリで変換できる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Base64エンコード・デコード|URL用・MIME・画像変換」

Base64とは:バイト列を64種類の文字で表す方法

Base64は、どんなバイト列でも英数字と記号2つ(合計64種類の文字)だけで表す変換方法です。 画像やPDFのようなバイナリデータを、JSONやXML、メール本文、URLといった「文字しか通せない場所」に載せるために使います。

仕組みはシンプルです。元のデータを3バイト(24ビット)ずつ区切り、それを6ビット×4つに分けて、1つずつ文字に置き換えます。3バイトが4文字になるので、変換後のサイズは元の約1.33倍です。元のバイト数が3で割り切れないときは、足りない分を = で埋めて4文字単位にそろえます。これがパディングです。

なお、Base64は暗号化ではなく、表記を変えているだけです。誰でも元に戻せるので、内容を隠す目的には向きません。隠したいデータは暗号化し、その結果のバイト列を運ぶためにBase64を使う、という役割分担になります。

基本:文字列をエンコード・デコードする

まずは一番よく使う形です。文字列はいったんバイト列にしてからエンコードします。このとき文字コード(UTF-8)を明示するのがポイントです。

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Basic {
    public static void main(String[] args) {
        String text = "こんにちは、Base64";

        // エンコード:文字列 → バイト列 → Base64文字列
        byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
        String encoded = Base64.getEncoder().encodeToString(bytes);
        System.out.println("エンコード: " + encoded);

        // デコード:Base64文字列 → バイト列 → 文字列
        byte[] decoded = Base64.getDecoder().decode(encoded);
        String restored = new String(decoded, StandardCharsets.UTF_8);
        System.out.println("デコード  : " + restored);

        System.out.println("元のバイト数: " + bytes.length + " / Base64の文字数: " + encoded.length());
        System.out.println("元に戻ったか: " + text.equals(restored));
    }
}

実行コマンドと出力です。

$ java Basic.java
エンコード: 44GT44KT44Gr44Gh44Gv44CBQmFzZTY0
デコード  : こんにちは、Base64
元のバイト数: 24 / Base64の文字数: 32
元に戻ったか: true

24バイトが32文字になりました。24 ÷ 3 × 4 = 32 で、計算どおりです。

文字列ではなく byte[] のまま受け取りたいときは encode(byte[])、大きなデータを少しずつ変換したいときは後述の wrap を使います。変換後の文字数は 4 × ceil(n ÷ 3) で、100バイトなら136文字、1,000バイトなら1,336文字、100万バイトなら1,333,336文字になることを実行して確かめました。

4種類の出力を比較:記号と改行の違い

ここからがこの記事の中心です。同じバイト列を、基本・パディングなし・URL用・MIME で変換して出力を並べます。 + / = がすべて現れるよう、入力には fb ff fe 3e の4バイトを選びました。改行の違いを見るために、100バイトの入力も基本とMIMEで変換します。

import java.util.Base64;

public class Compare {
    public static void main(String[] args) {
        // + / = がすべて現れるように選んだ4バイト
        byte[] data = {(byte) 0xfb, (byte) 0xff, (byte) 0xfe, (byte) 0x3e};

        System.out.println("getEncoder()                  : " + Base64.getEncoder().encodeToString(data));
        System.out.println("getEncoder().withoutPadding() : " + Base64.getEncoder().withoutPadding().encodeToString(data));
        System.out.println("getUrlEncoder()               : " + Base64.getUrlEncoder().encodeToString(data));
        System.out.println("getUrlEncoder().withoutPadding(): " + Base64.getUrlEncoder().withoutPadding().encodeToString(data));
        System.out.println("getMimeEncoder()              : " + Base64.getMimeEncoder().encodeToString(data));

        // 改行の違いを見るための100バイト(0xfb 0xff 0xfe の繰り返し + 1バイト)
        byte[] longData = new byte[100];
        for (int i = 0; i < longData.length; i++) {
            longData[i] = new byte[] {(byte) 0xfb, (byte) 0xff, (byte) 0xfe}[i % 3];
        }
        show("getEncoder()", Base64.getEncoder().encodeToString(longData));
        show("getMimeEncoder()", Base64.getMimeEncoder().encodeToString(longData));
    }

    static void show(String name, String s) {
        long cr = s.chars().filter(c -> c == '\r').count();
        long lf = s.chars().filter(c -> c == '\n').count();
        System.out.println("--- " + name + "  文字数=" + s.length() + " CR=" + cr + " LF=" + lf);
        System.out.println(s.replace("\r", "\\r").replace("\n", "\\n\n"));
    }
}
getEncoder()                  : +//+Pg==
getEncoder().withoutPadding() : +//+Pg
getUrlEncoder()               : -__-Pg==
getUrlEncoder().withoutPadding(): -__-Pg
getMimeEncoder()              : +//+Pg==
--- getEncoder()  文字数=136 CR=0 LF=0
+//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++w==
--- getMimeEncoder()  文字数=138 CR=1 LF=1
+//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//++//+\r\n
+//++//++//++//++//++//++//++//++//++//++//++//++//++//++w==

この出力を表にまとめます。

取得方法 4バイトの出力 62番目・63番目の文字 末尾の = 改行
getEncoder() +//+Pg== + と / 付く なし
getEncoder().withoutPadding() +//+Pg + と / 付かない なし
getUrlEncoder() -__-Pg== - と _ 付く なし
getUrlEncoder().withoutPadding() -__-Pg - と _ 付かない なし
getMimeEncoder() +//+Pg== + と / 付く 76文字ごとに CRLF

100バイトの結果からは、次のことも分かります。

  • 基本のエンコーダは、長くても改行を入れません。 136文字が1行で出ます(URL用も同じく改行なしです)。
  • MIMEは76文字で \r\n を入れます。 文字数が 136 → 138 に増えたのは、CR と LF の2文字分です。
  • MIMEの出力の最後には改行が付きません。 行と行の間にだけ入ります。
  • URL用でも = は付きます。 = を省きたいときは withoutPadding を重ねます。

どれを選ぶかの判断表

載せる先 おすすめ 理由
JSON・XML の値、設定ファイル、DBの文字列カラム getEncoder() 最も一般的で、ほとんどの相手がそのまま読める
URLのクエリやパス、ファイル名、Cookie getUrlEncoder().withoutPadding() + / = はURLで別の意味を持つため、使わない形にする
メール本文、PEM形式など行の長さに決まりがあるもの getMimeEncoder() または行長を指定 決まった長さで改行できる
HTMLやCSSに画像を埋め込む(data URI) getEncoder() data URI は基本のBase64を使う

withoutPadding:末尾の = を省く

パディングの = は、元のバイト数を3で割った余りで決まります。1バイトずつ増やして確かめます。

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Padding {
    public static void main(String[] args) {
        Base64.Encoder enc = Base64.getEncoder();
        Base64.Encoder noPad = Base64.getEncoder().withoutPadding();
        for (String s : new String[] {"a", "ab", "abc", "abcd", "abcde", "abcdef"}) {
            byte[] b = s.getBytes(StandardCharsets.UTF_8);
            System.out.printf("%-7s %dバイト  %-9s  %-8s%n", s, b.length, enc.encodeToString(b), noPad.encodeToString(b));
        }
        // パディングなしの文字列も、同じデコーダでそのまま戻せる
        String restored = new String(Base64.getDecoder().decode("YWJjZA"), StandardCharsets.UTF_8);
        System.out.println("YWJjZA をデコード: " + restored);
        String restored2 = new String(Base64.getDecoder().decode("YWJjZA=="), StandardCharsets.UTF_8);
        System.out.println("YWJjZA== をデコード: " + restored2);
    }
}
a       1バイト  YQ==       YQ      
ab      2バイト  YWI=       YWI     
abc     3バイト  YWJj       YWJj    
abcd    4バイト  YWJjZA==   YWJjZA  
abcde   5バイト  YWJjZGU=   YWJjZGU 
abcdef  6バイト  YWJjZGVm   YWJjZGVm
YWJjZA をデコード: abcd
YWJjZA== をデコード: abcd

3で割った余りが1なら = が2個、余りが2なら1個、割り切れれば0個です。

Javaのデコーダは、= がない文字列もそのまま読めます。 上の出力のとおり、YWJjZA と YWJjZA== はどちらも abcd に戻りました。公式ドキュメントにも、パディング文字は「受け付けるが必須ではない」と書かれています。ただし、= を付けるなら正しい個数が必要です(YWJjZA= のように1つだけだと例外になります)。

withoutPadding は新しいエンコーダを返すメソッドで、元のエンコーダの設定は変わりません。エンコーダとデコーダは、公式ドキュメントで「複数スレッドから同時に使って安全」と明記されているので、Base64.getUrlEncoder().withoutPadding() を定数に入れて共有できます。

getMimeEncoder:76文字ごとに改行する・行長を変える

MIMEエンコーダは、既定で76文字ごとに CRLF を入れます。引数つきの getMimeEncoder(int lineLength, byte[] lineSeparator) を使うと、行の長さと改行コードを自由に決められます。

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;

public class Mime {
    public static void main(String[] args) {
        byte[] data = "The quick brown fox jumps over the lazy dog. 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                .getBytes(StandardCharsets.UTF_8);

        // 既定:76文字ごとに \r\n
        String mime = Base64.getMimeEncoder().encodeToString(data);
        System.out.println("[既定 76文字・CRLF]");
        System.out.println(mime);

        // 行の長さと改行コードを指定:64文字ごとに \n
        String custom = Base64.getMimeEncoder(64, new byte[] {'\n'}).encodeToString(data);
        System.out.println("[64文字・LF]");
        System.out.println(custom);

        // 行の長さは4の倍数に切り下げられる(30 → 28)
        String l30 = Base64.getMimeEncoder(30, new byte[] {'\n'}).encodeToString(data);
        System.out.println("[30を指定] 1行目の長さ = " + l30.split("\n")[0].length());

        // 0以下を指定すると改行なし
        String l0 = Base64.getMimeEncoder(0, new byte[] {'\n'}).encodeToString(data);
        System.out.println("[0を指定] 改行を含むか = " + l0.contains("\n"));

        // MIMEデコーダは改行やアルファベット外の文字を読み飛ばす
        byte[] back = Base64.getMimeDecoder().decode(mime);
        System.out.println("MIMEデコーダで元に戻ったか = " + Arrays.equals(data, back));
        byte[] back2 = Base64.getMimeDecoder().decode("YW Jj\r\nZA==\n");
        System.out.println("空白と改行まじり → " + new String(back2, StandardCharsets.UTF_8));
    }
}
[既定 76文字・CRLF]
VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gMDEyMzQ1Njc4OSBB
QkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWg==
[64文字・LF]
VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gMDEy
MzQ1Njc4OSBBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWg==
[30を指定] 1行目の長さ = 28
[0を指定] 改行を含むか = false
MIMEデコーダで元に戻ったか = true
空白と改行まじり → abcd

行長に30を指定すると4の倍数の28文字に切り下げられ、0以下を指定すると改行なしになりました。

MIMEデコーダは、改行や空白が混ざった文字列をそのまま読めるのが特長です。ファイルやメールから読み込んだ、行で区切られたBase64を戻すときに役立ちます。一方で、アルファベット外の文字を黙って読み飛ばすので、入力を厳密にチェックしたい場面では基本のデコーダが向いています。

画像ファイルをBase64文字列に変換する・元に戻す

画像もバイト列なので、やることは文字列のときと同じです。Files.readAllBytes で読んで encodeToString、decode して Files.write の2ステップです。PNG・JPEG・GIF・PDF など、ファイルの種類を問いません。

次のコードは、確認用の小さなPNG(16×16ピクセル)を作るところから始めているので、そのまま実行できます。手元の画像を使う場合は、0番のブロックを省いて Path.of("sample.png") を自分のファイルに変えてください。

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import javax.imageio.ImageIO;

public class ImageBase64 {
    public static void main(String[] args) throws Exception {
        // 0. 確認用の画像を用意(手元の画像を使うときはこのブロックを省く)
        Path src = Path.of("sample.png");
        BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = img.createGraphics();
        g.setColor(Color.ORANGE);
        g.fillRect(0, 0, 16, 16);
        g.setColor(Color.BLUE);
        g.fillRect(4, 4, 8, 8);
        g.dispose();
        ImageIO.write(img, "png", src.toFile());

        // 1. 画像ファイル → Base64文字列
        byte[] imageBytes = Files.readAllBytes(src);
        String base64 = Base64.getEncoder().encodeToString(imageBytes);
        System.out.println("画像のバイト数 : " + imageBytes.length);
        System.out.println("Base64の文字数 : " + base64.length());
        System.out.println("Base64文字列   : " + base64);

        // 2. Base64文字列 → 画像ファイル
        Path dst = Path.of("restored.png");
        Files.write(dst, Base64.getDecoder().decode(base64));

        // 3. 元の画像と一致するか、画像として読めるか
        System.out.println("バイト列が一致 : " + Arrays.equals(imageBytes, Files.readAllBytes(dst)));
        BufferedImage check = ImageIO.read(dst.toFile());
        System.out.println("画像サイズ     : " + check.getWidth() + "x" + check.getHeight());
    }
}
画像のバイト数 : 105
Base64の文字数 : 140
Base64文字列   : iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAMElEQVR4XmP4f4KBJITOJ4jQ+QQROp8gQuUw/MeKRjXQVwMxCJ1PEKHzCSJ0PkEEAM7klRDO4X5VAAAAAElFTkSuQmCC
バイト列が一致 : true
画像サイズ     : 16x16

105バイトの画像が140文字になり、戻したファイルは元とバイト単位で一致しました。PNGをBase64にすると先頭が iVBORw0KGgo で始まります。これはPNGファイルの先頭に必ず置かれる8バイトを変換したもので、ログやDBの値を見たときに「PNGが入っている」と見分ける手がかりになります。

data URIを生成する・画像に戻す

data URI(data URL)は、ファイルの中身をURLの中に直接書き込む形式です。MDN の説明では、構文は次のとおりです。

data:[<media-type>][;base64],<data>

メディアタイプを省いた場合の既定値は text/plain;charset=US-ASCII なので、画像を埋め込むときは image/png などを必ず書きます。 次のコードは、前の節で作った sample.png を data URI にし、逆に data URI から画像ファイルへ戻します。

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

public class DataUri {
    public static void main(String[] args) throws Exception {
        Path src = Path.of("sample.png");

        // 画像ファイル → data URI
        String dataUri = toDataUri(src);
        System.out.println(dataUri);

        // data URI → 画像ファイル
        Path out = Path.of("from-data-uri.png");
        Files.write(out, fromDataUri(dataUri));
        System.out.println("一致: " + (Files.mismatch(src, out) == -1));
    }

    static String toDataUri(Path file) throws Exception {
        String mimeType = Files.probeContentType(file); // 拡張子などから種類を推定
        if (mimeType == null) {
            mimeType = "application/octet-stream";
        }
        String base64 = Base64.getEncoder().encodeToString(Files.readAllBytes(file));
        return "data:" + mimeType + ";base64," + base64;
    }

    static byte[] fromDataUri(String dataUri) {
        int comma = dataUri.indexOf(',');
        if (!dataUri.startsWith("data:") || comma < 0) {
            throw new IllegalArgumentException("data URI の形式ではありません");
        }
        String header = dataUri.substring(5, comma);   // 例: image/png;base64
        if (!header.endsWith(";base64")) {
            throw new IllegalArgumentException("base64 形式の data URI ではありません");
        }
        return Base64.getDecoder().decode(dataUri.substring(comma + 1));
    }
}
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAMElEQVR4XmP4f4KBJITOJ4jQ+QQROp8gQuUw/MeKRjXQVwMxCJ1PEKHzCSJ0PkEEAM7klRDO4X5VAAAAAElFTkSuQmCC
一致: true

できあがった data URI は、HTML の img 要素の src にそのまま書けます。

<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>data URI の確認</title></head>
<body>
  <img id="icon" alt="サンプル画像" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAMElEQVR4XmP4f4KBJITOJ4jQ+QQROp8gQuUw/MeKRjXQVwMxCJ1PEKHzCSJ0PkEEAM7klRDO4X5VAAAAAElFTkSuQmCC">
</body>
</html>

このHTMLを Chromium 141 で開き、画像が 16×16 ピクセルとして読み込まれることを確認しました。

data URI を扱うときのコツは次の3つです。

  • data URI は基本のBase64(getEncoder)で作ります。 MDN にも、data URL の中では + や / をそのまま使えると書かれています。
  • デコードするときは、カンマより後ろだけを渡します。 先頭の data:image/png;base64, ごと渡すと、: の位置で例外になります(後の節で再現します)。
  • メディアタイプは Files.probeContentType で推定できます。 判定できないときは null が返るので、既定値を用意しておくと安心です。

サイズは約1.33倍になるので、data URI は小さな画像に使うのが目安です。

大きなファイルはwrapでストリーム変換する

Files.readAllBytes と encodeToString の組み合わせは、ファイル全体と変換後の文字列をメモリに載せます。数百MBのファイルではメモリが足りなくなることがあります。そこで使うのが wrap です。ストリームを包んで、流れてくるデータを少しずつ変換します。

Base64.getEncoder().wrap(OutputStream) は書き込んだバイト列をBase64にして包んだ先へ書き出し、Base64.getDecoder().wrap(InputStream) は包んだ先のBase64を読んで元のバイト列を返します。

import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

public class StreamWrap {
    public static void main(String[] args) throws Exception {
        Path src = Path.of(args[0]);            // 元のファイル
        Path b64 = Path.of(args[0] + ".b64");   // Base64にしたファイル
        Path back = Path.of(args[0] + ".back"); // 元に戻したファイル

        // エンコード:ファイル → Base64ファイル(少しずつ読んで少しずつ書く)
        long t0 = System.nanoTime();
        try (InputStream in = Files.newInputStream(src);
             OutputStream out = Base64.getEncoder().wrap(Files.newOutputStream(b64))) {
            in.transferTo(out);
        } // out を閉じたときに、最後の端数とパディングが書き出される
        long t1 = System.nanoTime();

        // デコード:Base64ファイル → 元のファイル
        try (InputStream in = Base64.getDecoder().wrap(
                     new BufferedInputStream(Files.newInputStream(b64), 64 * 1024));
             OutputStream out = Files.newOutputStream(back)) {
            in.transferTo(out);
        }
        long t2 = System.nanoTime();

        System.out.println("元のサイズ     : " + Files.size(src));
        System.out.println("Base64のサイズ : " + Files.size(b64));
        System.out.println("戻したサイズ   : " + Files.size(back));
        System.out.println("一致           : " + (Files.mismatch(src, back) == -1));
        System.out.printf("エンコード %d ms / デコード %d ms%n", (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000);
    }
}

約200MB(209,715,201バイト)のファイルを用意し、ヒープを64MBに絞って実行しました。

$ head -c 209715201 /dev/urandom > big.bin
$ java -Xmx64m StreamWrap.java big.bin
元のサイズ     : 209715201
Base64のサイズ : 279620268
戻したサイズ   : 209715201
一致           : true
エンコード 438 ms / デコード 6811 ms

ヒープ64MBのまま、200MBのファイルを変換して元に戻せました。 同じ条件で Files.readAllBytes(path) と encodeToString を使う方法を試すと、java.lang.OutOfMemoryError: Java heap space で止まりました。

wrapを使うときの2つのポイント

1. エンコード側のストリームは必ず閉じる。 Base64は3バイト単位で変換するので、最後の端数は閉じたときに書き出されます。公式ドキュメントにも、使い終わったらすみやかに閉じることが推奨されています。次のコードで違いが分かります。

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class NoClose {
    public static void main(String[] args) throws Exception {
        byte[] data = "abcd".getBytes(StandardCharsets.UTF_8);

        // 閉じない場合:端数の1バイトが書き出されないまま
        ByteArrayOutputStream bos1 = new ByteArrayOutputStream();
        OutputStream out1 = Base64.getEncoder().wrap(bos1);
        out1.write(data);
        out1.flush();
        System.out.println("閉じる前: " + bos1);

        // 閉じた場合:端数とパディングまで書き出される
        out1.close();
        System.out.println("閉じた後: " + bos1);
    }
}
閉じる前: YWJj
閉じた後: YWJjZA==

flush しただけでは YWJj(abc の分)までしか出ておらず、閉じて初めて残りの ZA== が書き出されました。try-with-resources で書けば、閉じ忘れを防げます。 なお、返されたストリームを閉じると、包んだ先のストリームも一緒に閉じられます。

2. デコード側は BufferedInputStream をはさむ。 上の StreamWrap.java では、デコード側の入力を BufferedInputStream で包んでいます。これを外して Files.newInputStream を直接 wrap に渡した場合、同じ200MBのデコードに 209,777ミリ秒(約3分半)かかりました。バッファをはさんだ今回は約7秒です。デコード用の wrap に渡す入力は、バッファ付きにしておくと速くなります。

デコードで出るIllegalArgumentExceptionを再現する

デコードがうまくいかないとき、Javaは IllegalArgumentException を投げます。メッセージを読めば原因が分かるので、よくある13通りをまとめて再現しました。

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Errors {
    public static void main(String[] args) {
        byte[] data = {(byte) 0xfb, (byte) 0xff, (byte) 0xfe, (byte) 0x3e};
        String url = Base64.getUrlEncoder().encodeToString(data);          // -__-Pg==
        String mime = Base64.getMimeEncoder().encodeToString(new byte[60]); // 途中に \r\n が入る

        tryDecode("1. URL用の文字列を通常デコーダへ", Base64.getDecoder(), url);
        tryDecode("2. 通常の文字列をURL用デコーダへ", Base64.getUrlDecoder(), "+//+Pg==");
        tryDecode("3. 改行入り(\\r\\n)を通常デコーダへ", Base64.getDecoder(), mime);
        tryDecode("4. 途中に改行(\\n)が入った文字列", Base64.getDecoder(), "YWJj\nZA==");
        tryDecode("5. 末尾に改行(\\n)が付いた文字列", Base64.getDecoder(), "YWJjZA==\n");
        tryDecode("6. data URI をそのまま渡す", Base64.getDecoder(), "data:image/png;base64,iVBORw0KGgo=");
        tryDecode("7. 途中で切れた文字列(5文字)", Base64.getDecoder(), "YWJjZ");
        tryDecode("8. = が1つ足りない", Base64.getDecoder(), "YWJjZA=");
        tryDecode("9. = の後ろに文字が続く", Base64.getDecoder(), "YWJjZA==YQ==");
        tryDecode("10. 空白が混ざっている", Base64.getDecoder(), "YWJj ZA==");
        tryDecode("11. 改行入りをMIMEデコーダへ", Base64.getMimeDecoder(), mime);
        tryDecode("12. 末尾に改行をMIMEデコーダへ", Base64.getMimeDecoder(), "YWJjZA==\n");
        tryDecode("13. URL用の文字列をURL用デコーダへ", Base64.getUrlDecoder(), url);
    }

    static void tryDecode(String label, Base64.Decoder decoder, String input) {
        try {
            byte[] result = decoder.decode(input);
            System.out.println(label + " → OK (" + result.length + "バイト)");
        } catch (IllegalArgumentException e) {
            System.out.println(label + " → " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }
}
1. URL用の文字列を通常デコーダへ → IllegalArgumentException: Illegal base64 character 2d
2. 通常の文字列をURL用デコーダへ → IllegalArgumentException: Illegal base64 character 2b
3. 改行入り(\r\n)を通常デコーダへ → IllegalArgumentException: Illegal base64 character d
4. 途中に改行(\n)が入った文字列 → IllegalArgumentException: Illegal base64 character a
5. 末尾に改行(\n)が付いた文字列 → IllegalArgumentException: Input byte array has incorrect ending byte at 8
6. data URI をそのまま渡す → IllegalArgumentException: Illegal base64 character 3a
7. 途中で切れた文字列(5文字) → IllegalArgumentException: Last unit does not have enough valid bits
8. = が1つ足りない → IllegalArgumentException: Input byte array has wrong 4-byte ending unit
9. = の後ろに文字が続く → IllegalArgumentException: Input byte array has incorrect ending byte at 8
10. 空白が混ざっている → IllegalArgumentException: Illegal base64 character 20
11. 改行入りをMIMEデコーダへ → OK (60バイト)
12. 末尾に改行をMIMEデコーダへ → OK (4バイト)
13. URL用の文字列をURL用デコーダへ → OK (4バイト)

メッセージの読み方

Illegal base64 character の後ろの値は、受け付けられなかった文字の文字コード(16進数)です。これを知っておくと、原因をすぐに特定できます。

メッセージの値 文字 よくある原因 直し方
2d - URL用の文字列を通常デコーダに渡した getUrlDecoder を使う
5f _ 同上 getUrlDecoder を使う
2b + 通常の文字列をURL用デコーダに渡した getDecoder を使う
2f / 同上 getDecoder を使う
a LF(改行) 途中に改行が入っている getMimeDecoder を使う
d CR(復帰) CRLF の改行が入っている getMimeDecoder を使う
20 空白 空白が混ざっている。URL経由で + が空白に変わった場合も 空白を取り除く。URLにはURL用を使う
3a : data URI を丸ごと渡した カンマより後ろを渡す
25 % パーセントエンコードされたまま 先にURLデコードする

文字コード以外のメッセージは次のとおりです。

メッセージ 意味 直し方
Input byte array has incorrect ending byte at N = の後ろに余分な文字(改行など)がある strip() で前後を落とす
Input byte array has wrong 4-byte ending unit = の個数が合っていない = を正しい数にするか、すべて取り除く
Last unit does not have enough valid bits 最後の文字が半端(途中で切れている) 送る側・保存先の長さ制限を確認する

URL用の文字列を通常デコーダに渡すと、最初に現れた - で止まるため、今回の例では 2d が表示されました。先頭の対応しない文字だけが報告される、と覚えておくと読みやすくなります。5番の「末尾に改行」は incorrect ending byte という別のメッセージになります。

直す前と後

原因ごとの直し方です。表の「直した後」の書き方は、いずれも実行して元のデータに戻ることを確認しています。

入力の状態 直す前 直した後
末尾に改行 getDecoder().decode(s) getDecoder().decode(s.strip())
途中に改行 getDecoder().decode(s) getMimeDecoder().decode(s)
URL用の文字列 getDecoder().decode(s) getUrlDecoder().decode(s)
どちらの形式か分からない 片方のデコーダだけ試す - を + に、_ を / に置き換えてから getDecoder
data URI 丸ごと渡す s.substring(s.indexOf(',') + 1) を渡す

なお、wrap で作った入力ストリームの場合、デコードできないバイトを読んだときは IOException が投げられます(公式ドキュメントの記載)。ストリームで読むコードでは、IOException の側で受ける形になります。

Linuxのbase64コマンドと結果を比べる

Javaで作ったBase64が、ほかのツールでも同じ結果になるかを確かめます。比較用に、ファイルを読んで指定の形式で出力する小さなプログラムを用意しました。

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

public class Cmd {
    public static void main(String[] args) throws Exception {
        byte[] data = Files.readAllBytes(Path.of(args[0]));
        String mode = args[1];
        String s = switch (mode) {
            case "basic" -> Base64.getEncoder().encodeToString(data);
            case "url" -> Base64.getUrlEncoder().encodeToString(data);
            case "mime-lf" -> Base64.getMimeEncoder(76, new byte[] {'\n'}).encodeToString(data);
            default -> throw new IllegalArgumentException(mode);
        };
        System.out.print(s);
    }
}
$ printf 'こんにちは、Base64' > t.txt
$ base64 t.txt
44GT44KT44Gr44Gh44Gv44CBQmFzZTY0
$ java Cmd.java t.txt basic
44GT44KT44Gr44Gh44Gv44CBQmFzZTY0

$ head -c 100 /dev/urandom > s.bin
$ base64 s.bin
ukpPMwmSJwD/dn8+Gbu5L6VG84QneW6WWst0XIuQj81Fs119rXm6s+Yx2ntSHi+/2Jk6DzYkC1og
BiDd926/yD5z1XjKCOioaNTfMQ+UlyHl3rdp3UbKjoCUYiI5hlH0ReFwhw==
$ java Cmd.java s.bin mime-lf
ukpPMwmSJwD/dn8+Gbu5L6VG84QneW6WWst0XIuQj81Fs119rXm6s+Yx2ntSHi+/2Jk6DzYkC1og
BiDd926/yD5z1XjKCOioaNTfMQ+UlyHl3rdp3UbKjoCUYiI5hlH0ReFwhw==

$ basenc --base64url -w0 s.bin
ukpPMwmSJwD_dn8-Gbu5L6VG84QneW6WWst0XIuQj81Fs119rXm6s-Yx2ntSHi-_2Jk6DzYkC1ogBiDd926_yD5z1XjKCOioaNTfMQ-UlyHl3rdp3UbKjoCUYiI5hlH0ReFwhw==
$ java Cmd.java s.bin url
ukpPMwmSJwD_dn8-Gbu5L6VG84QneW6WWst0XIuQj81Fs119rXm6s-Yx2ntSHi-_2Jk6DzYkC1ogBiDd926_yD5z1XjKCOioaNTfMQ-UlyHl3rdp3UbKjoCUYiI5hlH0ReFwhw==

$ java Cmd.java s.bin basic | base64 -d | cmp - s.bin && echo 一致
一致
$ java Cmd.java s.bin url | base64 -d > /dev/null
base64: invalid input
$ java Cmd.java s.bin url | basenc --base64url -d | cmp - s.bin && echo 一致
一致

結果をまとめます。

Java Linuxのコマンド 結果
getEncoder() base64 -w0 ファイル 一致(200MBのファイルでも SHA-256 が同じ)
getMimeEncoder(76, new byte[] {'\n'}) base64 ファイル(既定で76文字ごとに改行) 文字の並びは一致。コマンド側だけ最後に改行が1つ付く
getUrlEncoder() basenc --base64url -w0 ファイル 一致
getUrlEncoder() の出力 base64 -d に渡す base64: invalid input(Javaと同じく受け付けない)

200MBのファイルについては、StreamWrap.java が書き出した big.bin.b64 と、base64 -w0 big.bin の出力を SHA-256 で比べました。

$ base64 -w0 big.bin | sha256sum
edba09d14175fe2e8551f570e32db2b5872e0c555a91ecb36c6f510eac5860b1  -
$ sha256sum big.bin.b64
edba09d14175fe2e8551f570e32db2b5872e0c555a91ecb36c6f510eac5860b1  big.bin.b64

覚えておくと便利な違いは2点です。

  • base64 コマンドは既定で76文字ごとに LF で改行し、最後にも改行を付けます。 Javaの getEncoder と同じ1行の出力がほしいときは -w0 を付けます。
  • JavaのMIMEエンコーダの改行は CRLF です。 コマンドの出力(LF)と行単位でそろえたいときは、getMimeEncoder(76, new byte[] {'\n'}) と改行コードを指定します。

よくある質問

Q. java.util.Base64 はどのバージョンから使えますか? A. Java 8(1.8)で追加されました。公式ドキュメントの Since 欄にも 1.8 と記載されています。この記事のコードは OpenJDK 21 で動かしていますが、Base64 の部分は Java 8 以降の標準APIだけを使っています(Files.mismatch や switch式など周辺のコードは、より新しいバージョンの機能です)。

Q. デコードした結果が文字化けします。 A. エンコードしたときの文字コードと、デコード後に文字列へ戻すときの文字コードがそろっているかを確認してください。実行して比べると、「日本語」は UTF-8 なら 5pel5pys6Kqe、Shift_JIS なら k/qWe4zq と、まったく別の文字列になります。両方で StandardCharsets.UTF_8 を明示するのが確実です。

Q. URL用で作った文字列に = が付いています。省いてもよいですか? A. 省けます。getUrlEncoder().withoutPadding() で = なしの文字列になり、Javaの getUrlDecoder はそのまま読めます。相手がJava以外のときは、パディングなしを受け付けるかを相手側の仕様で確かめておくと安心です。

まとめ

  • 基本は Base64.getEncoder().encodeToString(bytes) と Base64.getDecoder().decode(str)。 標準ライブラリだけで書けます。
  • URL用は + / が - _ に変わる。 = も省くなら getUrlEncoder().withoutPadding() を使います。
  • MIMEは76文字ごとに CRLF。 行長と改行コードは getMimeEncoder(長さ, 改行) で変えられ、getMimeDecoder は改行を読み飛ばします。
  • 画像は Files.readAllBytes と Files.write を組み合わせるだけ。 data URI は "data:image/png;base64," を前に付けます。
  • 大きなファイルは wrap。 エンコード側は必ず閉じ、デコード側は BufferedInputStream をはさみます。
  • Illegal base64 character 2d は「- を受け付けなかった」という意味。 後ろの値を文字コードとして読めば、原因がすぐ分かります。
  • Linuxの base64 -w0、basenc --base64url -w0 と結果が一致。 ほかのツールや言語とのやり取りにも、そのまま使えます。

参考・出典

  • Oracle「Base64 (Java SE 21 & JDK 21)」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Base64.html
  • Oracle「Base64.Encoder (Java SE 21 & JDK 21)」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Base64.Encoder.html
  • Oracle「Base64.Decoder (Java SE 21 & JDK 21)」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Base64.Decoder.html
  • MDN Web Docs「data: URLs」 https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Base64エンコード・デコード|URL用・MIME・画像変換」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?