Desugared library: GSON support

Bug: 167649682
Change-Id: I62efad3bd6a6834cffd0d78ec255c0999e9492a1
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index e61ff61..1a8d506 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import static com.android.tools.r8.graph.GenericSignature.EMPTY_TYPE_ARGUMENTS;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.NO_KOTLIN_INFO;
 import static com.google.common.base.Predicates.alwaysTrue;
 
@@ -480,7 +479,26 @@
     methodCollection.addDirectMethod(directMethod);
   }
 
-  public void addExtraInterfaces(List<DexType> extraInterfaces, DexItemFactory factory) {
+  public void replaceInterfaces(List<ClassTypeSignature> newInterfaces) {
+    if (newInterfaces.isEmpty()) {
+      return;
+    }
+    clearInterfaces();
+    addExtraInterfaces(newInterfaces);
+  }
+
+  private void clearInterfaces() {
+    interfaces = DexTypeList.empty();
+    if (classSignature.hasSignature()) {
+      classSignature =
+          new ClassSignature(
+              classSignature.formalTypeParameters,
+              classSignature.superClassSignature,
+              ImmutableList.of());
+    }
+  }
+
+  public void addExtraInterfaces(List<ClassTypeSignature> extraInterfaces) {
     if (extraInterfaces.isEmpty()) {
       return;
     }
@@ -488,25 +506,24 @@
     addExtraInterfacesToSignatureIfPresent(extraInterfaces);
   }
 
-  private void addExtraInterfacesToInterfacesArray(List<DexType> extraInterfaces) {
+  private void addExtraInterfacesToInterfacesArray(List<ClassTypeSignature> extraInterfaces) {
     DexType[] newInterfaces =
         Arrays.copyOf(interfaces.values, interfaces.size() + extraInterfaces.size());
     for (int i = interfaces.size(); i < newInterfaces.length; i++) {
-      newInterfaces[i] = extraInterfaces.get(i - interfaces.size());
+      newInterfaces[i] = extraInterfaces.get(i - interfaces.size()).type();
     }
     interfaces = new DexTypeList(newInterfaces);
   }
 
-  private void addExtraInterfacesToSignatureIfPresent(List<DexType> extraInterfaces) {
-    // We need to introduce the extra interfaces to the generic signature.
-    // At this point we cheat and pretend the extraInterfaces simply don't use any generic types.
+  private void addExtraInterfacesToSignatureIfPresent(List<ClassTypeSignature> extraInterfaces) {
+    // We introduce the extra interfaces to the generic signature.
     if (classSignature.hasNoSignature() || extraInterfaces.isEmpty()) {
       return;
     }
     ImmutableList.Builder<ClassTypeSignature> interfacesBuilder =
         ImmutableList.<ClassTypeSignature>builder().addAll(classSignature.superInterfaceSignatures);
-    for (DexType extraInterface : extraInterfaces) {
-      interfacesBuilder.add(new ClassTypeSignature(extraInterface, EMPTY_TYPE_ARGUMENTS));
+    for (ClassTypeSignature extraInterface : extraInterfaces) {
+      interfacesBuilder.add(extraInterface);
     }
     classSignature =
         new ClassSignature(
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignature.java b/src/main/java/com/android/tools/r8/graph/GenericSignature.java
index ffb7eed..a92dc75 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignature.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignature.java
@@ -391,7 +391,11 @@
     ClassTypeSignature enclosingTypeSignature;
     ClassTypeSignature innerTypeSignature;
 
-    ClassTypeSignature(DexType type, List<FieldTypeSignature> typeArguments) {
+    public ClassTypeSignature(DexType type) {
+      this(type, EMPTY_TYPE_ARGUMENTS);
+    }
+
+    public ClassTypeSignature(DexType type, List<FieldTypeSignature> typeArguments) {
       this(type, typeArguments, WildcardIndicator.NOT_AN_ARGUMENT);
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryEmulatedInterfaceDuplicator.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryEmulatedInterfaceDuplicator.java
new file mode 100644
index 0000000..6f72091
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryEmulatedInterfaceDuplicator.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GenericSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DesugaredLibraryEmulatedInterfaceDuplicator {
+
+  final AppView<?> appView;
+  final Map<DexType, DexType> emulatedInterfaces;
+
+  public DesugaredLibraryEmulatedInterfaceDuplicator(AppView<?> appView) {
+    this.appView = appView;
+    emulatedInterfaces =
+        appView.options().desugaredLibraryConfiguration.getEmulateLibraryInterface();
+  }
+
+  public void duplicateEmulatedInterfaces() {
+    // All classes implementing an emulated interface now implements the interface and the
+    // emulated one, as well as hidden overrides, for correct emulated dispatch.
+    // We not that duplicated interfaces won't feature the correct type parameters in the
+    // class signature since such signature is expected to be unused.
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      if (clazz.type == appView.dexItemFactory().objectType) {
+        continue;
+      }
+      if (emulatedInterfaces.containsKey(clazz.type)) {
+        transformEmulatedInterfaces(clazz);
+      } else {
+        duplicateEmulatedInterfaces(clazz);
+      }
+    }
+  }
+
+  private void transformEmulatedInterfaces(DexProgramClass clazz) {
+    List<ClassTypeSignature> newInterfaces = new ArrayList<>();
+    GenericSignature.ClassSignature classSignature = clazz.getClassSignature();
+    for (int i = 0; i < clazz.interfaces.size(); i++) {
+      DexType itf = clazz.interfaces.values[i];
+      assert emulatedInterfaces.containsKey(itf);
+      List<FieldTypeSignature> typeArguments;
+      if (classSignature == null) {
+        typeArguments = Collections.emptyList();
+      } else {
+        ClassTypeSignature classTypeSignature = classSignature.superInterfaceSignatures().get(i);
+        assert itf == classTypeSignature.type();
+        typeArguments = classTypeSignature.typeArguments();
+      }
+      newInterfaces.add(new ClassTypeSignature(emulatedInterfaces.get(itf), typeArguments));
+    }
+    clazz.replaceInterfaces(newInterfaces);
+  }
+
+  private void duplicateEmulatedInterfaces(DexProgramClass clazz) {
+    List<DexType> extraInterfaces = new ArrayList<>();
+    LinkedList<DexClass> workList = new LinkedList<>();
+    Set<DexType> processed = Sets.newIdentityHashSet();
+    workList.add(clazz);
+    while (!workList.isEmpty()) {
+      DexClass dexClass = workList.removeFirst();
+      if (processed.contains(dexClass.type)) {
+        continue;
+      }
+      processed.add(dexClass.type);
+      if (dexClass.superType != appView.dexItemFactory().objectType) {
+        processSuperType(clazz.superType, extraInterfaces, workList);
+      }
+      for (DexType itf : dexClass.interfaces) {
+        processSuperType(itf, extraInterfaces, workList);
+      }
+    }
+    extraInterfaces = removeDuplicates(extraInterfaces);
+    List<ClassTypeSignature> extraInterfaceSignatures = new ArrayList<>();
+    for (DexType extraInterface : extraInterfaces) {
+      extraInterfaceSignatures.add(new ClassTypeSignature(extraInterface));
+    }
+    clazz.addExtraInterfaces(extraInterfaceSignatures);
+  }
+
+  private List<DexType> removeDuplicates(List<DexType> extraInterfaces) {
+    if (extraInterfaces.size() <= 1) {
+      return extraInterfaces;
+    }
+    // TODO(b/161399032): It would be nice to remove duplicate based on inheritance, i.e.,
+    //  if there is ConcurrentMap<K,V> and Map<K,V>, Map<K,V> can be removed.
+    return new ArrayList<>(new HashSet<>(extraInterfaces));
+  }
+
+  void processSuperType(
+      DexType superType, List<DexType> extraInterfaces, LinkedList<DexClass> workList) {
+    if (emulatedInterfaces.containsKey(superType)) {
+      extraInterfaces.add(emulatedInterfaces.get(superType));
+    } else {
+      DexClass superClass = appView.definitionFor(superType);
+      if (shouldProcessSuperclass(superClass)) {
+        workList.add(superClass);
+      }
+    }
+  }
+
+  private boolean shouldProcessSuperclass(DexClass superclazz) {
+    if (appView.options().isDesugaredLibraryCompilation()) {
+      return false;
+    }
+    // TODO(b/161399032): Pay-as-you-go design: stop duplication on library boundaries.
+    return superclazz != null && superclazz.isLibraryClass();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
index 64dad3e..68a2df3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
@@ -375,7 +376,7 @@
       // applies up to 24.
       for (DexEncodedMethod method : methods) {
         clazz.addExtraInterfaces(
-            Collections.singletonList(dispatchInterfaceTypeFor(method)), appView.dexItemFactory());
+            Collections.singletonList(new ClassTypeSignature(dispatchInterfaceTypeFor(method))));
         if (clazz.lookupVirtualMethod(method.getReference()) == null) {
           DexEncodedMethod newMethod = createForwardingMethod(method, clazz);
           clazz.addVirtualMethod(newMethod);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 5cda030..61a5c56 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -54,7 +54,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.IdentityHashMap;
-import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
@@ -923,63 +922,6 @@
     return newMethods;
   }
 
-  private void duplicateEmulatedInterfaces() {
-    // All classes implementing an emulated interface now implements the interface and the
-    // emulated one, as well as hidden overrides, for correct emulated dispatch.
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (clazz.type == appView.dexItemFactory().objectType) {
-        continue;
-      }
-      List<DexType> extraInterfaces = new ArrayList<>();
-      for (DexType type : clazz.interfaces.values) {
-        if (emulatedInterfaces.containsKey(type)) {
-          extraInterfaces.add(emulatedInterfaces.get(type));
-        }
-      }
-      if (!appView.options().isDesugaredLibraryCompilation()) {
-        assert clazz.superType != null;
-        DexClass superClazz = appView.definitionFor(clazz.superType);
-        if (superClazz != null && superClazz.isLibraryClass()) {
-          List<DexType> itfs = emulatedInterfacesOf(superClazz);
-          for (DexType itf : itfs) {
-            extraInterfaces.add(emulatedInterfaces.get(itf));
-          }
-        }
-        // Remove duplicates.
-        if (extraInterfaces.size() > 1) {
-          extraInterfaces = new ArrayList<>(new LinkedHashSet<>(extraInterfaces));
-        }
-      }
-      clazz.addExtraInterfaces(extraInterfaces, appView.dexItemFactory());
-    }
-  }
-
-  private List<DexType> emulatedInterfacesOf(DexClass superClazz) {
-    if (superClazz.type == factory.objectType) {
-      return Collections.emptyList();
-    }
-    ArrayList<DexType> itfs = new ArrayList<>();
-    LinkedList<DexType> workList = new LinkedList<>();
-    workList.add(superClazz.type);
-    while (!workList.isEmpty()) {
-      DexType dexType = workList.removeFirst();
-      DexClass dexClass = appView.definitionFor(dexType);
-      if (dexClass != null) {
-        if (dexClass.superType != factory.objectType) {
-          workList.add(dexClass.superType);
-        }
-        for (DexType itf : dexClass.interfaces.values) {
-          if (emulatedInterfaces.containsKey(itf)) {
-            itfs.add(itf);
-          } else {
-            workList.add(itf);
-          }
-        }
-      }
-    }
-    return itfs;
-  }
-
   /**
    * Move static and default interface methods to companion classes, add missing methods to forward
    * to moved default methods implementation.
@@ -992,7 +934,7 @@
     if (appView.options().isDesugaredLibraryCompilation()) {
       generateEmulateInterfaceLibrary(builder);
     }
-    duplicateEmulatedInterfaces();
+    new DesugaredLibraryEmulatedInterfaceDuplicator(appView).duplicateEmulatedInterfaces();
 
     // Process all classes first. Add missing forwarding methods to
     // replace desugared default interface methods.
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/AllMapsTestClass.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/AllMapsTestClass.java
new file mode 100644
index 0000000..4c15672
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/AllMapsTestClass.java
@@ -0,0 +1,347 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class AllMapsTestClass {
+  // Program class extending ConcurrentHashMap.
+  static class NullableConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {
+    NullableConcurrentHashMap() {
+      super();
+    }
+
+    @SuppressWarnings("NullableProblems")
+    @Override
+    public V put(K key, V value) {
+      if (key == null || value == null) {
+        return null;
+      }
+      return super.put(key, value);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+  // Program class extending the library class HashMap, implementing Map.
+  static class NullableHashMap<K, V> extends HashMap<K, V> {
+    NullableHashMap() {
+      super();
+    }
+
+    @SuppressWarnings("NullableProblems")
+    @Override
+    public V put(K key, V value) {
+      if (key == null || value == null) {
+        return null;
+      }
+      return super.put(key, value);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+  // Program class implementing Map.
+  static class NullableMap<K, V> implements Map<K, V> {
+    private HashMap<K, V> map = new HashMap<>();
+
+    NullableMap() {
+      super();
+    }
+
+    @Override
+    public int size() {
+      return map.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return map.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+      return map.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+      return map.containsValue(value);
+    }
+
+    @Override
+    public V get(Object key) {
+      return map.get(key);
+    }
+
+    @Override
+    public V put(K key, V value) {
+      return map.put(key, value);
+    }
+
+    @Override
+    public V remove(Object key) {
+      return map.remove(key);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+
+    @Override
+    public void clear() {
+      map.clear();
+    }
+
+    @Override
+    public Set<K> keySet() {
+      return map.keySet();
+    }
+
+    @Override
+    public Collection<V> values() {
+      return map.values();
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+      return map.entrySet();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof NullableMap)) return false;
+      NullableMap<?, ?> that = (NullableMap<?, ?>) o;
+      return map.equals(that.map);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(map);
+    }
+  }
+  // Program class implementing ConcurrentMap.
+  static class NullableConcurrentMap<K, V> implements ConcurrentMap<K, V> {
+    private HashMap<K, V> map = new HashMap<>();
+
+    NullableConcurrentMap() {
+      super();
+    }
+
+    @Override
+    public int size() {
+      return map.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return map.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+      return map.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+      return map.containsValue(value);
+    }
+
+    @Override
+    public V get(Object key) {
+      return map.get(key);
+    }
+
+    @Override
+    public V put(K key, V value) {
+      return map.put(key, value);
+    }
+
+    @Override
+    public V remove(Object key) {
+      return map.remove(key);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+
+    @Override
+    public void clear() {
+      map.clear();
+    }
+
+    @Override
+    public Set<K> keySet() {
+      return map.keySet();
+    }
+
+    @Override
+    public Collection<V> values() {
+      return map.values();
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+      return map.entrySet();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof NullableConcurrentMap)) return false;
+      NullableConcurrentMap<?, ?> that = (NullableConcurrentMap<?, ?>) o;
+      return map.equals(that.map);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(map);
+    }
+
+    @Override
+    public V putIfAbsent(K key, V value) {
+      return null;
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+      return false;
+    }
+
+    @Override
+    public boolean replace(K key, V oldValue, V newValue) {
+      return false;
+    }
+
+    @Override
+    public V replace(K key, V value) {
+      return null;
+    }
+  }
+
+  static class Data {
+    final int id;
+    final String name;
+
+    Data(int id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof Data)) return false;
+      Data data = (Data) o;
+      return id == data.id && name.equals(data.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, name);
+    }
+
+    @Override
+    public String toString() {
+      return "Data{" + "id=" + id + ", name='" + name + '\'' + '}';
+    }
+  }
+
+  public static void main(String[] args) {
+    Gson gson = new Gson();
+
+    HashMap<Integer, Data> hashMap = new HashMap<>();
+    NullableHashMap<Integer, Data> nullableHashMap = new NullableHashMap<>();
+    NullableMap<Integer, Data> nullableMap = new NullableMap<>();
+    NullableConcurrentMap<Integer, Data> nullableConcurrentMap = new NullableConcurrentMap<>();
+    ConcurrentHashMap<Integer, Data> concurrentHashMap = new ConcurrentHashMap<>();
+    NullableConcurrentHashMap<Integer, Data> nullableConcurrentHashMap =
+        new NullableConcurrentHashMap<>();
+
+    fillMap(hashMap);
+    fillMap(nullableHashMap);
+    fillMap(nullableMap);
+    fillMap(nullableConcurrentMap);
+    fillMap(concurrentHashMap);
+    fillMap(nullableConcurrentHashMap);
+
+    // Serialization.
+    String hashMapJson = gson.toJson(hashMap);
+    String nullableHashMapJson = gson.toJson(nullableHashMap);
+    String nullableMapJson = gson.toJson(nullableMap);
+    String nullableConcurrentMapJson = gson.toJson(nullableConcurrentMap);
+    String concurrentHashMapJson = gson.toJson(concurrentHashMap);
+    String nullableConcurrentHashMapJson = gson.toJson(nullableConcurrentHashMap);
+
+    // Deserialization.
+    Type hashMapType = new TypeToken<HashMap<Integer, Data>>() {}.getType();
+    HashMap<Integer, Data> hashMapDeserialized = gson.fromJson(hashMapJson, hashMapType);
+    Type nullableHashMapType = new TypeToken<NullableHashMap<Integer, Data>>() {}.getType();
+    NullableHashMap<Integer, Data> nullableHashMapDeserialized =
+        gson.fromJson(nullableHashMapJson, nullableHashMapType);
+    Type nullableMapType = new TypeToken<NullableMap<Integer, Data>>() {}.getType();
+    NullableMap<Integer, Data> nullableMapDeserialized =
+        gson.fromJson(nullableMapJson, nullableMapType);
+    Type nullableConcurrentMapType =
+        new TypeToken<NullableConcurrentMap<Integer, Data>>() {}.getType();
+    NullableConcurrentMap<Integer, Data> nullableConcurrentMapDeserialized =
+        gson.fromJson(nullableConcurrentMapJson, nullableConcurrentMapType);
+    Type concurrentHashMapType = new TypeToken<ConcurrentHashMap<Integer, Data>>() {}.getType();
+    ConcurrentHashMap<Integer, Data> concurrentHashMapDeserialized =
+        gson.fromJson(concurrentHashMapJson, concurrentHashMapType);
+    Type nullableConcurrentHashMapType =
+        new TypeToken<NullableConcurrentHashMap<Integer, Data>>() {}.getType();
+    NullableConcurrentHashMap<Integer, Data> nullableConcurrentHashMapDeserialized =
+        gson.fromJson(nullableConcurrentHashMapJson, nullableConcurrentHashMapType);
+
+    // Printing.
+    System.out.println(hashMap.getClass() == hashMapDeserialized.getClass());
+    System.out.println(hashMap.equals(hashMapDeserialized));
+    System.out.println(nullableHashMap.getClass() == nullableHashMapDeserialized.getClass());
+    System.out.println(nullableHashMap.equals(nullableHashMapDeserialized));
+    System.out.println(nullableMap.getClass() == nullableMapDeserialized.getClass());
+    System.out.println(nullableMap.equals(nullableMapDeserialized));
+    System.out.println(
+        nullableConcurrentMap.getClass() == nullableConcurrentMapDeserialized.getClass());
+    System.out.println(nullableConcurrentMap.equals(nullableConcurrentMapDeserialized));
+    System.out.println(concurrentHashMap.getClass() == concurrentHashMapDeserialized.getClass());
+    System.out.println(concurrentHashMap.equals(concurrentHashMapDeserialized));
+    System.out.println(
+        nullableConcurrentHashMap.getClass() == nullableConcurrentHashMapDeserialized.getClass());
+    System.out.println(nullableConcurrentHashMap.equals(nullableConcurrentHashMapDeserialized));
+  }
+
+  public static void fillMap(Map<Integer, Data> map) {
+    map.put(1, new Data(1, "a"));
+    map.put(2, new Data(2, "b"));
+    map.put(3, new Data(3, "c"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GetGenericInterfaceTest.java
similarity index 97%
rename from src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java
rename to src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GetGenericInterfaceTest.java
index 29bb1cf..249f26f 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GetGenericInterfaceTest.java
@@ -1,14 +1,14 @@
 // Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-//  for details. All rights reserved. Use of this source code is governed by a
+// for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.desugar.desugaredlibrary;
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
 import dalvik.system.PathClassLoader;
 import java.sql.SQLDataException;
@@ -32,7 +32,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class LibrarySubclassInterfaceTest extends DesugaredLibraryTestBase {
+public class GetGenericInterfaceTest extends DesugaredLibraryTestBase {
 
   private final TestParameters parameters;
   private final boolean shrinkDesugaredLibrary;
@@ -43,7 +43,7 @@
         BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
   }
 
-  public LibrarySubclassInterfaceTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+  public GetGenericInterfaceTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
     this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
     this.parameters = parameters;
   }
@@ -53,7 +53,7 @@
     KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
     String stdOut =
         testForD8()
-            .addInnerClasses(LibrarySubclassInterfaceTest.class)
+            .addInnerClasses(GetGenericInterfaceTest.class)
             .setMinApi(parameters.getApiLevel())
             .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
             .compile()
@@ -73,7 +73,7 @@
     KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
     String stdOut =
         testForR8(Backend.DEX)
-            .addInnerClasses(LibrarySubclassInterfaceTest.class)
+            .addInnerClasses(GetGenericInterfaceTest.class)
             .addKeepMainRule(Executor.class)
             .noMinification()
             .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonAllMapsTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonAllMapsTest.java
new file mode 100644
index 0000000..7938465
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonAllMapsTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class GsonAllMapsTest extends GsonDesugaredLibraryTestBase {
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+  private static final String[] EXPECTED_RESULT =
+      new String[] {
+        "true", "true", "true", "true", "true", "true", "true", "true", "true", "true", "true",
+        "true"
+      };
+
+  @Parameters(name = "shrinkDesugaredLibrary: {0}, runtime: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public GsonAllMapsTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testGsonMapD8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(AllMapsTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), AllMapsTestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testGsonMapR8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(AllMapsTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .addKeepMainRule(AllMapsTestClass.class)
+        .addKeepRuleFiles(GSON_CONFIGURATION)
+        .allowUnusedProguardConfigurationRules()
+        .allowDiagnosticMessages()
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), AllMapsTestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonDesugaredLibraryTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonDesugaredLibraryTestBase.java
new file mode 100644
index 0000000..e761248
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonDesugaredLibraryTestBase.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
+
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public abstract class GsonDesugaredLibraryTestBase extends DesugaredLibraryTestBase {
+  protected static final Path GSON_CONFIGURATION =
+      Paths.get("src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg");
+  protected static final Path GSON_2_8_1_JAR = Paths.get("third_party/iosched_2019/gson-2.8.1.jar");
+  // We only need here, for all subclasses of java.util.Collection and java.util.Map,
+  // to keep the signature attribute.
+  protected static final String GSON_LIBRARY_KEEP_RULES =
+      "-keepattributes Signature\n"
+          + "-keepattributes EnclosingMethod\n"
+          + "-keepattributes InnerClasses\n";
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonMapTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonMapTest.java
deleted file mode 100644
index fa5c6d3..0000000
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonMapTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.desugar.desugaredlibrary.gson;
-
-import com.android.tools.r8.R8TestRunResult;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
-import com.android.tools.r8.desugar.desugaredlibrary.gson.TestClasses.TestClass;
-import com.android.tools.r8.utils.BooleanUtils;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-import org.junit.Assume;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class GsonMapTest extends DesugaredLibraryTestBase {
-
-  private final TestParameters parameters;
-  private final boolean shrinkDesugaredLibrary;
-  private static final Path GSON_CONFIGURATION =
-      Paths.get("src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg");
-  private static final Path GSON_2_8_1_JAR = Paths.get("third_party/iosched_2019/gson-2.8.1.jar");
-
-  @Parameters(name = "shrinkDesugaredLibrary: {0}, runtime: {1}")
-  public static List<Object[]> data() {
-    return buildParameters(
-        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
-  }
-
-  public GsonMapTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
-    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void testGsonMap() throws Exception {
-    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
-    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
-    R8TestRunResult runResult =
-        testForR8(parameters.getBackend())
-            .addProgramClassesAndInnerClasses(TestClasses.class)
-            .addProgramFiles(GSON_2_8_1_JAR)
-            .addKeepMainRule(TestClass.class)
-            .addKeepRuleFiles(GSON_CONFIGURATION)
-            .allowUnusedProguardConfigurationRules()
-            .addOptionsModification(opt -> opt.ignoreMissingClasses = true)
-            .allowDiagnosticMessages()
-            .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
-            .setMinApi(parameters.getApiLevel())
-            .compile()
-            .addDesugaredCoreLibraryRunClassPath(
-                this::buildDesugaredLibrary,
-                parameters.getApiLevel(),
-                keepRuleConsumer.get(),
-                shrinkDesugaredLibrary)
-            .run(parameters.getRuntime(), TestClass.class);
-    // TODO(b/167649682): Should be always true.
-    // .assertSuccessWithOutputLines("true", "true", "true", "true", "true");
-    if (shrinkDesugaredLibrary) {
-      runResult.assertSuccessWithOutputLines("true", "true", "false", "false", "false");
-    } else {
-      runResult.assertSuccessWithOutputLines("true", "true", "false", "true", "false");
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonOptionalTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonOptionalTest.java
new file mode 100644
index 0000000..6b75eb9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonOptionalTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class GsonOptionalTest extends GsonDesugaredLibraryTestBase {
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+
+  @Parameterized.Parameters(name = "shrinkDesugaredLibrary: {0}, runtime: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public GsonOptionalTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testGsonOptionalD8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(OptionalTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .addOptionsModification(opt -> opt.ignoreMissingClasses = true)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), OptionalTestClass.class)
+        .assertSuccessWithOutputLines("true", "true");
+  }
+
+  @Test
+  public void testGsonOptionalR8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(OptionalTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .addKeepMainRule(OptionalTestClass.class)
+        .addKeepRuleFiles(GSON_CONFIGURATION)
+        .allowUnusedProguardConfigurationRules()
+        .addOptionsModification(opt -> opt.ignoreMissingClasses = true)
+        .allowDiagnosticMessages()
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), OptionalTestClass.class)
+        .assertSuccessWithOutputLines("true", "true");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/OptionalTestClass.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/OptionalTestClass.java
new file mode 100644
index 0000000..fbc2c31
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/OptionalTestClass.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+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.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Objects;
+import java.util.Optional;
+
+public class OptionalTestClass {
+  static class Data {
+    final int id;
+    final String name;
+
+    Data(int id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof Data)) return false;
+      Data data = (Data) o;
+      return id == data.id && name.equals(data.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, name);
+    }
+
+    @Override
+    public String toString() {
+      return "Data{" + "id=" + id + ", name='" + name + '\'' + '}';
+    }
+  }
+
+  static class OptionalAdapter<T> extends TypeAdapter<Optional<T>> {
+    private final TypeAdapter<T> delegate;
+
+    public OptionalAdapter(TypeAdapter<T> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void write(JsonWriter out, Optional<T> value) throws IOException {
+      if (!value.isPresent()) {
+        out.nullValue();
+        return;
+      }
+      delegate.write(out, value.get());
+    }
+
+    @Override
+    public Optional<T> read(JsonReader in) throws IOException {
+      if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return Optional.empty();
+      }
+      return Optional.of(delegate.read(in));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static OptionalAdapter getInstance(TypeToken typeToken) {
+      TypeAdapter delegate;
+      Type type = typeToken.getType();
+      assert type instanceof ParameterizedType;
+      Type innerType = ((ParameterizedType) type).getActualTypeArguments()[0];
+      delegate = new Gson().getAdapter(TypeToken.get(innerType));
+      return new OptionalAdapter<>(delegate);
+    }
+  }
+
+  public static void main(String[] args) {
+    GsonBuilder builder = new GsonBuilder();
+    builder.registerTypeAdapter(
+        Optional.class, OptionalAdapter.getInstance(new TypeToken<Optional<Data>>() {}));
+    Gson gson = builder.create();
+    Optional<Data> optionalData = Optional.of(new Data(1, "a"));
+    String optionalDataSerialized = gson.toJson(optionalData);
+    Optional<Data> optionalDataDeserialized = gson.fromJson(optionalDataSerialized, Optional.class);
+    System.out.println(optionalData.getClass() == optionalDataDeserialized.getClass());
+    System.out.println(optionalData.equals(optionalDataDeserialized));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/TestClasses.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/TestClasses.java
deleted file mode 100644
index 7b57814..0000000
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/TestClasses.java
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.desugar.desugaredlibrary.gson;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import java.lang.reflect.Type;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-public class TestClasses {
-
-  // Program class extending ConcurrentHashMap.
-  static class NullableConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {
-    NullableConcurrentHashMap() {
-      super();
-    }
-
-    @SuppressWarnings("NullableProblems")
-    @Override
-    public V put(K key, V value) {
-      if (key == null || value == null) {
-        return null;
-      }
-      return super.put(key, value);
-    }
-
-    @Override
-    public void putAll(Map<? extends K, ? extends V> m) {
-      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
-        put(entry.getKey(), entry.getValue());
-      }
-    }
-  }
-
-  // Program class extending the library class HashMap, implementing Map.
-  static class NullableHashMap<K, V> extends HashMap<K, V> {
-    NullableHashMap() {
-      super();
-    }
-
-    @SuppressWarnings("NullableProblems")
-    @Override
-    public V put(K key, V value) {
-      if (key == null || value == null) {
-        return null;
-      }
-      return super.put(key, value);
-    }
-
-    @Override
-    public void putAll(Map<? extends K, ? extends V> m) {
-      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
-        put(entry.getKey(), entry.getValue());
-      }
-    }
-  }
-
-  // Program class implementing Map.
-  static class NullableMap<K, V> implements Map<K, V> {
-    private Map<K, V> map = new HashMap<>();
-
-    NullableMap() {
-      super();
-    }
-
-    @Override
-    public int size() {
-      return map.size();
-    }
-
-    @Override
-    public boolean isEmpty() {
-      return map.isEmpty();
-    }
-
-    @Override
-    public boolean containsKey(Object key) {
-      return map.containsKey(key);
-    }
-
-    @Override
-    public boolean containsValue(Object value) {
-      return map.containsValue(value);
-    }
-
-    @Override
-    public V get(Object key) {
-      return map.get(key);
-    }
-
-    @Nullable
-    @Override
-    public V put(K key, V value) {
-      return map.put(key, value);
-    }
-
-    @Override
-    public V remove(Object key) {
-      return map.remove(key);
-    }
-
-    @Override
-    public void putAll(@NotNull Map<? extends K, ? extends V> m) {
-      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
-        put(entry.getKey(), entry.getValue());
-      }
-    }
-
-    @Override
-    public void clear() {
-      map.clear();
-    }
-
-    @NotNull
-    @Override
-    public Set<K> keySet() {
-      return map.keySet();
-    }
-
-    @NotNull
-    @Override
-    public Collection<V> values() {
-      return map.values();
-    }
-
-    @NotNull
-    @Override
-    public Set<Entry<K, V>> entrySet() {
-      return map.entrySet();
-    }
-  }
-
-  static class Data {
-    final int id;
-    final String name;
-
-    Data(int id, String name) {
-      this.id = id;
-      this.name = name;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) return true;
-      if (!(o instanceof Data)) return false;
-      Data data = (Data) o;
-      return id == data.id && name.equals(data.name);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(id, name);
-    }
-
-    @Override
-    public String toString() {
-      return "Data{" + "id=" + id + ", name='" + name + '\'' + '}';
-    }
-  }
-
-  static class TestClass {
-
-    public static void main(String[] args) {
-      Gson gson = new Gson();
-      HashMap<Integer, Data> hashMap = new HashMap<>();
-      NullableHashMap<Integer, Data> nullableHashMap = new NullableHashMap<>();
-      NullableMap<Integer, Data> nullableMap = new NullableMap<>();
-      ConcurrentHashMap<Integer, Data> concurrentHashMap = new ConcurrentHashMap<>();
-      NullableConcurrentHashMap<Integer, Data> nullableConcurrentHashMap =
-          new NullableConcurrentHashMap<>();
-
-      fillMap(hashMap);
-      fillMap(nullableHashMap);
-      fillMap(nullableMap);
-      fillMap(concurrentHashMap);
-      fillMap(nullableConcurrentHashMap);
-
-      String hashMapJson = gson.toJson(hashMap);
-      String nullableHashMapJson = gson.toJson(nullableHashMap);
-      String nullableMapJson = gson.toJson(nullableMap);
-      String concurrentHashMapJson = gson.toJson(concurrentHashMap);
-      String nullableConcurrentHashMapJson = gson.toJson(nullableConcurrentHashMap);
-
-      Type hashMapType = new TypeToken<HashMap<Integer, Data>>() {}.getType();
-      HashMap<Integer, Data> hashMapDeserialized = gson.fromJson(hashMapJson, hashMapType);
-      Type nullableHashMapType = new TypeToken<HashMap<Integer, Data>>() {}.getType();
-      HashMap<Integer, Data> nullableHashMapDeserialized =
-          gson.fromJson(nullableHashMapJson, nullableHashMapType);
-      Type nullableMapType = new TypeToken<HashMap<Integer, Data>>() {}.getType();
-      HashMap<Integer, Data> nullableMapDeserialized =
-          gson.fromJson(nullableMapJson, nullableMapType);
-      Type concurrentHashMapType = new TypeToken<ConcurrentHashMap<Integer, Data>>() {}.getType();
-      ConcurrentHashMap<Integer, Data> concurrentHashMapDeserialized =
-          gson.fromJson(concurrentHashMapJson, concurrentHashMapType);
-      Type nullableConcurrentHashMapType =
-          new TypeToken<NullableConcurrentHashMap<Integer, Data>>() {}.getType();
-      NullableConcurrentHashMap<Integer, Data> nullableConcurrentHashMapDeserialized =
-          gson.fromJson(nullableConcurrentHashMapJson, nullableConcurrentHashMapType);
-
-      System.out.println(hashMap.equals(hashMapDeserialized));
-      System.out.println(nullableHashMap.equals(nullableHashMapDeserialized));
-      System.out.println(nullableMap.equals(nullableMapDeserialized));
-      System.out.println(concurrentHashMap.equals(concurrentHashMapDeserialized));
-      System.out.println(nullableConcurrentHashMap.equals(nullableConcurrentHashMapDeserialized));
-    }
-
-    public static void fillMap(Map<Integer, Data> map) {
-      map.put(1, new Data(1, "a"));
-      map.put(2, new Data(2, "b"));
-      map.put(3, new Data(3, "c"));
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
index 51f38a6..da7bcba 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
@@ -1,9 +1,8 @@
 # Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
 # for details. All rights reserved. Use of this source code is governed by a
 # BSD-style license that can be found in the LICENSE file.
-
-# Gson uses generic type information stored in a class file when working with fields. R8
-# removes such information by default, so configure it to keep all of it.
+# Gson uses generic type information stored in a class file when working with fields.
+# R8 removes such information by default, so configure it to keep all of it.
 -keepattributes Signature
 -keepattributes EnclosingMethod
 -keepattributes InnerClasses
@@ -14,10 +13,13 @@
 # Gson specific classes
 -dontwarn sun.misc.Unsafe
 
-#-keep class com.google.gson.stream.** { *; }
 # Application classes that will be serialized/deserialized over Gson
--keep class com.android.tools.r8.desugar.desugaredlibrary.gson.GsonMapTest.Data { <fields>; }
--keep class com.android.tools.r8.desugar.desugaredlibrary.gson.GsonMapTest.NullableConcurrentHashMap { <fields>; }
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$Data { <fields>; }
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableConcurrentHashMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableHashMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableConcurrentMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.OptionalTestClass$Data { <fields>; }
 
 # Prevent R8 from stripping interface information from TypeAdapter, TypeAdapterFactory,
 # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)