【本篇博文會介紹JSON解析的原理與實現,並一步一步寫出來一個簡單但實用的JSON解析器,項目地址:SimpleJSON。但願經過這篇博文,能讓咱們之後與JSON打交道時更加駕輕就熟。因爲我的水平有限,敘述中不免存在不許確或是不清晰的地方,但願你們能夠指正:)】java
相信你們在平時的開發中沒少與JSON打交道,那麼咱們日常使用的一些JSON解析庫都爲咱們作了哪些工做呢?這裏咱們以知乎日報API返回的JSON數據來介紹一下兩個主流JSON解析庫的用法。咱們對地址 http://news-at.zhihu.com/api/4/news/latest進行GET請求,返回的JSON響應的總體結構以下:git
{
date: "20140523", stories: [ { images:["http:\/\/pic1.zhimg.com\/4e7ecded780717589609d950bddbf95c.jpg"] type: 0, id: 3930445, ga_prefix: "052321", title: "中國古代傢俱發展到今天有兩個高峯,一個兩宋一個明末(多圖)", }, ... ], top_stories: [ {
image:"http:\/\/pic4.zhimg.com\/8f209bcfb5b6e0625ca808e43c0a0a73.jpg",
type:0,
id:8314043,
ga_prefix:"051717",
title:"怎樣才能找到本身的興趣所在,發自心裏地去工做?"
},
...
]
}
以上JSON響應表示的是某天的最新知乎日報內容。頂層的date的值表示的是日期;stories的值是一個數組,數組的每一個元素又包含images、type、id等域;top_stories的值也是一個數組,數組元素的結構與stories相似。咱們先把把以上返回的JSON數據表示爲一個model類:github
public class LatestNews { private String date; private List<TopStory> top_stories; private List<Story> stories; //省略LatestNews類的getter與setter public static class TopStory { private String image; private int type; private int id; private String title; //省略TopStory類的getter與setter } public static class Story implements Serializable { private List<String> images; private int type; private int id; private String title; //省略Story類的getter與setter } }
在以上的代碼中,咱們定義的域與返回的JSON響應的鍵一一對應。那麼接下來咱們就來完成JSON響應的解析吧。首先咱們使用org.json包來完成JSON的解析。相關代碼以下:json
1 public class JSONParsingTest { 2 public static final String urlString = "http://news-at.zhihu.com/api/4/news/latest"; 3 public static void main(String[] args) throws Exception { 4 try { 5 String jsonString = new String(HttpUtil.get(urlString)); 6 JSONObject latestNewsJSON = new JSONObject(jsonString); 7 String date = latestNewsJSON.getString("date"); 8 JSONArray top_storiesJSON = latestNewsJSON.getJSONArray("top_stories"); 9 LatestNews latest = new LatestNews(); 10 11 12 List<LatestNews.TopStory> stories = new ArrayList<>(); 13 14 for (int i = 0; i < top_storiesJSON.length(); i++) { 15 LatestNews.TopStory story = new LatestNews.TopStory(); 16 story.setId(((JObject) top_storiesJSON.get(i)).getInt("id")); 17 story.setType(((JObject) top_storiesJSON.get(i)).getInt("type")); 18 story.setImage(((JObject) top_storiesJSON.get(i)).getString("image")); 19 story.setTitle(((JObject) top_storiesJSON.get(i)).getString("title")); 20 stories.add(story); 21 } 22 latest.setDate(date); 23 24 System.out.println("date: " + latest.getDate()); 25 for (int i = 0; i < stories.size(); i++) { 26 System.out.println(stories.get(i)); 27 } 28 29 } catch (JSONException e) { 30 e.printStackTrace(); 31 } 32 } 33 34 }
相信Android開發的小夥伴對org.json都不陌生,由於Android SDK中提供的JSON解析類庫就是org.json,要是使用別的開發環境咱們可能就須要手動導入org.json包。api
第5行咱們調用了HttpUtil.get方法來獲取JSON格式的響應字符串,HttpUtil是咱們封裝的一個用於網絡請求的靜態代碼庫,代碼見這裏:數組
接着在第6行,咱們以JSON字符串爲參數構造了一個JSONObject對象;在第7行咱們調用JSONObject的實例方法getString根據鍵名「date」獲取了date對應的值並保存在了一個String變量中。網絡
在第8行咱們調用了JSONObject的getJSONArray方法來從JSONObject對象中獲取一個JSON數組,這個JSON數組的每一個元素均爲JSONObject(表明了一個TopStory),每一個JSONObject均可以經過在其上調用getInt、getString等方法獲取type、title等鍵的值。正如咱們在第14到21行所作的,咱們經過一個循環讀取JSONArray的每一個JSONObject中的title、id、type、image域的值,並把他們寫入TopStory對象的對應實例域。oracle
咱們能夠看到,當返回的JSON響應結構比較複雜時,使用org.json包來解析響應比較繁瑣。那麼咱們看看如何使用gson(Google出品的JSON解析庫,被普遍應用於Android開發中)來完成相同的工做:app
public class GsonTest { public static final String urlString = "http://news-at.zhihu.com/api/4/news/latest"; public static void main(String[] args) { LatestNews latest = new LatestNews(); String jsonString = new String(HttpUtil.get(urlString)); latest = (new Gson()).fromJson(jsonString, LatestNews.class); System.out.println(latest.getDate()); for (int i = 0; i < latest.getTop_stories().size(); i++) { System.out.println(latest.getTop_stories().get(i)); } } }
咱們能夠看到,使用gson完成一樣的工做只須要一行代碼。那麼讓咱們一塊兒來看一下gson是如何作到的。在上面的代碼中,咱們調用了Gson對象的fromJson方法,傳入了返回的JSON字符串和Latest.class做爲參數。看到Latest.class,咱們就大概可以知道fromJson方法的內部工做機制了。能夠經過反射獲取到LatestNews的各個實例域,而後幫助咱們讀取並填充這些實例域。那麼fromJson怎麼知道咱們要填充LatestNews的哪些實例域呢?實際上咱們必須保證LatestNews的域的名字與JSON字符串中對應的鍵的名字相同,這樣gson就可以把咱們的model類與JSON字符串「一一對應「起來,也就是說咱們要保證咱們的model類與JSON字符串具備相同的層級結構,這樣gson就能夠根據名稱從JSON字符串中爲咱們的實例域尋找一個對應的值。咱們能夠作個小實驗:把LatestNews中TopStory的title實例域的名字改成title1,這時再只執行以上程序,會發現每一個story的title1域均變爲null了。ide
經過上面的介紹,咱們感覺到了JSON解析庫帶給咱們的便利,接下來咱們一塊兒來實現org.json包提供給咱們的基本JSON解析功能,而後再進一步嘗試實現gson提供給咱們的更方便快捷的JSON解析功能。
如今,假設咱們沒有任何現成的JSON解析庫可用,咱們要本身完成JSON的解析工做。JSON解析的工做主要分一下幾步:
{
"date" : 20160517, "id" : 1 }
通過詞法分析後,會被分解爲如下token:「{」、 」date「、 「:」、 「20160517」、 「,"、 「id」、 「:」、 「1」、 「}」。
實際上,在進行詞法分析以前,JSON數據對計算機來講只是一個沒有意義的字符串而已。詞法分析的目的是把這些無心義的字符串變成一個一個的token,而這些token有着本身的類型和值,因此計算機可以區分不一樣的token,還能以token爲單位解讀JSON數據。接下來,語法分析的目的就是進一步處理token,把token構形成一棵抽象語法樹(Abstract Syntax Tree)(這棵樹的結點是咱們上面所說的抽象語法對象)。好比上面的JSON數據咱們通過詞法分析後獲得了一系列token,而後咱們把這些token做爲語法分析的輸入,就能夠構造出一個JSONObject對象(即只有一個結點的抽象語法樹),這個JSONObject對象有date和id兩個實例域。下面咱們來分別介紹詞法分析與語法分析的原理和實現。
JSON字符串中,一共有幾種token呢?根據http://www.json.org/對JSON格式的相關定義,咱們能夠把token分爲如下類型:
咱們能夠定義一個枚舉類型來表示不一樣的token類型:
public enum TokenType {
START_OBJ, END_OBJ, START_ARRAY, END_ARRAY, NULL, NUMBER, STRING, BOOLEAN, COLON, COMMA, END_DOC
}
而後,咱們還須要定義一個Token類用於表示token:
public class Token { private TokenType type; private String value; public Token(TokenType type, String value) { this.type = type; this.value = value; } public TokenType getType() { return type; } public String getValue() { return value; } public String toString() { return getValue(); } }
在這以後,咱們就能夠開始寫詞法分析器了,詞法分析器一般被稱爲lexer或是tokenizer。咱們可使用DFA(肯定有限狀態自動機)來實現tokenizer,也能夠直接使用使用Java的regex包。這裏咱們使用DFA來實現tokenizer。
實現詞法分析器(tokenizer)和語法分析器(parser)的依據都是JSON文法,完整的JSON文法以下(來自https://www.zhihu.com/question/24640264/answer/80500016):
object = {} | { members } members = pair | pair , members pair = string : value array = [] | [ elements ] elements = value | value , elements value = string | number | object | array | true | false | null string = "" | " chars " chars = char | char chars char = any-Unicode-character-except-"-or-\-or- control-character | \" | \\ | \/ | \b | \f | \n | \r | \t | \u four-hex-digits number = int | int frac | int exp | int frac exp int = digit | digit1-9 digits | - digit | - digit1-9 digits frac = . digits exp = e digits digits = digit | digit digits e = e | e+ | e- | E | E+ | E-
如今,咱們就能夠根據JSON的文法來構造DFA了,核心代碼以下:
1 private Token start() throws Exception { 2 c = '?'; 3 Token token = null; 4 do { //先讀一個字符,若爲空白符(ASCII碼在[0, 20H]上)則接着讀,直到剛讀的字符非空白符 5 c = read(); 6 } while (isSpace(c)); 7 if (isNull(c)) { 8 return new Token(TokenType.NULL, null); 9 } else if (c == ',') { 10 return new Token(TokenType.COMMA, ","); 11 } else if (c == ':') { 12 return new Token(TokenType.COLON, ":"); 13 } else if (c == '{') { 14 return new Token(TokenType.START_OBJ, "{"); 15 } else if (c == '[') { 16 return new Token(TokenType.START_ARRAY, "["); 17 } else if (c == ']') { 18 return new Token(TokenType.END_ARRAY, "]"); 19 } else if (c == '}') { 20 return new Token(TokenType.END_OBJ, "}"); 21 } else if (isTrue(c)) { 22 return new Token(TokenType.BOOLEAN, "true"); //the value of TRUE is not null 23 } else if (isFalse(c)) { 24 return new Token(TokenType.BOOLEAN, "false"); //the value of FALSE is null 25 } else if (c == '"') { 26 return readString(); 27 } else if (isNum(c)) { 28 unread(); 29 return readNum(); 30 } else if (c == -1) { 31 return new Token(TokenType.END_DOC, "EOF"); 32 } else { 33 throw new JsonParseException("Invalid JSON input."); 34 } 35 }
咱們能夠看到,tokenizer的核心代碼十分簡潔,由於咱們把一些稍繁雜的處理邏輯都封裝在了一個個方法中,好比上面的readNum方法、readString方法等。
以上代碼的第4到第6行的功能是消耗掉開頭的全部空白字符(如space、tab等),直到讀取到一個非空白字符,isSpace方法用於判斷一個字符是否屬於空白字符。也就是說,DFA從起始狀態開始,若讀到一個空字符,會在起始狀態不斷循環,直到遇到非空字符,狀態轉移狀況以下:
接下來咱們能夠看到從代碼的第7行到第33行是一個if語句塊,外層的全部if分支覆蓋了DFA的全部可能狀態。在第7行咱們會判斷讀入的是否是「null」,isNull方法的代碼以下:
private boolean isNull(int c) throws IOException { if (c == 'n') { c = read(); if (c == 'u') { c = read(); if (c == 'l') { c = read(); if (c == 'l') { return true; } else { throw new JsonParseException("Invalid JSON input."); } } else { throw new JsonParseException("Invalid JSON input."); } } else { throw new JsonParseException("Invalid JSON input."); } } else { return false; } }
也就是說,當第一個非空字符爲'n'時,咱們會判斷下一個是否爲‘u',接着判斷下面的是否是'u'、’l',這中間任何一步的判斷結果爲否,就說明咱們遇到了一個非法關鍵字(好比null拼寫錯誤,拼成了noll,這就是非法關鍵字),就會拋出異常,只有咱們依次讀取的4個字符分別爲'n'、'u'、'l'、'l'時,isNull方法纔會返回true。下面出現的isTrue、isFalse分別用來判斷「true」和「false」,具體實現與isNull相似。
如今讓咱們回到以上的代碼,接着看從第9行到第20行,咱們會根據下一個字符的不一樣轉移到不一樣的狀態。若下一個字符爲’{'、 '}'、 '['、 ']'、 ':'、 ','等6種中的一個,則DFA運行中止,此時咱們構造一個新的相應類型的Token對象,並直接返回這個token,做爲DFA本次運行的結果。這幾個狀態轉移的示意圖以下:
上圖中圓圈中的數字僅僅表示狀態的標號,咱們僅畫出了下一個字符分別爲'{'、'['、':'時的狀態轉移(省略了3種狀況)。
接下來,讓咱們看第25行到第26行的代碼。這部分代碼的主要做用是讀取一個由雙引號包裹的字符串字面量並構造一個TokenType爲STRING的Token對象。若剛讀取到的字符爲雙引號,意味着接下來的是一個字符串字面量,因此咱們調用readString方法來讀入一個字符串變量。readString方法的代碼以下:
1 private Token readString() throws IOException { 2 StringBuilder sb = new StringBuilder(); 3 while (true) { 4 c = read(); 5 if (isEscape()) { //判斷是否爲\", \\, \/, \b, \f, \n, \t, \r. 6 if (c == 'u') { 7 sb.append('\\' + (char) c); 8 for (int i = 0; i < 4; i++) { 9 c = read(); 10 if (isHex(c)) { 11 sb.append((char) c); 12 } else { 13 throw new JsonParseException("Invalid Json input."); 14 } 15 } 16 } else { 17 sb.append("\\" + (char) c); 18 } 19 } else if (c == '"') { 20 return new Token(TokenType.STRING, sb.toString()); 21 } else if (c == '\r' || c == '\n'){ 22 throw new JsonParseException("Invalid JSON input."); 23 } else { 24 sb.append((char) c); 25 } 26 } 27 }
咱們來看一下readString方法的代碼。第3到26行是一個無限循環,退出循環的條件有兩個:一個是又讀取到一個雙引號(意味着字符串的結束),第二個條件是讀取到了非法字符('\r'或’、'\n')。第5行的功能是判斷剛讀取的字符是不是轉義字符的開始,isEscape方法的代碼以下:
private boolean isEscape() throws IOException { if (c == '\\') { c = read(); if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' || c == 't' || c == 'r' || c == 'u') { return true; } else { throw new JsonParseException("Invalid JSON input."); } } else { return false; } }
咱們能夠看到這個方法是用來判斷接下來的輸入流中是否爲如下字符組合:\", \\, \/, \b, \f, \n, \t, \r, \uhhhh(hhhh表示四位十六進制數)。如果以上幾種中的一個,咱們會接着判斷是否是「\uhhhh「,並對他進行特殊處理,如readString方法的第7到15行所示,實際上就是先把'\u'添加到StringBuilder對象中,在依次讀取它後面的4個字符,如果十六進制數字,則append,不然拋出異常。
如今讓咱們回到start方法,接着看第27到29行的代碼,這兩行代碼用於讀入一個數字字面量。isNum方法用於判斷輸入流中接下來的內容是不是數字字面量,這個方法的源碼以下:
private boolean isNum(int c) { return isDigit(c) || c == '-'; }
根據上面咱們貼出的JSON文法,只有下一個字符爲數字0~9或是'-',接下來的內容纔多是一個數字字面量,isDigit方法用於判斷下一個字符是不是0~9這10個數字中的一個。
咱們注意到第28行有一個unread方法調用,意味着咱們下回調用read方法仍是返回上回調用read方法返回的那個字符,爲何這麼作咱們看一下readNum方法的代碼就知道了:
1 private Token readNum() throws IOException { 2 StringBuilder sb = new StringBuilder(); 3 int c = read(); 4 if (c == '-') { //- 5 sb.append((char) c); 6 c = read(); 7 if (c == '0') { //-0 8 sb.append((char) c); 9 numAppend(sb); 10 11 } else if (isDigitOne2Nine(c)) { //-digit1-9 12 do { 13 sb.append((char) c); 14 c = read(); 15 } while (isDigit(c)); 16 unread(); 17 numAppend(sb); 18 } else { 19 throw new JsonParseException("- not followed by digit"); 20 } 21 } else if (c == '0') { //0 22 sb.append((char) c); 23 numAppend(sb); 24 } else if (isDigitOne2Nine(c)) { //digit1-9 25 do { 26 sb.append((char) c); 27 c = read(); 28 } while (isDigit(c)); 29 unread(); 30 numAppend(sb); 31 } 32 return new Token(TokenType.NUMBER, sb.toString()); //the value of 0 is null 33 }
咱們來看一下第4到31行,外層的if語句有三種狀況:分別對應着剛讀取的字符爲'-'、'0'和數字1~9中的一個。咱們來看一下第5到9行的代碼,對應了剛讀取到的字符爲'-'這種狀況。這種狀況表示這個數字字面量是個負數。而後咱們再看這種狀況下的內層if語句,共有兩種狀況,一是負號後面的字符爲0,另外一個是負號後面的字符爲數字1~9中的一個。前者表示本次讀取的數字字面量爲-0(後面能夠跟着frac或是exp),後者表示本次讀取的字面量爲負整數(後面也能夠跟着frac或exp)。而後咱們看第9行調用的numAppend方法,它的源碼以下:
private void numAppend(StringBuilder sb) throws IOException { c = read(); if (c == '.') { //int frac sb.append((char) c); //apppend '.' appendFrac(sb); if (isExp(c)) { //int frac exp sb.append((char) c); //append 'e' or 'E'; appendExp(sb); } } else if (isExp(c)) { // int exp sb.append((char) c); //append 'e' or 'E' appendExp(sb); } else { unread(); } }
咱們上面貼的JSON文法中對數字字面量的定義以下:
number = int | int frac | int exp | int frac exp
numAppend方法的功能就是在咱們讀取了數字字面量的int部分後,接着讀取後面可能還有的frac或exp部分,上面的appendFrac方法用於讀取frac部分,appendExp方法用於讀取exp部分。具體的邏輯比較直接,你們直接看代碼就能夠了。( 這部分的處理邏輯是否正確未通過嚴格測試,若有錯誤但願你們能夠指出,謝謝:) )
到了這裏,tokenizer的核心——start()方法咱們已經介紹的差很少了,tokenizer的完整代碼請參考文章開頭給出的連接,接下來讓咱們看一下如何實現JSON parser。
通過前一步的詞法分析,咱們已經獲得了一個token序列,如今讓咱們來用這個序列構造出相似於org.json包的JSONObject與JSONArray對象。如今咱們的任務就是編寫一個語法分析器(parser),以詞法分析獲得的token序列爲輸入,產生JSONObject或是JSONArray抽象語法對象。語法分析的依據一樣是上面咱們貼出的JSON文法。
語法分析器依據JSON文法的如下部分實現:
object = {} | { members }
members = pair | pair , members pair = string : value array = [] | [ elements ] elements = value | value , elements value = string | number | object | array | true | false | null
具體代碼以下:
1 public class Parser { 2 private Tokenizer tokenizer; 3 4 public Parser(Tokenizer tokenizer) { 5 this.tokenizer = tokenizer; 6 } 7 8 private JObject object() { 9 tokenizer.next(); //consume '{' 10 Map<String, Value> map = new HashMap<>(); 11 if (isToken(TokenType.END_OBJ)) { 12 tokenizer.next(); //consume '}' 13 return new JObject(map); 14 } else if (isToken(TokenType.STRING)) { 15 map = key(map); 16 } 17 return new JObject(map); 18 } 19 20 private Map<String, Value> key(Map<String, Value> map) { 21 String key = tokenizer.next().getValue(); 22 if (!isToken(TokenType.COLON)) { 23 throw new JsonParseException("Invalid JSON input."); 24 } else { 25 tokenizer.next(); //consume ':' 26 if (isPrimary()) { 27 Value primary = new Primary(tokenizer.next().getValue()); 28 map.put(key, primary); 29 } else if (isToken(TokenType.START_ARRAY)) { 30 Value array = array(); 31 map.put(key, array); 32 } 33 if (isToken(TokenType.COMMA)) { 34 tokenizer.next(); //consume ',' 35 if (isToken(TokenType.STRING)) { 36 map = key(map); 37 } 38 } else if (isToken(TokenType.END_OBJ)) { 39 tokenizer.next(); //consume '}' 40 return map; 41 } else { 42 throw new JsonParseException("Invalid JSON input."); 43 } 44 } 45 return map; 46 } 47 48 private JArray array() { 49 tokenizer.next(); //consume '[' 50 List<Json> list = new ArrayList<>(); 51 JArray array = null; 52 if (isToken(TokenType.START_ARRAY)) { 53 array = array(); 54 list.add(array); 55 if (isToken(TokenType.COMMA)) { 56 tokenizer.next(); //consume ',' 57 list = element(list); 58 } 59 } else if (isPrimary()) { 60 list = element(list); 61 } else if (isToken(TokenType.START_OBJ)) { 62 list.add(object()); 63 while (isToken(TokenType.COMMA)) { 64 tokenizer.next(); //consume ',' 65 list.add(object()); 66 } 67 } else if (isToken(TokenType.END_ARRAY)) { 68 tokenizer.next(); //consume ']' 69 array = new JArray(list); 70 return array; 71 } 72 tokenizer.next(); //consume ']' 73 array = new JArray(list); 74 return array; 75 } 76 77 private List<Json> element(List<Json> list) { 78 list.add(new Primary(tokenizer.next().getValue())); 79 if (isToken(TokenType.COMMA)) { 80 tokenizer.next(); //consume ',' 81 if (isPrimary()) { 82 list = element(list); 83 } else if (isToken(TokenType.START_OBJ)) { 84 list.add(object()); 85 } else if (isToken(TokenType.START_ARRAY)) { 86 list.add(array()); 87 } else { 88 throw new JsonParseException("Invalid JSON input."); 89 } 90 } else if (isToken(TokenType.END_ARRAY)) { 91 return list; 92 } else { 93 throw new JsonParseException("Invalid JSON input."); 94 } 95 return list; 96 } 97 98 private Json json() { 99 TokenType type = tokenizer.peek(0).getType(); 100 if (type == TokenType.START_ARRAY) { 101 return array(); 102 } else if (type == TokenType.START_OBJ) { 103 return object(); 104 } else { 105 throw new JsonParseException("Invalid JSON input."); 106 } 107 } 108 109 private boolean isToken(TokenType tokenType) { 110 Token t = tokenizer.peek(0); 111 return t.getType() == tokenType; 112 } 113 114 private boolean isToken(String name) { 115 Token t = tokenizer.peek(0); 116 return t.getValue().equals(name); 117 } 118 119 private boolean isPrimary() { 120 TokenType type = tokenizer.peek(0).getType(); 121 return type == TokenType.BOOLEAN || type == TokenType.NULL || 122 type == TokenType.NUMBER || type == TokenType.STRING; 123 } 124 125 public Json parse() throws Exception { 126 Json result = json(); 127 return result; 128 } 129 130 }
咱們先來看以上代碼的第98到107行的json方法,這個方法能夠做爲語法分析的起點。它會根據第一個Token的類型是START_OBJ或START_ARRAY而選擇調用object方法或是array方法。object方法會返回一個JObject對象(JSONObject),array方法會返回一個JArray對象(JSONArray)。JArray與JObject的定義以下:
public class JArray extends Json implements Value { private List<Json> list = new ArrayList<>(); public JArray(List<Json> list) { this.list = list; } public int length() { return list.size(); } public void add(Json element) { list.add(element); } public Json get(int i) { return list.get(i); } @Override public Object value() { return this; } public String toString() { . . . } } public class JObject extends Json { private Map<String, Value> map = new HashMap<>(); public JObject(Map<String, Value> map) { this.map = map; } public int getInt(String key) { return Integer.parseInt((String) map.get(key).value()); } public String getString(String key) { return (String) map.get(key).value(); } public boolean getBoolean(String key) { return Boolean.parseBoolean((String) map.get(key).value()); } public JArray getJArray(String key) { return (JArray) map.get(key).value(); } public String toString() { . . . } }
JSON parser的邏輯也沒有太複雜的地方,若是哪位同窗不太理解,能夠寫一個test case跟着走幾遍。
接下來,咱們要進入有意思的部分了——實現相似org.json包的根據JSON字符串直接構造JSONObject與JSONArray。
基於以上的tokenizer與parser,咱們能夠實現兩個實用的JSON解析方法,有了這兩個方法,能夠說咱們就完成了一個基本的JSON解析庫。
該方法以一個JSON字符串爲輸入,返回一個JObject,代碼以下:
public static JObject parseJSONObject(String jsonString) throws Exception { Tokenizer tokenizer = new Tokenizer(new BufferedReader(new StringReader(jsonString))); tokenizer.tokenize(); Parser parser = new Parser(tokenizer); return parser.object(); }
該方法以一個JSON字符串爲輸入,返回一個JArray,代碼以下:
public static JObject parseJSONArray(String jsonString) throws Exception { Tokenizer tokenizer = new Tokenizer(new BufferedReader(new StringReader(jsonString))); tokenizer.tokenize(); Parser parser = new Parser(tokenizer); return parser.array(); }
接下來,咱們來測試如下這兩個放究竟能不能用,test case以下:
public static void main(String[] args) throws Exception { try { String jsonString = new String(HttpUtil.get(urlString)); JObject latestNewsJSON = parseJSONObject(jsonString); String date = latestNewsJSON.getString("date"); JArray top_storiesJSON = latestNewsJSON.getJArray("top_stories"); LatestNews latest = new LatestNews(); List<LatestNews.TopStory> stories = new ArrayList<>(); for (int i = 0; i < top_storiesJSON.length(); i++) { LatestNews.TopStory story = new LatestNews.TopStory(); story.setId(((JObject) top_storiesJSON.get(i)).getInt("id")); story.setType(((JObject) top_storiesJSON.get(i)).getInt("type")); story.setImage(((JObject) top_storiesJSON.get(i)).getString("image")); story.setTitle(((JObject) top_storiesJSON.get(i)).getString("title")); stories.add(story); } latest.setDate(date); System.out.println("date: " + latest.getDate()); for (int i = 0; i < stories.size(); i++) { System.out.println(stories.get(i)); } } catch (JSONException e) { e.printStackTrace(); } }
實際上,上面的代碼只是把咱們使用org.json包的代碼稍做修改。而後咱們能夠獲得了同使用org.json包同樣的輸出,這說明咱們的JSON解析器工做正常。以上代碼中的getInt方法與getString方法定義在JObject中,只須要根據要取得的值的類型作類型轉換便可,具體實現能夠參考開頭給出的項目地址。接下來,讓咱們更上一層樓,實現一個相似與gson中fromJson方法的便捷方法。
這個方法的核心思想是:根據給定的JSON字符串和model類的class對象,經過反射獲取model類的各個實例域的類型及名稱。而後用java.lang.reflect包提供給咱們的方法在運行時建立一個model類的對象,而後根據它的實例域的名稱從JObject中獲取相應的值併爲model類對象的對應實例域賦值。若實例域爲List<T>,咱們須要特殊進行處理,這裏咱們實現了一個inflateList方法來處理這種狀況。fromJson方法的代碼以下:
1 public static <T> T fromJson(String jsonString, Class<T> classOfT) throws Exception { 2 Tokenizer tokenizer = new Tokenizer(new BufferedReader(new StringReader(jsonString))); 3 tokenizer.tokenize(); 4 Parser parser = new Parser(tokenizer); 5 JObject result = parser.object(); 6 7 Constructor<T> constructor = classOfT.getConstructor(); 8 Object latestNews = constructor.newInstance(); 9 Field[] fields = classOfT.getDeclaredFields(); 10 int numField = fields.length; 11 String[] fieldNames = new String[numField]; 12 String[] fieldTypes = new String[numField]; 13 for (int i = 0; i < numField; i++) { 14 String type = fields[i].getType().getTypeName(); 15 String name = fields[i].getName(); 16 fieldTypes[i] = type; 17 fieldNames[i] = name; 18 } 19 for (int i = 0; i < numField; i++) { 20 if (fieldTypes[i].equals("java.lang.String")) { 21 fields[i].setAccessible(true); 22 fields[i].set(latestNews, result.getString(fieldNames[i])); 23 } else if (fieldTypes[i].equals("java.util.List")) { 24 fields[i].setAccessible(true); 25 JArray array = result.getJArray(fieldNames[i]); 26 ParameterizedType pt = (ParameterizedType) fields[i].getGenericType(); 27 Type elementType = pt.getActualTypeArguments()[0]; 28 String elementTypeName = elementType.getTypeName(); 29 Class<?> elementClass = Class.forName(elementTypeName); 30 fields[i].set(latestNews, inflateList(array, elementClass));//類型捕獲 31 32 } else if (fieldTypes[i].equals("int")) { 33 fields[i].setAccessible(true); 34 fields[i].set(latestNews, result.getInt(fieldNames[i])); 35 } 36 } 37 return (T) latestNews; 38 }
在第8行,咱們構造了一個LatestNews對象。在第9到18行,咱們獲取了LatestNews類的全部實例域,並把它們的名稱存在了String數組fieldNames中,把它們的類型存在了String數組fieldTypes中。而後在第19到36行,咱們遍歷Field數組fields,對每一個實例域進行賦值。若實例域的類型爲int或是String或是primitive types(int、double等基本類型),則直接調用set方法對相應實例域賦值(簡單起見,上面只實現了對String類型實例域的處理,對於primitive types的處理與之相似,感興趣的同窗能夠本身嘗試實現下);若實例域的類型爲List,則咱們須要爲這個List中的每一個元素賦值。在第26到29行,咱們獲取了List中存儲的元素的類型名稱,而後根據這個名稱獲取了對應的class對象。在第30行,咱們調用了inflateList方法來「填充「這個List,這裏存在一個」類型捕獲「,具體來講,就是inflateList方法接收的第2個參數Class<T>中的類型參數T捕獲了List中存儲元素的實際類型(第29行咱們獲取了這個實際類型並用類型通配符接收了它)。inflateList方法的代碼以下:
1 public static <T> List<T> inflateList(JArray array, Class<T> clz) throws Exception { 2 int size = array.length(); 3 4 List<T> list = new ArrayList<T>(); 5 Constructor<T> constructor = clz.getConstructor(); 6 String className = clz.getName(); 7 if (className.equals("java.lang.String")) { 8 for (int i = 0; i < size; i++) { 9 String element = (String) ((Primary) array.get(i)).value(); 10 list.add((T) element); 11 return list; 12 } 13 } 14 Field[] fields = clz.getDeclaredFields(); 15 int numField = fields.length; 16 String[] fieldNames = new String[numField]; 17 String[] fieldTypes = new String[numField]; 18 19 for (int i = 0; i < numField; i++) { 20 String type = fields[i].getType().getTypeName(); 21 String name = fields[i].getName(); 22 fieldTypes[i] = type; 23 fieldNames[i] = name; 24 } 25 for (int i = 0; i < size; i++) { 26 T element = constructor.newInstance(); 27 JObject object = (JObject) array.get(i); 28 for (int j = 0; j < numField; j++) { 29 if (fieldTypes[j].equals("java.lang.String")) { 30 fields[j].setAccessible(true); 31 fields[j].set(element, (object.getString(fieldNames[j]))); 32 } else if (fieldTypes[j].equals("java.util.List")) { 33 fields[j].setAccessible(true); 34 JArray nestArray = object.getJArray(fieldNames[j]); 35 ParameterizedType pt = (ParameterizedType) fields[j].getGenericType(); 36 Type elementType = pt.getActualTypeArguments()[0]; 37 String elementTypeName = elementType.getTypeName(); 38 Class<?> elementClass = Class.forName(elementTypeName); 39 String value = null; 40 41 fields[j].set(element, inflateList(nestArray, elementClass));//Type Capture 42 } else if (fieldTypes[j].equals("int")) { 43 fields[j].setAccessible(true); 44 fields[j].set(element, object.getInt(fieldNames[j])); 45 } 46 47 } 48 list.add(element); 49 } 50 return list; 51 }
在這個方法中,咱們會根據對JSON解析獲取的JArray所含的元素個數,以及咱們以前獲取到的元素的類型,構造相應數目的對象,並添加到list中去。具體的執行過程你們能夠參考代碼,邏輯比較直接。
須要注意的是以上代碼的第7到13行,它的意思是若列表的元素類型爲String,咱們就應直接從相應的JArray中獲取元素並添加到list中,而後直接返回list。實際上,對於primitive types咱們都應該作類似處理,簡單起見,這裏只對String類型作了處理,其餘primitive types的處理方式相似。
接下來測試一下咱們實現的fromJson方法是否能如咱們預期那樣工做,test case仍是解析上面的知乎日報API返回的數據:
public class SimpleJSONTest { public static final String urlString = "http://news-at.zhihu.com/api/4/news/latest"; public static void main(String[] args) throws Exception { LatestNews latest = new LatestNews(); String jsonString = new String(HttpUtil.get(urlString)); latest = Parser.fromJson(jsonString, LatestNews.class); System.out.println(latest.getDate()); for (int i = 0; i < latest.getTop_stories().size(); i++) { System.out.println(latest.getTop_stories().get(i)); } } }
咱們還能夠對比一下咱們的實現與gson的實現的性能,我這裏測試的結果是SimpleJSON的速度大約是gson速度的三倍,考慮到咱們的SimpleJSON在很多地方」偷懶「了,這個測試結果並不能說明咱們的實現性能要優於gson,不過這或許能夠說明咱們的JSON解析庫仍是具有必定的實用性...
因爲本篇博文重點在介紹一個JSON解析器的實現思路,在具體實現上不少部分作的並很差。好比沒有作足夠多的測試來驗證JSON解析的正確性,業務邏輯上也儘可能使用直接的方式,許多地方沒使用更加高效的實現,另外在拋出異常方面也比較隨便,「一言不合」就拋異常...因爲我的水平有限,代碼中不免存在謬誤,但願你們多多包涵,更但願能夠指出不足之處,謝謝你們:)
2. https://www.zhihu.com/question/24640264/answer/80500016
3. http://docs.oracle.com/javase/specs/jls/se8/jls8.pdf
4. 《Java核心技術(卷一)》