前幾天在公司對經過 FTP 方式上傳的數據文件按照事先規定的格式進行解析後入庫,代碼的大概實現思路是這樣的:先使用流進行文件讀取,對文件的每一行數據解析封裝成一個個對象,而後進行入庫操做。本覺得很簡單的一個操做,而後寫完代碼後本身測試發現對文件的每一行進行字符串分割的時候存在問題,在這裏作個簡單的記錄總結。在 Java 中使用 split 方法對字符串進行分割是常常使用的方法,常常在一些文本處理、字符串分割的邏輯中,須要按照必定的分隔符進行分割拆解。這樣的功能,大多數狀況下咱們都會使用 String 中的 split 方法。關於這個方法,稍不注意很容易踩坑。java
(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("|"))); } }
以上代碼的結果輸出爲:apache
[] [m, |, g, |, h, |, i, |, o]
上面出錯的緣由是由於 . 和 | 都是正則表達式,應該用轉義字符進行處理:api
"m.g.h.i.o".split("\\.") "m|g|h|i|o".split("\\|")
在 String 類中還有其它的和這個類似的方法,例如:replaceAll。oracle
(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("_"))); } }
以上代碼輸出結果爲:this
[m, g, h, i, o] [m, g, h, i, o] [m, , g, h, i, o]
像第2、三個輸出結果其實和咱們的預期是不符的,由於像一些文件上傳其實有的字段一般是能夠爲空的,若是使用單個參數的 split 方法進行處理就會有問題。經過查看 API 文檔 後,發現其實 String 中的 split 方法還有一個帶兩個參數的方法。第二個參數是一個整型類型變量,表明最多匹配上多少個,0 表示只匹配到最後一個有值的地方,單個參數的 split 方法的第二個參數其實就是 0,要想強制匹配能夠選擇使用負數(一般傳入 -1 ),換成如下的寫法,輸出結果就和咱們的預期一致了。google
"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 方法是如何實現的。
(1)特殊狀況判斷
(((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,多注意這些細節對於閱讀代碼和本身寫代碼都有很大的幫助。