概要

日本の業務システムでは、半角カナと全角カナの混在が日常的に発生します。銀行の全銀フォーマットは半角カナを要求し、画面入力やデータベースには全角カナが格納されていることが多いため、相互変換の処理は避けて通れません。単純な1文字ずつの置換で済めばよいのですが、半角カナの濁音(ガ)は基本文字 + 濁点の2文字で表現されるのに対し、全角カナの濁音(ガ)は1文字です。この2文字結合の処理を正しく実装しないと、変換結果がおかしくなります。 見落としがちなのが文字化けです。Java の内部エンコーディングは UTF-16 ですが、業務システムでは Shift_JIS(または Windows-31J / MS932)のファイルを扱う場面が多く、読み込み時のエンコーディング指定を誤ると半角カナが意図しない文字に化けて、変換処理自体が正しく動きません。カナ変換とエンコーディング処理はセットで確認しておくと安全です。 変換自体は Pure Java だけで完結しますが、ひらがな・ローマ字変換も含む複合的な変換が必要な場合は ICU4J の Transliterator が有力な選択肢です。この記事では、変換テーブルの設計、濁音・半濁音の結合ロジック、文字化けを防ぐエンコーディング指定まで、実務で必要な実装を整理します。

使いどころ

全銀フォーマットのデータ作成時に、顧客名や口座名義をデータベースの全角カナから半角カナに変換する

OCR で読み取った半角カナ混じりのテキストを全角カナに正規化し、名寄せや検索の精度を上げる

CSV 取込時に全角・半角カナが混在するデータを統一形式に変換し、データベース格納前に正規化する

コード例

HalfKanaConverter.java
import java.util.Map;

public class HalfKanaConverter {

    private static final String HALF_KANA =
        "ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン";
    private static final String FULL_KANA =
        "ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン";

    private static final Map<Character, Character> DAKUTEN_MAP = Map.ofEntries(
        Map.entry('カ', 'ガ'), Map.entry('キ', 'ギ'), Map.entry('ク', 'グ'),
        Map.entry('ケ', 'ゲ'), Map.entry('コ', 'ゴ'), Map.entry('サ', 'ザ'),
        Map.entry('シ', 'ジ'), Map.entry('ス', 'ズ'), Map.entry('セ', 'ゼ'),
        Map.entry('ソ', 'ゾ'), Map.entry('タ', 'ダ'), Map.entry('チ', 'ヂ'),
        Map.entry('ツ', 'ヅ'), Map.entry('テ', 'デ'), Map.entry('ト', 'ド'),
        Map.entry('ハ', 'バ'), Map.entry('ヒ', 'ビ'), Map.entry('フ', 'ブ'),
        Map.entry('ヘ', 'ベ'), Map.entry('ホ', 'ボ'), Map.entry('ウ', 'ヴ')
    );
    private static final Map<Character, Character> HANDAKUTEN_MAP = Map.of(
        'ハ', 'パ', 'ヒ', 'ピ', 'フ', 'プ', 'ヘ', 'ペ', 'ホ', 'ポ'
    );

    /** 半角カナ → 全角カナ(濁音・半濁音の結合処理付き) */
    public static String toFullWidth(String input) {
        if (input == null) return null;
        var sb = new StringBuilder();
        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            char next = (i + 1 < input.length()) ? input.charAt(i + 1) : 0;

            if (next == '゙' && DAKUTEN_MAP.containsKey(c)) {
                sb.append(DAKUTEN_MAP.get(c));
                i++;
                continue;
            }
            if (next == '゚' && HANDAKUTEN_MAP.containsKey(c)) {
                sb.append(HANDAKUTEN_MAP.get(c));
                i++;
                continue;
            }
            int idx = HALF_KANA.indexOf(c);
            sb.append(idx >= 0 ? FULL_KANA.charAt(idx) : c);
        }
        return sb.toString();
    }

    /** 全角カナ → 半角カナ(濁音・半濁音は2文字に展開) */
    public static String toHalfWidth(String input) {
        if (input == null) return null;
        var sb = new StringBuilder();
        for (char c : input.toCharArray()) {
            var dakuten = DAKUTEN_MAP.entrySet().stream()
                .filter(e -> e.getValue() == c).findFirst();
            if (dakuten.isPresent()) {
                sb.append(dakuten.get().getKey()).append('゙');
                continue;
            }
            var handakuten = HANDAKUTEN_MAP.entrySet().stream()
                .filter(e -> e.getValue() == c).findFirst();
            if (handakuten.isPresent()) {
                sb.append(handakuten.get().getKey()).append('゚');
                continue;
            }
            int idx = FULL_KANA.indexOf(c);
            sb.append(idx >= 0 ? HALF_KANA.charAt(idx) : c);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(toFullWidth("ガギグゲゴ"));  // ガギグゲゴ
        System.out.println(toFullWidth("パピプペポ"));   // パピプペポ
        System.out.println(toFullWidth("ABC アイウ 123")); // ABC アイウ 123

        System.out.println(toHalfWidth("ガギグゲゴ"));    // ガギグゲゴ
        System.out.println(toHalfWidth("パピプペポ"));    // パピプペポ

        // 往復変換テスト
        String original = "ガギグゲゴ";
        String full = toFullWidth(original);
        String back = toHalfWidth(full);
        System.out.println("往復一致: " + original.equals(back)); // true
    }
}

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

