Disallow direct manipulation of method arrays

This CL changes the API of DexClass, such that usages can no longer manipulate the arrays DexClass.directMethods and DexClass.virtualMethods directly. To achieve this, the methods DexClass.directMethods() and DexClass.virtualMethods() no longer return a direct reference to the underlying arrays. Instead, these arrays need to be mutated via appropriate methods on DexClass.

The motivation for this encapsulation is to be able to easily introduce an assertion that will fail upon the first change to DexClass.directMethods or DexClass.virtualMethods that introduce a duplicate method definition.

Bug: 123730537
Change-Id: Ia9459453391fe9a52b04b692b1ce277dfa7f27c7
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/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 5e71072..1af80fa 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 42ebde9..60893c7 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 {
@@ -77,8 +76,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 1a6264e..d2ce1ef 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,13 @@
 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 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 +22,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 = {};
 
@@ -124,16 +132,82 @@
     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;
+  }
+
+  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;
+  }
+
+  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;
   }
 
   public void setDirectMethods(DexEncodedMethod[] values) {
     directMethods = MoreObjects.firstNonNull(values, NO_METHODS);
   }
 
-  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;
+  }
+
+  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;
+  }
+
+  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;
   }
 
   public void setVirtualMethods(DexEncodedMethod[] values) {
@@ -289,14 +363,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 +451,9 @@
   }
 
   public DexEncodedMethod getClassInitializer() {
-    return Arrays.stream(directMethods()).filter(DexEncodedMethod::isClassInitializer).findAny()
+    return Arrays.stream(directMethods)
+        .filter(DexEncodedMethod::isClassInitializer)
+        .findAny()
         .orElse(null);
   }
 
@@ -553,7 +629,7 @@
 
   public boolean isValid() {
     assert !isInterface()
-        || Arrays.stream(virtualMethods()).noneMatch(method -> method.accessFlags.isFinal());
+        || Arrays.stream(virtualMethods).noneMatch(method -> method.accessFlags.isFinal());
     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/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 bff3ca1..ec788a4 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) {
@@ -149,7 +145,7 @@
         helper.merge(rewriter.getOrCreateInterfaceInfo(clazz, current, type));
       }
 
-      accumulatedVirtualMethods.addAll(Arrays.asList(clazz.virtualMethods()));
+      accumulatedVirtualMethods.addAll(clazz.virtualMethods());
 
       List<DexEncodedMethod> defaultMethodsInDirectInterface = helper.createFullList();
 
@@ -202,7 +198,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 466dc83..0c76382 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 fd875af..9e18c94 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()]));
     }
@@ -169,7 +169,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 df52ef1..4e29e6f 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
@@ -129,33 +129,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 4cc3207..ff330e1 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
@@ -179,9 +179,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(
@@ -194,7 +194,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);
@@ -209,9 +209,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);
@@ -229,13 +229,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);
@@ -249,7 +249,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 aae8429..6211270 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 6284338..0fd8cc4 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
@@ -548,16 +548,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);
@@ -567,7 +564,7 @@
           methodMapping.put(originalMethod, newMethod.method);
         }
       }
-      hostClass.setDirectMethods(newMethods);
+      hostClass.appendDirectMethods(newMethods);
     }
   }
 
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/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 f502692..dd8bcd6 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 75bb98b..fe401a1 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.
@@ -1283,8 +1279,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) {
@@ -1292,7 +1288,7 @@
         i++;
       }
       // Add methods from target.
-      System.arraycopy(targetMethods, 0, result, i, targetMethods.length);
+      System.arraycopy(targetMethods, 0, result, i, targetMethods.size());
       return result;
     }
 
@@ -1435,8 +1431,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()));
       }
@@ -1450,20 +1446,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/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/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/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 a9058ff..aa083da 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 11ee3af..c7b3781 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;
 
@@ -89,7 +90,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;
@@ -113,12 +114,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);