2015-03-03 16:40

[Java] 自定字符集 Charset

網路上找到有關自定 Charset 的資料很少,大部分的人都是不建議自行定義一個 Charset,的確沒有什麼特別的理由,實在不需要去做這樣的事,但實際遇到的是工作上用的 DB 實在太舊,還在用 Big5 編碼,以及一堆的自造字,偏偏大部分都還是人名,讓別人的名子變成奇怪的字,實在很不禮貌,基於這個理由我延伸了 Big5 Charset。

要製作一個 Charset 一定要瞭解 Java 的字串核心,如果之前是寫 C 語言的朋友,首先要認知 char 跟 btye 是不一樣的,在 Java 的 char 是固定 Unicode 編碼,所有輸入都會轉成 Unicode,輸出時在轉成指定編碼,如下圖:



那麼要實做哪些東西呢??
class Charset
定義 Charset 的名稱跟提供 Encoder 跟 Decoder,跟據 IANA 的規範自定的 Charset 名稱必須以 X-x- 開頭。
class CharsetDecoder
字符解碼器負責將 byte[] 轉換成 char[]
class CharsetEncoder
字符編碼器負責將 char[] 轉換成 byte[]
class CharsetProvider
Charset 提供者,以名稱提供 Charset,在 Charset.forName("xxx") 調用時會尋訪所有的 Provider 來取得 Charset。
META-INF/services/java.nio.charset.spi.CharsetProvider
告知 JVM 將 CharsetProvider 註冊到延伸清單中,JVM 在啟動的時候會掃描所有這個路徑的檔案。

最後再將 {Charset}.jar 檔複製到 {jre}/lib/ext 目錄下就部署完畢


Big5_Extend

  1. public class Big5_Extend extends Charset { 
  2.  
  3.    private static final String BASE_CHARSET = "Big5"; 
  4.    private static final String NAME = "X-Big5-Extend"; 
  5.    private static final String[] ALIASES = { "X-Big5_Extend" }; 
  6.    private Charset baseCharset; 
  7.  
  8.    public Big5_Extend() { 
  9.        this(NAME, ALIASES); 
  10.    } 
  11.  
  12.    public Big5_Extend(String canonical, String[] aliases) { 
  13.        super(canonical, aliases); 
  14.        baseCharset = Charset.forName(BASE_CHARSET); 
  15.    } 
  16.  
  17.    public boolean contains(Charset cs) { 
  18.        return this.getClass().isInstance(cs) || 
  19.                baseCharset.getClass().isInstance(cs); 
  20.    } 
  21.  
  22.    public CharsetDecoder newDecoder() { 
  23.        return new Decoder(this, baseCharset.newDecoder()); 
  24.    } 
  25.  
  26.    public CharsetEncoder newEncoder() { 
  27.        return new Encoder(this, baseCharset.newEncoder()); 
  28.    } 
  29.  
  30.    // ... 
  31. } 
繼承自 Charset,我們只要實作名稱定義跟 Encoder / Decoder 兩件事,當然如果有 char mapping array 也是在這裡初始化,讓所有 Encoder 跟 Decoder 可以共用同一個記憶體空間。


解碼處理

  1. protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { 
  2.    base.reset(); /* 重置狀態 */ 
  3.  
  4.    /* 先用原生的 Big5 進行解碼 */ 
  5.    CoderResult result = base.decode(in, out, true); 
  6.    if(!result.isUnmappable() || in.remaining() < 2){ return result; } 
  7.  
  8.  
  9.    /* 無法轉換,進一步使用自訂的解碼 */ 
  10.    int pos = in.position(); 
  11.    char big5Char = (char)(in.get(pos) << 8 | in.get(pos + 1)); 
  12.    char outChar; 
  13.  
  14.    switch (big5Char) { 
  15.        case '\uFA40': outChar = '\u5803'; break; /* 堃 */ 
  16.        case '\uFA41': outChar = '\u83D3'; break; /* 菓 */ 
  17.        case '\uFA42': outChar = '\u854B'; break; /* 蕋 */ 
  18.        case '\uFA43': outChar = '\u4F8A'; break; /* 侊 */ 
  19.        default: return result; /* 不在清單內直接回傳 */ 
  20.    } 
  21.    out.put(outChar); 
  22.  
  23.    in.position(pos + 2); 
  24.    return decodeLoop(in, out); /* 遞迴處理*/ 
  25. } 
解碼的部分就是先呼叫 big5 原本的 decode,當發生無法解碼的四種狀況就會停止解碼,回傳解碼狀態,我們只要針對 isUnmappable 的狀態接著處裡,base.reset() 是為了清除 Decoder 內部的狀態紀錄,不然會被前一次的解碼結果所影響。


