Skip to content

Commit f5a69c3

Browse files
adobkluever
authored andcommitted
Ensure that the set returned by ImmutableMap<K, V>.keySet() is serializable when K is serializable, and similarly for values().
Set<T> should be serializable when T is serializable but that is not always the case for the set returned by ImmutableMap.keySet() due to a reference from the returned set back to the original map. When serializing this set, the original map is serialized is well. This change changes this so that only the keys are serialized. RELNOTES=`collect`: Ensure that the set returned by `ImmutableMap<K, V>.keySet()` is serializable when `K` is serializable (and similarly for `values()`). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=324749502
1 parent 0f9977b commit f5a69c3

File tree

13 files changed

+349
-156
lines changed

13 files changed

+349
-156
lines changed

android/guava-tests/test/com/google/common/collect/ImmutableMapTest.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.common.collect;
1818

1919
import static com.google.common.testing.SerializableTester.reserialize;
20+
import static com.google.common.truth.Truth.assertThat;
2021

2122
import com.google.common.annotations.GwtCompatible;
2223
import com.google.common.annotations.GwtIncompatible;
@@ -45,12 +46,15 @@
4546
import com.google.common.testing.EqualsTester;
4647
import com.google.common.testing.NullPointerTester;
4748
import com.google.common.testing.SerializableTester;
49+
import java.io.ByteArrayOutputStream;
50+
import java.io.ObjectOutputStream;
4851
import java.io.Serializable;
4952
import java.util.Collection;
5053
import java.util.Collections;
5154
import java.util.LinkedHashMap;
5255
import java.util.Map;
5356
import java.util.Map.Entry;
57+
import java.util.Set;
5458
import junit.framework.Test;
5559
import junit.framework.TestCase;
5660
import junit.framework.TestSuite;
@@ -768,6 +772,58 @@ public void testViewSerialization() {
768772
assertTrue(reserializedValues instanceof ImmutableCollection);
769773
}
770774

