Java字符串按照字节截取

这是一道常见的Java面试题, 很多人都遇到过, 这里涉及的知识点还挺多, 网上的论断和实现也很多, 我这不过是一家之言, 有相同的地方, 有不同的地方. 本想全面摘录各家之言做总结, 鉴于节约各方时间, 仅仅列述了本人所领受和有价值的部分.

主要的知识点备注:
因为ASCII代表了通用英文字符集, 在互联网背景下, 为了统一世界各国各种字符集, 流行开了Unicode字符集, 显然Java也原生支持Unicode, 但有几个概念容易混淆, 即Java中char里存的既不是ASCII字符, 也不是GBK字符或UTF-8字符, 里面存的是Unicode代码点值, 不论是’A’还是’中’都占两个字节, 这也是Java规范中对char类型的相关规定. 这也就说明, 一个char表示了UCS-2通用字符集, 但Java中没法直接表示UCS-4中的扩展字符集, 比如聊天表情字符, 因为占用4个字节, 一个char装不下, 而UCS-2和UCS-4都属于Unicode字符集, 所以会发现提供了codePointAt这样的方法, 很多方法除了可接受char类型参数, 还有同名方法接受或返回四字节的int, 使UCS-4的代码点值不至于溢出.

其次需要知道的是, 不应在使用ASCII, ISO-8859-1(也称为Latin-1, 在英文ASCII基础上包括了西欧字符), GB2312, GBK, GB18030. 而是应该使用Unicode, 但为了在传输中, 程序中, 存储中区分这些字符集, Unicode有多种存储实现方案, 包括UTF-8, UTF-8 BOM, UTF-16 BOM, UTF-16BE, UTF-16LE, UTF-32BE, UTF-32LE. 其中BOM是byte order mark, 即字节序标记, UTF-8的BOM是3个字节, 并不代表字节序, 而是表示这是UTF-8编码的, 因为Windows即想兼容ASCII, 也想支持Unicode, 而Linux下执行shell脚本时不期望前三个字节是BOM, 换句话说, 他们根本不鸟这种BOM, 所以为了不引起兼容问题, 不要使用UTF-8 with BOM的格式. 另外, 像”a”.getBytes(“UTF-16”)返回4个字节, “ab”.getBytes(“UTF-16”)返回6个字节, 也就是说, 如果不指定字节序, Java中UTF-16其实是使用UTF-16 BOM, 前两个字节表示当前处理器使用的字节序. 这在不同机器上交互时同样可能产生不兼容乱码, 所以也不要这样使用. 绝大多数应用肯定用不到UTF-32xx级别的格式, 因为太浪费啦. 如果确定所有的机器都有相同字节序, 并且以中文文本传输存储为主, 可以考虑UTF-16BE或者UTF-16LE, 为什么这么说呢, UTF-16BE和UTF-16LE中英文和中文都占两个字节, 而UTF-8中, 英文占1个字节, 中文却占3个字节, 如果文本长度可观, 肯定UTF-16BE和UTF-16LE更节约. 但UTF-8没有字节序问题, 而UTF-16LE适合用于Little endian(将低序字节存储在起始地址, intel, AMD及X86, IA架构)处理器, UTF-16BE适合Big endian(将高序字节存储在起始地址, Java字节序, 网络字节序, ARM及RISC架构为主)处理器. 所以说了这么多, 涉及跨平台, 互联网的应用, 应一律采用无BOM的UTF-8编码格式;

再次, 为什么UTF-8不存在字节序问题呢, 不是说中文用3个字节表示吗? 这里不存在顺序问题吗? 以下代码和说明解决了我的疑惑:

char data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97};//UTF-8编码
char16_t data_utf16[]={0x6C49,0x5B57};//UTF-16编码
char32_t data_utf32[]={0x00006C49,0x00005B57};//UTF-32编码

这里用char、char16_t、char32_t分别表示无符号8位整数,无符号16位整数和无符号32位整数。UTF-8、UTF-16、UTF-32分别以char、char16_t、char32_t作为编码单位。(注: char16_t 和 char32_t 是 C++ 11 标准新增的关键字。如果你的编译器不支持 C++ 11 标准,请改用 unsigned short 和 unsigned long。)“汉字”的UTF-8编码需要6个字节。“汉字”的UTF-16编码需要两个char16_t,大小是4个字节。“汉字”的UTF-32编码需要两个char32_t,大小是8个字节。

