在Java中按字节长度限制拆分字符串

6
我想把一个字符串拆分成一个满足以下条件的字符串数组String[]
  • s.getBytes(encoding).length 不应超过 maxsize(int)

  • 如果我使用 StringBuilder+ 运算符连接拆分后的字符串,结果应该与原始字符串完全相同。

  • 输入字符串可能有 Unicode 字符,在编码为 UTF-8 等时可能有多个字节。

下面是期望的原型。
public static String[] SplitStringByByteLength(String src,String encoding, int maxsize)

还有测试代码:

public boolean isNice(String str, String encoding, int max)
{
    //boolean success=true;
    StringBuilder b=new StringBuilder();
    String[] splitted= SplitStringByByteLength(str,encoding,max);
    for(String s: splitted)
    {
        if(s.getBytes(encoding).length>max)
            return false;
        b.append(s);
    }
    if(str.compareTo(b.toString()!=0)
        return false;
    return true;
}

尽管输入字符串仅由ASCII字符组成时似乎很容易,但它可能包含多字节字符的事实让我感到困惑。
提前感谢您。
编辑:我添加了我的代码实现。(低效)
public static String[] SplitStringByByteLength(String src,String encoding, int maxsize) throws UnsupportedEncodingException
{
    ArrayList<String> splitted=new ArrayList<String>();
    StringBuilder builder=new StringBuilder();
    //int l=0;
    int i=0;
    while(true)
    {
        String tmp=builder.toString();
        char c=src.charAt(i);
        if(c=='\0')
            break;
        builder.append(c);
        if(builder.toString().getBytes(encoding).length>maxsize)
        {
            splitted.add(new String(tmp));
            builder=new StringBuilder();
        }
        ++i;
    }
    return splitted.toArray(new String[splitted.size()]);
}

这是解决这个问题的唯一方法吗?


你为什么要将其转换为字节而不是基于字符进行操作呢? - Andreas Hartmann
因为我必须将字符串发送到另一个接受UTF-8 VNOTE文件的设备,而它无法处理超过400字节的字符串。因此,我必须将我的字符串拆分成不超过400字节的字符串。 - KYHSGeekCode
我投票关闭此问题,因为请求审核工作代码应该转到https://codereview.stackexchange.com/。 - GhostCat
@Neijwiert 不是这样。我想根据字节长度进行拆分,以防止目标设备将其修剪,该设备接受的最大字节数为400。 - KYHSGeekCode
1
由于当前的代码并没有真正满足完整的要求,我不确定它是否真的与SO的主题无关。 - Serge Ballesta
显示剩余2条评论
2个回答

12
CharsetEncode 类中提供了您需求的功能。以下是 Encode 方法的 Javadoc 提取:
public final CoderResult encode(CharBuffer in,
                            ByteBuffer out,
                            boolean endOfInput)

尽可能从给定的输入缓冲区中编码更多的字符,将结果写入给定的输出缓冲区...

除了从输入缓冲区读取字符并向输出缓冲区写入字节之外,该方法还返回一个CoderResult对象,以描述其终止原因:

...

CoderResult.OVERFLOW表示输出缓冲区中没有足够的空间来编码更多的字符。应该使用剩余更多字节的输出缓冲区再次调用此方法。通常通过从输出缓冲区中排出任何已编码的字节来完成。

public static String[] SplitStringByByteLength(String src,String encoding, int maxsize) {
    Charset cs = Charset.forName(encoding);
    CharsetEncoder coder = cs.newEncoder();
    ByteBuffer out = ByteBuffer.allocate(maxsize);  // output buffer of required size
    CharBuffer in = CharBuffer.wrap(src);
    List<String> ss = new ArrayList<>();            // a list to store the chunks
    int pos = 0;
    while(true) {
        CoderResult cr = coder.encode(in, out, true); // try to encode as much as possible
        int newpos = src.length() - in.length();
        String s = src.substring(pos, newpos);
        ss.add(s);                                  // add what has been encoded to the list
        pos = newpos;                               // store new input position
        out.rewind();                               // and rewind output buffer
        if (! cr.isOverflow()) {
            break;                                  // everything has been encoded
        }
    }
    return ss.toArray(new String[0]);
}

这将把原始字符串分成块,当以字节编码时,它们尽可能适合给定大小的字节数组(当然假设maxsize不是非常小)。


2
问题在于Unicode "补充字符"的存在(请参阅Character类的Javadoc),它们占用字符串中的两个“字符位置”(代理对),因此您不应该在这样的对中间拆分字符串。
一种简单的拆分方法是坚持最坏情况,即单个Unicode代码点在UTF-8中最多占用四个字节,并在每99个代码点后拆分字符串(使用string.offsetByCodePoints(pos, 99))。在大多数情况下,您不会填满400个字节,但您将处于安全的一面。
关于代码点和字符的一些说明:
当Java开始时,Unicode少于65536个字符,因此Java认为16位足以表示一个字符。后来,Unicode标准超过了16位限制,Java遇到了问题:单个Unicode元素(现在称为“代码点”)不再适合单个Java字符。
他们决定进行16位实体编码,对于大多数常见代码点是1:1,对于16位限制之外的奇特代码点则占用两个“字符”(由所谓的“代理字符”构建的一对,来自65535以下的备用代码范围)。因此,现在可能发生例如string.charAt(5)和string.charAt(6)必须结合在一起作为“代理对”,共同编码一个Unicode代码点。
这就是为什么您不应该在任意索引处拆分字符串的原因。
为了帮助应用程序员,String类随后获得了一组新的方法,以代码点单位工作,例如string.offsetByCodePoints(pos, 99)表示:从索引pos开始,向前移动99个代码点,给出一个索引,该索引通常是pos+99(如果字符串不包含任何奇特内容),但如果所有以下字符串元素恰好是代理对,则可能高达pos+198。
使用代码点方法,您可以安全地避免落在代理对中间。

嗯,我认为你已经有了点头了。但是我对string.offsetByCodePoints(pos, 99)这个方法和代码点没有基本的了解。你能否更多地解释一下这个方法和代码点? - KYHSGeekCode
非常感谢您的迅速和热心回答,但是我恐怕在下面得到了一个更有效的答案。抱歉反悔了。无论如何,我非常感谢您的帮助! - KYHSGeekCode
我完全同意。Serge的回答比我的更好,我已经点赞了。 - Ralf Kleberhoff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接