Version Coverage

Map.ofEntries + Map.entry で不変マップを宣言的に初期化できる。var による型推論でループ内の変数宣言も簡潔になる。

Java 17
// Java 17: Map.ofEntries で宣言的に初期化
private static final Map<Character, Character> DAKUTEN_MAP =
    Map.ofEntries(
        Map.entry('カ', 'ガ'), Map.entry('キ', 'ギ'),
        Map.entry('ク', 'グ'), Map.entry('ケ', 'ゲ'),
        Map.entry('コ', 'ゴ')
        // ... 以下同様
    );

Library Comparison

標準 API(String + Map)半角カナ・全角カナの変換だけで済む場合。外部依存なしで変換テーブルを管理したいとき。変換テーブルの定義が冗長になる。ただし業務要件に合わせた細かい制御がしやすく、Shift_JIS / MS932 との組み合わせも明示的に制御できる。
ICU4J(Transliterator)半角・全角変換だけでなく、ひらがな・カタカナ変換やローマ字変換も必要な場合。Unicode 正規化(NFC/NFD)と組み合わせた多言語テキスト処理。JAR サイズ約12MB と大きく、カナ変換だけのために導入するのは過剰。使う場合は Transliterator t = Transliterator.getInstance("[\\uFF61-\\uFF9F] Halfwidth-Fullwidth"); のようにフィルタ付きで取得し、t.transliterate(str) を呼ぶ。フィルタなしでは半角英数字まで全角化されるため注意。
Apache Commons Lang(StringUtils)基本的な文字列操作ユーティリティと合わせて使いたい場合。半角カナ・全角カナの変換機能は提供されていないため、変換ロジック自体は自前で実装する必要がある。依存を追加する意味が薄い。

注意点

半角カナの濁音は「基本文字 + 濁点(゙)」の2文字で構成される。1文字ずつ変換すると濁音が正しく処理されないため、必ず次の文字を先読みするロジックが必要

半濁音の変換テーブルと濁音の変換テーブルは分離する。ハ行は濁音(バ)と半濁音(パ)の両方を持つため、先に半濁点(゚)をチェックしてから濁点(゙)を処理する順序にするとバグを防げる

全角カナから半角カナへの逆変換では、濁音1文字を半角2文字に展開する必要があるため、文字列の長さが変わる点に注意

変換テーブルに「ヴ」(ヴ → ヴ)を含めるかどうかは業務要件による。全銀フォーマットでは「ヴ」は非対応のケースがある

ファイル I/O でのエンコーディング不一致が文字化けの主因になる。InputStreamReader / OutputStreamWriter には必ず Charset.forName("MS932") または Charset.forName("Shift_JIS") を明示し、デフォルトエンコーディング(JVM 起動ロケール依存)に頼らないこと。MS932(Windows-31J)は Shift_JIS の Microsoft 拡張版で、①や㈱などの拡張文字を含む業務ファイルでは MS932 の方が安全

Shift_JIS ファイルを誤って ISO-8859-1 や UTF-8 で読み込むと、半角カナのバイト値(0xA1〜0xDF)が別の文字として解釈される不可逆な文字化けになる。一度化けた文字は変換テーブルでは元に戻せないため、ファイル受け取り時のエンコーディング確認をチェックリストに含めること

FAQ

ICU4J の Transliterator で半角カナだけを全角カナに変換するにはどうしますか。

Transliterator.getInstance("[\uFF61-\uFF9F] Halfwidth-Fullwidth") でインスタンスを生成し transliterate(str) を呼びます。U+FF61〜FF9F の範囲フィルタを付けないと半角英数字まで全角化されるため、フィルタは必須です。逆変換は "Fullwidth-Halfwidth" を使います。

Shift_JIS ファイルに含まれる半角カナを Java で正しく読み込むにはどうすればよいですか。

new InputStreamReader(stream, Charset.forName("MS932")) のようにエンコーディングを明示します。MS932(Windows-31J)は Shift_JIS の拡張版で業務ファイルでは安全です。誤って ISO-8859-1 で読み込むと半角カナのバイト値が別文字として解釈され、変換テーブルでは元に戻せない文字化けになります。

往復変換(半角→全角→半角)で元に戻らないケースはありますか。

「ヴ」(ヴ)を変換テーブルに含めた場合、全角「ヴ」を半角に戻す処理がなければ往復で一致しません。業務で使う文字範囲を決めてテーブルが対称になっているかテストで確認するのが確実です。

関連書籍

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

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