編碼處理

  1. protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { 
  2.    base.reset(); /* 重置狀態 */ 
  3.  
  4.    /* 先用原生的 Big5 進行編碼 */ 
  5.    CoderResult result = base.encode(in, out, true); 
  6.    if(!result.isUnmappable() || out.remaining() < 2){ return result; } 
  7.  
  8.  
  9.    /* 無法轉換,進一步使用自訂的編碼 */ 
  10.    int pos = in.position(); 
  11.    char uniChar = in.get(pos); 
  12.    char outChar; 
  13.  
  14.    switch (uniChar) { 
  15.        case '\u5803': outChar = '\uFA40'; break; /* 堃 */ 
  16.        case '\u83D3': outChar = '\uFA41'; break; /* 菓 */ 
  17.        case '\u854B': outChar = '\uFA42'; break; /* 蕋 */ 
  18.        case '\u4F8A': outChar ='\uFA43'; break; /* 侊 */ 
  19.        default: return result; /* 不在清單內直接回傳 */ 
  20.    } 
  21.    out.put((byte)(outChar >> 8)); 
  22.    out.put((byte)outChar); 
  23.  
  24.    in.position(pos + 1); 
  25.    return encodeLoop(in, out); /* 遞迴處理*/ 
  26. } 
編碼的部分跟解碼採用相同的方式,一樣是先呼叫 big5 原本的 encode。


CoderResult 四種狀態

  • UNDERFLOW 欠位
  • OVERFLOW 溢位
  • MALFORMED 有缺陷的輸入
  • UNMAPPABLE 無映射字符


完整的 Big5_Extend

  1. package com.custom.nio.charset; 
  2.  
  3. import java.nio.CharBuffer; 
  4. import java.nio.ByteBuffer; 
  5. import java.nio.charset.Charset; 
  6. import java.nio.charset.CharsetEncoder; 
  7. import java.nio.charset.CharsetDecoder; 
  8. import java.nio.charset.CoderResult; 
  9.  
  10.  
  11. public class Big5_Extend extends Charset { 
  12.  
  13.    private static final String BASE_CHARSET = "Big5"; 
  14.    private static final String NAME = "X-Big5-Extend"; 
  15.    private static final String[] ALIASES = { "X-Big5_Extend" }; 
  16.    private Charset baseCharset; 
  17.  
  18.    public Big5_Extend() { 
  19.        this(NAME, ALIASES); 
  20.    } 
  21.  
  22.    public Big5_Extend(String canonical, String[] aliases) { 
  23.        super(canonical, aliases); 
  24.        baseCharset = Charset.forName(BASE_CHARSET); 
  25.    } 
  26.  
  27.    public boolean contains(Charset cs) { 
  28.        return this.getClass().isInstance(cs) || 
  29.                baseCharset.getClass().isInstance(cs); 
  30.    } 
  31.  
  32.    public CharsetDecoder newDecoder() { 
  33.        return new Decoder(this, baseCharset.newDecoder()); 
  34.    } 
  35.  
  36.    public CharsetEncoder newEncoder() { 
  37.        return new Encoder(this, baseCharset.newEncoder()); 
  38.    } 
  39.  
  40.  
  41.  
  42.    private class Decoder extends CharsetDecoder { 
  43.        /* Java 原生的 Big5 解碼器 */ 
  44.        private final CharsetDecoder base; 
  45.  
  46.        Decoder(Charset cs, CharsetDecoder base) { 
  47.            super(cs, base.averageCharsPerByte(), base.maxCharsPerByte()); 
  48.            this.base = base; 
  49.        } 
  50.  
  51.        @Override 
  52.        protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { 
  53.            base.reset(); /* 重置狀態 */ 
  54.  
  55.            /* 先用原生的 Big5 進行解碼 */ 
  56.            CoderResult result = base.decode(in, out, true); 
  57.            if(!result.isUnmappable() || in.remaining() < 2){ return result; } 
  58.  
  59.  
  60.            /* 無法轉換,進一步使用自訂的解碼 */ 
  61.            int pos = in.position(); 
  62.            char big5Char = (char)(in.get(pos) << 8 | in.get(pos + 1)); 
  63.            char outChar; 
  64.  
  65.            switch (big5Char) { 
  66.                case '\uFA40': outChar = '\u5803'; break; /* 堃 */ 
  67.                case '\uFA41': outChar = '\u83D3'; break; /* 菓 */ 
  68.                case '\uFA42': outChar = '\u854B'; break; /* 蕋 */ 
  69.                case '\uFA43': outChar = '\u4F8A'; break; /* 侊 */ 
  70.                default: return result; /* 不在清單內直接回傳 */ 
  71.            } 
  72.  
  73.            out.put(outChar); 
  74.  
  75.            in.position(pos + 2); 
  76.            return decodeLoop(in, out); 
  77.        } 
  78.    } 
  79.  
  80.  
  81.  
  82.    private class Encoder extends CharsetEncoder { 
  83.        /* Java 原生的 Big5 編碼器 */ 
  84.        private final CharsetEncoder base; 
  85.  
  86.        Encoder(Charset cs, CharsetEncoder base) { 
  87.            super(cs, base.averageBytesPerChar(), base.maxBytesPerChar()); 
  88.            this.base = base; 
  89.        } 
  90.  
  91.        @Override 
  92.        protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { 
  93.            base.reset(); /* 重置狀態 */ 
  94.  
  95.            /* 先用原生的 Big5 進行編碼 */ 
  96.            CoderResult result = base.encode(in, out, true); 
  97.            if(!result.isUnmappable() || out.remaining() < 2){ return result; } 
  98.  
  99.  
  100.            /* 無法轉換,進一步使用自訂的編碼 */ 
  101.            int pos = in.position(); 
  102.            char uniChar = in.get(pos); 
  103.            char outChar; 
  104.  
  105.            switch (uniChar) { 
  106.                case '\u5803': outChar = '\uFA40'; break; /* 堃 */ 
  107.                case '\u83D3': outChar = '\uFA41'; break; /* 菓 */ 
  108.                case '\u854B': outChar = '\uFA42'; break; /* 蕋 */ 
  109.                case '\u4F8A': outChar ='\uFA43'; break; /* 侊 */ 
  110.                default: return result; /* 不在清單內直接回傳 */ 
  111.            } 
  112.  
  113.            out.put((byte)(outChar >> 8)); 
  114.            out.put((byte)outChar); 
  115.  
  116.            in.position(pos + 1); 
  117.            return encodeLoop(in, out); 
  118.        } 
  119.    } 
  120. } 


