Version 1.4.42

Cherry pick: Fix interface method minification bug
CL: https://r8-review.googlesource.com/c/r8/+/34402

Cherry pick: Reproduce interface method minification bug
CL: https://r8-review.googlesource.com/c/r8/+/34380

Cherry pick: Separate interface method minification from class method minification
CL: https://r8-review.googlesource.com/c/r8/+/34300

Cherry pick: Simplify interface method minification
CL: https://r8-review.googlesource.com/c/r8/+/34280

Cherry pick: Strengthen no duplicate methods check
CL: https://r8-review.googlesource.com/c/r8/+/34123

Cherry pick: Verify absence of duplicate methods
CL: https://r8-review.googlesource.com/c/r8/+/33930

Cherry pick: Disallow direct manipulation of method arrays
CL: https://r8-review.googlesource.com/c/r8/+/33929

Bug: 123730537
Change-Id: If8ca0780f693aeec4a39946e5a2a652db5abe0a8
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 89efb56..049854e 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -611,15 +611,17 @@
         new DiscardedChecker(rootSet, application, options).run();
       }
 
-      timing.begin("Minification");
-      // If we did not have keep rules, everything will be marked as keep, so no minification
-      // will happen. Just avoid the overhead.
-      NamingLens namingLens =
-          options.enableMinification
-              ? new Minifier(appView.appInfo().withLiveness(), rootSet, desugaredCallSites, options)
-                  .run(timing)
-              : NamingLens.getIdentityLens();
-      timing.end();
+      // Perform minification.
+      NamingLens namingLens;
+      if (options.enableMinification) {
+        timing.begin("Minification");
+        namingLens =
+            new Minifier(appView.appInfo().withLiveness(), rootSet, desugaredCallSites, options)
+                .run(timing);
+        timing.end();
+      } else {
+        namingLens = NamingLens.getIdentityLens();
+      }
 
       ProguardMapSupplier proguardMapSupplier;
 
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinker.java b/src/main/java/com/android/tools/r8/ResourceShrinker.java
index 47ff5ee..6267db1 100644
--- a/src/main/java/com/android/tools/r8/ResourceShrinker.java
+++ b/src/main/java/com/android/tools/r8/ResourceShrinker.java
@@ -241,11 +241,11 @@
               .filter(DexEncodedField::hasAnnotation)
               .flatMap(f -> Arrays.stream(f.annotations.annotations));
       Stream<DexAnnotation> virtualMethodAnnotations =
-          Arrays.stream(classDef.virtualMethods())
+          classDef.virtualMethods().stream()
               .filter(DexEncodedMethod::hasAnnotation)
               .flatMap(m -> Arrays.stream(m.annotations.annotations));
       Stream<DexAnnotation> directMethodAnnotations =
-          Arrays.stream(classDef.directMethods())
+          classDef.directMethods().stream()
               .filter(DexEncodedMethod::hasAnnotation)
               .flatMap(m -> Arrays.stream(m.annotations.annotations));
       Stream<DexAnnotation> classAnnotations = Arrays.stream(classDef.annotations.annotations);
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 27a34f7..0d8b672 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.4.41";
+  public static final String LABEL = "1.4.42";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 73f7921..e8ebbbb 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -563,7 +563,7 @@
   }
 
   private void writeEncodedFields(DexEncodedField[] fields) {
-    assert PresortedComparable.isSorted(fields);
+    assert PresortedComparable.isSorted(Arrays.asList(fields));
     int currentOffset = 0;
     for (DexEncodedField field : fields) {
       int nextOffset = mapping.getOffsetFor(field.field);
@@ -574,7 +574,7 @@
     }
   }
 
