From 14f16e2d0c3c60338d69a3e0f653b8b876fdc1be Mon Sep 17 00:00:00 2001 From: Joel Leitch Date: Thu, 11 Oct 2012 03:15:49 +0000 Subject: [PATCH] Adding Red-Black Tree implementation and tying it into the Gson bindings. --- .../main/java/com/google/gson/JsonObject.java | 25 +- .../gson/internal/ConstructorConstructor.java | 19 +- .../google/gson/internal/LinkedTreeMap.java | 494 +++++++++++++++ .../com/google/gson/internal/StringMap.java | 565 ------------------ .../gson/internal/bind/ObjectTypeAdapter.java | 5 +- .../java/com/google/gson/JsonObjectTest.java | 1 + .../java/com/google/gson/JsonParserTest.java | 2 +- .../functional/DefaultTypeAdaptersTest.java | 6 +- .../com/google/gson/functional/MapTest.java | 35 +- .../gson/internal/LinkedTreeMapTest.java | 140 +++++ .../google/gson/internal/StringMapTest.java | 67 --- 11 files changed, 689 insertions(+), 670 deletions(-) create mode 100644 gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java delete mode 100644 gson/src/main/java/com/google/gson/internal/StringMap.java create mode 100644 gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java delete mode 100644 gson/src/test/java/com/google/gson/internal/StringMapTest.java diff --git a/gson/src/main/java/com/google/gson/JsonObject.java b/gson/src/main/java/com/google/gson/JsonObject.java index 165247a6..78c7a177 100644 --- a/gson/src/main/java/com/google/gson/JsonObject.java +++ b/gson/src/main/java/com/google/gson/JsonObject.java @@ -16,8 +16,8 @@ package com.google.gson; -import com.google.gson.internal.$Gson$Preconditions; -import com.google.gson.internal.StringMap; +import com.google.gson.internal.LinkedTreeMap; + import java.util.Map; import java.util.Set; @@ -30,17 +30,8 @@ import java.util.Set; * @author Joel Leitch */ public final class JsonObject extends JsonElement { - // We are using a linked hash map because it is important to preserve - // 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 StringMap members = new StringMap(); - - /** - * Creates an empty JsonObject. - */ - public JsonObject() { - } + private final LinkedTreeMap members = + new LinkedTreeMap(); @Override JsonObject deepCopy() { @@ -63,7 +54,7 @@ public final class JsonObject extends JsonElement { if (value == null) { value = JsonNull.INSTANCE; } - members.put($Gson$Preconditions.checkNotNull(property), value); + members.put(property, value); } /** @@ -158,11 +149,7 @@ public final class JsonObject extends JsonElement { * @return the member matching the name. Null if no such member exists. */ public JsonElement get(String memberName) { - if (members.containsKey(memberName)) { - JsonElement member = members.get(memberName); - return member == null ? JsonNull.INSTANCE : member; - } - return null; + return members.get(memberName); } /** 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 2c67f467..f69d1bbb 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -18,6 +18,7 @@ package com.google.gson.internal; import com.google.gson.InstanceCreator; import com.google.gson.reflect.TypeToken; + import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; @@ -31,7 +32,9 @@ import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.SortedMap; import java.util.SortedSet; +import java.util.TreeMap; import java.util.TreeSet; /** @@ -144,26 +147,32 @@ public final class ConstructorConstructor { } if (Map.class.isAssignableFrom(rawType)) { - if (type instanceof ParameterizedType - && ((ParameterizedType) type).getActualTypeArguments()[0] == String.class) { + if (SortedMap.class.isAssignableFrom(rawType)) { return new ObjectConstructor() { public T construct() { - return (T) new StringMap(); + return (T) new TreeMap(); } }; - } else { + } else if (type instanceof ParameterizedType && !(String.class.isAssignableFrom( + TypeToken.get(((ParameterizedType) type).getActualTypeArguments()[0]).getRawType()))) { return new ObjectConstructor() { public T construct() { return (T) new LinkedHashMap(); } }; + } else { + return new ObjectConstructor() { + public T construct() { + return (T) new LinkedTreeMap(); + } + }; } - // TODO: SortedMap ? } return null; } + private ObjectConstructor newUnsafeAllocator( final Type type, final Class rawType) { return new ObjectConstructor() { diff --git a/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java new file mode 100644 index 00000000..cd2f4ca7 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed 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.io.ObjectStreamException; +import java.io.Serializable; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * A map with a {@code Comparable} key that is implemented as a red-black tree. + * + *

A red-black tree offers quicker insert operations than AVL trees; however, slower "find" + * operations. + * + *

This implementation was derived from the JDK's TreeMap class. + */ +public class LinkedTreeMap, V> + extends AbstractMap implements Serializable { + private static final boolean BLACK = false; + private static final boolean RED = true; + + // Size stored as a field for optimization instead of recursing tree. + private int size = 0; + + private TreeNode root; + + // Store the head and tail to preserve the ordering of nodes inserted into tree + private TreeNode head; + private TreeNode tail; + + public Set> entrySet() { + return new EntrySet(); + } + + public boolean containsKey(K key) { + return (find(key) != null); + } + + public V get(K key) { + TreeNode entry = find(key); + return (entry == null) ? null : entry.getValue(); + } + + public V put(K key, V value) { + $Gson$Preconditions.checkNotNull(key); + if (root == null) { + root = new TreeNode(null, null, key, value); + head = root; + tail = root; + size++; + return null; + } else { + return findAndUpdateOrCreateNode(key, value); + } + } + + private V findAndUpdateOrCreateNode(K key, V value) { + TreeNode parent; + int lastCompare; + + TreeNode entry = root; + do { + parent = entry; + lastCompare = key.compareTo(entry.key); + if (lastCompare < 0) { + entry = entry.left; + } else if (lastCompare > 0) { + entry = entry.right; + } else { + V rval = entry.getValue(); + entry.setValue(value); + return rval; + } + } while (entry != null); + + size++; + + // Create a new node and set up the tree edges + TreeNode newEntry = new TreeNode(parent, tail, key, value); + if (lastCompare < 0) { + parent.left = newEntry; + } else if (lastCompare > 0) { + parent.right = newEntry; + } + + tail.next = newEntry; + tail = newEntry; + rebalanceAfterInsert(newEntry); + return null; + } + + private void rebalanceAfterInsert(TreeNode x) { + x.color = RED; + + while (x != null && x != root && x.parent.color == RED) { + if (x.parent == leftOf(parentOf(parentOf(x)))) { + TreeNode y = rightOf(parentOf(parentOf(x))); + if (colorOf(y) == RED) { + setColor(parentOf(x), BLACK); + setColor(y, BLACK); + setColor(parentOf(parentOf(x)), RED); + x = parentOf(parentOf(x)); + } else { + if (x == rightOf(parentOf(x))) { + x= parentOf(x); + rotateLeft(x); + } + setColor(parentOf(x), BLACK); + setColor(parentOf(parentOf(x)), RED); + rotateRight(parentOf(parentOf(x))); + } + } else { + TreeNode y = leftOf(parentOf(parentOf(x))); + if (colorOf(y) == RED) { + setColor(parentOf(x), BLACK); + setColor(y, BLACK); + setColor(parentOf(parentOf(x)), RED); + x = parentOf(parentOf(x)); + } else { + if (x == leftOf(parentOf(x))) { + x = parentOf(x); + rotateRight(x); + } + setColor(parentOf(x), BLACK); + setColor(parentOf(parentOf(x)), RED); + rotateLeft(parentOf(parentOf(x))); + } + } + } + root.color = BLACK; + } + + private static , V> TreeNode parentOf(TreeNode e) { + return (e != null ? e.parent : null); + } + + private static , V> boolean colorOf(TreeNode e) { + return (e != null ? e.color : BLACK); + } + + private static , V> TreeNode leftOf(TreeNode e) { + return (e != null ? e.left : null); + } + + private static , V> TreeNode rightOf(TreeNode e) { + return (e != null ? e.right : null); + } + + private static , V> void setColor(TreeNode e, boolean c) { + if (e != null){ + e.color = c; + } + } + + private static , V> TreeNode successor(TreeNode t) { + if (t == null) { + return null; + } else if (t.right != null) { + TreeNode p = t.right; + while (p.left != null) { + p = p.left; + } + return p; + } else { + TreeNode p = t.parent; + TreeNode ch = t; + while (p != null && ch == p.right) { + ch = p; + p = p.parent; + } + return p; + } + } + + private void rotateLeft(TreeNode p) { + if (p != null) { + TreeNode r = p.right; + p.right = r.left; + if (r.left != null) { + r.left.parent = p; + } + r.parent = p.parent; + if (p.parent == null) { + root = r; + } else if (p.parent.left == p) { + p.parent.left = r; + } else { + p.parent.right = r; + } + r.left = p; + p.parent = r; + } + } + + private void rotateRight(TreeNode p) { + if (p != null) { + TreeNode l = p.left; + p.left = l.right; + if (l.right != null) { + l.right.parent = p; + } + l.parent = p.parent; + if (p.parent == null) { + root = l; + } else if (p.parent.right == p) { + p.parent.right = l; + } else { + p.parent.left = l; + } + l.right = p; + p.parent = l; + } + } + + public V remove(K key) { + TreeNode entry = find(key); + if (entry == null) { + return null; + } else { + size--; + V rval = entry.getValue(); + preserveOrderForRemoval(entry); + removeNode(entry); + return rval; + } + } + + private void removeNode(TreeNode p) { + if (p.left != null && p.right != null) { + TreeNode s = successor(p); + p.key = s.key; + p.value = s.value; + p = s; + } + + TreeNode replacement = (p.left != null ? p.left : p.right); + if (replacement != null) { + // Link replacement to parent + replacement.parent = p.parent; + if (p.parent == null) { + root = replacement; + } else if (p == p.parent.left) { + p.parent.left = replacement; + } else { + p.parent.right = replacement; + } + + // Null out links so they are OK to use by fixAfterDeletion. + p.left = null; + p.right = null; + p.parent = null; + + // Fix replacement + if (p.color == BLACK) { + fixAfterDeletion(replacement); + } + } else if (p.parent == null) { // return if we are the only node. + root = null; + } else { // No children. Use self as phantom replacement and unlink. + if (p.color == BLACK) { + fixAfterDeletion(p); + } + + if (p.parent != null) { + if (p == p.parent.left) { + p.parent.left = null; + } else if (p == p.parent.right) { + p.parent.right = null; + } + p.parent = null; + } + } + } + + private void preserveOrderForRemoval(TreeNode p) { + // Preserve insertion order for entry set iteration + if (p == head) { + head = p.next; + } + if (p == tail) { + tail = p.previous; + } + + TreeNode previousNode = p.previous; + TreeNode nextNode = p.next; + if (previousNode != null) { + previousNode.next = nextNode; + } + if (nextNode != null) { + nextNode.previous = previousNode; + } + } + + private void fixAfterDeletion(TreeNode x) { + while (x != root && colorOf(x) == BLACK) { + if (x == leftOf(parentOf(x))) { + TreeNode sib = rightOf(parentOf(x)); + + if (colorOf(sib) == RED) { + setColor(sib, BLACK); + setColor(parentOf(x), RED); + rotateLeft(parentOf(x)); + sib = rightOf(parentOf(x)); + } + + if (colorOf(leftOf(sib)) == BLACK && + colorOf(rightOf(sib)) == BLACK) { + setColor(sib, RED); + x = parentOf(x); + } else { + if (colorOf(rightOf(sib)) == BLACK) { + setColor(leftOf(sib), BLACK); + setColor(sib, RED); + rotateRight(sib); + sib = rightOf(parentOf(x)); + } + setColor(sib, colorOf(parentOf(x))); + setColor(parentOf(x), BLACK); + setColor(rightOf(sib), BLACK); + rotateLeft(parentOf(x)); + x = root; + } + } else { // symmetric + TreeNode sib = leftOf(parentOf(x)); + + if (colorOf(sib) == RED) { + setColor(sib, BLACK); + setColor(parentOf(x), RED); + rotateRight(parentOf(x)); + sib = leftOf(parentOf(x)); + } + + if (colorOf(rightOf(sib)) == BLACK && + colorOf(leftOf(sib)) == BLACK) { + setColor(sib, RED); + x = parentOf(x); + } else { + if (colorOf(leftOf(sib)) == BLACK) { + setColor(rightOf(sib), BLACK); + setColor(sib, RED); + rotateLeft(sib); + sib = leftOf(parentOf(x)); + } + setColor(sib, colorOf(parentOf(x))); + setColor(parentOf(x), BLACK); + setColor(leftOf(sib), BLACK); + rotateRight(parentOf(x)); + x = root; + } + } + } + + setColor(x, BLACK); + } + + public int size() { + return size; + } + + /** + * If somebody is unlucky enough to have to serialize one of these, serialize + * it as a LinkedHashMap so that they won't need Gson on the other side to + * deserialize it. Using serialization defeats our DoS defence, so most apps + * shouldn't use it. + */ + private Object writeReplace() throws ObjectStreamException { + return new LinkedHashMap(this); + } + + private TreeNode find(K key) { + for (TreeNode entry = root; entry != null; ) { + int compareVal = key.compareTo(entry.key); + if (compareVal < 0) { + entry = entry.left; + } else if (compareVal > 0) { + entry = entry.right; + } else { + return entry; + } + } + return null; + } + + private static class TreeNode, V> implements Map.Entry { + private K key; + private V value; + private TreeNode parent; + private TreeNode left; + private TreeNode right; + + // Used for rebalance tree + private boolean color = BLACK; + + // This is used for preserving the insertion order + private TreeNode next; + private TreeNode previous; + + TreeNode(TreeNode parent, TreeNode previous, K key, V value) { + this.parent = parent; + this.previous = previous; + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + // I'd like to make this throw an UnsupportedOperationException; however, + public V setValue(V value) { + V rval = this.value; + this.value = value; + return rval; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Entry)) { + return false; + } + Entry e = (Entry) o; + Object eValue = e.getValue(); + return key.equals(e.getKey()) + && (value == null ? eValue == null : value.equals(eValue)); + } + + @Override + public final int hashCode() { + return key.hashCode() ^ (value == null ? 0 : value.hashCode()); + } + + @Override + public final String toString() { + return key + "=" + value; + } + } + + class EntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new LinkedTreeIterator(head); + } + + @Override + public int size() { + return LinkedTreeMap.this.size(); + } + } + + private class LinkedTreeIterator implements Iterator> { + private TreeNode current; + + private LinkedTreeIterator(TreeNode first) { + this.current = first; + } + + public boolean hasNext() { + return current != null; + } + + public Map.Entry next() { + TreeNode rval = current; + current = current.next; + return rval; + } + + public final void remove() { + LinkedTreeMap.this.remove(current.getKey()); + } + } +} diff --git a/gson/src/main/java/com/google/gson/internal/StringMap.java b/gson/src/main/java/com/google/gson/internal/StringMap.java deleted file mode 100644 index 85cfb43c..00000000 --- a/gson/src/main/java/com/google/gson/internal/StringMap.java +++ /dev/null @@ -1,565 +0,0 @@ -/* - * 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.io.ObjectStreamException; -import java.io.Serializable; -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.LinkedHashMap; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Random; -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 - * map does not support null keys. - * - *

This implementation was derived from Android 4.0's LinkedHashMap. - */ -public final class StringMap extends AbstractMap implements Serializable { - /** - * 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; - - /** - * Max number of collisions in a single bucket before falling back to - * an unpredictable hash code. - */ - private static final int MAX_COLLISIONS = 512; - - /** - * 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. - */ - @SuppressWarnings("rawtypes") - 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; - - /** - * True to use String.hashCode(), which is cached per-string. False to use - * less predictable (but uncached) hash algorithm. - */ - private boolean useFastHash = true; - - // 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 key instanceof String && getEntry((String) key) != null; - } - - @Override public V get(Object key) { - if (key instanceof String) { - LinkedEntry entry = getEntry((String) key); - return entry != null ? entry.value : null; - } else { - return null; - } - } - - private LinkedEntry getEntry(String key) { - if (key == null) { - return null; - } - - int hash = useFastHash ? fastHash(key) : unpredictableHash(key); - LinkedEntry[] tab = table; - for (LinkedEntry e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { - String eKey = e.key; - if (eKey == key || (e.hash == hash && key.equals(eKey))) { - return e; - } - } - return null; - } - - @Override public V put(String key, V value) { - if (key == null) { - throw new NullPointerException("key == null"); - } - - int collisionCount = 0; - int hash = useFastHash ? fastHash(key) : unpredictableHash(key); - LinkedEntry[] tab = table; - int index = hash & (tab.length - 1); - for (LinkedEntry e = tab[index]; e != null; e = e.next) { - collisionCount++; - 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); - - /* - * If we suffer a very large number of collisions, fall back from the cached - * String.hashCode() to an (uncached) hash code that isn't predictable. - */ - if (useFastHash && collisionCount >= MAX_COLLISIONS) { - LinkedEntry entry = header.nxt; - - // clear the table - Arrays.fill(table, null); - size = 0; - header.nxt = header.prv = header; - useFastHash = false; - - // fill it up in iteration order - for (; entry != header; entry = entry.nxt) { - put(entry.key, entry.value); - } - } - - return null; - } - - private void addNewEntry(String 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 || !(key instanceof String)) { - return null; - } - int hash = useFastHash ? fastHash(key) : unpredictableHash((String) key); - 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 String 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(String 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 String getKey() { - return key; - } - - public final V getValue() { - return value; - } - - public final V setValue(V value) { - 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; - Object eValue = e.getValue(); - return key.equals(e.getKey()) - && (value == null ? eValue == null : value.equals(eValue)); - } - - @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 || !(key instanceof String)) { - return false; - } - - int hash = useFastHash ? fastHash(key) : unpredictableHash((String) key); - 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 == null ? e.value != null : !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 - } - - /** - * If somebody is unlucky enough to have to serialize one of these, serialize - * it as a LinkedHashMap so that they won't need Gson on the other side to - * deserialize it. Using serialization defeats our DoS defence, so most apps - * shouldn't use it. - */ - private Object writeReplace() throws ObjectStreamException { - return new LinkedHashMap(this); - } - - 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 String 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(); - } - } - - private static int fastHash(Object key) { - int h = key.hashCode(); - // Apply Doug Lea's supplemental hash function to avoid collisions for - // hashes that do not differ in lower or upper bits. - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); - } - - private static final int seed = new Random().nextInt(); - private static int unpredictableHash(String key) { - // Ensuring that the hash is unpredictable and well distributed. - // - // Finding unpredictable hash functions is a bit of a dark art as we need to balance - // good unpredictability (to avoid DoS) and good distribution (for performance). - // - // We achieve this by using the same algorithm as the Perl version, but this implementation - // is being written from scratch by inder who has never seen the - // Perl version (for license compliance). - // - // TODO: investigate http://code.google.com/p/cityhash/ and http://code.google.com/p/smhasher/ - // both of which may have better distribution and/or unpredictability. - int h = seed; - for (int i = 0; i < key.length(); ++i) { - int h2 = h + key.charAt(i); - int h3 = h2 + h2 << 10; // h2 * 1024 - h = h3 ^ (h3 >>> 6); // h3 / 64 - } - - // Apply Doug Lea's supplemental hash function to avoid collisions for - // hashes that do not differ in lower or upper bits. - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); - } -} 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 0ba5ddac..b753246e 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,11 +19,12 @@ 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.internal.LinkedTreeMap; 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.List; @@ -63,7 +64,7 @@ public final class ObjectTypeAdapter extends TypeAdapter { return list; case BEGIN_OBJECT: - Map map = new StringMap(); + Map map = new LinkedTreeMap(); in.beginObject(); while (in.hasNext()) { map.put(in.nextName(), read(in)); diff --git a/gson/src/test/java/com/google/gson/JsonObjectTest.java b/gson/src/test/java/com/google/gson/JsonObjectTest.java index a03450ef..9423a24d 100644 --- a/gson/src/test/java/com/google/gson/JsonObjectTest.java +++ b/gson/src/test/java/com/google/gson/JsonObjectTest.java @@ -40,6 +40,7 @@ public class JsonObjectTest extends TestCase { JsonElement removedElement = jsonObj.remove(propertyName); assertEquals(value, removedElement); assertFalse(jsonObj.has(propertyName)); + assertNull(jsonObj.get(propertyName)); } public void testAddingNullPropertyValue() throws Exception { diff --git a/gson/src/test/java/com/google/gson/JsonParserTest.java b/gson/src/test/java/com/google/gson/JsonParserTest.java index 181d4ab7..7efa7fd2 100644 --- a/gson/src/test/java/com/google/gson/JsonParserTest.java +++ b/gson/src/test/java/com/google/gson/JsonParserTest.java @@ -107,7 +107,7 @@ public class JsonParserTest extends TestCase { public void testReadWriteTwoObjects() throws Exception { Gson gson = new Gson(); - CharArrayWriter writer= new CharArrayWriter(); + CharArrayWriter writer = new CharArrayWriter(); BagOfPrimitives expectedOne = new BagOfPrimitives(1, 1, true, "one"); writer.write(gson.toJson(expectedOne).toCharArray()); BagOfPrimitives expectedTwo = new BagOfPrimitives(2, 2, false, "two"); diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java index 2321f443..9b27ef15 100644 --- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java @@ -590,11 +590,13 @@ public class DefaultTypeAdaptersTest extends TestCase { assertEquals("{\"foo\":1,\"bar\":2}", gson.toJson(object, JsonElement.class)); } - public void testJsonObjectDeerialization() { + public void testJsonObjectDeserialization() { JsonObject object = new JsonObject(); object.add("foo", new JsonPrimitive(1)); object.add("bar", new JsonPrimitive(2)); - assertEquals(object, gson.fromJson("{\"foo\":1,\"bar\":2}", JsonElement.class)); + + JsonElement actual = gson.fromJson("{\"foo\":1,\"bar\":2}", JsonElement.class); + assertEquals(object, actual); } public void testJsonNullDeerialization() { diff --git a/gson/src/test/java/com/google/gson/functional/MapTest.java b/gson/src/test/java/com/google/gson/functional/MapTest.java index bf38a141..3b55fa5e 100755 --- a/gson/src/test/java/com/google/gson/functional/MapTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -16,15 +16,6 @@ package com.google.gson.functional; -import java.lang.reflect.Type; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.TreeMap; - -import junit.framework.TestCase; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; @@ -40,6 +31,16 @@ import com.google.gson.common.TestTypes; import com.google.gson.internal.$Gson$Types; import com.google.gson.reflect.TypeToken; +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + /** * Functional test for Json serialization and deserialization for Maps * @@ -156,6 +157,22 @@ public class MapTest extends TestCase { assertEquals("456", map.get(123)); } + public void testHashMapDeserialization() throws Exception { + Type typeOfMap = new TypeToken>() {}.getType(); + HashMap map = gson.fromJson("{\"123\":\"456\"}", typeOfMap); + assertEquals(1, map.size()); + assertTrue(map.containsKey(123)); + assertEquals("456", map.get(123)); + } + + public void testSortedMap() throws Exception { + Type typeOfMap = new TypeToken>() {}.getType(); + SortedMap map = gson.fromJson("{\"123\":\"456\"}", typeOfMap); + assertEquals(1, map.size()); + assertTrue(map.containsKey(123)); + assertEquals("456", map.get(123)); + } + public void testParameterizedMapSubclassSerialization() { MyParameterizedMap map = new MyParameterizedMap(10); map.put("a", "b"); diff --git a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java new file mode 100644 index 00000000..538ee5f4 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed 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 com.google.gson.common.MoreAsserts; + +import junit.framework.TestCase; + +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Unit tests for {@code LinkedTreeMap} class. + * + * @author Joel Leitch + */ +public class LinkedTreeMapTest extends TestCase { + + public void testPutAndGet() throws Exception { + LinkedTreeMap map = new LinkedTreeMap(); + map.put("B", 2); + map.put("A", 1); + map.put("C", 3); + + assertTrue(map.containsKey("A")); + assertTrue(map.containsKey("B")); + assertTrue(map.containsKey("C")); + assertFalse(map.containsKey("D")); + + assertEquals(1, (int) map.get("A")); + assertEquals(2, (int) map.get("B")); + assertEquals(3, (int) map.get("C")); + assertEquals(3, map.entrySet().size()); + + assertEquals(1, (int) map.put("A", 4)); + assertTrue(map.containsKey("A")); + assertEquals(4, (int) map.get("A")); + assertEquals(3, map.entrySet().size()); + + // Ensure entry set size is same as map size + assertEquals(map.size(), map.entrySet().size()); + } + + public void testSingleElement() throws Exception { + LinkedTreeMap map = new LinkedTreeMap(); + map.put("A", 1); + assertEquals(1, map.size()); + + assertEquals(1, (int) map.get("A")); + map.remove("A"); + assertEquals(0, map.size()); + + // Ensure the map and entry set are empty + assertTrue(map.entrySet().isEmpty()); + assertTrue(map.isEmpty()); + } + + public void testAddAndRemove() throws Exception { + LinkedTreeMap map = new LinkedTreeMap(); + map.put("A", 1); + map.put("B", 2); + map.put("C", 3); + map.put("D", 4); + map.put("E", 5); + map.put("F", 6); + + assertEquals(3, (int) map.remove("C")); + assertEquals(5, map.size()); + assertIterationOrder(map.entrySet(), + new String[] { "A", "B", "D", "E", "F" }, new int[] { 1, 2, 4, 5, 6 }); + + // Remove a non-existent key + assertNull(map.remove("G")); + assertEquals(5, map.size()); + + // Remove the first element + assertEquals(1, (int) map.remove("A")); + assertIterationOrder(map.entrySet(), + new String[] { "B", "D", "E", "F" }, new int[] { 2, 4, 5, 6 }); + + // Remove the last element + assertEquals(6, (int) map.remove("F")); + assertIterationOrder(map.entrySet(), + new String[] { "B", "D", "E" }, new int[] { 2, 4, 5 }); + } + + public void testInsertionOrderPreserved() throws Exception { + LinkedTreeMap map = new LinkedTreeMap(); + String[] keys = { "B", "A", "D", "C", "Z", "W", "E", "F", "T" }; + int[] values = new int[keys.length]; + for (int i = 0; i < keys.length; ++i) { + values[i] = i; + map.put(keys[i], i); + } + + Set> entries = map.entrySet(); + assertEquals(keys.length, entries.size()); + assertIterationOrder(entries, keys, values); + } + + public void testEqualsAndHashCode() throws Exception { + LinkedTreeMap map1 = new LinkedTreeMap(); + map1.put("A", 1); + map1.put("B", 2); + map1.put("C", 3); + map1.put("D", 4); + + LinkedTreeMap map2 = new LinkedTreeMap(); + map2.put("C", 3); + map2.put("B", 2); + map2.put("D", 4); + map2.put("A", 1); + + MoreAsserts.assertEqualsAndHashCode(map1, map2); + } + + private void assertIterationOrder(Set> entries, String[] keys, int[] values) { + int i = 0; + for (Iterator> iterator = entries.iterator(); iterator.hasNext(); ++i) { + Map.Entry entry = iterator.next(); + assertEquals(keys[i], entry.getKey()); + assertEquals(values[i], (int) entry.getValue()); + } + } +} diff --git a/gson/src/test/java/com/google/gson/internal/StringMapTest.java b/gson/src/test/java/com/google/gson/internal/StringMapTest.java deleted file mode 100644 index 169c3bcf..00000000 --- a/gson/src/test/java/com/google/gson/internal/StringMapTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * Licensed 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.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import junit.framework.TestCase; - -public final class StringMapTest extends TestCase { - public void testFallbackFromTooManyCollisions() { - int count = 10000; - StringMap map = new StringMap(); - int index = 0; - List collidingStrings = collidingStrings(1 << 20, count); - for (String string : collidingStrings) { - map.put(string, index++); - } - assertEquals(collidingStrings.size(), map.size()); - Iterator> iterator = map.entrySet().iterator(); - for (int i = 0; i < count; i++) { - Map.Entry entry = iterator.next(); - assertEquals(collidingStrings.get(i), entry.getKey()); - assertEquals(Integer.valueOf(i), entry.getValue()); - } - } - - /** - * @param h0 the hash code of the generated strings - */ - private List collidingStrings(int h0, int count) { - List result = new ArrayList(count); - int p1 = 31; - int p0 = 31 * 31; - int maxChar = Character.MAX_VALUE; - for (char c0 = 0; c0 <= maxChar && c0 <= h0 / p0; c0++) { - int h1 = h0 - c0 * p0; - for (char c1 = 0; c1 <= maxChar && c1 <= h1 / p1; c1++) { - int h2 = h1 - c1 * p1; - char c2 = (char) h2; - if (h2 != c2) { - continue; - } - result.add(new String(new char[] { c0, c1, c2 } )); - if (result.size() == count) { - return result; - } - } - } - throw new IllegalArgumentException("Couldn't find " + count + " strings with hashCode " + h0); - } -}