Java字符串分割为不超过固定字节长度的子串数组列表

最近有一个保存邮件内容到数据库的需求,由于数据库中对应字段是有长度限制的,我就到百度上寻找对长度比较大的字符串做分段保存的实现,但搜索引擎上往往只出现一些使用String.split的方法,而这种分割,是根据字符串内的字符分割的,如果是UTF-8编码,一个中文是4字节,String.split后实际上长度并不是字节长度。
想必有人会认为:数据库中直接保存字节数组不就好了吗?
答案是肯定的,但考虑到查看的时候并不方便,而且邮件内容为html格式,有时候出现线上问题想手工修改的时候往往就麻烦,而且以前也遇到过想把字符串按字节截取出来的问题,所以就自己动手写了一下,在这里也希望能分享给大家,如果有误请指正。
首先第一个问题,按字节截取字符串分割子串还不简单吗?直接str.getBytes()后对字节数组下标分段为子字节数组就好啦,我相信大家都会这么觉得。
但是按字节截取字符串实际上是存在一个问题,就是如果截取的位置正好是该字符编码的中间位置就会导致这个字符成为乱码。以下代码就是阐述该问题,字符串是17个中文‘一’:

public class Test { public static void main(String[] args) throws UnsupportedEncodingException { int len = 5; String str = "一一一一一一一一一一一一一一一一一"; byte[] bt = str.getBytes(StandardCharsets.UTF_8); byte[] br = new byte[len]; System.arraycopy(bt, 0, br, 0, len); String res = new String(br, StandardCharsets.UTF_8); System.out.println(res); }

输出的结果为:一?。因为一个中文4字节,而我们要该字符串的5个字节,当然就会把第二个‘一’的一部分字节切割得到乱码了。
这时候大家也会想说:没关系吧,截取之后,就算乱码拼起来不就没事了。我也是天真这么以为的,所以我byte数组转字符串后,进行拼接,发现,更乱了,代码如下:
public static void main(String[] args) throws UnsupportedEncodingException { int len = 5; String str = "一一一一一一一一一一一一一一一一一"; byte[] bt = str.getBytes(StandardCharsets.UTF_8); byte[] br = new byte[len]; System.arraycopy(bt, 0, br, 0, len); String res = new String(br, StandardCharsets.UTF_8); System.arraycopy(bt, 5, br, 0, len); String resnext = new String(br, StandardCharsets.UTF_8); System.out.println(res + resnext); }

输出结果:一??一?
以前遇到这个按字节截位的问题的时候,我查了很多的文章,大部分方法是使用Charset先设置好编码集,再用CharBuffer封装原字符串后做截取,由于一直都没怎么使用过这两个类,所以当时也没有看懂,也就放弃使用这个了。
后面看到了一些大佬的实现,他们解决办法非常巧妙,如下:
1.先按字节数组进行截取,获得一个长度为固定截取长度的子字节数组, 2.把字节数组转字符串得到一个新String子串, 3.再次把新String子串转byte数组,两数组长度进行比较(因为新String子串再转byte数组时, 会对截取了一半的字符进行补全为对应编码集一个字符的长度), 4.如果 新String子串的字节数组 比 步骤1中按长度截取的子串字节数组 长, 说明存在截取一半的字符,这个字符会在最后一个位置,要舍弃, 所以把新String子串按字符串长度截取减少1位,得到的字符串就是没有截取一半的字符, 且长度小于等于需要的字节长度的子串。

上面只是从字符串中截取一个不超过固定字节长度的子串,那么我们如何把一个长字符串分割为不超过固定字节长度的子串字符数组列表呢?
具体代码如下:
/** * 方法:字符串按字节固定长度分割数组 * startPos 子串在原字符串字节数组的开始截取下标 * startStrPos 子串在原字符串开始截取的下标 * strLen 原字符串字节数组长度 * 背景:由于编码格式不同,直接截取可能会拿到一个被砍一半的乱码,如utf-8 4byte 一个中文,如果截取的时候是5byte,就会出现乱码 * 原理:1、先按字节数组进行截取,获得一个长度不大于固定截取长度的字节数组 *2、把字节数组转字符串得到一个新子串,再转byte数组后,两数组长度进行比较(新子串再转byte数组时,会对截取了一半的字符进行补全为对应编码集一个字符的长度), *如果新子串的字节数组比按长度截取的子串字节数组长,说明存在截取一半的字符,这个字符会在最后一个位置,要舍弃 *所以,新子串按字符串长度截取减少1位,得到的字符串就是没有截取一半的字符,且长度小于等于需要的字节长度的子串。 * * 1.当 子串字节数组开始截取下标 小于 原字符串字节数组长度 一直循环 * 2.子串字节数组大小 需要根据 当前父串字节数组的截取下标和长度差值 与 预想截取的字节长度 比较来创建(否则用System.arraycopy会报错) * 3.根据 子串在原字符串字节数组的开始截取下标 拷贝父字节数组的内容到子字节数组 * 4.根据 子串在原字符串开始截取的下标 与 子字节数组转为字符串的长度 在父字符串截取一个伪子串(可能最后一个字符被截取一半是乱码) * 5.比较伪子串转字节数组后长度 与 预想截取的字节数组长度,大于,则伪子串截取字符串长度-1 * 6.子串字节数组开始截取下标 + 得到的子串字节长度;子串在原字符串开始截取的下标 + 得到子串的字符长度 * @param str 原字符串 * @param len 分割字串字节长度 * @param charSet 编码字符集 * @return List 分割后的子串 * @throws UnsupportedEncodingException */ public static final List divideStrByBytes(String str,int len, String charSet) throws UnsupportedEncodingException{ List strSection = new ArrayList<>(); byte[] bt = str.getBytes(charSet); int strLen = bt.length; int startPos = 0; int startStrPos = 0; while (startPos < strLen) { Integer subSectionLen = len; if (strLen - startPos < len) { subSectionLen = strLen - startPos; } byte[] br = new byte[subSectionLen]; System.arraycopy(bt, startPos, br, 0, subSectionLen); String res = new String(br, charSet); int resLen = res.length(); if (str.substring(startStrPos, startStrPos + resLen).getBytes(charSet).length > len) { res = res.substring(0, resLen - 1); } startStrPos += res.length(); strSection.add(res); startPos += res.getBytes(charSet).length; } return strSection; }

下面我们试试效果:
public static void main(String[] args) throws UnsupportedEncodingException { int len = 5; String str = "一一一一一一一一一一一一一一一一一"; List stringList = divideStrByBytes(str,len,"UTF-8"); stringList.forEach(item -> { System.out.println(item); }); } }

【Java字符串分割为不超过固定字节长度的子串数组列表】输出结果:17个中文‘一’
希望这块代码能帮助到大家,如有误,请大家一起交流指正,谢谢!

    推荐阅读