CharsetProvider

  1. package com.custom.nio.charset; 
  2.  
  3. import java.nio.charset.Charset; 
  4. import java.util.Collection; 
  5. import java.util.HashMap; 
  6. import java.util.HashSet; 
  7. import java.util.Iterator; 
  8. import java.util.Map; 
  9.  
  10. /** 字元編碼器連結器,用來向 JVM 提交自訂的編碼器 
  11. */ 
  12. public class CharsetProvider extends java.nio.charset.spi.CharsetProvider { 
  13.  
  14.    static Map<String, Charset> name2charset; 
  15.    static Collection<Charset> charsets; 
  16.  
  17.    public Charset charsetForName(String charsetName) { 
  18.        if (charsets == null){ init(); } 
  19.        return name2charset.get(charsetName.toLowerCase()); 
  20.    } 
  21.  
  22.    public Iterator<Charset> charsets() { 
  23.        if (charsets == null){ init(); } 
  24.        return charsets.iterator(); 
  25.    } 
  26.  
  27.    void init() { 
  28.        name2charset = new HashMap<String, Charset>(); 
  29.  
  30.        charsets = new HashSet<Charset>(); 
  31.        charsets.add(new Big5_Extend()); 
  32.  
  33.        for (Charset charset : charsets) { 
  34.            name2charset.put(charset.name().toLowerCase(), charset); 
  35.            for (String aliase: charset.aliases()) { 
  36.                name2charset.put(aliase.toLowerCase(), charset); 
  37.            } 
  38.        } 
  39.    } 
  40. } 


java.nio.charset.spi.CharsetProvider

  1. com.custom.nio.charset.CharsetProvider 
內容就一行類別定義


測試

  1. public class Test { 
  2.  
  3.    public static void main(String[] args) throws Throwable { 
  4.        String charset = "X-Big5-Extend"; 
  5.        String source = "堃菓蕋侊"; 
  6.  
  7.        byte[] bytes = source.getBytes(charset); 
  8.        for (byte b : bytes) { 
  9.            System.out.printf("%x ", b); 
  10.        } 
  11.        System.out.println("\n"); 
  12.        // fa 40 fa 41 fa 42 fa 43 
  13.  
  14.        String result = new String(bytes, charset); 
  15.        System.out.println(result); 
  16.        // 堃菓蕋侊 
  17.    } 
  18. } 

參考自:Java字符编码解码的实现详解_java_脚本之家

0 回應: