diff --git a/gson/src/main/java/com/google/gson/JsonObject.java b/gson/src/main/java/com/google/gson/JsonObject.java index b6284627..7333400d 100644 --- a/gson/src/main/java/com/google/gson/JsonObject.java +++ b/gson/src/main/java/com/google/gson/JsonObject.java @@ -17,7 +17,7 @@ package com.google.gson; import com.google.gson.internal.$Gson$Preconditions; -import java.util.LinkedHashMap; +import com.google.gson.internal.StringMap; import java.util.Map; import java.util.Set; @@ -34,7 +34,7 @@ public final class JsonObject extends JsonElement { // the order in which elements are inserted. This is needed to ensure // that the fields of an object are inserted in the order they were // defined in the class. - private final Map members = new LinkedHashMap(); + private final StringMap members = new StringMap(); /** * Creates an empty JsonObject. diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java index 9c6ca2d5..ca53946b 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -144,6 +144,7 @@ public final class ConstructorConstructor { if (Map.class.isAssignableFrom(rawType)) { return new ObjectConstructor() { public T construct() { + // TODO: if the map's key type is a string, should this be StringMap? return (T) new LinkedHashMap(); } }; diff --git a/gson/src/main/java/com/google/gson/internal/StringMap.java b/gson/src/main/java/com/google/gson/internal/StringMap.java new file mode 100644 index 00000000..55ff016d --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/StringMap.java @@ -0,0 +1,491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson.internal; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * A map of strings to values. Like LinkedHashMap, this map's iteration order is + * well defined: it is the order that elements were inserted into the map. + * + *

This implementation was derived from Android 4.0's LinkedHashMap. + */ +public final class StringMap extends AbstractMap { + // TODO: defend against predictable hash collisions + + /** + * Min capacity (other than zero) for a HashMap. Must be a power of two + * greater than 1 (and less than 1 << 30). + */ + private static final int MINIMUM_CAPACITY = 4; + + /** + * Max capacity for a HashMap. Must be a power of two >= MINIMUM_CAPACITY. + */ + private static final int MAXIMUM_CAPACITY = 1 << 30; + + /** + * A dummy entry in the circular linked list of entries in the map. + * The first real entry is header.nxt, and the last is header.prv. + * If the map is empty, header.nxt == header && header.prv == header. + */ + private LinkedEntry header; + + /** + * An empty table shared by all zero-capacity maps (typically from default + * constructor). It is never written to, and replaced on first put. Its size + * is set to half the minimum, so that the first resize will create a + * minimum-sized table. + */ + private static final Entry[] EMPTY_TABLE = new LinkedEntry[MINIMUM_CAPACITY >>> 1]; + + /** + * The hash table. If this hash map contains a mapping for null, it is + * not represented this hash table. + */ + private LinkedEntry[] table; + + /** + * The number of mappings in this hash map. + */ + private int size; + + /** + * The table is rehashed when its size exceeds this threshold. + * The value of this field is generally .75 * capacity, except when + * the capacity is zero, as described in the EMPTY_TABLE declaration + * above. + */ + private int threshold; + + // Views - lazily initialized + private Set keySet; + private Set> entrySet; + private Collection values; + + @SuppressWarnings("unchecked") + public StringMap() { + table = (LinkedEntry[]) EMPTY_TABLE; + threshold = -1; // Forces first put invocation to replace EMPTY_TABLE + header = new LinkedEntry(); + } + + @Override public int size() { + return size; + } + + @Override public boolean containsKey(Object key) { + return get(key) != null; + } + + @Override public V get(Object key) { + if (key == null) { + return null; + } + + // Doug Lea's supplemental secondaryHash function (inlined) + int hash = key.hashCode(); + hash ^= (hash >>> 20) ^ (hash >>> 12); + hash ^= (hash >>> 7) ^ (hash >>> 4); + + LinkedEntry[] tab = table; + for (LinkedEntry e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { + K eKey = e.key; + if (eKey == key || (e.hash == hash && key.equals(eKey))) { + return e.value; + } + } + return null; + } + + @Override public V put(K key, V value) { + if (key == null || value == null) { + throw new IllegalArgumentException(); + } + + int hash = secondaryHash(key.hashCode()); + LinkedEntry[] tab = table; + int index = hash & (tab.length - 1); + for (LinkedEntry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && key.equals(e.key)) { + V oldValue = e.value; + e.value = value; + return oldValue; + } + } + + // No entry for (non-null) key is present; create one + if (size++ > threshold) { + tab = doubleCapacity(); + index = hash & (tab.length - 1); + } + addNewEntry(key, value, hash, index); + return null; + } + + private void addNewEntry(K key, V value, int hash, int index) { + LinkedEntry header = this.header; + + // Create new entry, link it on to list, and put it into table + LinkedEntry oldTail = header.prv; + LinkedEntry newTail = new LinkedEntry( + key, value, hash, table[index], header, oldTail); + table[index] = oldTail.nxt = header.prv = newTail; + } + + /** + * Allocate a table of the given capacity and set the threshold accordingly. + * @param newCapacity must be a power of two + */ + private LinkedEntry[] makeTable(int newCapacity) { + @SuppressWarnings("unchecked") + LinkedEntry[] newTable = (LinkedEntry[]) new LinkedEntry[newCapacity]; + table = newTable; + threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity + return newTable; + } + + /** + * Doubles the capacity of the hash table. Existing entries are placed in + * the correct bucket on the enlarged table. If the current capacity is, + * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which + * will be new unless we were already at MAXIMUM_CAPACITY. + */ + private LinkedEntry[] doubleCapacity() { + LinkedEntry[] oldTable = table; + int oldCapacity = oldTable.length; + if (oldCapacity == MAXIMUM_CAPACITY) { + return oldTable; + } + int newCapacity = oldCapacity * 2; + LinkedEntry[] newTable = makeTable(newCapacity); + if (size == 0) { + return newTable; + } + + for (int j = 0; j < oldCapacity; j++) { + /* + * Rehash the bucket using the minimum number of field writes. + * This is the most subtle and delicate code in the class. + */ + LinkedEntry e = oldTable[j]; + if (e == null) { + continue; + } + int highBit = e.hash & oldCapacity; + LinkedEntry broken = null; + newTable[j | highBit] = e; + for (LinkedEntry n = e.next; n != null; e = n, n = n.next) { + int nextHighBit = n.hash & oldCapacity; + if (nextHighBit != highBit) { + if (broken == null) { + newTable[j | nextHighBit] = n; + } else { + broken.next = n; + } + broken = e; + highBit = nextHighBit; + } + } + if (broken != null) { + broken.next = null; + } + } + return newTable; + } + + @Override public V remove(Object key) { + if (key == null) { + return null; + } + int hash = secondaryHash(key.hashCode()); + LinkedEntry[] tab = table; + int index = hash & (tab.length - 1); + for (LinkedEntry e = tab[index], prev = null; + e != null; prev = e, e = e.next) { + if (e.hash == hash && key.equals(e.key)) { + if (prev == null) { + tab[index] = e.next; + } else { + prev.next = e.next; + } + size--; + unlink(e); + return e.value; + } + } + return null; + } + + private void unlink(LinkedEntry e) { + e.prv.nxt = e.nxt; + e.nxt.prv = e.prv; + e.nxt = e.prv = null; // Help the GC (for performance) + } + + @Override public void clear() { + if (size != 0) { + Arrays.fill(table, null); + size = 0; + } + + // Clear all links to help GC + LinkedEntry header = this.header; + for (LinkedEntry e = header.nxt; e != header; ) { + LinkedEntry nxt = e.nxt; + e.nxt = e.prv = null; + e = nxt; + } + + header.nxt = header.prv = header; + } + + @Override public Set keySet() { + Set ks = keySet; + return (ks != null) ? ks : (keySet = new KeySet()); + } + + @Override public Collection values() { + Collection vs = values; + return (vs != null) ? vs : (values = new Values()); + } + + public Set> entrySet() { + Set> es = entrySet; + return (es != null) ? es : (entrySet = new EntrySet()); + } + + static class LinkedEntry implements Entry { + final K key; + V value; + final int hash; + LinkedEntry next; + LinkedEntry nxt; + LinkedEntry prv; + + /** Create the header entry */ + LinkedEntry() { + this(null, null, 0, null, null, null); + nxt = prv = this; + } + + LinkedEntry(K key, V value, int hash, LinkedEntry next, + LinkedEntry nxt, LinkedEntry prv) { + this.key = key; + this.value = value; + this.hash = hash; + this.next = next; + this.nxt = nxt; + this.prv = prv; + } + + public final K getKey() { + return key; + } + + public final V getValue() { + return value; + } + + public final V setValue(V value) { + if (value == null) { + throw new IllegalArgumentException(); + } + V oldValue = this.value; + this.value = value; + return oldValue; + } + + @Override public final boolean equals(Object o) { + if (!(o instanceof Entry)) { + return false; + } + Entry e = (Entry) o; + return key.equals(e.getKey()) && value.equals(e.getValue()); + } + + @Override public final int hashCode() { + return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); + } + + @Override public final String toString() { + return key + "=" + value; + } + } + + /** + * Removes the mapping from key to value and returns true if this mapping + * exists; otherwise, returns does nothing and returns false. + */ + private boolean removeMapping(Object key, Object value) { + if (key == null || value == null) { + return false; + } + + int hash = secondaryHash(key.hashCode()); + LinkedEntry[] tab = table; + int index = hash & (tab.length - 1); + for (LinkedEntry e = tab[index], prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && key.equals(e.key)) { + if (!value.equals(e.value)) { + return false; // Map has wrong value for key + } + if (prev == null) { + tab[index] = e.next; + } else { + prev.next = e.next; + } + size--; + unlink(e); + return true; + } + } + return false; // No entry for key + } + + private abstract class LinkedHashIterator implements Iterator { + LinkedEntry next = header.nxt; + LinkedEntry lastReturned = null; + + public final boolean hasNext() { + return next != header; + } + + final LinkedEntry nextEntry() { + LinkedEntry e = next; + if (e == header) { + throw new NoSuchElementException(); + } + next = e.nxt; + return lastReturned = e; + } + + public final void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + StringMap.this.remove(lastReturned.key); + lastReturned = null; + } + } + + private final class KeySet extends AbstractSet { + public Iterator iterator() { + return new LinkedHashIterator() { + public final K next() { + return nextEntry().key; + } + }; + } + + public int size() { + return size; + } + + public boolean contains(Object o) { + return containsKey(o); + } + + public boolean remove(Object o) { + int oldSize = size; + StringMap.this.remove(o); + return size != oldSize; + } + + public void clear() { + StringMap.this.clear(); + } + } + + private final class Values extends AbstractCollection { + public Iterator iterator() { + return new LinkedHashIterator() { + public final V next() { + return nextEntry().value; + } + }; + } + + public int size() { + return size; + } + + public boolean contains(Object o) { + return containsValue(o); + } + + public void clear() { + StringMap.this.clear(); + } + } + + private final class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return new LinkedHashIterator>() { + public final Map.Entry next() { + return nextEntry(); + } + }; + } + + public boolean contains(Object o) { + if (!(o instanceof Entry)) { + return false; + } + Entry e = (Entry) o; + V mappedValue = get(e.getKey()); + return mappedValue != null && mappedValue.equals(e.getValue()); + } + + public boolean remove(Object o) { + if (!(o instanceof Entry)) { + return false; + } + Entry e = (Entry) o; + return removeMapping(e.getKey(), e.getValue()); + } + + public int size() { + return size; + } + + public void clear() { + StringMap.this.clear(); + } + } + + /** + * Applies a supplemental hash function to a given hashCode, which defends + * against poor quality hash functions. This is critical because HashMap + * uses power-of-two length hash tables, that otherwise encounter collisions + * for hashCodes that do not differ in lower or upper bits. + */ + private static int secondaryHash(int h) { + // Doug Lea's supplemental hash function + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); + } +} \ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java index 6d36778c..0f60c922 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java @@ -19,13 +19,13 @@ package com.google.gson.internal.bind; import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.StringMap; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -63,7 +63,7 @@ public final class ObjectTypeAdapter extends TypeAdapter { return list; case BEGIN_OBJECT: - Map map = new LinkedHashMap(); + Map map = new StringMap(); in.beginObject(); while (in.hasNext()) { map.put(in.nextName(), read(in)); diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index aef278bd..06e5688a 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -401,9 +401,6 @@ public class JsonReader implements Closeable { * Consumes the non-execute prefix if it exists. */ private void consumeNonExecutePrefix() throws IOException { - // TODO: there's a bug here. We're going to call nextNonWhitespace and we have a character that - // we can't necessarily push back (because pos could be 0) - // fast forward through the leading whitespace nextNonWhitespace(true); pos--;