概要

業務システムでは、外部システムとのファイル連携やレガシーデータベースの読み書きで、Shift_JIS や MS932 と UTF-8 の間で文字コード変換が必要になる場面が少なくありません。Java の Charset.forName("Shift_JIS") と Charset.forName("MS932") は名前が似ていますが、マッピングに微妙な違いがあり、波ダッシュ(〜)や丸数字(①②③)、ローマ数字(ⅠⅡⅢ)といった文字で変換結果が食い違う原因になります。この記事では、Shift_JIS と MS932 の違いを具体的なコードポイントレベルで整理し、CharsetEncoder と CharsetDecoder を使って変換不能文字を検出・制御する安全な実装パターンを解説します。CodingErrorAction の REPLACE・IGNORE・REPORT の使い分けや、変換できなかった文字をログに残す方法など、本番運用で必要になる実践的なポイントを取り上げます。

使いどころ

取引先から受信した Shift_JIS の CSV ファイルを UTF-8 に変換して取り込む際に、変換不能文字を検出してエラー行を報告する

外部システムが MS932 で送信してくる固定長電文を読み込み、社内の UTF-8 データベースに格納する際にマッピング差異を吸収する

レガシーな Oracle データベース(JA16SJISTILDE)から取得した文字列を UTF-8 の Web API レスポンスとして返すとき、波ダッシュが化けないよう変換を制御する

コード例

SafeCharsetConverter.java
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * CharsetEncoder/Decoder を使った安全な文字コード変換ユーティリティ。
 * 変換不能文字を検出してログに記録する。
 */
public class SafeCharsetConverter {

    record ConversionResult(byte[] data, List<String> warnings) {
        boolean hasWarnings() { return !warnings.isEmpty(); }
    }

    /**
     * 文字列を指定した文字コードのバイト列に変換する。
     * 変換不能文字は ? に置換し、その位置と文字を warnings に記録する。
     */
    public static ConversionResult encode(String input, Charset targetCharset) {
        var warnings = new ArrayList<String>();
        var encoder = targetCharset.newEncoder();

        // REPORT モードの canEncode で変換不能文字を事前検出する
        for (int i = 0; i < input.length(); i++) {
            var ch = input.charAt(i);
            if (!encoder.canEncode(ch)) {
                warnings.add("位置 %d: '%c' (U+%04X) は %s に変換できません"
                        .formatted(i, ch, (int) ch, targetCharset.name()));
            }
        }

        // 実際の変換は REPLACE モードで安全に実行する
        var safeEncoder = targetCharset.newEncoder()
                .onMalformedInput(CodingErrorAction.REPLACE)
                .onUnmappableCharacter(CodingErrorAction.REPLACE);

        try {
            var buffer = safeEncoder.encode(CharBuffer.wrap(input));
            var result = new byte[buffer.remaining()];
            buffer.get(result);
            return new ConversionResult(result, warnings);
        } catch (CharacterCodingException e) {
            // REPLACE モードでは通常発生しないが、念のため
            throw new RuntimeException("文字コード変換に失敗しました", e);
        }
    }

    /**
     * バイト列を指定した文字コードで文字列にデコードする。
     * 不正バイトは REPORT で例外をスローする(厳格モード)。
     */
    public static String decodeStrict(byte[] data, Charset sourceCharset)
            throws CharacterCodingException {
        var decoder = sourceCharset.newDecoder()
                .onMalformedInput(CodingErrorAction.REPORT)
                .onUnmappableCharacter(CodingErrorAction.REPORT);
        var charBuffer = decoder.decode(ByteBuffer.wrap(data));
        return charBuffer.toString();
    }