-  private void writeEncodedMethods(DexEncodedMethod[] methods, boolean clearBodies) {
+  private void writeEncodedMethods(List<DexEncodedMethod> methods, boolean clearBodies) {
     assert PresortedComparable.isSorted(methods);
     int currentOffset = 0;
     for (DexEncodedMethod method : methods) {
@@ -602,8 +602,8 @@
     mixedSectionOffsets.setOffsetFor(clazz, dest.position());
     dest.putUleb128(clazz.staticFields().length);
     dest.putUleb128(clazz.instanceFields().length);
-    dest.putUleb128(clazz.directMethods().length);
-    dest.putUleb128(clazz.virtualMethods().length);
+    dest.putUleb128(clazz.directMethods().size());
+    dest.putUleb128(clazz.virtualMethods().size());
     writeEncodedFields(clazz.staticFields());
     writeEncodedFields(clazz.instanceFields());
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
index fa5d8a8..1ef2f68 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.OrderedMergingIterator;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.Function;
 
@@ -37,10 +38,11 @@
         parameterAnnotations.add(method);
       }
     }
-    assert isSorted(clazz.staticFields());
-    assert isSorted(clazz.instanceFields());
+    assert isSorted(Arrays.asList(clazz.staticFields()));
+    assert isSorted(Arrays.asList(clazz.instanceFields()));
     OrderedMergingIterator<DexEncodedField, DexField> fields =
-        new OrderedMergingIterator<>(clazz.staticFields(), clazz.instanceFields());
+        new OrderedMergingIterator<>(
+            Arrays.asList(clazz.staticFields()), Arrays.asList(clazz.instanceFields()));
     fieldAnnotations = new ArrayList<>();
     while (fields.hasNext()) {
       DexEncodedField field = fields.next();
@@ -108,11 +110,13 @@
     throw new Unreachable();
   }
 
-  private static <T extends PresortedComparable<T>> boolean isSorted(KeyedDexItem<T>[] items) {
+  private static <T extends PresortedComparable<T>> boolean isSorted(
+      List<? extends KeyedDexItem<T>> items) {
     return isSorted(items, KeyedDexItem::getKey);
   }
 
-  private static <S, T extends Comparable<T>> boolean isSorted(S[] items, Function<S, T> getter) {
+  private static <S, T extends Comparable<T>> boolean isSorted(
+      List<S> items, Function<S, T> getter) {
     T current = null;
     for (S item : items) {
       T next = getter.apply(item);
diff --git a/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java b/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java
index 21900a01..19d7e24 100644
--- a/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/DexByteCodeWriter.java
@@ -11,7 +11,6 @@
 import java.io.PrintStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.Arrays;
 import java.util.function.Consumer;
 
 public abstract class DexByteCodeWriter {
@@ -76,8 +75,8 @@
 
   private boolean anyMethodMatches(DexClass clazz) {
     return !options.hasMethodsFilter()
-        || Arrays.stream(clazz.virtualMethods()).anyMatch(options::methodMatchesFilter)
-        || Arrays.stream(clazz.directMethods()).anyMatch(options::methodMatchesFilter);
+        || clazz.virtualMethods().stream().anyMatch(options::methodMatchesFilter)
+        || clazz.directMethods().stream().anyMatch(options::methodMatchesFilter);
   }
 
   private void writeClass(DexProgramClass clazz, PrintStream ps) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index b229074..828d98d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -8,10 +8,14 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.kotlin.KotlinInfo;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -19,6 +23,11 @@
 
 public abstract class DexClass extends DexDefinition {
 
+  public interface MethodSetter {
+
+    void setMethod(int index, DexEncodedMethod method);
+  }
+
   private static final DexEncodedMethod[] NO_METHODS = {};
   private static final DexEncodedField[] NO_FIELDS = {};
 
@@ -29,22 +38,17 @@
   public DexTypeList interfaces;
   public DexString sourceFile;
 
-  /**
-   * Access has to be synchronized during concurrent collection/writing phase.
-   */
-  protected DexEncodedField[] staticFields;
-  /**
-   * Access has to be synchronized during concurrent collection/writing phase.
-   */
-  protected DexEncodedField[] instanceFields;
-  /**
-   * Access has to be synchronized during concurrent collection/writing phase.
-   */
-  protected DexEncodedMethod[] directMethods;
-  /**
-   * Access has to be synchronized during concurrent collection/writing phase.
-   */
-  protected DexEncodedMethod[] virtualMethods;
+  /** Access has to be synchronized during concurrent collection/writing phase. */
+  protected DexEncodedField[] staticFields = NO_FIELDS;
+
+  /** Access has to be synchronized during concurrent collection/writing phase. */
+  protected DexEncodedField[] instanceFields = NO_FIELDS;
+
+  /** Access has to be synchronized during concurrent collection/writing phase. */
+  protected DexEncodedMethod[] directMethods = NO_METHODS;
+
+  /** Access has to be synchronized during concurrent collection/writing phase. */
+  protected DexEncodedMethod[] virtualMethods = NO_METHODS;
 
   /** Enclosing context of this class if it is an inner class, null otherwise. */
   private EnclosingMethodAttribute enclosingMethod;
@@ -124,20 +128,129 @@
     throw new Unreachable();
   }
 
-  public DexEncodedMethod[] directMethods() {
-    return directMethods;
+  public List<DexEncodedMethod> directMethods() {
+    assert directMethods != null;
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(directMethods));
+    }
+    return Arrays.asList(directMethods);
+  }
+
+  public void appendDirectMethod(DexEncodedMethod method) {
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[directMethods.length + 1];
+    System.arraycopy(directMethods, 0, newMethods, 0, directMethods.length);
+    newMethods[directMethods.length] = method;
+    directMethods = newMethods;
+    assert verifyCorrectnessOfMethodHolder(method);
+    assert verifyNoDuplicateMethods();
+  }
+
+  public void appendDirectMethods(Collection<DexEncodedMethod> methods) {
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[directMethods.length + methods.size()];
+    System.arraycopy(directMethods, 0, newMethods, 0, directMethods.length);
+    int i = directMethods.length;
+    for (DexEncodedMethod method : methods) {
+      newMethods[i] = method;
+      i++;
+    }
+    directMethods = newMethods;
+    assert verifyCorrectnessOfMethodHolders(methods);
+    assert verifyNoDuplicateMethods();
+  }
+
+  public void removeDirectMethod(int index) {
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[directMethods.length - 1];
+    System.arraycopy(directMethods, 0, newMethods, 0, index);
+    System.arraycopy(directMethods, index + 1, newMethods, index, directMethods.length - index - 1);
+    directMethods = newMethods;
+  }
+
+  public void setDirectMethod(int index, DexEncodedMethod method) {
+    directMethods[index] = method;
+    assert verifyCorrectnessOfMethodHolder(method);
+    assert verifyNoDuplicateMethods();
   }
 
   public void setDirectMethods(DexEncodedMethod[] values) {
     directMethods = MoreObjects.firstNonNull(values, NO_METHODS);
+    assert verifyCorrectnessOfMethodHolders(directMethods());
+    assert verifyNoDuplicateMethods();
   }
 
-  public DexEncodedMethod[] virtualMethods() {
-    return virtualMethods;
+  public List<DexEncodedMethod> virtualMethods() {
+    assert virtualMethods != null;
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(virtualMethods));
+    }
+    return Arrays.asList(virtualMethods);
+  }
+
+  public void appendVirtualMethod(DexEncodedMethod method) {
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[virtualMethods.length + 1];
+    System.arraycopy(virtualMethods, 0, newMethods, 0, virtualMethods.length);
+    newMethods[virtualMethods.length] = method;
+    virtualMethods = newMethods;
+    assert verifyCorrectnessOfMethodHolder(method);
+    assert verifyNoDuplicateMethods();
+  }
+
+  public void appendVirtualMethods(Collection<DexEncodedMethod> methods) {
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[virtualMethods.length + methods.size()];
+    System.arraycopy(virtualMethods, 0, newMethods, 0, virtualMethods.length);
+    int i = virtualMethods.length;
+    for (DexEncodedMethod method : methods) {
+      newMethods[i] = method;
+      i++;
+    }
+    virtualMethods = newMethods;
+    assert verifyCorrectnessOfMethodHolders(methods);
+    assert verifyNoDuplicateMethods();
+  }
+
+  public void removeVirtualMethod(int index) {
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[virtualMethods.length - 1];
+    System.arraycopy(virtualMethods, 0, newMethods, 0, index);
+    System.arraycopy(
+        virtualMethods, index + 1, newMethods, index, virtualMethods.length - index - 1);
+    virtualMethods = newMethods;
+  }
+
+  public void setVirtualMethod(int index, DexEncodedMethod method) {
+    virtualMethods[index] = method;
+    assert verifyCorrectnessOfMethodHolder(method);
+    assert verifyNoDuplicateMethods();
   }
 
   public void setVirtualMethods(DexEncodedMethod[] values) {
     virtualMethods = MoreObjects.firstNonNull(values, NO_METHODS);
+    assert verifyCorrectnessOfMethodHolders(virtualMethods());
+    assert verifyNoDuplicateMethods();
+  }
+
+  private boolean verifyCorrectnessOfMethodHolder(DexEncodedMethod method) {
+    assert method.method.holder == type
+        : "Expected method `"
+            + method.method.toSourceString()
+            + "` to have holder `"
+            + type.toSourceString()
+            + "`";
+    return true;
+  }
+
+  private boolean verifyCorrectnessOfMethodHolders(Iterable<DexEncodedMethod> methods) {
+    for (DexEncodedMethod method : methods) {
+      assert verifyCorrectnessOfMethodHolder(method);
+    }
+    return true;
+  }
+
+  private boolean verifyNoDuplicateMethods() {
+    Set<DexMethod> unique = Sets.newIdentityHashSet();
+    for (DexEncodedMethod method : methods()) {
+      boolean changed = unique.add(method.method);
+      assert changed : "Duplicate method `" + method.method.toSourceString() + "`";
+    }
+    return true;
   }
 
   public void forEachMethod(Consumer<DexEncodedMethod> consumer) {
@@ -289,14 +402,14 @@
    * Find direct method in this class matching method.
    */
   public DexEncodedMethod lookupDirectMethod(DexMethod method) {
-    return lookupTarget(directMethods(), method);
+    return lookupTarget(directMethods, method);
   }
 
   /**
    * Find virtual method in this class matching method.
    */
   public DexEncodedMethod lookupVirtualMethod(DexMethod method) {
-    return lookupTarget(virtualMethods(), method);
+    return lookupTarget(virtualMethods, method);
   }
 
   /**
@@ -377,7 +490,9 @@
   }
 
   public DexEncodedMethod getClassInitializer() {
-    return Arrays.stream(directMethods()).filter(DexEncodedMethod::isClassInitializer).findAny()
+    return Arrays.stream(directMethods)
+        .filter(DexEncodedMethod::isClassInitializer)
+        .findAny()
         .orElse(null);
   }
 
@@ -543,7 +658,9 @@
 
   public boolean isValid() {
     assert !isInterface()
-        || Arrays.stream(virtualMethods()).noneMatch(method -> method.accessFlags.isFinal());
+        || Arrays.stream(virtualMethods).noneMatch(method -> method.accessFlags.isFinal());
+    assert verifyCorrectnessOfMethodHolders(methods());
+    assert verifyNoDuplicateMethods();
     return true;
   }
 }
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 dafb07c..57b08fb 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -226,7 +226,7 @@
 
   public boolean hasMethodsOrFields() {
     int numberOfFields = staticFields().length + instanceFields().length;
-    int numberOfMethods = directMethods().length + virtualMethods().length;
+    int numberOfMethods = directMethods().size() + virtualMethods().size();
     return numberOfFields + numberOfMethods > 0;
   }
 
@@ -269,7 +269,7 @@
     // It does not actually hurt to compute this multiple times. So racing on staticValues is OK.
     if (staticValues == SENTINEL_NOT_YET_COMPUTED) {
       synchronized (staticFields) {
-        assert PresortedComparable.isSorted(staticFields);
+        assert PresortedComparable.isSorted(Arrays.asList(staticFields));
         DexEncodedField[] fields = staticFields;
         int length = 0;
         List<DexValue> values = new ArrayList<>(fields.length);
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 0c48ea9..9182811 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions.OutlineOptions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -239,25 +240,23 @@
    * language, where interfaces "extend" their superinterface.
    */
   public void forAllImplementsSubtypes(Consumer<DexType> f) {
-    if (hierarchyLevel != INTERFACE_LEVEL) {
-      return;
+    allImplementsSubtypes().forEach(f);
+  }
+
+  public Iterable<DexType> allImplementsSubtypes() {
+    if (hierarchyLevel == INTERFACE_LEVEL) {
+      return Iterables.filter(directSubtypes, subtype -> !subtype.isInterface());
     }
-    for (DexType subtype : directSubtypes) {
-      // Filter out other interfaces.
-      if (subtype.hierarchyLevel != INTERFACE_LEVEL) {
-        f.accept(subtype);
-      }
-    }
+    return ImmutableList.of();
+  }
+
+  public static Iterable<DexType> allInterfaces(DexItemFactory dexItemFactory) {
+    assert dexItemFactory.objectType.hierarchyLevel == ROOT_LEVEL;
+    return Iterables.filter(dexItemFactory.objectType.directSubtypes, DexType::isInterface);
   }
 
   public static void forAllInterfaces(DexItemFactory factory, Consumer<DexType> f) {
-    DexType object = factory.objectType;
-    assert object.hierarchyLevel == ROOT_LEVEL;
-    for (DexType subtype : object.directSubtypes) {
-      if (subtype.isInterface()) {
-        f.accept(subtype);
-      }
-    }
+    allInterfaces(factory).forEach(f);
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/graph/PresortedComparable.java b/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
index a4ee2ee..563b40a 100644
--- a/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
+++ b/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
@@ -4,15 +4,23 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.naming.NamingLens;
+import java.util.Arrays;
+import java.util.List;
 import java.util.function.Function;
 
 public interface PresortedComparable<T> extends Presorted, Comparable<T> {
 
-  static <T extends PresortedComparable<T>> boolean isSorted(KeyedDexItem<T>[] items) {
+  static <T extends PresortedComparable<T>> boolean isSorted(
+      List<? extends KeyedDexItem<T>> items) {
     return isSorted(items, KeyedDexItem::getKey);
   }
 
   static <S, T extends Comparable<T>> boolean isSorted(S[] items, Function<S, T> getter) {
+    return isSorted(Arrays.asList(items), getter);
+  }
+
+  static <S, T extends Comparable<T>> boolean isSorted(
+      List<? extends S> items, Function<S, T> getter) {
     T current = null;
     for (S item : items) {
       T next = getter.apply(item);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index d2a233b..968b200 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
@@ -85,17 +84,14 @@
     }
 
     // Add the methods.
-    DexEncodedMethod[] existing = clazz.virtualMethods();
-    clazz.setVirtualMethods(new DexEncodedMethod[existing.length + methodsToImplement.size()]);
-    System.arraycopy(existing, 0, clazz.virtualMethods(), 0, existing.length);
-
-    for (int i = 0; i < methodsToImplement.size(); i++) {
-      DexEncodedMethod method = methodsToImplement.get(i);
+    List<DexEncodedMethod> newForwardingMethods = new ArrayList<>(methodsToImplement.size());
+    for (DexEncodedMethod method : methodsToImplement) {
       assert method.accessFlags.isPublic() && !method.accessFlags.isAbstract();
       DexEncodedMethod newMethod = addForwardingMethod(method, clazz);
-      clazz.virtualMethods()[existing.length + i] = newMethod;
+      newForwardingMethods.add(newMethod);
       createdMethods.put(newMethod, method);
     }
+    clazz.appendVirtualMethods(newForwardingMethods);
   }
 
   private DexEncodedMethod addForwardingMethod(DexEncodedMethod defaultMethod, DexClass clazz) {
@@ -148,7 +144,7 @@
         helper.merge(rewriter.getOrCreateInterfaceInfo(clazz, current, type));
       }
 
-      accumulatedVirtualMethods.addAll(Arrays.asList(clazz.virtualMethods()));
+      accumulatedVirtualMethods.addAll(clazz.virtualMethods());
 
       List<DexEncodedMethod> defaultMethodsInDirectInterface = helper.createFullList();
 
@@ -201,7 +197,7 @@
     current = clazz;
     while (true) {
       // Hide candidates by virtual method of the class.
-      hideCandidates(Arrays.asList(current.virtualMethods()), candidates, toBeImplemented);
+      hideCandidates(current.virtualMethods(), candidates, toBeImplemented);
       if (candidates.isEmpty()) {
         return toBeImplemented;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CovariantReturnTypeAnnotationTransformer.java b/src/main/java/com/android/tools/r8/ir/desugar/CovariantReturnTypeAnnotationTransformer.java
index 955abf0..2219cf8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CovariantReturnTypeAnnotationTransformer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CovariantReturnTypeAnnotationTransformer.java
@@ -96,16 +96,7 @@
           method.annotations.keepIf(x -> !isCovariantReturnTypeAnnotation(x.annotation));
     }
     // Add the newly constructed methods to the class.
-    DexEncodedMethod[] oldVirtualMethods = clazz.virtualMethods();
-    DexEncodedMethod[] newVirtualMethods =
-        new DexEncodedMethod[oldVirtualMethods.length + covariantReturnTypeMethods.size()];
-    System.arraycopy(oldVirtualMethods, 0, newVirtualMethods, 0, oldVirtualMethods.length);
-    int i = oldVirtualMethods.length;
-    for (DexEncodedMethod syntheticMethod : covariantReturnTypeMethods) {
-      newVirtualMethods[i] = syntheticMethod;
-      i++;
-    }
-    clazz.setVirtualMethods(newVirtualMethods);
+    clazz.appendVirtualMethods(covariantReturnTypeMethods);
   }
 
   // Processes all the dalvik.annotation.codegen.CovariantReturnType and dalvik.annotation.codegen.
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
index afe0c8e..9fe23de 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
@@ -114,7 +114,7 @@
     }
 
     // If at least one bridge method was removed then update the table.
-    if (remainingMethods.size() < iface.virtualMethods().length) {
+    if (remainingMethods.size() < iface.virtualMethods().size()) {
       iface.setVirtualMethods(remainingMethods.toArray(
           new DexEncodedMethod[remainingMethods.size()]));
     }
@@ -170,7 +170,7 @@
         }
       }
     }
-    if (remainingMethods.size() < iface.directMethods().length) {
+    if (remainingMethods.size() < iface.directMethods().size()) {
       iface.setDirectMethods(remainingMethods.toArray(
           new DexEncodedMethod[remainingMethods.size()]));
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index b78f6df..a0bc184 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -513,9 +513,9 @@
       DexMethod implMethod = descriptor.implHandle.asMethod();
       DexClass implMethodHolder = definitionFor(implMethod.holder);
 
-      DexEncodedMethod[] directMethods = implMethodHolder.directMethods();
-      for (int i = 0; i < directMethods.length; i++) {
-        DexEncodedMethod encodedMethod = directMethods[i];
+      List<DexEncodedMethod> directMethods = implMethodHolder.directMethods();
+      for (int i = 0; i < directMethods.size(); i++) {
+        DexEncodedMethod encodedMethod = directMethods.get(i);
         if (implMethod.match(encodedMethod)) {
           // We need to create a new static method with the same code to be able to safely
           // relax its accessibility without making it virtual.
@@ -539,7 +539,7 @@
           dexCode.setDebugInfo(dexCode.debugInfoWithAdditionalFirstParameter(null));
           assert (dexCode.getDebugInfo() == null)
               || (callTarget.getArity() == dexCode.getDebugInfo().parameters.length);
-          directMethods[i] = newMethod;
+          implMethodHolder.setDirectMethod(i, newMethod);
           return true;
         }
       }
@@ -579,20 +579,11 @@
 
       // We may arrive here concurrently so we need must update the methods of the class atomically.
       synchronized (accessorClass) {
-        accessorClass.setDirectMethods(
-            appendMethod(accessorClass.directMethods(), accessorEncodedMethod));
+        accessorClass.appendDirectMethod(accessorEncodedMethod);
       }
 
       rewriter.converter.optimizeSynthesizedMethod(accessorEncodedMethod);
       return true;
     }
-
-    private DexEncodedMethod[] appendMethod(DexEncodedMethod[] methods, DexEncodedMethod method) {
-      int size = methods.length;
-      DexEncodedMethod[] newMethods = new DexEncodedMethod[size + 1];
-      System.arraycopy(methods, 0, newMethods, 0, size);
-      newMethods[size] = method;
-      return newMethods;
-    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
index eba995f..ade6feb 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
@@ -128,33 +128,26 @@
 
   /** Remove lambda deserialization methods. */
   public boolean removeLambdaDeserializationMethods(Iterable<DexProgramClass> classes) {
-    boolean anyRemoved = false;
     for (DexProgramClass clazz : classes) {
       // Search for a lambda deserialization method and remove it if found.
-      DexEncodedMethod[] directMethods = clazz.directMethods();
+      List<DexEncodedMethod> directMethods = clazz.directMethods();
       if (directMethods != null) {
-        int methodCount = directMethods.length;
+        int methodCount = directMethods.size();
         for (int i = 0; i < methodCount; i++) {
-          DexEncodedMethod encoded = directMethods[i];
+          DexEncodedMethod encoded = directMethods.get(i);
           DexMethod method = encoded.method;
           if (method.isLambdaDeserializeMethod(appInfo.dexItemFactory)) {
             assert encoded.accessFlags.isStatic();
             assert encoded.accessFlags.isSynthetic();
-
-            DexEncodedMethod[] newMethods = new DexEncodedMethod[methodCount - 1];
-            System.arraycopy(directMethods, 0, newMethods, 0, i);
-            System.arraycopy(directMethods, i + 1, newMethods, i, methodCount - i - 1);
-            clazz.setDirectMethods(newMethods);
-
-            anyRemoved = true;
+            clazz.removeDirectMethod(i);
 
             // We assume there is only one such method in the class.
-            break;
+            return true;
           }
         }
       }
     }
-    return anyRemoved;
+    return false;
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
index 6bd668c..c83602a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
@@ -178,9 +178,9 @@
           }
 
           // Change the return type of direct methods that return an uninstantiated type to void.
-          DexEncodedMethod[] directMethods = clazz.directMethods();
-          for (int i = 0; i < directMethods.length; ++i) {
-            DexEncodedMethod encodedMethod = directMethods[i];
+          List<DexEncodedMethod> directMethods = clazz.directMethods();
+          for (int i = 0; i < directMethods.size(); ++i) {
+            DexEncodedMethod encodedMethod = directMethods.get(i);
             DexMethod method = encodedMethod.method;
             RewrittenPrototypeDescription prototypeChanges =
                 prototypeChangesPerMethod.getOrDefault(
@@ -193,7 +193,7 @@
               // TODO(b/110806787): Can be extended to handle collisions by renaming the given
               // method.
               if (usedSignatures.add(wrapper)) {
-                directMethods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
+                clazz.setDirectMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
                 methodMapping.put(method, newMethod);
                 if (removedArgumentsInfo.hasRemovedArguments()) {
                   removedArgumentsInfoPerMethod.put(newMethod, removedArgumentsInfo);
@@ -208,9 +208,9 @@
           // all supertypes of the current class are always visited prior to the current class.
           // This is important to ensure that a method that used to override a method in its super
           // class will continue to do so after this optimization.
-          DexEncodedMethod[] virtualMethods = clazz.virtualMethods();
-          for (int i = 0; i < virtualMethods.length; ++i) {
-            DexEncodedMethod encodedMethod = virtualMethods[i];
+          List<DexEncodedMethod> virtualMethods = clazz.virtualMethods();
+          for (int i = 0; i < virtualMethods.size(); ++i) {
+            DexEncodedMethod encodedMethod = virtualMethods.get(i);
             DexMethod method = encodedMethod.method;
             RewrittenPrototypeDescription prototypeChanges =
                 getPrototypeChanges(encodedMethod, DISALLOW_ARGUMENT_REMOVAL);
@@ -228,13 +228,13 @@
                 boolean signatureIsAvailable = usedSignatures.add(wrapper);
                 assert signatureIsAvailable;
 
-                virtualMethods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
+                clazz.setVirtualMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
                 methodMapping.put(method, newMethod);
               }
             }
           }
-          for (int i = 0; i < virtualMethods.length; ++i) {
-            DexEncodedMethod encodedMethod = virtualMethods[i];
+          for (int i = 0; i < virtualMethods.size(); ++i) {
+            DexEncodedMethod encodedMethod = virtualMethods.get(i);
             DexMethod method = encodedMethod.method;
             RewrittenPrototypeDescription prototypeChanges =
                 getPrototypeChanges(encodedMethod, DISALLOW_ARGUMENT_REMOVAL);
@@ -248,7 +248,7 @@
               if (!methodPool.hasSeen(wrapper) && usedSignatures.add(wrapper)) {
                 methodPool.seen(wrapper);
 
-                virtualMethods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
+                clazz.setVirtualMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
                 methodMapping.put(method, newMethod);
 
                 boolean added =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
index f0aa91a..7056d32 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -184,12 +184,13 @@
     for (DexEncodedMethod method : clazz.methods()) {
       signatures.markSignatureAsUsed(method.method);
     }
-    for (int i = 0; i < clazz.directMethods().length; i++) {
-      DexEncodedMethod method = clazz.directMethods()[i];
+    List<DexEncodedMethod> directMethods = clazz.directMethods();
+    for (int i = 0; i < directMethods.size(); i++) {
+      DexEncodedMethod method = directMethods.get(i);
       RemovedArgumentsInfo unused = collectUnusedArguments(method);
       DexEncodedMethod newMethod = signatures.removeArguments(method, unused);
       if (newMethod != null) {
-        clazz.directMethods()[i] = newMethod;
+        clazz.setDirectMethod(i, newMethod);
         synchronized (this) {
           methodMapping.put(method.method, newMethod.method);
           removedArguments.put(newMethod.method, unused);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java
index 8469004..234bcc6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java
@@ -144,8 +144,7 @@
   }
 
   void validateDirectMethods(DexClass lambda) throws LambdaStructureError {
-    DexEncodedMethod[] directMethods = lambda.directMethods();
-    for (DexEncodedMethod method : directMethods) {
+    for (DexEncodedMethod method : lambda.directMethods()) {
       if (method.isClassInitializer()) {
         // We expect to see class initializer only if there is a singleton field.
         if (lambda.staticFields().length != 1) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index 6dac0a0..ff4a12e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -545,16 +545,13 @@
     }
 
     // Process static methods.
-    if (candidateClass.directMethods().length > 0) {
-      DexEncodedMethod[] oldMethods = hostClass.directMethods();
-      DexEncodedMethod[] extraMethods = candidateClass.directMethods();
-      DexEncodedMethod[] newMethods = new DexEncodedMethod[oldMethods.length + extraMethods.length];
-      System.arraycopy(oldMethods, 0, newMethods, 0, oldMethods.length);
-      for (int i = 0; i < extraMethods.length; i++) {
-        DexEncodedMethod method = extraMethods[i];
+    List<DexEncodedMethod> extraMethods = candidateClass.directMethods();
+    if (!extraMethods.isEmpty()) {
+      List<DexEncodedMethod> newMethods = new ArrayList<>(extraMethods.size());
+      for (DexEncodedMethod method : extraMethods) {
         DexEncodedMethod newMethod = method.toTypeSubstitutedMethod(
             factory().createMethod(hostType, method.method.proto, method.method.name));
-        newMethods[oldMethods.length + i] = newMethod;
+        newMethods.add(newMethod);
         staticizedMethods.add(newMethod);
         staticizedMethods.remove(method);
         DexMethod originalMethod = methodMapping.inverse().get(method.method);
@@ -564,7 +561,7 @@
           methodMapping.put(originalMethod, newMethod.method);
         }
       }
-      hostClass.setDirectMethods(newMethods);
+      hostClass.appendDirectMethods(newMethods);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index e53f959..bc81538 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -96,7 +96,7 @@
     // Initialize top-level naming state.
     topLevelState = new Namespace(
         getPackageBinaryNameFromJavaType(options.getProguardConfiguration().getPackagePrefix()));
-    states.computeIfAbsent("", k -> topLevelState);
+    states.put("", topLevelState);
   }
 
   static class ClassRenaming {
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
index bbb3391..be339b6 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import java.util.Map;
 import java.util.function.Function;
@@ -26,14 +27,14 @@
   Function<DexType, ?> getKeyTransform() {
     if (overloadAggressively) {
       // Use the type as the key, hence reuse names per type.
-      return a -> a;
+      return Function.identity();
     } else {
       // Always use the same key, hence do not reuse names per type.
       return a -> Void.class;
     }
   }
 
-  Map<DexField, DexString> computeRenaming(Timing timing) {
+  FieldRenaming computeRenaming(Timing timing) {
     // Reserve names in all classes first. We do this in subtyping order so we do not
     // shadow a reserved field in subclasses. While there is no concept of virtual field
     // dispatch in Java, field resolution still traverses the super type chain and external
@@ -55,7 +56,20 @@
     timing.begin("rename-references");
     renameNonReboundReferences();
     timing.end();
-    return renaming;
+    return new FieldRenaming(renaming);
+  }
+
+  static class FieldRenaming {
+
+    final Map<DexField, DexString> renaming;
+
+    private FieldRenaming(Map<DexField, DexString> renaming) {
+      this.renaming = renaming;
+    }
+
+    public static FieldRenaming empty() {
+      return new FieldRenaming(ImmutableMap.of());
+    }
   }
 
   private void reserveNamesInSubtypes(DexType type, NamingState<DexType, ?> state) {
@@ -83,7 +97,7 @@
     if (clazz == null) {
       return;
     }
-    NamingState<DexType, ?> state = getState(clazz.type);
+    NamingState<DexType, ?> state = minifierState.getState(clazz.type);
     assert state != null;
     clazz.forEachField(field -> renameField(field, state));
     type.forAllExtendsSubtypes(this::renameFieldsInSubtypes);
@@ -92,9 +106,7 @@
   private void renameField(DexEncodedField encodedField, NamingState<DexType, ?> state) {
     DexField field = encodedField.field;
     if (!state.isReserved(field.name, field.type)) {
-      renaming.put(
-          field,
-          state.assignNewNameFor(field.name, field.type, useUniqueMemberNames));
+      renaming.put(field, state.assignNewNameFor(field.name, field.type, useUniqueMemberNames));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
new file mode 100644
index 0000000..b643be3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
@@ -0,0 +1,400 @@
+// Copyright (c) 2019, 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.naming;
+
+import static com.android.tools.r8.naming.MethodNameMinifier.shuffleMethods;
+
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.naming.MethodNameMinifier.FrontierState;
+import com.android.tools.r8.naming.MethodNameMinifier.MethodNamingState;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class InterfaceMethodNameMinifier {
+
+  private final AppInfoWithLiveness appInfo;
+  private final Set<DexCallSite> desugaredCallSites;
+  private final Equivalence<DexMethod> equivalence;
+  private final FrontierState frontierState;
+  private final MemberNameMinifier<DexMethod, DexProto>.State minifierState;
+  private final InternalOptions options;
+
+  private final Map<DexCallSite, DexString> callSiteRenamings = new IdentityHashMap<>();
+
+  /** A map from DexMethods to all the states linked to interfaces they appear in. */
+  private final Map<Wrapper<DexMethod>, Set<NamingState<DexProto, ?>>> globalStateMap =
+      new HashMap<>();
+
+  /** A map from DexMethods to the first interface state it was seen in. Used to pick good names. */
+  private final Map<Wrapper<DexMethod>, NamingState<DexProto, ?>> originStates = new HashMap<>();
+
+  /**
+   * A map from DexMethods to all the definitions seen. Needed as the Wrapper equalizes them all.
+   */
+  private final Map<Wrapper<DexMethod>, Set<DexMethod>> sourceMethodsMap = new HashMap<>();
+
+  InterfaceMethodNameMinifier(
+      AppInfoWithLiveness appInfo,
+      Set<DexCallSite> desugaredCallSites,
+      Equivalence<DexMethod> equivalence,
+      FrontierState frontierState,
+      MemberNameMinifier<DexMethod, DexProto>.State minifierState,
+      InternalOptions options) {
+    this.appInfo = appInfo;
+    this.desugaredCallSites = desugaredCallSites;
+    this.equivalence = equivalence;
+    this.frontierState = frontierState;
+    this.minifierState = minifierState;
+    this.options = options;
+  }
+
+  public Comparator<Wrapper<DexMethod>> createDefaultInterfaceMethodOrdering() {
+    return (a, b) -> globalStateMap.get(b).size() - globalStateMap.get(a).size();
+  }
+
+  Map<DexCallSite, DexString> getCallSiteRenamings() {
+    return callSiteRenamings;
+  }
+
+  private void reserveNamesInInterfaces() {
+    for (DexType type : DexType.allInterfaces(appInfo.dexItemFactory)) {
+      assert type.isInterface();
+      frontierState.allocateNamingStateAndReserve(type, type, null);
+    }
+  }
+
+  void assignNamesToInterfaceMethods(Timing timing) {
+    // Reserve all the names that are required for interfaces.
+    reserveNamesInInterfaces();
+
+    // First compute a map from method signatures to a set of naming states for interfaces and
+    // frontier states of classes that implement them. We add the frontier states so that we can
+    // reserve the names for later method naming.
+    timing.begin("Compute map");
+    for (DexType type : DexType.allInterfaces(appInfo.dexItemFactory)) {
+      assert type.isInterface();
+      DexClass clazz = appInfo.definitionFor(type);
+      if (clazz != null) {
+        assert clazz.isInterface();
+        Set<NamingState<DexProto, ?>> collectedStates = getReachableStates(type);
+        for (DexEncodedMethod method : shuffleMethods(clazz.methods(), options)) {
+          addStatesToGlobalMapForMethod(method.method, collectedStates, type);
+        }
+      }
+    }
+
+    // Collect the live call sites for multi-interface lambda expression renaming. For code with
+    // desugared lambdas this is a conservative estimate, as we don't track if the generated
+    // lambda classes survive into the output. As multi-interface lambda expressions are rare
+    // this is not a big deal.
+    Set<DexCallSite> liveCallSites = Sets.union(desugaredCallSites, appInfo.callSites);
+    // If the input program contains a multi-interface lambda expression that implements
+    // interface methods with different protos, we need to make sure that
+    // the implemented lambda methods are renamed to the same name.
+    // To achieve this, we map each DexCallSite that corresponds to a lambda expression to one of
+    // the DexMethods it implements, and we unify the DexMethods that need to be renamed together.
+    Map<DexCallSite, DexMethod> callSites = new IdentityHashMap<>();
+    // Union-find structure to keep track of methods that must be renamed together.
+    // Note that if the input does not use multi-interface lambdas,
+    // unificationParent will remain empty.
+    Map<Wrapper<DexMethod>, Wrapper<DexMethod>> unificationParent = new HashMap<>();
+    liveCallSites.forEach(
+        callSite -> {
+          Set<Wrapper<DexMethod>> callSiteMethods = new HashSet<>();
+          // Don't report errors, as the set of call sites is a conservative estimate, and can
+          // refer to interfaces which has been removed.
+          Set<DexEncodedMethod> implementedMethods =
+              appInfo.lookupLambdaImplementedMethods(callSite);
+          if (implementedMethods.isEmpty()) {
+            return;
+          }
+          callSites.put(callSite, implementedMethods.iterator().next().method);
+          for (DexEncodedMethod method : implementedMethods) {
+            DexType iface = method.method.holder;
+            assert iface.isInterface();
+            Set<NamingState<DexProto, ?>> collectedStates = getReachableStates(iface);
+            addStatesToGlobalMapForMethod(method.method, collectedStates, iface);
+            callSiteMethods.add(equivalence.wrap(method.method));
+          }
+          if (callSiteMethods.size() > 1) {
+            // Implemented interfaces have different return types. Unify them.
+            Wrapper<DexMethod> mainKey = callSiteMethods.iterator().next();
+            mainKey = unificationParent.getOrDefault(mainKey, mainKey);
+            for (Wrapper<DexMethod> key : callSiteMethods) {
+              unificationParent.put(key, mainKey);
+            }
+          }
+        });
+    Map<Wrapper<DexMethod>, Set<Wrapper<DexMethod>>> unification = new HashMap<>();
+    for (Wrapper<DexMethod> key : unificationParent.keySet()) {
+      // Find root with path-compression.
+      Wrapper<DexMethod> root = unificationParent.get(key);
+      while (unificationParent.get(root) != root) {
+        Wrapper<DexMethod> k = unificationParent.get(unificationParent.get(root));
+        unificationParent.put(root, k);
+        root = k;
+      }
+      unification.computeIfAbsent(root, k -> new HashSet<>()).add(key);
+    }
+    timing.end();
+    // Go over every method and assign a name.
+    timing.begin("Allocate names");
+
+    // Sort the methods by the number of dependent states, so that we use short names for methods
+    // that are referenced in many places.
+    List<Wrapper<DexMethod>> interfaceMethods =
+        globalStateMap.keySet().stream()
+            .filter(wrapper -> unificationParent.getOrDefault(wrapper, wrapper).equals(wrapper))
+            .sorted(options.testing.minifier.createInterfaceMethodOrdering(this))
+            .collect(Collectors.toList());
+
+    // Propagate reserved names to all states.
+    List<Wrapper<DexMethod>> reservedInterfaceMethods =
+        interfaceMethods.stream()
+            .filter(wrapper -> anyIsReserved(wrapper, unification))
+            .collect(Collectors.toList());
+    for (Wrapper<DexMethod> key : reservedInterfaceMethods) {
+      propagateReservedNames(key, unification);
+    }
+
+    // Verify that there is no more to propagate.
+    assert reservedInterfaceMethods.stream()
+        .noneMatch(key -> propagateReservedNames(key, unification));
+
+    // Assign names to unreserved interface methods.
+    for (Wrapper<DexMethod> key : interfaceMethods) {
+      if (!reservedInterfaceMethods.contains(key)) {
+        assignNameToInterfaceMethod(key, unification);
+      }
+    }
+
+    for (Entry<DexCallSite, DexMethod> entry : callSites.entrySet()) {
+      DexMethod method = entry.getValue();
+      DexString renamed = minifierState.getRenaming(method);
+      if (originStates.get(equivalence.wrap(method)).isReserved(method.name, method.proto)) {
+        assert renamed == null;
+        callSiteRenamings.put(entry.getKey(), method.name);
+      } else {
+        assert renamed != null;
+        callSiteRenamings.put(entry.getKey(), renamed);
+      }
+    }
+    timing.end();
+  }
+
+  private boolean propagateReservedNames(
+      Wrapper<DexMethod> key, Map<Wrapper<DexMethod>, Set<Wrapper<DexMethod>>> unification) {
+    Set<Wrapper<DexMethod>> unifiedMethods =
+        unification.getOrDefault(key, Collections.singleton(key));
+    boolean changed = false;
+    for (Wrapper<DexMethod> wrapper : unifiedMethods) {
+      DexMethod unifiedMethod = wrapper.get();
+      assert unifiedMethod != null;
+      for (NamingState<DexProto, ?> namingState : globalStateMap.get(wrapper)) {
+        if (!namingState.isReserved(unifiedMethod.name, unifiedMethod.proto)) {
+          namingState.reserveName(unifiedMethod.name, unifiedMethod.proto);
+          changed = true;
+        }
+      }
+    }
+    return changed;
+  }
+
+  private void assignNameToInterfaceMethod(
+      Wrapper<DexMethod> key, Map<Wrapper<DexMethod>, Set<Wrapper<DexMethod>>> unification) {
+    List<MethodNamingState> collectedStates = new ArrayList<>();
+    Set<DexMethod> sourceMethods = Sets.newIdentityHashSet();
+    for (Wrapper<DexMethod> k : unification.getOrDefault(key, Collections.singleton(key))) {
+      DexMethod unifiedMethod = k.get();
+      assert unifiedMethod != null;
+      sourceMethods.addAll(sourceMethodsMap.get(k));
+      for (NamingState<DexProto, ?> namingState : globalStateMap.get(k)) {
+        collectedStates.add(
+            new MethodNamingState(namingState, unifiedMethod.name, unifiedMethod.proto));
+      }
+    }
+
+    DexMethod method = key.get();
+    assert method != null;
+
+    Set<String> loggingFilter = options.extensiveInterfaceMethodMinifierLoggingFilter;
+    if (!loggingFilter.isEmpty()) {
+      if (sourceMethods.stream().map(DexMethod::toSourceString).anyMatch(loggingFilter::contains)) {
+        print(method, sourceMethods, collectedStates, System.out);
+      }
+    }
+
+    MethodNamingState originState =
+        new MethodNamingState(originStates.get(key), method.name, method.proto);
+    assignNameForInterfaceMethodInAllStates(collectedStates, sourceMethods, originState);
+  }
+
+  private void assignNameForInterfaceMethodInAllStates(
+      List<MethodNamingState> collectedStates,
+      Set<DexMethod> sourceMethods,
+      MethodNamingState originState) {
+    assert !anyIsReserved(collectedStates);
+
+    // We use the origin state to allocate a name here so that we can reuse names between different
+    // unrelated interfaces. This saves some space. The alternative would be to use a global state
+    // for allocating names, which would save the work to search here.
+    DexString previousCandidate = null;
+    DexString candidate;
+    do {
+      candidate = originState.assignNewName();
+
+      // If the state returns the same candidate for two consecutive trials, it should be this case:
+      //   1) an interface method with the same signature (name, param) but different return type
+      //   has been already renamed; and 2) -useuniqueclassmembernames is set.
+      // The option forces the naming state to return the same renamed name for the same signature.
+      // So, here we break the loop in an ad-hoc manner.
+      if (candidate != null && candidate == previousCandidate) {
+        assert minifierState.useUniqueMemberNames();
+        break;
+      }
+      for (MethodNamingState state : collectedStates) {
+        if (!state.isAvailable(candidate)) {
+          previousCandidate = candidate;
+          candidate = null;
+          break;
+        }
+      }
+    } while (candidate == null);
+    for (MethodNamingState state : collectedStates) {
+      state.addRenaming(candidate);
+    }
+    // Rename all methods in interfaces that gave rise to this renaming.
+    for (DexMethod sourceMethod : sourceMethods) {
+      minifierState.putRenaming(sourceMethod, candidate);
+    }
+  }
+
+  private void addStatesToGlobalMapForMethod(
+      DexMethod method, Set<NamingState<DexProto, ?>> collectedStates, DexType originInterface) {
+    Wrapper<DexMethod> key = equivalence.wrap(method);
+    globalStateMap.computeIfAbsent(key, k -> new HashSet<>()).addAll(collectedStates);
+    sourceMethodsMap.computeIfAbsent(key, k -> new HashSet<>()).add(method);
+    originStates.putIfAbsent(key, minifierState.getState(originInterface));
+  }
+
+  private boolean anyIsReserved(
+      Wrapper<DexMethod> key, Map<Wrapper<DexMethod>, Set<Wrapper<DexMethod>>> unification) {
+    Set<Wrapper<DexMethod>> unifiedMethods =
+        unification.getOrDefault(key, Collections.singleton(key));
+    for (Wrapper<DexMethod> wrapper : unifiedMethods) {
+      DexMethod unifiedMethod = wrapper.get();
+      assert unifiedMethod != null;
+
+      for (NamingState<DexProto, ?> namingState : globalStateMap.get(wrapper)) {
+        if (namingState.isReserved(unifiedMethod.name, unifiedMethod.proto)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private boolean anyIsReserved(List<MethodNamingState> collectedStates) {
+    DexString name = collectedStates.get(0).getName();
+    Map<DexProto, Boolean> globalStateCache = new IdentityHashMap<>();
+    for (MethodNamingState state : collectedStates) {
+      assert state.getName() == name;
+      boolean isReservedInGlobalState =
+          globalStateCache.computeIfAbsent(
+              state.getProto(), proto -> minifierState.isReservedInGlobalState(name, proto));
+      // TODO(christofferqa): Should this be using logical OR instead?
+      if (isReservedInGlobalState && state.isReserved()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private Set<NamingState<DexProto, ?>> getReachableStates(DexType type) {
+    if (minifierState.useUniqueMemberNames()) {
+      return ImmutableSet.of(minifierState.globalState());
+    }
+
+    Set<DexType> reachableInterfaces = Sets.newIdentityHashSet();
+    reachableInterfaces.add(type);
+    collectSuperInterfaces(type, reachableInterfaces);
+    collectSubInterfaces(type, reachableInterfaces);
+
+    Set<NamingState<DexProto, ?>> reachableStates = new HashSet<>();
+    for (DexType reachableInterface : reachableInterfaces) {
+      // Add the interface itself.
+      reachableStates.add(minifierState.getState(reachableInterface));
+
+      // And the frontiers that correspond to the classes that implement the interface.
+      for (DexType frontier : reachableInterface.allImplementsSubtypes()) {
+        NamingState<DexProto, ?> state = minifierState.getState(frontierState.get(frontier));
+        assert state != null;
+        reachableStates.add(state);
+      }
+    }
+    return reachableStates;
+  }
+
+  private void collectSuperInterfaces(DexType iface, Set<DexType> interfaces) {
+    DexClass clazz = appInfo.definitionFor(iface);
+    // In cases where we lack the interface's definition, we can at least look at subtypes and
+    // tie those up to get proper naming.
+    if (clazz != null) {
+      for (DexType type : clazz.interfaces.values) {
+        if (interfaces.add(type)) {
+          collectSuperInterfaces(type, interfaces);
+        }
+      }
+    }
+  }
+
+  private void collectSubInterfaces(DexType iface, Set<DexType> interfaces) {
+    for (DexType subtype : iface.allExtendsSubtypes()) {
+      assert subtype.isInterface();
+      if (interfaces.add(subtype)) {
+        collectSubInterfaces(subtype, interfaces);
+      }
+    }
+  }
+
+  private void print(
+      DexMethod method,
+      Set<DexMethod> sourceMethods,
+      List<MethodNamingState> collectedStates,
+      PrintStream out) {
+    out.println("-----------------------------------------------------------------------");
+    out.println("assignNameToInterfaceMethod(`" + method.toSourceString() + "`)");
+    out.println("-----------------------------------------------------------------------");
+    out.println("Source methods:");
+    for (DexMethod sourceMethod : sourceMethods) {
+      out.println("  " + sourceMethod.toSourceString());
+    }
+    out.println("States:");
+    collectedStates.forEach(state -> state.print("  ", minifierState::getStateKey, out));
+    out.println();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java
index 7f28744..419bfbb 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -22,11 +24,16 @@
   protected final List<String> dictionary;
 
   protected final Map<MemberType, DexString> renaming = new IdentityHashMap<>();
-  protected final Map<DexType, NamingState<StateType, ?>> states = new IdentityHashMap<>();
   protected final NamingState<StateType, ?> globalState;
   protected final boolean useUniqueMemberNames;
   protected final boolean overloadAggressively;
 
+  protected final State minifierState = new State();
+
+  // The use of a bidirectional map allows us to map a naming state to the type it represents,
+  // which is useful for debugging.
+  private final BiMap<DexType, NamingState<StateType, ?>> states = HashBiMap.create();
+
   MemberNameMinifier(AppInfoWithLiveness appInfo, RootSet rootSet, InternalOptions options) {
     this.appInfo = appInfo;
     this.rootSet = rootSet;
@@ -46,7 +53,36 @@
     return useUniqueMemberNames ? globalState : states.computeIfAbsent(type, f);
   }
 
-  protected NamingState<StateType, ?> getState(DexType type) {
-    return useUniqueMemberNames ? globalState : states.get(type);
+  // A class that provides access to the minification state. An instance of this class is passed
+  // from the method name minifier to the interface method name minifier.
+  class State {
+
+    DexString getRenaming(MemberType key) {
+      return renaming.get(key);
+    }
+
+    void putRenaming(MemberType key, DexString value) {
+      renaming.put(key, value);
+    }
+
+    NamingState<StateType, ?> getState(DexType type) {
+      return useUniqueMemberNames ? globalState : states.get(type);
+    }
+
+    DexType getStateKey(NamingState<StateType, ?> state) {
+      return states.inverse().get(state);
+    }
+
+    NamingState<StateType, ?> globalState() {
+      return globalState;
+    }
+
+    boolean isReservedInGlobalState(DexString name, StateType state) {
+      return globalState.isReserved(name, state);
+    }
+
+    boolean useUniqueMemberNames() {
+      return useUniqueMemberNames;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
index 5f990ac..8864271 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
@@ -18,15 +18,11 @@
 import com.android.tools.r8.utils.Timing;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
-import java.util.Collections;
+import com.google.common.collect.ImmutableMap;
+import java.io.PrintStream;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.IdentityHashMap;
-import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.Function;
 
@@ -91,18 +87,15 @@
  */
 class MethodNameMinifier extends MemberNameMinifier<DexMethod, DexProto> {
 
-  private final Set<DexCallSite> desugaredCallSites;
-
   private final Equivalence<DexMethod> equivalence;
-  private final Map<DexCallSite, DexString> callSiteRenaming = new IdentityHashMap<>();
+
+  private final FrontierState frontierState = new FrontierState();
 
   MethodNameMinifier(
       AppInfoWithLiveness appInfo,
       RootSet rootSet,
-      Set<DexCallSite> desugaredCallSites,
       InternalOptions options) {
     super(appInfo, rootSet, options);
-    this.desugaredCallSites = desugaredCallSites;
     equivalence =
         overloadAggressively
             ? MethodSignatureEquivalence.get()
@@ -121,6 +114,7 @@
   }
 
   static class MethodRenaming {
+
     final Map<DexMethod, DexString> renaming;
     final Map<DexCallSite, DexString> callSiteRenaming;
 
@@ -129,37 +123,37 @@
       this.renaming = renaming;
       this.callSiteRenaming = callSiteRenaming;
     }
+
+    public static MethodRenaming empty() {
+      return new MethodRenaming(ImmutableMap.of(), ImmutableMap.of());
+    }
   }
 
-  MethodRenaming computeRenaming(Timing timing) {
+  MethodRenaming computeRenaming(Set<DexCallSite> desugaredCallSites, Timing timing) {
     // Phase 1: Reserve all the names that need to be kept and allocate linked state in the
     //          library part.
     timing.begin("Phase 1");
-    Map<DexType, DexType> frontierMap = new IdentityHashMap<>();
-    reserveNamesInClasses(appInfo.dexItemFactory.objectType,
-        appInfo.dexItemFactory.objectType,
-        null, frontierMap);
+    reserveNamesInClasses();
     timing.end();
-    // Phase 2: Reserve all the names that are required for interfaces.
+    // Phase 2: Reserve all the names that are required for interfaces, and then assign names to
+    //          interface methods. These are assigned by finding a name that is free in all naming
+    //          states that may hold an implementation
     timing.begin("Phase 2");
-    DexType.forAllInterfaces(
-        appInfo.dexItemFactory, iface -> reserveNamesInInterfaces(iface, frontierMap));
+    InterfaceMethodNameMinifier interfaceMethodNameMinifier =
+        new InterfaceMethodNameMinifier(
+            appInfo, desugaredCallSites, equivalence, frontierState, minifierState, options);
+    interfaceMethodNameMinifier.assignNamesToInterfaceMethods(timing);
     timing.end();
-    // Phase 3: Assign names to interface methods. These are assigned by finding a name that is
-    //          free in all naming states that may hold an implementation.
+    // Phase 3: Assign names top-down by traversing the subtype hierarchy.
     timing.begin("Phase 3");
-    assignNamesToInterfaceMethods(frontierMap, timing);
-    timing.end();
-    // Phase 4: Assign names top-down by traversing the subtype hierarchy.
-    timing.begin("Phase 4");
     assignNamesToClassesMethods(appInfo.dexItemFactory.objectType, false);
     timing.end();
     // Phase 4: Do the same for private methods.
-    timing.begin("Phase 5");
+    timing.begin("Phase 4");
     assignNamesToClassesMethods(appInfo.dexItemFactory.objectType, true);
     timing.end();
 
-    return new MethodRenaming(renaming, callSiteRenaming);
+    return new MethodRenaming(renaming, interfaceMethodNameMinifier.getCallSiteRenamings());
   }
 
   private void assignNamesToClassesMethods(DexType type, boolean doPrivates) {
@@ -167,7 +161,7 @@
     if (holder != null && !holder.isLibraryClass()) {
       Map<Wrapper<DexMethod>, DexString> renamingAtThisLevel = new HashMap<>();
       NamingState<DexProto, ?> state =
-          computeStateIfAbsent(type, k -> getState(holder.superType).createChild());
+          computeStateIfAbsent(type, k -> minifierState.getState(holder.superType).createChild());
       for (DexEncodedMethod method : holder.allMethodsSorted()) {
         assignNameToMethod(method, state, renamingAtThisLevel, doPrivates);
       }
@@ -201,291 +195,77 @@
     }
   }
 
-  private Set<NamingState<DexProto, ?>> getReachableStates(DexType type,
-      Map<DexType, DexType> frontierMap) {
-    Set<DexType> interfaces = Sets.newIdentityHashSet();
-    interfaces.add(type);
-    collectSuperInterfaces(type, interfaces);
-    collectSubInterfaces(type, interfaces);
-    Set<NamingState<DexProto, ?>> reachableStates = new HashSet<>();
-    for (DexType iface : interfaces) {
-      // Add the interface itself
-      reachableStates.add(getState(iface));
-      // And the frontiers that correspond to the classes that implement the interface.
-      iface.forAllImplementsSubtypes(t -> {
-        NamingState<DexProto, ?> state = getState(frontierMap.get(t));
-        assert state != null;
-        reachableStates.add(state);
-      });
-    }
-    return reachableStates;
+  private void reserveNamesInClasses() {
+    reserveNamesInClasses(
+        appInfo.dexItemFactory.objectType, appInfo.dexItemFactory.objectType, null);
   }
 
-  private void assignNamesToInterfaceMethods(Map<DexType, DexType> frontierMap, Timing timing) {
-    // First compute a map from method signatures to a set of naming states for interfaces and
-    // frontier states of classes that implement them. We add the frontier states so that we can
-    // reserve the names for later method naming.
-    timing.begin("Compute map");
-    // A map from DexMethods to all the states linked to interfaces they appear in.
-    Map<Wrapper<DexMethod>, Set<NamingState<DexProto, ?>>> globalStateMap = new HashMap<>();
-    // A map from DexMethods to all the definitions seen. Needed as the Wrapper equalizes them all.
-    Map<Wrapper<DexMethod>, Set<DexMethod>> sourceMethodsMap = new HashMap<>();
-    // A map from DexMethods to the first interface state it was seen in. Used to pick good names.
-    Map<Wrapper<DexMethod>, NamingState<DexProto, ?>> originStates = new HashMap<>();
-    DexType.forAllInterfaces(appInfo.dexItemFactory, iface -> {
-      assert iface.isInterface();
-      DexClass clazz = appInfo.definitionFor(iface);
-      if (clazz != null) {
-        Set<NamingState<DexProto, ?>> collectedStates = getReachableStates(iface, frontierMap);
-        for (DexEncodedMethod method : shuffleMethods(clazz.methods())) {
-          addStatesToGlobalMapForMethod(
-              method, collectedStates, globalStateMap, sourceMethodsMap, originStates, iface);
-        }
-      }
-    });
-    // Collect the live call sites for multi-interface lambda expression renaming. For code with
-    // desugared lambdas this is a conservative estimate, as we don't track if the generated
-    // lambda classes survive into the output. As multi-interface lambda expressions are rare
-    // this is not a big deal.
-    Set<DexCallSite> liveCallSites = Sets.union(desugaredCallSites, appInfo.callSites);
-    // If the input program contains a multi-interface lambda expression that implements
-    // interface methods with different protos, we need to make sure that
-    // the implemented lambda methods are renamed to the same name.
-    // To achieve this, we map each DexCallSite that corresponds to a lambda expression to one of
-    // the DexMethods it implements, and we unify the DexMethods that need to be renamed together.
-    Map<DexCallSite, DexMethod> callSites = new IdentityHashMap<>();
-    // Union-find structure to keep track of methods that must be renamed together.
-    // Note that if the input does not use multi-interface lambdas,
-    // unificationParent will remain empty.
-    Map<Wrapper<DexMethod>, Wrapper<DexMethod>> unificationParent = new HashMap<>();
-    liveCallSites.forEach(
-        callSite -> {
-          Set<Wrapper<DexMethod>> callSiteMethods = new HashSet<>();
-          // Don't report errors, as the set of call sites is a conservative estimate, and can
-          // refer to interfaces which has been removed.
-          Set<DexEncodedMethod> implementedMethods =
-              appInfo.lookupLambdaImplementedMethods(callSite);
-          if (implementedMethods.isEmpty()) {
-            return;
-          }
-          callSites.put(callSite, implementedMethods.iterator().next().method);
-          for (DexEncodedMethod method : implementedMethods) {
-            DexType iface = method.method.holder;
-            assert iface.isInterface();
-            Set<NamingState<DexProto, ?>> collectedStates = getReachableStates(iface, frontierMap);
-            addStatesToGlobalMapForMethod(
-                method, collectedStates, globalStateMap, sourceMethodsMap, originStates, iface);
-            callSiteMethods.add(equivalence.wrap(method.method));
-          }
-          if (callSiteMethods.size() > 1) {
-            // Implemented interfaces have different return types. Unify them.
-            Wrapper<DexMethod> mainKey = callSiteMethods.iterator().next();
-            mainKey = unificationParent.getOrDefault(mainKey, mainKey);
-            for (Wrapper<DexMethod> key : callSiteMethods) {
-              unificationParent.put(key, mainKey);
-            }
-          }
-        });
-    Map<Wrapper<DexMethod>, Set<Wrapper<DexMethod>>> unification = new HashMap<>();
-    for (Wrapper<DexMethod> key : unificationParent.keySet()) {
-      // Find root with path-compression.
-      Wrapper<DexMethod> root = unificationParent.get(key);
-      while (unificationParent.get(root) != root) {
-        Wrapper<DexMethod> k = unificationParent.get(unificationParent.get(root));
-        unificationParent.put(root, k);
-        root = k;
-      }
-      unification.computeIfAbsent(root, k -> new HashSet<>()).add(key);
-    }
-    timing.end();
-    // Go over every method and assign a name.
-    timing.begin("Allocate names");
-    // Sort the methods by the number of dependent states, so that we use short names for methods
-    // references in many places.
-    List<Wrapper<DexMethod>> methods = new ArrayList<>(globalStateMap.keySet());
-    methods.sort((a, b) -> globalStateMap.get(b).size() - globalStateMap.get(a).size());
-    for (Wrapper<DexMethod> key : methods) {
-      if (!unificationParent.getOrDefault(key, key).equals(key)) {
-        continue;
-      }
-      List<MethodNamingState> collectedStates = new ArrayList<>();
-      Set<DexMethod> sourceMethods = Sets.newIdentityHashSet();
-      for (Wrapper<DexMethod> k : unification.getOrDefault(key, Collections.singleton(key))) {
-        DexMethod unifiedMethod = k.get();
-        assert unifiedMethod != null;
-        sourceMethods.addAll(sourceMethodsMap.get(k));
-        for (NamingState<DexProto, ?> namingState : globalStateMap.get(k)) {
-          collectedStates.add(
-              new MethodNamingState(namingState, unifiedMethod.name, unifiedMethod.proto));
-        }
-      }
-      DexMethod method = key.get();
-      assert method != null;
-      MethodNamingState originState =
-          new MethodNamingState(originStates.get(key), method.name, method.proto);
-      assignNameForInterfaceMethodInAllStates(collectedStates, sourceMethods, originState);
-    }
-    for (Entry<DexCallSite, DexMethod> entry : callSites.entrySet()) {
-      DexMethod method = entry.getValue();
-      DexString renamed = renaming.get(method);
-      if (originStates.get(equivalence.wrap(method)).isReserved(method.name, method.proto)) {
-        assert renamed == null;
-        callSiteRenaming.put(entry.getKey(), method.name);
-      } else {
-        assert renamed != null;
-        callSiteRenaming.put(entry.getKey(), renamed);
-      }
-    }
-    timing.end();
-  }
-
-  private void collectSuperInterfaces(DexType iface, Set<DexType> interfaces) {
-    DexClass clazz = appInfo.definitionFor(iface);
-    // In cases where we lack the interface's definition, we can at least look at subtypes and
-    // tie those up to get proper naming.
-    if (clazz != null) {
-      for (DexType type : clazz.interfaces.values) {
-        if (interfaces.add(type)) {
-          collectSuperInterfaces(type, interfaces);
-        }
-      }
-    }
-  }
-
-  private void collectSubInterfaces(DexType iface, Set<DexType> interfaces) {
-    iface.forAllExtendsSubtypes(subtype -> {
-      assert subtype.isInterface();
-      if (interfaces.add(subtype)) {
-        collectSubInterfaces(subtype, interfaces);
-      }
-    });
-  }
-
-  private void addStatesToGlobalMapForMethod(
-      DexEncodedMethod method,
-      Set<NamingState<DexProto, ?>> collectedStates,
-      Map<Wrapper<DexMethod>, Set<NamingState<DexProto, ?>>> globalStateMap,
-      Map<Wrapper<DexMethod>, Set<DexMethod>> sourceMethodsMap,
-      Map<Wrapper<DexMethod>, NamingState<DexProto, ?>> originStates,
-      DexType originInterface) {
-    Wrapper<DexMethod> key = equivalence.wrap(method.method);
-    Set<NamingState<DexProto, ?>> stateSet =
-        globalStateMap.computeIfAbsent(key, k -> new HashSet<>());
-    stateSet.addAll(collectedStates);
-    sourceMethodsMap.computeIfAbsent(key, k -> new HashSet<>()).add(method.method);
-    originStates.putIfAbsent(key, getState(originInterface));
-  }
-
-  private void assignNameForInterfaceMethodInAllStates(
-      List<MethodNamingState> collectedStates,
-      Set<DexMethod> sourceMethods,
-      MethodNamingState originState) {
-    if (anyIsReserved(collectedStates)) {
-      // This method's name is reserved in at least one naming state, so reserve it everywhere.
-      for (MethodNamingState state : collectedStates) {
-        state.reserveName();
-      }
-      return;
-    }
-    // We use the origin state to allocate a name here so that we can reuse names between different
-    // unrelated interfaces. This saves some space. The alternative would be to use a global state
-    // for allocating names, which would save the work to search here.
-    DexString previousCandidate = null;
-    DexString candidate;
-    do {
-      candidate = originState.assignNewNameFor(false);
-      // If the state returns the same candidate for two consecutive trials, it should be this case:
-      //   1) an interface method with the same signature (name, param) but different return type
-      //   has been already renamed; and 2) -useuniqueclassmembernames is set.
-      // The option forces the naming state to return the same renamed name for the same signature.
-      // So, here we break the loop in an ad-hoc manner.
-      if (candidate != null && candidate == previousCandidate) {
-        assert useUniqueMemberNames;
-        break;
-      }
-      for (MethodNamingState state : collectedStates) {
-        if (!state.isAvailable(candidate)) {
-          previousCandidate = candidate;
-          candidate = null;
-          break;
-        }
-      }
-    } while (candidate == null);
-    for (MethodNamingState state : collectedStates) {
-      state.addRenaming(candidate);
-    }
-    // Rename all methods in interfaces that gave rise to this renaming.
-    for (DexMethod sourceMethod : sourceMethods) {
-      renaming.put(sourceMethod, candidate);
-    }
-  }
-
-  private boolean anyIsReserved(List<MethodNamingState> collectedStates) {
-    DexString name = collectedStates.get(0).getName();
-    Map<DexProto, Boolean> globalStateCache = new HashMap<>();
-    for (MethodNamingState state : collectedStates) {
-      assert state.getName() == name;
-      if (globalStateCache.computeIfAbsent(
-              state.getProto(), proto -> globalState.isReserved(name, proto))
-          && state.isReserved()) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private void reserveNamesInClasses(DexType type, DexType libraryFrontier,
-      NamingState<DexProto, ?> parent,
-      Map<DexType, DexType> frontierMap) {
+  private void reserveNamesInClasses(
+      DexType type, DexType libraryFrontier, NamingState<DexProto, ?> parent) {
     assert !type.isInterface();
-    DexClass holder = appInfo.definitionFor(type);
-    NamingState<DexProto, ?> state = allocateNamingStateAndReserve(holder, type, libraryFrontier,
-        parent, frontierMap);
+    NamingState<DexProto, ?> state =
+        frontierState.allocateNamingStateAndReserve(type, libraryFrontier, parent);
+
     // If this is a library class (or effectively a library class as it is missing) move the
     // frontier forward.
-    type.forAllExtendsSubtypes(subtype -> {
-      assert !subtype.isInterface();
-      reserveNamesInClasses(subtype,
-          holder == null || holder.isLibraryClass() ? subtype : libraryFrontier,
-          state, frontierMap);
-    });
-  }
-
-  private void reserveNamesInInterfaces(DexType type, Map<DexType, DexType> frontierMap) {
-    assert type.isInterface();
-    frontierMap.put(type, type);
     DexClass holder = appInfo.definitionFor(type);
-    allocateNamingStateAndReserve(holder, type, type, null, frontierMap);
-  }
-
-  private NamingState<DexProto, ?> allocateNamingStateAndReserve(DexClass holder, DexType type,
-      DexType libraryFrontier,
-      NamingState<DexProto, ?> parent,
-      Map<DexType, DexType> frontierMap) {
-    frontierMap.put(type, libraryFrontier);
-    NamingState<DexProto, ?> state =
-        computeStateIfAbsent(
-            libraryFrontier,
-            ignore -> parent == null
-                ? NamingState.createRoot(
-                    appInfo.dexItemFactory, dictionary, getKeyTransform(), useUniqueMemberNames)
-                : parent.createChild());
-    if (holder != null) {
-      boolean keepAll = holder.isLibraryClass() || holder.accessFlags.isAnnotation();
-      for (DexEncodedMethod method : shuffleMethods(holder.methods())) {
-        reserveNamesForMethod(method, keepAll, state);
-      }
+    for (DexType subtype : type.allExtendsSubtypes()) {
+      assert !subtype.isInterface();
+      reserveNamesInClasses(
+          subtype, holder == null || holder.isLibraryClass() ? subtype : libraryFrontier, state);
     }
-    return state;
   }
 
-  private void reserveNamesForMethod(
-      DexEncodedMethod encodedMethod, boolean keepAll, NamingState<DexProto, ?> state) {
-    DexMethod method = encodedMethod.method;
-    if (keepAll || rootSet.noObfuscation.contains(method)) {
+  class FrontierState {
+
+    private final Map<DexType, DexType> frontiers = new IdentityHashMap<>();
+
+    NamingState<DexProto, ?> allocateNamingStateAndReserve(
+        DexType type, DexType frontier, NamingState<DexProto, ?> parent) {
+      if (frontier != type) {
+        frontiers.put(type, frontier);
+      }
+
+      NamingState<DexProto, ?> state =
+          computeStateIfAbsent(
+              frontier,
+              ignore ->
+                  parent == null
+                      ? NamingState.createRoot(
+                          appInfo.dexItemFactory,
+                          dictionary,
+                          getKeyTransform(),
+                          useUniqueMemberNames)
+                      : parent.createChild());
+
+      DexClass holder = appInfo.definitionFor(type);
+      if (holder != null) {
+        boolean keepAll = holder.isLibraryClass() || holder.accessFlags.isAnnotation();
+        for (DexEncodedMethod method : shuffleMethods(holder.methods(), options)) {
+          // TODO(christofferqa): Wouldn't it be sufficient only to reserve names for non-private
+          //  methods?
+          if (keepAll || rootSet.noObfuscation.contains(method.method)) {
+            reserveNamesForMethod(method.method, state);
+          }
+        }
+      }
+
+      return state;
+    }
+
+    private void reserveNamesForMethod(DexMethod method, NamingState<DexProto, ?> state) {
       state.reserveName(method.name, method.proto);
       globalState.reserveName(method.name, method.proto);
     }
+
+    public DexType get(DexType type) {
+      return frontiers.getOrDefault(type, type);
+    }
+
+    public DexType put(DexType type, DexType frontier) {
+      assert frontier != type;
+      return frontiers.put(type, frontier);
+    }
   }
 
   /**
@@ -493,7 +273,7 @@
    * simply defers to parent.METHOD(name, proto, ...). This allows R8 to assign the same name to
    * methods with different prototypes, which is needed for multi-interface lambdas.
    */
-  private static class MethodNamingState {
+  static class MethodNamingState {
 
     private final NamingState<DexProto, ?> parent;
     private final DexString name;
@@ -506,8 +286,8 @@
       this.proto = proto;
     }
 
-    DexString assignNewNameFor(boolean markAsUsed) {
-      return parent.assignNewNameFor(name, proto, markAsUsed);
+    DexString assignNewName() {
+      return parent.assignNewNameFor(name, proto, false);
     }
 
     void reserveName() {
@@ -533,11 +313,25 @@
     DexProto getProto() {
       return proto;
     }
+
+    void print(
+        String indentation,
+        Function<NamingState<DexProto, ?>, DexType> stateKeyGetter,
+        PrintStream out) {
+      DexType stateKey = stateKeyGetter.apply(parent);
+      out.print(indentation);
+      out.print(stateKey != null ? stateKey.toSourceString() : "<?>");
+      out.print(".");
+      out.print(name.toSourceString());
+      out.println(proto.toSmaliString());
+      parent.printState(proto, indentation + "  ", out);
+    }
   }
 
   // Shuffles the given methods if assertions are enabled and deterministic debugging is disabled.
   // Used to ensure that the generated output is deterministic.
-  private Iterable<DexEncodedMethod> shuffleMethods(Iterable<DexEncodedMethod> methods) {
+  static Iterable<DexEncodedMethod> shuffleMethods(
+      Iterable<DexEncodedMethod> methods, InternalOptions options) {
     return options.testing.irOrdering.order(methods);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java b/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java
index 3ae9d06..e3d09b6 100644
--- a/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java
+++ b/src/main/java/com/android/tools/r8/naming/MinifiedNameMapPrinter.java
@@ -16,6 +16,7 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
@@ -38,6 +39,12 @@
     return copy;
   }
 
+  private <T> List<T> sortedCopy(List<T> source, Comparator<? super T> comparator) {
+    List<T> copy = new ArrayList<>(source);
+    Collections.sort(copy, comparator);
+    return copy;
+  }
+
   private void writeClass(DexProgramClass clazz, StringBuilder out) {
     seenTypes.add(clazz.type);
     DexString descriptor = namingLens.lookupDescriptor(clazz.type);
@@ -87,7 +94,7 @@
     out.append(renamed).append(NEW_LINE);
   }
 
-  private void writeMethods(DexEncodedMethod[] methods, StringBuilder out) {
+  private void writeMethods(List<DexEncodedMethod> methods, StringBuilder out) {
     for (DexEncodedMethod encodedMethod : methods) {
       DexMethod method = encodedMethod.method;
       DexString renamed = namingLens.lookupName(method);
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index e4ccbcd..5333125 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.ClassNameMinifier.ClassRenaming;
+import com.android.tools.r8.naming.FieldNameMinifier.FieldRenaming;
 import com.android.tools.r8.naming.MethodNameMinifier.MethodRenaming;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
@@ -56,21 +57,28 @@
     ClassNameMinifier classNameMinifier = new ClassNameMinifier(appInfo, rootSet, options);
     ClassRenaming classRenaming = classNameMinifier.computeRenaming(timing);
     timing.end();
+
+    assert new MinifiedRenaming(
+            classRenaming, MethodRenaming.empty(), FieldRenaming.empty(), appInfo)
+        .verifyNoCollisions(appInfo.classes(), appInfo.dexItemFactory);
+
     timing.begin("MinifyMethods");
     MethodRenaming methodRenaming =
-        new MethodNameMinifier(appInfo, rootSet, desugaredCallSites, options)
-            .computeRenaming(timing);
+        new MethodNameMinifier(appInfo, rootSet, options)
+            .computeRenaming(desugaredCallSites, timing);
     timing.end();
+
+    assert new MinifiedRenaming(classRenaming, methodRenaming, FieldRenaming.empty(), appInfo)
+        .verifyNoCollisions(appInfo.classes(), appInfo.dexItemFactory);
+
     timing.begin("MinifyFields");
-    Map<DexField, DexString> fieldRenaming =
+    FieldRenaming fieldRenaming =
         new FieldNameMinifier(appInfo, rootSet, options).computeRenaming(timing);
     timing.end();
-    NamingLens lens =
-        new MinifiedRenaming(
-            classRenaming,
-            methodRenaming,
-            fieldRenaming,
-            appInfo);
+
+    NamingLens lens = new MinifiedRenaming(classRenaming, methodRenaming, fieldRenaming, appInfo);
+    assert lens.verifyNoCollisions(appInfo.classes(), appInfo.dexItemFactory);
+
     timing.begin("MinifyIdentifiers");
     new IdentifierMinifier(
         appInfo, options.getProguardConfiguration().getAdaptClassStrings(), lens).run();
@@ -87,14 +95,14 @@
     private MinifiedRenaming(
         ClassRenaming classRenaming,
         MethodRenaming methodRenaming,
-        Map<DexField, DexString> fieldRenaming,
+        FieldRenaming fieldRenaming,
         AppInfo appInfo) {
       this.appInfo = appInfo;
       this.packageRenaming = classRenaming.packageRenaming;
       renaming.putAll(classRenaming.classRenaming);
       renaming.putAll(methodRenaming.renaming);
       renaming.putAll(methodRenaming.callSiteRenaming);
-      renaming.putAll(fieldRenaming);
+      renaming.putAll(fieldRenaming.renaming);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/naming/NamingLens.java b/src/main/java/com/android/tools/r8/naming/NamingLens.java
index cee867e..aecae1e 100644
--- a/src/main/java/com/android/tools/r8/naming/NamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/NamingLens.java
@@ -7,10 +7,14 @@
 import static com.android.tools.r8.utils.DescriptorUtils.descriptorToJavaType;
 
 import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -19,7 +23,10 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import java.util.Arrays;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -70,6 +77,40 @@
     return lookupName(reference.asDexField());
   }
 
+  public final DexField lookupField(DexField field, DexItemFactory dexItemFactory) {
+    return dexItemFactory.createField(
+        lookupType(field.clazz, dexItemFactory),
+        lookupType(field.type, dexItemFactory),
+        lookupName(field));
+  }
+
+  public final DexMethod lookupMethod(DexMethod method, DexItemFactory dexItemFactory) {
+    return dexItemFactory.createMethod(
+        lookupType(method.holder, dexItemFactory),
+        lookupProto(method.proto, dexItemFactory),
+        lookupName(method));
+  }
+
+  private DexProto lookupProto(DexProto proto, DexItemFactory dexItemFactory) {
+    return dexItemFactory.createProto(
+        lookupType(proto.returnType, dexItemFactory),
+        Arrays.stream(proto.parameters.values)
+            .map(type -> lookupType(type, dexItemFactory))
+            .toArray(DexType[]::new));
+  }
+
+  public final DexType lookupType(DexType type, DexItemFactory dexItemFactory) {
+    if (type.isPrimitiveType() || type.isVoidType()) {
+      return type;
+    }
+    if (type.isArrayType()) {
+      DexType newBaseType = lookupType(type.toBaseType(dexItemFactory), dexItemFactory);
+      return type.replaceBaseType(newBaseType, dexItemFactory);
+    }
+    assert type.isClassType();
+    return dexItemFactory.createType(lookupDescriptor(type));
+  }
+
   public static NamingLens getIdentityLens() {
     return new IdentityLens();
   }
@@ -97,6 +138,34 @@
    */
   public abstract boolean checkTargetCanBeTranslated(DexMethod item);
 
+  public final boolean verifyNoCollisions(
+      Iterable<DexProgramClass> classes, DexItemFactory dexItemFactory) {
+    Set<DexReference> references = Sets.newIdentityHashSet();
+    for (DexProgramClass clazz : classes) {
+      {
+        DexType newType = lookupType(clazz.type, dexItemFactory);
+        boolean referencesChanged = references.add(newType);
+        assert referencesChanged
+            : "Duplicate definition of type `" + newType.toSourceString() + "`";
+      }
+
+      for (DexEncodedField field : clazz.fields()) {
+        DexField newField = lookupField(field.field, dexItemFactory);
+        boolean referencesChanged = references.add(newField);
+        assert referencesChanged
+            : "Duplicate definition of field `" + newField.toSourceString() + "`";
+      }
+
+      for (DexEncodedMethod method : clazz.methods()) {
+        DexMethod newMethod = lookupMethod(method.method, dexItemFactory);
+        boolean referencesChanged = references.add(newMethod);
+        assert referencesChanged
+            : "Duplicate definition of method `" + newMethod.toSourceString() + "`";
+      }
+    }
+    return true;
+  }
+
   private static class IdentityLens extends NamingLens {
 
     private IdentityLens() {
diff --git a/src/main/java/com/android/tools/r8/naming/NamingState.java b/src/main/java/com/android/tools/r8/naming/NamingState.java
index 999579a..d414b3b 100644
--- a/src/main/java/com/android/tools/r8/naming/NamingState.java
+++ b/src/main/java/com/android/tools/r8/naming/NamingState.java
@@ -11,10 +11,12 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
+import java.io.PrintStream;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.Function;
 
@@ -129,7 +131,19 @@
     state.addRenaming(original, key, newName);
   }
 
-  private class InternalState {
+  void printState(ProtoType proto, String indentation, PrintStream out) {
+    KeyType key = keyTransform.apply(proto);
+    InternalState state = findInternalStateFor(key);
+    if (state != null) {
+      state.printReservedNames(indentation, out);
+      state.printRenamings(indentation, out);
+    } else {
+      out.print(indentation);
+      out.println("<NO STATE>");
+    }
+  }
+
+  class InternalState {
 
     private static final int INITIAL_NAME_COUNT = 1;
     private final char[] EMPTY_CHAR_ARRAY = new char[0];
@@ -229,5 +243,49 @@
         return StringUtils.numberToIdentifier(EMPTY_CHAR_ARRAY, nameCount++, false);
       }
     }
+
+    void printReservedNames(String indentation, PrintStream out) {
+      out.print(indentation);
+      out.print("Reserved names:");
+      if (reservedNames == null || reservedNames.isEmpty()) {
+        out.print(" <NO RESERVED NAMES>");
+      } else {
+        for (DexString reservedName : reservedNames) {
+          out.print(System.lineSeparator());
+          out.print(indentation);
+          out.print("  ");
+          out.print(reservedName.toSourceString());
+        }
+      }
+      out.println();
+      if (parentInternalState != null) {
+        parentInternalState.printReservedNames(indentation + "  ", out);
+      }
+    }
+
+    void printRenamings(String indentation, PrintStream out) {
+      out.print(indentation);
+      out.print("Renamings:");
+      if (renamings == null || renamings.isEmpty()) {
+        out.print(" <NO RENAMINGS>");
+      } else {
+        for (DexString original : renamings.rowKeySet()) {
+          Map<KeyType, DexString> row = renamings.row(original);
+          for (Entry<KeyType, DexString> entry : row.entrySet()) {
+            out.print(System.lineSeparator());
+            out.print(indentation);
+            out.print("  ");
+            out.print(original.toSourceString());
+            out.print(entry.getKey().toString());
+            out.print(" -> ");
+            out.print(entry.getValue().toSourceString());
+          }
+        }
+      }
+      out.println();
+      if (parentInternalState != null) {
+        parentInternalState.printRenamings(indentation + "  ", out);
+      }
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
index d5add45..b0e2f4c 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClass.MethodSetter;
 import com.android.tools.r8.graph.DexEncodedAnnotation;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -25,6 +26,7 @@
 import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -317,18 +319,18 @@
       clazz.superType = substituteType(clazz.superType, null);
       clazz.interfaces = substituteTypesIn(clazz.interfaces);
       clazz.annotations = substituteTypesIn(clazz.annotations);
-      clazz.setDirectMethods(substituteTypesIn(clazz.directMethods()));
-      clazz.setVirtualMethods(substituteTypesIn(clazz.virtualMethods()));
+      fixupMethods(clazz.directMethods(), clazz::setDirectMethod);
+      fixupMethods(clazz.virtualMethods(), clazz::setVirtualMethod);
       clazz.setStaticFields(substituteTypesIn(clazz.staticFields()));
       clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields()));
     }
 
-    private DexEncodedMethod[] substituteTypesIn(DexEncodedMethod[] methods) {
+    private void fixupMethods(List<DexEncodedMethod> methods, MethodSetter setter) {
       if (methods == null) {
-        return null;
+        return;
       }
-      for (int i = 0; i < methods.length; i++) {
-        DexEncodedMethod encodedMethod = methods[i];
+      for (int i = 0; i < methods.size(); i++) {
+        DexEncodedMethod encodedMethod = methods.get(i);
         DexMethod appliedMethod = appliedLense.lookupMethod(encodedMethod.method);
         DexType newHolderType = substituteType(appliedMethod.holder, encodedMethod);
         DexProto newProto = substituteTypesIn(appliedMethod.proto, encodedMethod);
@@ -341,9 +343,8 @@
           newMethod = appliedMethod;
         }
         // Explicitly fix members.
-        methods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
+        setter.setMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
       }
-      return methods;
     }
 
     private DexEncodedField[] substituteTypesIn(DexEncodedField[] fields) {
diff --git a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
index 59caf25..f2e6c6d 100644
--- a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
@@ -12,7 +12,7 @@
 import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.google.common.collect.Sets;
-import java.util.Arrays;
+import java.util.List;
 import java.util.Set;
 
 public class VisibilityBridgeRemover {
@@ -24,11 +24,17 @@
   }
 
   private void removeUnneededVisibilityBridgesFromClass(DexProgramClass clazz) {
-    clazz.setDirectMethods(removeUnneededVisibilityBridges(clazz.directMethods()));
-    clazz.setVirtualMethods(removeUnneededVisibilityBridges(clazz.virtualMethods()));
+    DexEncodedMethod[] newDirectMethods = removeUnneededVisibilityBridges(clazz.directMethods());
+    if (newDirectMethods != null) {
+      clazz.setDirectMethods(newDirectMethods);
+    }
+    DexEncodedMethod[] newVirtualMethods = removeUnneededVisibilityBridges(clazz.virtualMethods());
+    if (newVirtualMethods != null) {
+      clazz.setVirtualMethods(newVirtualMethods);
+    }
   }
 
-  private DexEncodedMethod[] removeUnneededVisibilityBridges(DexEncodedMethod[] methods) {
+  private DexEncodedMethod[] removeUnneededVisibilityBridges(List<DexEncodedMethod> methods) {
     Set<DexEncodedMethod> methodsToBeRemoved = null;
     for (DexEncodedMethod method : methods) {
       if (isUnneededVisibilityBridge(method)) {
@@ -40,11 +46,11 @@
     }
     if (methodsToBeRemoved != null) {
       Set<DexEncodedMethod> finalMethodsToBeRemoved = methodsToBeRemoved;
-      return Arrays.stream(methods)
+      return methods.stream()
           .filter(method -> !finalMethodsToBeRemoved.contains(method))
           .toArray(DexEncodedMethod[]::new);
     }
-    return methods;
+    return null;
   }
 
   private boolean isUnneededVisibilityBridge(DexEncodedMethod method) {
diff --git a/src/main/java/com/android/tools/r8/shaking/AbstractMethodRemover.java b/src/main/java/com/android/tools/r8/shaking/AbstractMethodRemover.java
index ae88ba7..0bdbcee 100644
--- a/src/main/java/com/android/tools/r8/shaking/AbstractMethodRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AbstractMethodRemover.java
@@ -38,20 +38,23 @@
     DexClass holder = appInfo.definitionFor(type);
     scope = scope.newNestedScope();
     if (holder != null && !holder.isLibraryClass()) {
-      holder.setVirtualMethods(processMethods(holder.virtualMethods()));
+      DexEncodedMethod[] newVirtualMethods = processMethods(holder.virtualMethods());
+      if (newVirtualMethods != null) {
+        holder.setVirtualMethods(newVirtualMethods);
+      }
     }
     type.forAllExtendsSubtypes(this::processClass);
     scope = scope.getParent();
   }
 
-  private DexEncodedMethod[] processMethods(DexEncodedMethod[] virtualMethods) {
+  private DexEncodedMethod[] processMethods(List<DexEncodedMethod> virtualMethods) {
     if (virtualMethods == null) {
       return null;
     }
     // Removal of abstract methods is rare, so avoid copying the array until we find one.
     List<DexEncodedMethod> methods = null;
-    for (int i = 0; i < virtualMethods.length; i++) {
-      DexEncodedMethod method = virtualMethods[i];
+    for (int i = 0; i < virtualMethods.size(); i++) {
+      DexEncodedMethod method = virtualMethods.get(i);
       if (scope.addMethodIfMoreVisible(method)
           || !method.accessFlags.isAbstract()
           || appInfo.isPinned(method.method)) {
@@ -60,9 +63,9 @@
         }
       } else {
         if (methods == null) {
-          methods = new ArrayList<>(virtualMethods.length - 1);
+          methods = new ArrayList<>(virtualMethods.size() - 1);
           for (int j = 0; j < i; j++) {
-            methods.add(virtualMethods[j]);
+            methods.add(virtualMethods.get(j));
           }
         }
         if (Log.ENABLED) {
@@ -70,7 +73,10 @@
         }
       }
     }
-    return methods == null ? virtualMethods : methods.toArray(new DexEncodedMethod[methods.size()]);
+    if (methods != null) {
+      return methods.toArray(new DexEncodedMethod[methods.size()]);
+    }
+    return null;
   }
 
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 8ce5cb0..3fa9db5 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -18,7 +18,6 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
-import java.util.Arrays;
 import java.util.List;
 
 public class AnnotationRemover {
@@ -184,8 +183,9 @@
       return original;
     }
     assert definition.isInterface();
-    boolean liveGetter = Arrays.stream(definition.virtualMethods())
-        .anyMatch(method -> method.method.name == original.name);
+    boolean liveGetter =
+        definition.virtualMethods().stream()
+            .anyMatch(method -> method.method.name == original.name);
     return liveGetter ? original : null;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index f94954b..a0c9735 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -495,14 +495,16 @@
       }
       // In compat mode traverse all direct methods in the hierarchy.
       if (clazz == startingClass || options.forceProguardCompatibility) {
-        Arrays.stream(clazz.directMethods())
+        clazz
+            .directMethods()
             .forEach(
                 method -> {
                   DexDefinition precondition = testAndGetPrecondition(method, preconditionSupplier);
                   markMethod(method, memberKeepRules, methodsMarked, rule, precondition);
                 });
       }
-      Arrays.stream(clazz.virtualMethods())
+      clazz
+          .virtualMethods()
           .forEach(
               method -> {
                 DexDefinition precondition = testAndGetPrecondition(method, preconditionSupplier);
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index a51116b..42b7e21 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -30,8 +30,10 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Multiset.Entry;
 import com.google.common.collect.Streams;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Predicate;
@@ -257,7 +259,7 @@
     if (appView.appInfo().neverMerge.contains(clazz.type)) {
       return MergeGroup.DONT_MERGE;
     }
-    if (clazz.staticFields().length + clazz.directMethods().length + clazz.virtualMethods().length
+    if (clazz.staticFields().length + clazz.directMethods().size() + clazz.virtualMethods().size()
         == 0) {
       return MergeGroup.DONT_MERGE;
     }
@@ -268,10 +270,10 @@
         .anyMatch(field -> appView.appInfo().isPinned(field.field))) {
       return MergeGroup.DONT_MERGE;
     }
-    if (Arrays.stream(clazz.directMethods()).anyMatch(DexEncodedMethod::isInitializer)) {
+    if (clazz.directMethods().stream().anyMatch(DexEncodedMethod::isInitializer)) {
       return MergeGroup.DONT_MERGE;
     }
-    if (!Arrays.stream(clazz.virtualMethods()).allMatch(DexEncodedMethod::isPrivateMethod)) {
+    if (!clazz.virtualMethods().stream().allMatch(DexEncodedMethod::isPrivateMethod)) {
       return MergeGroup.DONT_MERGE;
     }
     if (Streams.stream(clazz.methods())
@@ -445,7 +447,7 @@
       return false;
     }
     // Check that all of the members are private or public.
-    if (!Arrays.stream(clazz.directMethods())
+    if (!clazz.directMethods().stream()
         .allMatch(method -> method.accessFlags.isPrivate() || method.accessFlags.isPublic())) {
       return false;
     }
@@ -458,7 +460,7 @@
     // virtual methods are private. Therefore, we don't need to consider check if there are any
     // package-private or protected instance fields or virtual methods here.
     assert Arrays.stream(clazz.instanceFields()).count() == 0;
-    assert Arrays.stream(clazz.virtualMethods()).allMatch(method -> method.accessFlags.isPrivate());
+    assert clazz.virtualMethods().stream().allMatch(method -> method.accessFlags.isPrivate());
 
     // Check that no methods access package-private or protected members.
     IllegalAccessDetector registry = new IllegalAccessDetector(appView, clazz);
@@ -489,9 +491,9 @@
     numberOfMergedClasses++;
 
     // Move members from source to target.
-    targetClass.setDirectMethods(
+    targetClass.appendDirectMethods(
         mergeMethods(sourceClass.directMethods(), targetClass.directMethods(), targetClass));
-    targetClass.setVirtualMethods(
+    targetClass.appendVirtualMethods(
         mergeMethods(sourceClass.virtualMethods(), targetClass.virtualMethods(), targetClass));
     targetClass.setStaticFields(
         mergeFields(sourceClass.staticFields(), targetClass.staticFields(), targetClass));
@@ -502,30 +504,25 @@
     sourceClass.setStaticFields(DexEncodedField.EMPTY_ARRAY);
   }
 
-  private DexEncodedMethod[] mergeMethods(
-      DexEncodedMethod[] sourceMethods,
-      DexEncodedMethod[] targetMethods,
+  private List<DexEncodedMethod> mergeMethods(
+      List<DexEncodedMethod> sourceMethods,
+      List<DexEncodedMethod> targetMethods,
       DexProgramClass targetClass) {
-    DexEncodedMethod[] result = new DexEncodedMethod[sourceMethods.length + targetMethods.length];
-
-    // Move all target methods to result.
-    System.arraycopy(targetMethods, 0, result, 0, targetMethods.length);
-
     // Move source methods to result one by one, renaming them if needed.
     MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get();
     Set<Wrapper<DexMethod>> existingMethods =
-        Arrays.stream(targetMethods)
+        targetMethods.stream()
             .map(targetMethod -> equivalence.wrap(targetMethod.method))
             .collect(Collectors.toSet());
 
     Predicate<DexMethod> availableMethodSignatures =
         method -> !existingMethods.contains(equivalence.wrap(method));
 
-    int i = targetMethods.length;
+    List<DexEncodedMethod> newMethods = new ArrayList<>(sourceMethods.size());
     for (DexEncodedMethod sourceMethod : sourceMethods) {
       DexEncodedMethod sourceMethodAfterMove =
           renameMethodIfNeeded(sourceMethod, targetClass, availableMethodSignatures);
-      result[i++] = sourceMethodAfterMove;
+      newMethods.add(sourceMethodAfterMove);
 
       DexMethod originalMethod =
           methodMapping.inverse().getOrDefault(sourceMethod.method, sourceMethod.method);
@@ -533,8 +530,7 @@
 
       existingMethods.add(equivalence.wrap(sourceMethodAfterMove.method));
     }
-
-    return result;
+    return newMethods;
   }
 
   private DexEncodedField[] mergeFields(
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 6cf0da0..acc1e78 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -99,8 +100,15 @@
         // The class is used and must be kept. Remove the unused fields and methods from
         // the class.
         usagePrinter.visiting(clazz);
-        clazz.setDirectMethods(reachableMethods(clazz.directMethods(), clazz));
-        clazz.setVirtualMethods(reachableMethods(clazz.virtualMethods(), clazz));
+        DexEncodedMethod[] reachableDirectMethods = reachableMethods(clazz.directMethods(), clazz);
+        if (reachableDirectMethods != null) {
+          clazz.setDirectMethods(reachableDirectMethods);
+        }
+        DexEncodedMethod[] reachableVirtualMethods =
+            reachableMethods(clazz.virtualMethods(), clazz);
+        if (reachableVirtualMethods != null) {
+          clazz.setVirtualMethods(reachableVirtualMethods);
+        }
         clazz.setInstanceFields(reachableFields(clazz.instanceFields()));
         clazz.setStaticFields(reachableFields(clazz.staticFields()));
         clazz.removeInnerClasses(this::isAttributeReferencingPrunedType);
@@ -145,9 +153,9 @@
   }
 
   private <S extends PresortedComparable<S>, T extends KeyedDexItem<S>> int firstUnreachableIndex(
-      T[] items, Predicate<S> live) {
-    for (int i = 0; i < items.length; i++) {
-      if (!live.test(items[i].getKey())) {
+      List<T> items, Predicate<S> live) {
+    for (int i = 0; i < items.size(); i++) {
+      if (!live.test(items.get(i).getKey())) {
         return i;
       }
     }
@@ -159,18 +167,18 @@
         && method.method.proto.parameters.isEmpty();
   }
 
-  private DexEncodedMethod[] reachableMethods(DexEncodedMethod[] methods, DexClass clazz) {
+  private DexEncodedMethod[] reachableMethods(List<DexEncodedMethod> methods, DexClass clazz) {
     int firstUnreachable = firstUnreachableIndex(methods, appInfo.liveMethods::contains);
     // Return the original array if all methods are used.
     if (firstUnreachable == -1) {
-      return methods;
+      return null;
     }
-    ArrayList<DexEncodedMethod> reachableMethods = new ArrayList<>(methods.length);
+    ArrayList<DexEncodedMethod> reachableMethods = new ArrayList<>(methods.size());
     for (int i = 0; i < firstUnreachable; i++) {
-      reachableMethods.add(methods[i]);
+      reachableMethods.add(methods.get(i));
     }
-    for (int i = firstUnreachable; i < methods.length; i++) {
-      DexEncodedMethod method = methods[i];
+    for (int i = firstUnreachable; i < methods.size(); i++) {
+      DexEncodedMethod method = methods.get(i);
       if (appInfo.liveMethods.contains(method.getKey())) {
         reachableMethods.add(method);
       } else if (options.debugKeepRules && isDefaultConstructor(method)) {
@@ -222,7 +230,8 @@
             appInfo.liveFields.contains(field)
                 || appInfo.fieldsRead.contains(field)
                 || appInfo.fieldsWritten.contains(field);
-    int firstUnreachable = firstUnreachableIndex(fields, isReachableOrReferencedField);
+    int firstUnreachable =
+        firstUnreachableIndex(Arrays.asList(fields), isReachableOrReferencedField);
     // Return the original array if all fields are used.
     if (firstUnreachable == -1) {
       return fields;
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 145f24b..b68bf80 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClass.MethodSetter;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -993,11 +994,6 @@
         return false;
       }
 
-      DexEncodedMethod[] mergedDirectMethods =
-          mergeMethods(directMethods.values(), target.directMethods());
-      DexEncodedMethod[] mergedVirtualMethods =
-          mergeMethods(virtualMethods.values(), target.virtualMethods());
-
       // Step 2: Merge fields
       Set<DexString> existingFieldNames = new HashSet<>();
       for (DexEncodedField field : target.fields()) {
@@ -1044,8 +1040,8 @@
               ? DexTypeList.empty()
               : new DexTypeList(interfaces.toArray(new DexType[0]));
       // Step 2: replace fields and methods.
-      target.setDirectMethods(mergedDirectMethods);
-      target.setVirtualMethods(mergedVirtualMethods);
+      target.appendDirectMethods(directMethods.values());
+      target.appendVirtualMethods(virtualMethods.values());
       target.setInstanceFields(mergedInstanceFields);
       target.setStaticFields(mergedStaticFields);
       // Step 3: Unlink old class to ease tree shaking.
@@ -1272,8 +1268,8 @@
     }
 
     private DexEncodedMethod[] mergeMethods(
-        Collection<DexEncodedMethod> sourceMethods, DexEncodedMethod[] targetMethods) {
-      DexEncodedMethod[] result = new DexEncodedMethod[sourceMethods.size() + targetMethods.length];
+        Collection<DexEncodedMethod> sourceMethods, List<DexEncodedMethod> targetMethods) {
+      DexEncodedMethod[] result = new DexEncodedMethod[sourceMethods.size() + targetMethods.size()];
       // Add methods from source.
       int i = 0;
       for (DexEncodedMethod method : sourceMethods) {
@@ -1281,7 +1277,7 @@
         i++;
       }
       // Add methods from target.
-      System.arraycopy(targetMethods, 0, result, i, targetMethods.length);
+      System.arraycopy(targetMethods, 0, result, i, targetMethods.size());
       return result;
     }
 
@@ -1425,8 +1421,8 @@
     private GraphLense fixupTypeReferences(GraphLense graphLense) {
       // Globally substitute merged class types in protos and holders.
       for (DexProgramClass clazz : appInfo.classes()) {
-        clazz.setDirectMethods(substituteTypesIn(clazz.directMethods()));
-        clazz.setVirtualMethods(substituteTypesIn(clazz.virtualMethods()));
+        fixupMethods(clazz.directMethods(), clazz::setDirectMethod);
+        fixupMethods(clazz.virtualMethods(), clazz::setVirtualMethod);
         clazz.setStaticFields(substituteTypesIn(clazz.staticFields()));
         clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields()));
       }
@@ -1440,20 +1436,19 @@
       return lense.build(application.dexItemFactory, graphLense);
     }
 
-    private DexEncodedMethod[] substituteTypesIn(DexEncodedMethod[] methods) {
+    private void fixupMethods(List<DexEncodedMethod> methods, MethodSetter setter) {
       if (methods == null) {
-        return null;
+        return;
       }
-      for (int i = 0; i < methods.length; i++) {
-        DexEncodedMethod encodedMethod = methods[i];
+      for (int i = 0; i < methods.size(); i++) {
+        DexEncodedMethod encodedMethod = methods.get(i);
         DexMethod method = encodedMethod.method;
         DexMethod newMethod = fixupMethod(method);
         if (newMethod != method) {
           lense.move(method, newMethod);
-          methods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
+          setter.setMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
         }
       }
-      return methods;
     }
 
     private DexEncodedField[] substituteTypesIn(DexEncodedField[] fields) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index cad6397..86cb9d0 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -20,15 +20,18 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.optimize.Inliner;
+import com.android.tools.r8.naming.InterfaceMethodNameMinifier;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.utils.IROrdering.IdentityIROrdering;
 import com.android.tools.r8.utils.IROrdering.NondeterministicIROrdering;
+import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -218,6 +221,9 @@
   }
 
   public Set<String> extensiveLoggingFilter = getExtensiveLoggingFilter();
+  public Set<String> extensiveInterfaceMethodMinifierLoggingFilter =
+      getExtensiveInterfaceMethodMinifierLoggingFilter();
+
   public List<String> methodsFilter = ImmutableList.of();
   public int minApiLevel = AndroidApiLevel.getDefault().getLevel();
   // Skipping min_api check and compiling an intermediate result intended for later merging.
@@ -288,6 +294,19 @@
     return ImmutableSet.of();
   }
 
+  private static Set<String> getExtensiveInterfaceMethodMinifierLoggingFilter() {
+    String property =
+        System.getProperty("com.android.tools.r8.extensiveInterfaceMethodMinifierLoggingFilter");
+    if (property != null) {
+      ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+      for (String method : property.split(";")) {
+        builder.add(method);
+      }
+      return builder.build();
+    }
+    return ImmutableSet.of();
+  }
+
   public static class InvalidParameterAnnotationInfo {
 
     final DexMethod method;
@@ -532,6 +551,21 @@
     public boolean noLocalsTableOnInput = false;
     public boolean forceNameReflectionOptimization = false;
     public boolean disallowLoadStoreOptimization = false;
+
+    public MinifierTestingOptions minifier = new MinifierTestingOptions();
+
+    public static class MinifierTestingOptions {
+
+      public Comparator<DexMethod> interfaceMethodOrdering = null;
+
+      public Comparator<Wrapper<DexMethod>> createInterfaceMethodOrdering(
+          InterfaceMethodNameMinifier minifier) {
+        if (interfaceMethodOrdering != null) {
+          return (a, b) -> interfaceMethodOrdering.compare(a.get(), b.get());
+        }
+        return minifier.createDefaultInterfaceMethodOrdering();
+      }
+    }
   }
 
   private boolean hasMinApi(AndroidApiLevel level) {
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index c9be1b8..f5a4b81 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -341,7 +341,7 @@
   private static IdentityHashMap<DexString, List<DexEncodedMethod>> groupMethodsByRenamedName(
       NamingLens namingLens, DexProgramClass clazz) {
     IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByRenamedName =
-        new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
+        new IdentityHashMap<>(clazz.directMethods().size() + clazz.virtualMethods().size());
     for (DexEncodedMethod method : clazz.methods()) {
       // Add method only if renamed or contains positions.
       DexString renamedName = namingLens.lookupName(method.method);
diff --git a/src/main/java/com/android/tools/r8/utils/OrderedMergingIterator.java b/src/main/java/com/android/tools/r8/utils/OrderedMergingIterator.java
index dd4c73a..33f9d7c 100644
--- a/src/main/java/com/android/tools/r8/utils/OrderedMergingIterator.java
+++ b/src/main/java/com/android/tools/r8/utils/OrderedMergingIterator.java
@@ -7,48 +7,49 @@
 import com.android.tools.r8.graph.KeyedDexItem;
 import com.android.tools.r8.graph.PresortedComparable;
 import java.util.Iterator;
+import java.util.List;
 import java.util.NoSuchElementException;
 
 public class OrderedMergingIterator<T extends KeyedDexItem<S>, S extends PresortedComparable<S>>
     implements Iterator<T> {
 
-  private final T[] one;
-  private final T[] other;
+  private final List<T> one;
+  private final List<T> other;
   private int oneIndex = 0;
   private int otherIndex = 0;
 
-  public OrderedMergingIterator(T[] one, T[] other) {
+  public OrderedMergingIterator(List<T> one, List<T> other) {
     this.one = one;
     this.other = other;
   }
 
-  private static <T> T getNextChecked(T[] array, int position) {
-    if (position >= array.length) {
+  private static <T> T getNextChecked(List<T> list, int position) {
+    if (position >= list.size()) {
       throw new NoSuchElementException();
     }
-    return array[position];
+    return list.get(position);
   }
 
   @Override
   public boolean hasNext() {
-    return oneIndex < one.length || otherIndex < other.length;
+    return oneIndex < one.size() || otherIndex < other.size();
   }
 
   @Override
   public T next() {
-    if (oneIndex >= one.length) {
+    if (oneIndex >= one.size()) {
       return getNextChecked(other, otherIndex++);
     }
-    if (otherIndex >= other.length) {
+    if (otherIndex >= other.size()) {
       return getNextChecked(one, oneIndex++);
     }
-    int comparison = one[oneIndex].getKey().compareTo(other[otherIndex].getKey());
+    int comparison = one.get(oneIndex).getKey().compareTo(other.get(otherIndex).getKey());
     if (comparison < 0) {
-      return one[oneIndex++];
+      return one.get(oneIndex++);
     }
     if (comparison == 0) {
       throw new InternalCompilerError("Source arrays are not disjoint.");
     }
-    return other[otherIndex++];
+    return other.get(otherIndex++);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestRunResult.java b/src/test/java/com/android/tools/r8/R8TestRunResult.java
index 55730eb..ecf247c 100644
--- a/src/test/java/com/android/tools/r8/R8TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestRunResult.java
@@ -57,7 +57,6 @@
     return self();
   }
 
-
   public String proguardMap() {
     return proguardMap;
   }
diff --git a/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java b/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java
index 36a4f00..bfd929e 100644
--- a/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java
+++ b/src/test/java/com/android/tools/r8/R8UnreachableCodeTest.java
@@ -45,7 +45,7 @@
     IRConverter converter = new IRConverter(new AppInfoWithSubtyping(application), options);
     converter.optimize(application);
     DexProgramClass clazz = application.classes().iterator().next();
-    assertEquals(4, clazz.directMethods().length);
+    assertEquals(4, clazz.directMethods().size());
     for (DexEncodedMethod method : clazz.directMethods()) {
       if (!method.method.name.toString().equals("main")) {
         assertEquals(2, method.getCode().asDexCode().instructions.length);
diff --git a/src/test/java/com/android/tools/r8/cf/IdenticalCatchHandlerTest.java b/src/test/java/com/android/tools/r8/cf/IdenticalCatchHandlerTest.java
index 3d75846..a958c3a 100644
--- a/src/test/java/com/android/tools/r8/cf/IdenticalCatchHandlerTest.java
+++ b/src/test/java/com/android/tools/r8/cf/IdenticalCatchHandlerTest.java
@@ -72,7 +72,7 @@
   private int countCatchHandlers(AndroidApp inputApp) throws Exception {
     CodeInspector inspector = new CodeInspector(inputApp);
     DexClass dexClass = inspector.clazz(TestClass.class).getDexClass();
-    Code code = dexClass.virtualMethods()[0].getCode();
+    Code code = dexClass.virtualMethods().get(0).getCode();
     if (code.isCfCode()) {
       CfCode cfCode = code.asCfCode();
       Set<CfLabel> targets = Sets.newIdentityHashSet();
diff --git a/src/test/java/com/android/tools/r8/cf/NonidenticalCatchHandlerTest.java b/src/test/java/com/android/tools/r8/cf/NonidenticalCatchHandlerTest.java
index 2664ef4..5d536cf 100644
--- a/src/test/java/com/android/tools/r8/cf/NonidenticalCatchHandlerTest.java
+++ b/src/test/java/com/android/tools/r8/cf/NonidenticalCatchHandlerTest.java
@@ -71,7 +71,7 @@
   private int countCatchHandlers(AndroidApp inputApp) throws Exception {
     CodeInspector inspector = new CodeInspector(inputApp);
     DexClass dexClass = inspector.clazz(TestClass.class).getDexClass();
-    Code code = dexClass.virtualMethods()[0].getCode();
+    Code code = dexClass.virtualMethods().get(0).getCode();
     if (code.isCfCode()) {
       CfCode cfCode = code.asCfCode();
       Set<CfLabel> targets = Sets.newIdentityHashSet();
diff --git a/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java b/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java
index 68daeb8..9c9e785 100644
--- a/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java
+++ b/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java
@@ -59,11 +59,11 @@
     assertThat(clazz, isPresent());
 
     // There are two direct methods, but only because one is <init>.
-    assertEquals(2, clazz.getDexClass().directMethods().length);
+    assertEquals(2, clazz.getDexClass().directMethods().size());
     assertThat(clazz.method("void", "<init>", ImmutableList.of()), isPresent());
 
     // There is only one virtual method.
-    assertEquals(1, clazz.getDexClass().virtualMethods().length);
+    assertEquals(1, clazz.getDexClass().virtualMethods().size());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
index b8c4b47..7e9269d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
@@ -183,7 +183,7 @@
           if (check.match(clazz)) {
             // Validate static initializer.
             if (check instanceof Group) {
-              assertEquals(clazz.directMethods().length, ((Group) check).singletons == 0 ? 1 : 2);
+              assertEquals(clazz.directMethods().size(), ((Group) check).singletons == 0 ? 1 : 2);
             }
 
             list.remove(clazz);
@@ -228,8 +228,8 @@
     } else {
       assertTrue(isJStyleLambdaOrGroup(clazz));
       // Taking the number of any virtual method parameters seems to be good enough.
-      assertTrue(clazz.virtualMethods().length > 0);
-      return clazz.virtualMethods()[0].method.proto.parameters.size();
+      assertTrue(clazz.virtualMethods().size() > 0);
+      return clazz.virtualMethods().get(0).method.proto.parameters.size();
     }
     fail("Failed to get arity for " + clazz.type.descriptor.toString());
     throw new AssertionError();
diff --git a/src/test/java/com/android/tools/r8/naming/InterfaceMethodNameMinifierTest.java b/src/test/java/com/android/tools/r8/naming/InterfaceMethodNameMinifierTest.java
new file mode 100644
index 0000000..f2e1a60
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/InterfaceMethodNameMinifierTest.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2019, 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.naming;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.utils.FileUtils;
+import java.nio.file.Path;
+import org.junit.Test;
+
+/** Regression test for b/123730537. */
+public class InterfaceMethodNameMinifierTest extends TestBase {
+
+  @Test
+  public void test() throws Exception {
+    Path dictionary = temp.getRoot().toPath().resolve("dictionary.txt");
+    FileUtils.writeTextFile(dictionary, "a", "b", "c");
+
+    testForR8(Backend.DEX)
+        .addInnerClasses(InterfaceMethodNameMinifierTest.class)
+        .addKeepRules(
+            "-keep,allowobfuscation interface * { <methods>; }",
+            "-keep,allowobfuscation class * { <methods>; }",
+            "-keep interface " + L.class.getTypeName() + " { <methods>; }",
+            "-obfuscationdictionary " + dictionary.toString())
+        // Minify the interface methods in alphabetic order.
+        .addOptionsModification(
+            options -> options.testing.minifier.interfaceMethodOrdering = DexMethod::slowCompareTo)
+        .compile();
+  }
+
+  interface I {}
+
+  interface J extends I {
+
+    // Will be renamed first, to a().
+    void a();
+
+    // Will be renamed secondly. Should be renamed to c(), and not b(), because `void K.b()` will
+    // be renamed to b() because `void L.b()` is reserved.
+    void c();
+  }
+
+  interface K extends I {
+
+    // Will be renamed thirdly, to b(), because `void L.b()` is reserved.
+    void b();
+  }
+
+  interface L {
+
+    // Reserved. Will be renamed together with `void K.b()`.
+    void b();
+  }
+
+  static class Implementation implements J, K {
+
+    @Override
+    public void a() {}
+
+    @Override
+    public void b() {}
+
+    @Override
+    public void c() {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/AsterisksTest.java b/src/test/java/com/android/tools/r8/shaking/AsterisksTest.java
index d28770a..5b02222 100644
--- a/src/test/java/com/android/tools/r8/shaking/AsterisksTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/AsterisksTest.java
@@ -100,7 +100,7 @@
     assertThat(classSubject, isPresent());
     assertThat(classSubject, not(isRenamed()));
     DexClass clazz = classSubject.getDexClass();
-    assertEquals(3, clazz.virtualMethods().length);
+    assertEquals(3, clazz.virtualMethods().size());
     for (DexEncodedMethod encodedMethod : clazz.virtualMethods()) {
       assertTrue(encodedMethod.method.name.toString().startsWith("foo"));
       MethodSubject methodSubject =
@@ -141,7 +141,7 @@
     assertThat(classSubject, isPresent());
     assertThat(classSubject, not(isRenamed()));
     DexClass clazz = classSubject.getDexClass();
-    assertEquals(3, clazz.virtualMethods().length);
+    assertEquals(3, clazz.virtualMethods().size());
     for (DexEncodedMethod encodedMethod : clazz.virtualMethods()) {
       assertTrue(encodedMethod.method.name.toString().startsWith("foo"));
       MethodSubject methodSubject =
@@ -163,7 +163,7 @@
     assertThat(classSubject, isPresent());
     assertThat(classSubject, not(isRenamed()));
     DexClass clazz = classSubject.getDexClass();
-    assertEquals(3, clazz.virtualMethods().length);
+    assertEquals(3, clazz.virtualMethods().size());
     for (DexEncodedMethod encodedMethod : clazz.virtualMethods()) {
       assertTrue(encodedMethod.method.name.toString().startsWith("foo"));
       MethodSubject methodSubject =
diff --git a/src/test/java/com/android/tools/r8/smali/OutlineTest.java b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
index eacc338..bfeac7a 100644
--- a/src/test/java/com/android/tools/r8/smali/OutlineTest.java
+++ b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
@@ -848,13 +848,13 @@
     CodeInspector inspector = new CodeInspector(processedApplication);
     ClassSubject clazz = inspector.clazz(OutlineOptions.CLASS_NAME);
     assertTrue(clazz.isPresent());
-    assertEquals(3, clazz.getDexClass().directMethods().length);
+    assertEquals(3, clazz.getDexClass().directMethods().size());
     // Collect the return types of the putlines for the body of method1 and method2.
     List<DexType> r = new ArrayList<>();
-    for (int i = 0; i < clazz.getDexClass().directMethods().length; i++) {
-      if (clazz.getDexClass().directMethods()[i].getCode().asDexCode().instructions[0]
+    for (int i = 0; i < clazz.getDexClass().directMethods().size(); i++) {
+      if (clazz.getDexClass().directMethods().get(i).getCode().asDexCode().instructions[0]
           instanceof InvokeVirtual) {
-        r.add(clazz.getDexClass().directMethods()[i].method.proto.returnType);
+        r.add(clazz.getDexClass().directMethods().get(i).method.proto.returnType);
       }
     }
     assert r.size() == 2;
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
index d870316..fe98075 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -185,7 +185,7 @@
   }
 
   static <S, T extends Subject> void forAll(
-      S[] items,
+      List<? extends S> items,
       BiFunction<S, FoundClassSubject, ? extends T> constructor,
       FoundClassSubject clazz,
       Consumer<T> consumer) {
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
index ed4a67f..ebaf5af 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
 import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
 
@@ -84,7 +85,7 @@
         : new FoundMethodSubject(codeInspector, encoded, this);
   }
 
-  private DexEncodedMethod findMethod(DexEncodedMethod[] methods, DexMethod dexMethod) {
+  private DexEncodedMethod findMethod(List<DexEncodedMethod> methods, DexMethod dexMethod) {
     for (DexEncodedMethod method : methods) {
       if (method.method.equals(dexMethod)) {
         return method;
@@ -108,12 +109,12 @@
   @Override
   public void forAllFields(Consumer<FoundFieldSubject> inspection) {
     CodeInspector.forAll(
-        dexClass.staticFields(),
+        Arrays.asList(dexClass.staticFields()),
         (dexField, clazz) -> new FoundFieldSubject(codeInspector, dexField, clazz),
         this,
         inspection);
     CodeInspector.forAll(
-        dexClass.instanceFields(),
+        Arrays.asList(dexClass.instanceFields()),
         (dexField, clazz) -> new FoundFieldSubject(codeInspector, dexField, clazz),
         this,
         inspection);