2525import java .io .UncheckedIOException ;
2626import java .lang .reflect .Type ;
2727import java .time .Instant ;
28- import java .util .ArrayDeque ;
2928import java .util .ArrayList ;
3029import java .util .Arrays ;
31- import java .util .Deque ;
3230import java .util .List ;
3331import java .util .Map ;
3432import java .util .function .BiFunction ;
@@ -47,13 +45,13 @@ public class JsonInput implements Closeable {
4745 private JsonTypeCoercer coercer ;
4846 private PropertySetting setter ;
4947 private final Input input ;
50- // Used when reading maps and collections so that we handle de-nesting and
51- // figuring out whether we're expecting a NAME properly.
52- private final Deque <Container > stack = new ArrayDeque <>();
48+ // Stack of open containers, used to handle de-nesting and to figure out
49+ // whether we're expecting a NAME. Kept as plain arrays (rather than a deque
50+ // of objects) because the top of the stack is touched for every value read.
51+ private byte [] containerState = new byte [16 ];
5352 // Parallel stack tracking whether the current container has seen at least
5453 // one element. Used by hasNext() to enforce comma separators between
55- // elements while remaining lenient about a single trailing comma. Kept as
56- // a plain array because it is touched for every element read.
54+ // elements while remaining lenient about a single trailing comma.
5755 private boolean [] containerHasElement = new boolean [16 ];
5856 private int containerDepth ;
5957 // Memoized type of the pending token; cleared whenever the token is consumed.
@@ -314,9 +312,7 @@ public Number nextNumber() {
314312 }
315313 return Long .valueOf (builder .toString ());
316314 }
317- // JSON's number grammar is a subset of what parseDouble accepts, and parseDouble is
318- // considerably cheaper than going through BigDecimal.
319- double value = Double .parseDouble (builder .toString ());
315+ double value = parseDouble (builder );
320316 if (Double .isInfinite (value ) || Double .isNaN (value )) {
321317 throw new JsonException ("Number is out of range for a double: " + builder + ". " + input );
322318 }
@@ -330,6 +326,96 @@ private static boolean isDigit(int c) {
330326 return c >= '0' && c <= '9' ;
331327 }
332328
329+ /** Powers of ten that are exactly representable as doubles. */
330+ private static final double [] POW_10 = {
331+ 1e0 , 1e1 , 1e2 , 1e3 , 1e4 , 1e5 , 1e6 , 1e7 , 1e8 , 1e9 , 1e10 , 1e11 , 1e12 , 1e13 , 1e14 , 1e15 , 1e16 ,
332+ 1e17 , 1e18 , 1e19 , 1e20 , 1e21 , 1e22
333+ };
334+
335+ /**
336+ * Parse a JSON number that contains a fraction or exponent, as lexed into {@code raw} by {@link
337+ * #nextNumber}.
338+ *
339+ * <p>Uses Clinger's fast path where possible: when the significand has at most 15 digits it is
340+ * exactly representable as a double, as are powers of ten up to 10^22, so a single floating-point
341+ * multiply or divide performs the one correctly-rounded step the conversion needs. Everything
342+ * else (long significands, large exponents) falls back to {@link Double#parseDouble}, so results
343+ * are always bit-for-bit identical to the JDK.
344+ */
345+ private static double parseDouble (StringBuilder raw ) {
346+ int length = raw .length ();
347+ int index = 0 ;
348+ boolean negative = false ;
349+ if (raw .charAt (0 ) == '-' ) {
350+ negative = true ;
351+ index = 1 ;
352+ }
353+
354+ long significand = 0 ;
355+ int digits = 0 ;
356+ int fractionDigits = 0 ;
357+
358+ while (index < length ) {
359+ char c = raw .charAt (index );
360+ if (c < '0' || c > '9' ) {
361+ break ;
362+ }
363+ significand = significand * 10 + (c - '0' );
364+ digits ++;
365+ index ++;
366+ }
367+
368+ if (index < length && raw .charAt (index ) == '.' ) {
369+ index ++;
370+ while (index < length ) {
371+ char c = raw .charAt (index );
372+ if (c < '0' || c > '9' ) {
373+ break ;
374+ }
375+ significand = significand * 10 + (c - '0' );
376+ digits ++;
377+ fractionDigits ++;
378+ index ++;
379+ }
380+ }
381+
382+ int exponent = 0 ;
383+ if (index < length ) {
384+ // By construction the remainder is ('e' | 'E') ('+' | '-')? 1*DIGIT.
385+ index ++;
386+ boolean exponentNegative = false ;
387+ char sign = raw .charAt (index );
388+ if (sign == '+' || sign == '-' ) {
389+ exponentNegative = sign == '-' ;
390+ index ++;
391+ }
392+ while (index < length ) {
393+ // Clamp rather than overflow; anything this large falls back below anyway.
394+ if (exponent < 100_000 ) {
395+ exponent = exponent * 10 + (raw .charAt (index ) - '0' );
396+ }
397+ index ++;
398+ }
399+ if (exponentNegative ) {
400+ exponent = -exponent ;
401+ }
402+ }
403+
404+ int netExponent = exponent - fractionDigits ;
405+
406+ if (digits <= 15 && netExponent >= -22 && netExponent <= 22 ) {
407+ double value = (double ) significand ;
408+ if (netExponent > 0 ) {
409+ value = value * POW_10 [netExponent ];
410+ } else if (netExponent < 0 ) {
411+ value = value / POW_10 [-netExponent ];
412+ }
413+ return negative ? -value : value ;
414+ }
415+
416+ return Double .parseDouble (raw .toString ());
417+ }
418+
333419 private static String describeChar (int c ) {
334420 return c == Input .EOF ? "<EOF>" : "'" + (char ) c + "'" ;
335421 }
@@ -377,7 +463,7 @@ public void nextEnd() {
377463 * @throws UncheckedIOException if an I/O exception is encountered
378464 */
379465 public boolean hasNext () {
380- if (stack . isEmpty () ) {
466+ if (containerDepth == 0 ) {
381467 throw new JsonException (
382468 "Unable to determine if an item has next when not in a container type. " + input );
383469 }
@@ -417,8 +503,7 @@ public boolean hasNext() {
417503 */
418504 public void beginArray () {
419505 expect (JsonType .START_COLLECTION );
420- stack .addFirst (Container .COLLECTION );
421- pushContainer ();
506+ pushContainer (COLLECTION );
422507 input .read ();
423508 }
424509
@@ -429,12 +514,11 @@ public void beginArray() {
429514 */
430515 public void endArray () {
431516 expect (JsonType .END_COLLECTION );
432- if (stack . peekFirst () != Container . COLLECTION ) {
517+ if (topContainer () != COLLECTION ) {
433518 // The only other thing we could be closing is a map
434519 throw new JsonException (
435520 "Attempt to close a JSON List, but a JSON Object was expected. " + input );
436521 }
437- stack .removeFirst ();
438522 containerDepth --;
439523 input .read ();
440524 }
@@ -446,8 +530,7 @@ public void endArray() {
446530 */
447531 public void beginObject () {
448532 expect (JsonType .START_MAP );
449- stack .addFirst (Container .MAP_NAME );
450- pushContainer ();
533+ pushContainer (MAP_NAME );
451534 input .read ();
452535 }
453536
@@ -458,10 +541,9 @@ public void beginObject() {
458541 */
459542 public void endObject () {
460543 expect (JsonType .END_MAP );
461- if (stack . peekFirst () != Container . MAP_NAME ) {
544+ if (topContainer () != MAP_NAME ) {
462545 throw new JsonException ("Attempt to close a JSON Map, but not ready to. " + input );
463546 }
464- stack .removeFirst ();
465547 containerDepth --;
466548 input .read ();
467549 }
@@ -587,7 +669,7 @@ public <T> List<T> readArray(Type type) {
587669 * @return {@code true} is awaiting a property name; otherwise {@code false}
588670 */
589671 private boolean isReadingName () {
590- return stack . peekFirst () == Container . MAP_NAME ;
672+ return topContainer () == MAP_NAME ;
591673 }
592674
593675 /**
@@ -607,14 +689,13 @@ private void expect(JsonType type) {
607689 peekedType = null ;
608690
609691 // Special map handling. Woo!
610- Container top = stack . peekFirst ();
692+ byte top = topContainer ();
611693
612694 if (type == JsonType .NAME ) {
613- if (top == Container .MAP_NAME ) {
614- stack .removeFirst ();
615- stack .addFirst (Container .MAP_VALUE );
695+ if (top == MAP_NAME ) {
696+ containerState [containerDepth - 1 ] = MAP_VALUE ;
616697 return ;
617- } else if (top != null ) {
698+ } else if (top != NONE ) {
618699 throw new JsonException ("Unexpected attempt to read name. " + input );
619700 }
620701
@@ -626,20 +707,26 @@ private void expect(JsonType type) {
626707 // Closing the container - don't treat as a new element in it.
627708 return ;
628709 }
629- if (top == Container .MAP_VALUE ) {
630- stack .removeFirst ();
631- stack .addFirst (Container .MAP_NAME );
710+ if (top == MAP_VALUE ) {
711+ containerState [containerDepth - 1 ] = MAP_NAME ;
632712 markElementRead ();
633- } else if (top == Container . COLLECTION ) {
713+ } else if (top == COLLECTION ) {
634714 markElementRead ();
635715 }
636716 }
637717
638- private void pushContainer () {
639- if (containerDepth == containerHasElement .length ) {
718+ private byte topContainer () {
719+ return containerDepth == 0 ? NONE : containerState [containerDepth - 1 ];
720+ }
721+
722+ private void pushContainer (byte state ) {
723+ if (containerDepth == containerState .length ) {
724+ containerState = Arrays .copyOf (containerState , containerDepth * 2 );
640725 containerHasElement = Arrays .copyOf (containerHasElement , containerDepth * 2 );
641726 }
642- containerHasElement [containerDepth ++] = false ;
727+ containerState [containerDepth ] = state ;
728+ containerHasElement [containerDepth ] = false ;
729+ containerDepth ++;
643730 }
644731
645732 private void markElementRead () {
@@ -788,14 +875,15 @@ private void skipWhitespace(Input input) {
788875 input .skipWhitespace ();
789876 }
790877
791- /** Used to track the current container processing state . */
792- private enum Container {
878+ /** Container processing states: not in a container . */
879+ private static final byte NONE = 0 ;
793880
794- /** Processing a JSON array */
795- COLLECTION ,
796- /** Processing a JSON object property name */
797- MAP_NAME ,
798- /** Processing a JSON object property value */
799- MAP_VALUE ,
800- }
881+ /** Container processing states: processing a JSON array. */
882+ private static final byte COLLECTION = 1 ;
883+
884+ /** Container processing states: processing a JSON object property name. */
885+ private static final byte MAP_NAME = 2 ;
886+
887+ /** Container processing states: processing a JSON object property value. */
888+ private static final byte MAP_VALUE = 3 ;
801889}
0 commit comments