    /**
     * Shift_JIS と MS932 で同じバイト列のデコード結果が異なる文字を検出する。
     */
    public static void compareShiftJisAndMs932(byte[] data) {
        var sjis = Charset.forName("Shift_JIS");
        var ms932 = Charset.forName("MS932");

        var sjisDecoder = sjis.newDecoder()
                .onMalformedInput(CodingErrorAction.REPLACE)
                .onUnmappableCharacter(CodingErrorAction.REPLACE);
        var ms932Decoder = ms932.newDecoder()
                .onMalformedInput(CodingErrorAction.REPLACE)
                .onUnmappableCharacter(CodingErrorAction.REPLACE);

        try {
            var sjisText = sjisDecoder.decode(ByteBuffer.wrap(data)).toString();
            var ms932Text = ms932Decoder.decode(ByteBuffer.wrap(data)).toString();

            if (sjisText.equals(ms932Text)) {
                System.out.println("Shift_JIS と MS932 のデコード結果は同一です");
            } else {
                System.out.println("デコード結果に差異があります:");
                for (int i = 0; i < Math.min(sjisText.length(), ms932Text.length()); i++) {
                    if (sjisText.charAt(i) != ms932Text.charAt(i)) {
                        System.out.printf("  位置 %d: Shift_JIS='%c'(U+%04X)  MS932='%c'(U+%04X)%n",
                                i, sjisText.charAt(i), (int) sjisText.charAt(i),
                                ms932Text.charAt(i), (int) ms932Text.charAt(i));
                    }
                }
            }
        } catch (CharacterCodingException e) {
            System.err.println("デコードに失敗しました: " + e.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        // 波ダッシュを含む文字列の変換テスト
        var testText = "株式会社〜テスト①②③ⅠⅡⅢ";
        System.out.println("元の文字列: " + testText);

        // MS932 へのエンコード(丸数字・ローマ数字は変換可能)
        System.out.println("\n=== MS932 エンコード ===");
        var ms932Result = encode(testText, Charset.forName("MS932"));
        System.out.println("バイト数: " + ms932Result.data().length);
        if (ms932Result.hasWarnings()) {
            ms932Result.warnings().forEach(w -> System.out.println("  警告: " + w));
        } else {
            System.out.println("  変換不能文字なし");
        }

        // Shift_JIS へのエンコード(丸数字・ローマ数字は変換不能)
        System.out.println("\n=== Shift_JIS エンコード ===");
        var sjisResult = encode(testText, Charset.forName("Shift_JIS"));
        System.out.println("バイト数: " + sjisResult.data().length);
        if (sjisResult.hasWarnings()) {
            sjisResult.warnings().forEach(w -> System.out.println("  警告: " + w));
        } else {
            System.out.println("  変換不能文字なし");
        }

        // 波ダッシュのバイト列比較
        System.out.println("\n=== 波ダッシュ(0x8160)のデコード比較 ===");
        var waveDashBytes = new byte[]{(byte) 0x81, (byte) 0x60};
        compareShiftJisAndMs932(waveDashBytes);

        // UTF-8 BOM 付き出力の例
        System.out.println("\n=== UTF-8 BOM 付き CSV 出力例 ===");
        var csvContent = "名前,金額\n田中,10000\n鈴木,20000";
        var bom = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
        var csvBytes = csvContent.getBytes(StandardCharsets.UTF_8);
        var withBom = new byte[bom.length + csvBytes.length];
        System.arraycopy(bom, 0, withBom, 0, bom.length);
        System.arraycopy(csvBytes, 0, withBom, bom.length, csvBytes.length);
        System.out.println("BOM なし: " + csvBytes.length + " bytes");
        System.out.println("BOM 付き: " + withBom.length + " bytes");
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

API の基本構造は Java 8 と同じだが、Shift_JIS/MS932 のマッピングに互換性を維持したまま内部実装が整理されている。var と record で変換結果の管理が簡潔に書ける。

Java 17
// Java 17: var + メソッドチェーンで簡潔に記述
var encoder = Charset.forName("MS932").newEncoder()
    .onUnmappableCharacter(CodingErrorAction.REPORT);
try {
    var result = encoder.encode(CharBuffer.wrap(input));
} catch (CharacterCodingException e) {
    System.err.println("変換不能文字を検出: " + e.getMessage());
}

Library Comparison

標準 API(CharsetEncoder / CharsetDecoder)変換不能文字の検出と制御を細かく行いたい場合。CodingErrorAction で REPLACE・IGNORE・REPORT を選べ、依存ゼロで動く。波ダッシュ問題などマッピング差異の吸収は自前で実装する必要がある。Shift_JIS と MS932 の使い分けをコード側で判断しなければならない。
ICU4J(CharsetDetector / CharsetICU)文字コードの自動判定が必要な場合や、Unicode 正規化(NFC/NFD)を含む高度な変換を行いたいとき。JAR サイズが約14MBと大きく、文字コード変換だけのために導入するには重い。自動判定の精度も100%ではないため、過信は禁物。
Apache Commons Codec(CharEncoding 定数)文字コード名の定数定義として使う程度。変換ロジック自体は提供していない。Java 7 以降は StandardCharsets で定数が揃うため、文字コード変換の目的では導入する理由がほぼない。

注意点

Charset.forName("Shift_JIS") は JIS X 0208 準拠のマッピングを使い、U+301C(波ダッシュ)を 0x8160 にマッピングする。一方 MS932 は U+FF5E(全角チルダ)を 0x8160 にマッピングするため、同じバイト列でも decode 結果が異なる

CodingErrorAction.REPLACE をデフォルトで使うと、変換できなかった文字が ? や \uFFFD に静かに置き換わり、データ欠損に気づけない。本番環境では REPORT で例外を受けてログに記録するか、少なくとも置換が発生した件数を監視すること

String.getBytes(String charsetName) は変換不能文字を黙って ? に置換する。変換エラーを検出したい場合は CharsetEncoder を直接使い、onUnmappableCharacter(REPORT) を設定する必要がある

Java の Shift_JIS は NEC 特殊文字(丸数字 ①〜⑳ など)をマッピングしない。Windows 環境で作成されたファイルにこれらの文字が含まれる場合は MS932(= Windows-31J)を使わないと文字化けする

FAQ

波ダッシュ問題とは何ですか。どう対処すればよいですか。

Unicode の U+301C(波ダッシュ)と U+FF5E(全角チルダ)が、Shift_JIS と MS932 で同じバイト 0x8160 に対応する問題です。Windows 環境のファイルなら MS932 で読み、JIS 準拠のデータなら Shift_JIS で読むのが基本的な対処法です。

MS932 と Shift_JIS はどちらを使うべきですか。

Windows で作成されたファイルや、丸数字・ローマ数字を含むデータには MS932 を使います。JIS X 0208 準拠が求められる通信プロトコルや、仕様書に Shift_JIS と明記されている場合はそちらを使います。迷ったら MS932 のほうが文字化けは少なくなります。

UTF-8 の BOM(Byte Order Mark)は付けるべきですか。

Java の StandardCharsets.UTF_8 は BOM なしです。Excel で開く CSV には BOM(0xEF 0xBB 0xBF)を先頭に付けると文字化けを防げます。それ以外の用途では BOM を付けないのが一般的です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます