使用Apache POI eventmodel實現一個Excel流式讀取類,目標是100萬行,每行46列,文件大小152MB的Excel文件能在20s讀取並處理完。一開始實現的程序須要260s,離目標差太遠了,使用jvisualvm分析各方法執行時間,結果以下:git
能夠看到,程序中的splitLine和getRowNum方法消耗了大量時間。這兩個方法都特別簡單。splitLine方法將相似「123==hello」這樣的字符串分解成{"123","hello"}數組,使用了String.split方法,getRowNum從Excel單元格地址字符串(好比「AB123456」)中獲取行號「123456」,如下是原始實現方法:正則表達式
private String getRowNum(String cellRef){ if(cellRef == null || cellRef == ""){ return "-1"; } String[] nums = cellRef.split("\\D+"); if(nums.length > 1){ return nums[1]; } return "-1; } private String[] splitLine(String line){ return line.split("=="); }
兩個如此簡單的方法卻消耗了這麼多時間,一時間不知如何優化。最後忽然想到:split的性能是否最優呢?對於如此簡單的字符串分割,使用indexOf + subString性能如何呢?因而,我作了以下的實驗:數組
public static void main(String[] args) throws ParseException{ String str = "AB123456"; long start = System.currentTimeMillis(); for(int i = 0 ; i < 10 * 10000 ; i ++){ String[] lines = str.split("\\D+"); } long end = System.currentTimeMillis(); System.out.println("split time consumed:" + (end - start) / 1000.0 + "s"); start = System.currentTimeMillis(); int index = -1; for(int i = 0 ; i < 10 * 10000 ; i ++){ index = -1; for(int k = 0 ; k < str.length() ; k ++){ if(str.charAt(k) >= '0' && str.charAt(k) <= '9'){ index = k; break; } } if(index > 0){ String[] lines = new String[]{str.substring(0, index),str.substring(index)}; } } end = System.currentTimeMillis(); System.out.println("indexof time consumed:" + (end - start) / 1000.0 + "s"); }
如下是輸出結果:
split time consumed:0.104s
indexof time consumed:0.007s性能
雖然表面上看,split比index + subString要簡單不少,但後者性能是前者的將近15倍。用這種方法改寫前面的splitLine和getRowNum,代碼以下:測試
private String getRowNum(String cellRef){ int index = -1; for(int k = 0 ; k < cellRef.length() ; k ++){ if(cellRef.charAt(k) >= '0' && cellRef.charAt(k) <= '9'){ index = k; break; } } if(index >= 0){ String[] nums = new String[]{cellRef.substring(0, index),cellRef.substring(index)}; if(nums.length > 1){ return nums[1]; } } return "-1"; } private String[] splitLine(String line){ int index = line.indexOf("=="); if(index > 0){ return new String[]{line.substring(0, index),line.substring(index + 2)}; } return new String[0]; }
優化後再用jvisualvm測試各方法執行時間:大數據
能夠看到,我本身的數據處理方法已不是明顯的性能瓶頸,而Apache POI的zip解壓和文件讀取佔用了絕大部分時間。總體時間也從260s降低到了160s,已有了明顯的提升。優化
咱們知道indexOf就是暴力搜索,split內部使用正則表達式作匹配,在搜索字符串較簡單時確定是indexOf性能好。大多數狀況下調用split時都用不到正則表達式的那些高大上功能,因此徹底不必圖方便在任什麼時候候都用split,而是有所取捨:當簡單分割字符串時本身用indexOf實現split,而涉及到複雜的分割操做,不得不用正則表達式時,才用split。爲了看清String.split方法在作什麼,咱們看看JDK中String.split的源碼:this
public String[] split(String regex, int limit) { /* fastpath if the regex is a (1)one-char String and this character is not one of the RegEx's meta characters ".$|()[{^?*+\\", or (2)two-char String and the first char is the backslash and the second is not the ascii digit or ascii letter. */ 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); }
儘管split方法的實現仍是挺優化的,但仍作了太多的操做。spa
想想我過去寫的代碼常常圖方便濫用String.split,這樣是經不起大數據量考驗的,學了這麼長時間Java,竟從沒想過這樣的問題,不由感嘆本身仍是菜鳥。雖然像Java或C#這種語言各類方法使用起來方便,但其庫方法之下隱藏的性能開銷,須要每個使用者注意。code
(全文完)