前几天在公司对通过 FTP 方式上传的数据文件按照事先规定的格式进行解析后入库,代码的大概实现思路是这样的:先使用流进行文件读取,对文件的每一行数据解析封装成一个个对象,然后进行入库操作。本以为很简单的一个操作,然后写完代码后自己测试发现对文件的每一行进行字符串分割的时候存在问题,在这里做个简单的记录总结。在 Java 中使用 split 方法对字符串进行分割是经常使用的方法,经常在一些文本处理、字符串分割的逻辑中,需要按照一定的分隔符进行分割拆解。这样的功能,大多数情况下我们都会使用 String 中的 split 方法。关于这个方法,稍不注意很容易踩坑。
(1)split 的参数是正则表达式
首先一个常见的问题,就是忘记了 String 的 split 方法的参数不是普通的字符串,而是正则表达式,例如下面的这两种使用方式都达不到我们的预期:
/** * @author mghio * @date: 2019-10-13 * @version: 1.0 * @description: Java 字符串 split 踩坑记 * @since JDK 1.8 */ public class JavaStringSplitTests { @Test public void testStringSplitRegexArg() { System.out.println(Arrays.toString("m.g.h.i.o".split("."))); System.out.println(Arrays.toString("m|g|h|i|o".split("|"))); } }
<!-- more -->
以上代码的结果输出为:
[] [m, |, g, |, h, |, i, |, o]
上面出错的原因是因为 . 和 | 都是正则表达式,应该用转义字符进行处理:
"m.g.h.i.o".split("//.") "m|g|h|i|o".split("//|")
在 String 类中还有其它的和这个相似的方法,例如:replaceAll。
(2)split 会忽略分割后的空字符串
大多数情况下我们都只会使用带一个参数的 split 方法,但是只带一个参数的 split 方法有个坑:就是此方法只会匹配到最后一个有值的地方,后面的会忽略掉,例如:
/** * @author mghio * @date: 2019-10-13 * @version: 1.0 * @description: Java 字符串 split 踩坑记 * @since JDK 1.8 */ public class JavaStringSplitTests { @Test public void testStringSplitSingleArg() { System.out.println(Arrays.toString("m_g_h_i_o".split("_"))); System.out.println(Arrays.toString("m_g_h_i_o__".split("_"))); System.out.println(Arrays.toString("m__g_h_i_o_".split("_"))); } }
以上代码输出结果为:
[m, g, h, i, o] [m, g, h, i, o] [m, , g, h, i, o]
像第二、三个输出结果其实和我们的预期是不符的,因为像一些文件上传其实有的字段通常是可以为空的,如果使用单个参数的 split 方法进行处理就会有问题。通过查看 API 文档 后,发现其实 String 中的 split 方法还有一个带两个参数的方法。第二个参数是一个整型类型变量,代表最多匹配上多少个,0 表示只匹配到最后一个有值的地方,单个参数的 split 方法的第二个参数其实就是 0,要想强制匹配可以选择使用负数(通常传入 -1 ),换成以下的写法,输出结果就和我们的预期一致了。
"m_g_h_i_o".split("_", -1) // [m, g, h, i, o] "m_g_h_i_o__".split("_", -1) // [m, g, h, i, o, , ] "m__g_h_i_o_".split("_", -1) // [m, , g, h, i, o, ]
(3)JDK 中字符串切割的其它 API
在 JDK 中还有一个叫做 StringTokenizer 的类也可以对字符串进行切割,用法如下所示:
/** * @author mghio * @date: 2019-10-13 * @version: 1.0 * @description: Java 字符串 split 踩坑记 * @since JDK 1.8 */ public class JavaStringSplitTests { @Test public void testStringTokenizer() { StringTokenizer st = new StringTokenizer("This|is|a|mghio's|blog", "|"); while (st.hasMoreElements()) { System.out.println(st.nextElement()); } } }
不过,我们从源码的 javadoc 上得知,这是从 JDK 1.0 开始就已经存在了,属于历史遗留的类,并且推荐使用 String 的 split 方法。
通过查看 JDK 中 String 类的源码,我们得知在 String 类中单个参数的 split 方法( split(String regex) )里面调用了两个参数的 split 方法( split(String regex, int limit) ),两个参数的 split 方法,先根据传入第一个参数 regex 正则表达式分割字符串,第二个参数 limit 限定了分割后的字符串个数,超过数量限制的情况下前limit-1个子字符串正常分割,最后一个子字符串包含剩下所有字符。单个参数的重载方法将 limit 设置为 0。源码如下:
public String[] split(String regex, int limit) { char ch = 0; if (((regex.value.length == 1 && ".$|()[{^?*+//".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == '//' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)) { int off = 0; int next = 0; boolean limited = limit > 0; ArrayList<String> list = new ArrayList<>(); while ((next = indexOf(ch, off)) != -1) { if (!limited || list.size() < limit - 1) { list.add(substring(off, next)); off = next + 1; } else { // last one //assert (list.size() == limit - 1); list.add(substring(off, value.length)); off = value.length; break; } } // If no match was found, return this if (off == 0) return new String[]{this}; // Add remaining segment if (!limited || list.size() < limit) list.add(substring(off, value.length)); // Construct result int resultSize = list.size(); if (limit == 0) { while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { resultSize--; } } String[] result = new String[resultSize]; return list.subList(0, resultSize).toArray(result); } return Pattern.compile(regex).split(this, limit); }
接下来让我们一起看看 String 的 split 方法是如何实现的。
(((regex.value.length == 1 && ".$|()[{^?*+//".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == '//' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE))
(2)字符串分割
第一次分割时,使用 off 和 next,off 指向每次分割的起始位置,next 指向分隔符的下标,完成一次分割后更新 off 的值,当 list 的大小等于 limit - 1 时,直接添加剩下的子字符串。
(3)正则匹配
String 的 split 方法在不是上面的特殊情况下,会使用两个类 Pattern 与 Matcher 进行分割匹配处理,而且 Strig 中涉及正则的操作都是调用这两个类进行处理的。
return Pattern.compile(regex).split(this, limit);
首先调用 Pattern 类的静态方法 compile 获取 Pattern 模式类对象
public static Pattern compile(String regex) { return new Pattern(regex, 0); }
接着调用 Pattern 的 split(CharSequence input, int limit) 方法,在这个方法中调 matcher(CharSequence input) 方法返回一个 Matcher 匹配器类的实例 m,与 String 类中 split 方法的特殊情况有些类似。
/** * @author mghio * @date: 2019-10-13 * @version: 1.0 * @description: Java 字符串 split 踩坑记 * @since JDK 1.8 */ public class JavaStringSplitTests { @Test public void testApacheCommonsLangStringUtils() { System.out.println(Arrays.toString(StringUtils.split("m.g.h.i.o", "."))); System.out.println(Arrays.toString(StringUtils.split("m__g_h_i_o_", "_"))); } }
输出结果:
[m, g, h, i, o] [m, g, h, i, o]
/** * @author mghio * @date: 2019-10-13 * @version: 1.0 * @description: Java 字符串 split 踩坑记 * @since JDK 1.8 */ public class JavaStringSplitTests { @Test public void testApacheCommonsLangStringUtils() { Iterable<String> result = Splitter.on("_").split("m__g_h_i_o_"); List<String> resultList = Lists.newArrayList(); result.forEach(resultList::add); System.out.println("stringList's size: " + resultList.size()); result.forEach(System.out::println); } }
输出结果:
stringList's size: 7 m g h i o
String 类中除了 split 方法外,有正则表达式接口的方法都是调用 Pattern(模式类)和 Matcher(匹配器类)进行实现的。JDK 源码的每一个如 final 、 private 的关键字都设计的十分严谨,多读类和方法中的javadoc,多注意这些细节对于阅读代码和自己写代码都有很大的帮助。