最后, 确定了所有UTF-8, 我们可以根据UTF-8的编码规则, 处理”Java字符串按照字节截取”的问题(这里所说的和以下宣称的UTF-8, 全是无BOM的那个):
0000 0000-0000 007F | 0xxxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
第一行包括ASCII英文数字及标点字符集, 即一个字节表示;
第二行包括中文简体和繁体, 日文, 韩文的常用字符集, 即三个字节表示;
2个字节的, 3个以上字节的处于简化和需求, 都不考虑了.

所以说, 假定从网络传过来也好, 还是文件打开也好, 亦或者内存中的字符串也好, 都能获得其字节数组形式, 现在截取前n个字节的代码而又不使后面的字符因为字节截断而成乱码, 可以通过判断字节首位的方式解决这个问题, 示例如下:

import java.io.UnsupportedEncodingException;

public class StrTruncByByteCountTest {
    public static void main(String[] args) throws Exception{
        final int len = 15;
        for(byte b : "E".getBytes("UTF-8")) {
            System.out.println(byteToBinary(b));
        }
        System.out.println("=================");
        for(byte b : "中".getBytes("UTF-8")) {
            System.out.println(byteToBinary(b));
        }
        System.out.println("=================");
        final String string = "ab中国c人12大厦3";
        final byte[] bytes = string.getBytes("UTF-8"); //byte array also can from IO stream
        final TruncateTips tips = new TruncateTips();
        System.out.println(substrUTF8(bytes, len, tips));
        System.out.println("=================");
        System.out.println("多截取出的无法形成有效字符的无用字节数: " + tips.getOver());
        System.out.println("再加上以下数目的字节将拼凑出下一个完整字符: " + tips.getNeed());
    }

    private static String substrUTF8(byte[] bytes, int len, TruncateTips tips)
            throws UnsupportedEncodingException {
        if (bytes == null || bytes.length == 0 || len <= 0) {
            return "";
        }
        if (len >= bytes.length) {
            return new String(bytes, "UTF-8");
        }
        int index = 0;
        while (index < len) {
            final byte b = bytes[index];
            if ((b & 0x80/*0b10000000*/) == 0) {// is ascii
                ++index;
            } else {
                int count = 1;
                byte t = 0x40/*0b01000000*/;
                for (int i = 1; i < 8; ++i) {
                    if ((b & t) != 0) {
                        ++count;
                        t >>>= 1;
                    } else {
                        break;
                    }
                }
                final int sum = index + count;
                if (sum <= len) {
                    index = sum;
                } else {
                    if (tips != null) {
                       tips.setNeed(sum - len);
                       tips.setOver(len - index);
                    }
                    break;
                }
            }
        }
        return new String(bytes, 0, index, "UTF-8");
    }

    public static final class TruncateTips {
        private int over;
        private int need;
        public void setNeed(int need) {
            this.need = need;
        }
        public void setOver(int over) {
            this.over = over;
        }
        public int getNeed() {
            return need;
        }
        public int getOver() {
            return over;
        }
    }

    private static String byteToBinary(byte b) {
        final char[] chars = new char[8];
        byte t = 0x1/*0b10000000*/;
        for (int i = 7; i >= 0; --i) {
            if ((b & t) == 0) {
                chars[i] = '0';
            } else {
                chars[i] = '1';
            }
            t <<= 1;
        }
        return String.valueOf(chars);
    }
}

上面这个我在实战中是有用过的, 尤其网络接收数据且拼成的字符串很大不宜全部读入内存时. 但如果是理想主义面试题, 说不能假定字符集, 可以使用类库方法, 且方法签名必须如下:
static String substr(String originString, String charsetName, int byteLen);
借助网上的实现, 可以考虑这样做:

private static String substr(String originString, String charsetName, int byteLen)
            throws UnsupportedEncodingException {
        if (originString == null || originString.isEmpty() || byteLen <= 0) {
            return "";
        }
        char[] chars = originString.toCharArray();
        int length = 0, index = chars.length;
        for (int i = 0; i < chars.length; ++i) {
            final int len = String.valueOf(chars[i]).getBytes(charsetName).length + length;
            if (len <= byteLen) {
                length = len;
            } else {
                index = i;
                break;
            }
        }
        return String.valueOf(chars, 0, index);
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值