775+
@GwtIncompatible // SerializableTester
776+
public void testKeySetIsSerializable_regularImmutableMap() {
777+
class NonSerializableClass {}
778+
779+
Map<String, NonSerializableClass> map =
780+
RegularImmutableMap.create(1, new Object[] {"one", new NonSerializableClass()});
781+
Set<String> set = map.keySet();
782+
783+
LenientSerializableTester.reserializeAndAssertLenient(set);
784+
}
785+
786+
@GwtIncompatible // SerializableTester
787+
public void testValuesCollectionIsSerializable_regularImmutableMap() {
788+
class NonSerializableClass {}
789+
790+
Map<NonSerializableClass, String> map =
791+
RegularImmutableMap.create(1, new Object[] {new NonSerializableClass(), "value"});
792+
Collection<String> collection = map.values();
793+
794+
LenientSerializableTester.reserializeAndAssertElementsEqual(collection);
795+
}
796+
797+
// TODO: Re-enable this test after moving to new serialization format in ImmutableMap.
798+
@GwtIncompatible // SerializableTester
799+
@SuppressWarnings("unchecked")
800+
public void ignore_testSerializationNoDuplication_regularImmutableMap() throws Exception {
801+
// Tests that searializing a map, its keySet, and values only writes the underlying data once.
802+
803+
Object[] entries = new Object[2000];
804+
for (int i = 0; i < entries.length; i++) {
805+
entries[i] = i;
806+
}
807+
808+
ImmutableMap<Integer, Integer> map = RegularImmutableMap.create(entries.length / 2, entries);
809+
Set<Integer> keySet = map.keySet();
810+
Collection<Integer> values = map.values();
811+
812+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
813+
ObjectOutputStream oos = new ObjectOutputStream(bytes);
814+
oos.writeObject(map);
815+
oos.flush();
816+
817+
int mapSize = bytes.size();
818+
oos.writeObject(keySet);
819+
oos.writeObject(values);
820+
oos.close();
821+
822+
int finalSize = bytes.size();
823+
824+
assertThat(finalSize - mapSize).isLessThan(100);
825+
}
826+
771827
public void testEquals() {
772828
new EqualsTester()
773829
.addEqualityGroup(ImmutableMap.of(), ImmutableMap.builder().build())

android/guava-tests/test/com/google/common/collect/LenientSerializableTester.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.common.annotations.GwtIncompatible;
2525
import com.google.common.testing.SerializableTester;
2626
import com.google.errorprone.annotations.CanIgnoreReturnValue;
27+
import java.util.Collection;
2728
import java.util.Set;
2829

2930
/**
@@ -60,5 +61,14 @@ static <E> Multiset<E> reserializeAndAssertLenient(Multiset<E> original) {
6061
return copy;
6162
}
6263

64+
@CanIgnoreReturnValue
65+
@GwtIncompatible // SerializableTester
66+
static <E> Collection<E> reserializeAndAssertElementsEqual(Collection<E> original) {
67+
Collection<E> copy = reserialize(original);
68+
assertTrue(Iterables.elementsEqual(original, copy));
69+
assertTrue(copy instanceof ImmutableCollection);
70+
return copy;
71+
}
72+
6373
private LenientSerializableTester() {}
6474
}

android/guava/src/com/google/common/collect/ImmutableBiMap.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,22 +351,21 @@ public V forcePut(K key, V value) {
351351
* <p>Since the bimap is immutable, ImmutableBiMap doesn't require special logic for keeping the
352352
* bimap and its inverse in sync during serialization, the way AbstractBiMap does.
353353
*/
354-
private static class SerializedForm extends ImmutableMap.SerializedForm {
355-
SerializedForm(ImmutableBiMap<?, ?> bimap) {
354+
private static class SerializedForm<K, V> extends ImmutableMap.SerializedForm<K, V> {
355+
SerializedForm(ImmutableBiMap<K, V> bimap) {
356356
super(bimap);
357357
}
358358

359359
@Override
360-
Object readResolve() {
361-
Builder<Object, Object> builder = new Builder<>();
362-
return createMap(builder);
360+
Builder<K, V> makeBuilder(int size) {
361+
return new Builder<>(size);
363362
}
364363

365364
private static final long serialVersionUID = 0;
366365
}
367366

368367
@Override
369368
Object writeReplace() {
370-
return new SerializedForm(this);
369+
return new SerializedForm<>(this);
371370
}
372371
}

android/guava/src/com/google/common/collect/ImmutableMap.java

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -706,37 +706,85 @@ public String toString() {
706706
* reconstructed using public factory methods. This ensures that the implementation types remain
707707
* as implementation details.
708708
*/
709-
static class SerializedForm implements Serializable {
710-
private final Object[] keys;
711-
private final Object[] values;
712-
713-
SerializedForm(ImmutableMap<?, ?> map) {
714-
keys = new Object[map.size()];
715-
values = new Object[map.size()];
716-
int i = 0;
717-
for (Entry<?, ?> entry : map.entrySet()) {
718-
keys[i] = entry.getKey();
719-
values[i] = entry.getValue();
720-
i++;
709+
static class SerializedForm<K, V> implements Serializable {
710+
// This object retains references to collections returned by keySet() and value(). This saves
711+
// bytes when the both the map and its keySet or value collection are written to the same
712+
// instance of ObjectOutputStream.
713+
714+
// TODO(b/160980469): remove support for the old serialization format after some time
715+
private static final boolean USE_LEGACY_SERIALIZATION = true;
716+
717+
private final Object keys;
718+
private final Object values;
719+
720+
SerializedForm(ImmutableMap<K, V> map) {
721+
if (USE_LEGACY_SERIALIZATION) {
722+
Object[] keys = new Object[map.size()];
723+
Object[] values = new Object[map.size()];
724+
int i = 0;
725+
for (Entry<?, ?> entry : map.entrySet()) {
726+
keys[i] = entry.getKey();
727+
values[i] = entry.getValue();
728+
i++;
729+
}
730+
this.keys = keys;
731+
this.values = values;
732+
return;
721733
}
734+
this.keys = map.keySet();
735+
this.values = map.values();
722736
}
723737

724-
Object readResolve() {
725-
Builder<Object, Object> builder = new Builder<>(keys.length);
726-
return createMap(builder);
738+
@SuppressWarnings("unchecked")
739+
final Object readResolve() {
740+
if (!(this.keys instanceof ImmutableSet)) {
741+
return legacyReadResolve();
742+
}
743+
744+
ImmutableSet<K> keySet = (ImmutableSet<K>) this.keys;
745+
ImmutableCollection<V> values = (ImmutableCollection<V>) this.values;
746+
747+
Builder<K, V> builder = makeBuilder(keySet.size());
748+
749+
UnmodifiableIterator<K> keyIter = keySet.iterator();
750+
UnmodifiableIterator<V> valueIter = values.iterator();
751+
752+
while (keyIter.hasNext()) {
753+
builder.put(keyIter.next(), valueIter.next());
754+
}
755+
756+
return builder.build();
727757
}
728758

729-
Object createMap(Builder<Object, Object> builder) {
759+
@SuppressWarnings("unchecked")
760+
final Object legacyReadResolve() {
761+
K[] keys = (K[]) this.keys;
762+
V[] values = (V[]) this.values;
763+
764+
Builder<K, V> builder = makeBuilder(keys.length);
765+
730766
for (int i = 0; i < keys.length; i++) {
731767
builder.put(keys[i], values[i]);
732768
}
733769
return builder.build();
734770
}
735771

772+
/**
773+
* Returns a builder that builds the unserialized type. Subclasses should override this method.
774+
*/
775+
Builder<K, V> makeBuilder(int size) {
776+
return new Builder<>(size);
777+
}
778+
736779
private static final long serialVersionUID = 0;
737780
}
738781

782+
/**
783+
* Returns a serializable form of this object. Non-public subclasses should not override this
784+
* method. Publicly-accessible subclasses must override this method and should return a subclass
785+
* of SerializedForm whose readResolve() method returns objects of the subclass type.
786+
*/
739787
Object writeReplace() {
740-
return new SerializedForm(this);
788+
return new SerializedForm<>(this);
741789
}
742790
}

android/guava/src/com/google/common/collect/ImmutableSortedMap.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -881,27 +881,25 @@ public ImmutableSortedSet<K> descendingKeySet() {
881881
* are reconstructed using public factory methods. This ensures that the implementation types
882882
* remain as implementation details.
883883
*/
884-
private static class SerializedForm extends ImmutableMap.SerializedForm {
885-
private final Comparator<Object> comparator;
884+
private static class SerializedForm<K, V> extends ImmutableMap.SerializedForm<K, V> {
885+
private final Comparator<? super K> comparator;
886886

887-
@SuppressWarnings("unchecked")
888-
SerializedForm(ImmutableSortedMap<?, ?> sortedMap) {
887+
SerializedForm(ImmutableSortedMap<K, V> sortedMap) {
889888
super(sortedMap);
890-
comparator = (Comparator<Object>) sortedMap.comparator();
889+
comparator = sortedMap.comparator();
891890
}
892891

893892
@Override
894-
Object readResolve() {
895-
Builder<Object, Object> builder = new Builder<>(comparator);
896-
return createMap(builder);
893+
Builder<K, V> makeBuilder(int size) {
894+
return new Builder<>(comparator);
897895
}
898896

899897
private static final long serialVersionUID = 0;
900898
}
901899

902900
@Override
903901
Object writeReplace() {
904-
return new SerializedForm(this);
902+
return new SerializedForm<>(this);
905903
}
906904

907905
// This class is never actually serialized directly, but we have to make the

0 commit comments

Comments
 (0)