概要
業務システムでは、外部システムとのファイル連携やレガシーデータベースの読み書きで、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 レスポンスとして返すとき、波ダッシュが化けないよう変換を制御する
コード例
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");
}
}Version Coverage
API の基本構造は Java 8 と同じだが、Shift_JIS/MS932 のマッピングに互換性を維持したまま内部実装が整理されている。var と record で変換結果の管理が簡潔に書ける。
// 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
注意点
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 で読むのが基本的な対処法です。
Windows で作成されたファイルや、丸数字・ローマ数字を含むデータには MS932 を使います。JIS X 0208 準拠が求められる通信プロトコルや、仕様書に Shift_JIS と明記されている場合はそちらを使います。迷ったら MS932 のほうが文字化けは少なくなります。
Java の StandardCharsets.UTF_8 は BOM なしです。Excel で開く CSV には BOM(0xEF 0xBB 0xBF)を先頭に付けると文字化けを防げます。それ以外の用途では BOM を付けないのが一般的です。