Introduce a custom method collection with replaceable backing.

This CL is a pure refactoring moving the existing dual-array representation into
a backing structure. Follow-up work will simplify the backing API and introduce
a map-based backing.

Change-Id: I2dbe9af024ed3afdfd79a3eab1db0254e35adabf
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index d1e70ed..5f2b826 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -381,7 +381,7 @@
       return false;
     }
     assert potentialHolder.isInterface();
-    for (DexEncodedMethod virtualMethod : potentialHolder.virtualMethods) {
+    for (DexEncodedMethod virtualMethod : potentialHolder.virtualMethods()) {
       if (virtualMethod.method.hasSameProtoAndName(method.method)
           && virtualMethod.accessFlags.isSameVisibility(method.accessFlags)) {
         return true;
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 a75fd4e..4c0019b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
-import com.android.tools.r8.utils.PredicateUtils;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
@@ -22,7 +21,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
-import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -37,8 +35,6 @@
     void setMethod(int index, DexEncodedMethod method);
   }
 
-  private Optional<DexEncodedMethod> cachedClassInitializer = null;
-
   public final Origin origin;
   public DexType type;
   public final ClassAccessFlags accessFlags;
@@ -55,10 +51,7 @@
   protected DexEncodedField[] instanceFields = DexEncodedField.EMPTY_ARRAY;
 
   /** Access has to be synchronized during concurrent collection/writing phase. */
-  protected DexEncodedMethod[] directMethods = DexEncodedMethod.EMPTY_ARRAY;
-
-  /** Access has to be synchronized during concurrent collection/writing phase. */
-  protected DexEncodedMethod[] virtualMethods = DexEncodedMethod.EMPTY_ARRAY;
+  protected final MethodCollection methodCollection;
 
   /** Enclosing context of this class if it is an inner class, null otherwise. */
   private EnclosingMethodAttribute enclosingMethod;
@@ -96,6 +89,7 @@
     this.type = type;
     setStaticFields(staticFields);
     setInstanceFields(instanceFields);
+    this.methodCollection = new MethodCollection(this);
     setDirectMethods(directMethods);
     setVirtualMethods(virtualMethods);
     this.nestHost = nestHost;
@@ -147,145 +141,59 @@
   }
 
   public List<DexEncodedMethod> directMethods() {
-    assert directMethods != null;
-    if (InternalOptions.assertionsEnabled()) {
-      return Collections.unmodifiableList(Arrays.asList(directMethods));
-    }
-    return Arrays.asList(directMethods);
+    return methodCollection.directMethods();
   }
 
   public Iterable<DexEncodedMethod> directMethods(Predicate<? super DexEncodedMethod> predicate) {
-    return Iterables.filter(Arrays.asList(directMethods), predicate::test);
+    return Iterables.filter(directMethods(), predicate::test);
   }
 
   public void appendDirectMethod(DexEncodedMethod method) {
-    cachedClassInitializer = null;
-    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();
+    methodCollection.appendDirectMethod(method);
   }
 
   public void appendDirectMethods(Collection<DexEncodedMethod> methods) {
-    cachedClassInitializer = null;
-    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();
+    methodCollection.appendDirectMethods(methods);
   }
 
   public void removeDirectMethod(int index) {
-    cachedClassInitializer = null;
-    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;
+    methodCollection.removeDirectMethod(index);
   }
 
   public void removeDirectMethod(DexMethod method) {
-    int index = -1;
-    for (int i = 0; i < directMethods.length; i++) {
-      if (method.match(directMethods[i])) {
-        index = i;
-        break;
-      }
-    }
-    if (index >= 0) {
-      removeDirectMethod(index);
-    }
+    methodCollection.removeDirectMethod(method);
   }
 
   public void setDirectMethod(int index, DexEncodedMethod method) {
-    cachedClassInitializer = null;
-    directMethods[index] = method;
-    assert verifyCorrectnessOfMethodHolder(method);
-    assert verifyNoDuplicateMethods();
+    methodCollection.setDirectMethod(index, method);
   }
 
   public void setDirectMethods(DexEncodedMethod[] methods) {
-    cachedClassInitializer = null;
-    directMethods = MoreObjects.firstNonNull(methods, DexEncodedMethod.EMPTY_ARRAY);
-    assert verifyCorrectnessOfMethodHolders(directMethods());
-    assert verifyNoDuplicateMethods();
+    methodCollection.setDirectMethods(methods);
   }
 
   public List<DexEncodedMethod> virtualMethods() {
-    assert virtualMethods != null;
-    if (InternalOptions.assertionsEnabled()) {
-      return Collections.unmodifiableList(Arrays.asList(virtualMethods));
-    }
-    return Arrays.asList(virtualMethods);
+    return methodCollection.virtualMethods();
   }
 
   public Iterable<DexEncodedMethod> virtualMethods(Predicate<? super DexEncodedMethod> predicate) {
-    return Iterables.filter(Arrays.asList(virtualMethods), predicate::test);
+    return Iterables.filter(virtualMethods(), predicate::test);
   }
 
   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();
+    methodCollection.appendVirtualMethod(method);
   }
 
   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;
+    methodCollection.appendVirtualMethods(methods);
   }
 
   public void setVirtualMethod(int index, DexEncodedMethod method) {
-    virtualMethods[index] = method;
-    assert verifyCorrectnessOfMethodHolder(method);
-    assert verifyNoDuplicateMethods();
+    methodCollection.setVirtualMethod(index, method);
   }
 
   public void setVirtualMethods(DexEncodedMethod[] methods) {
-    virtualMethods = MoreObjects.firstNonNull(methods, DexEncodedMethod.EMPTY_ARRAY);
-    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;
+    methodCollection.setVirtualMethods(methods);
   }
 
   private boolean verifyNoAbstractMethodsOnNonAbstractClasses(
@@ -301,75 +209,16 @@
     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) {
-    for (DexEncodedMethod method : directMethods()) {
-      consumer.accept(method);
-    }
-    for (DexEncodedMethod method : virtualMethods()) {
-      consumer.accept(method);
-    }
+    methodCollection.forEachMethod(consumer);
   }
 
-  public DexEncodedMethod[] allMethodsSorted() {
-    int vLen = virtualMethods.length;
-    int dLen = directMethods.length;
-    DexEncodedMethod[] result = new DexEncodedMethod[vLen + dLen];
-    System.arraycopy(virtualMethods, 0, result, 0, vLen);
-    System.arraycopy(directMethods, 0, result, vLen, dLen);
-    Arrays.sort(result,
-        (DexEncodedMethod a, DexEncodedMethod b) -> a.method.slowCompareTo(b.method));
-    return result;
-  }
-
-  public DexEncodedMethod[] directMethodsSorted() {
-    DexEncodedMethod[] result = new DexEncodedMethod[directMethods.length];
-    System.arraycopy(directMethods, 0, result, 0, directMethods.length);
-    Arrays.sort(
-        result, (DexEncodedMethod a, DexEncodedMethod b) -> a.method.slowCompareTo(b.method));
-    return result;
-  }
-
-  public DexEncodedMethod[] virtualMethodsSorted() {
-    DexEncodedMethod[] result = new DexEncodedMethod[virtualMethods.length];
-    System.arraycopy(virtualMethods, 0, result, 0, virtualMethods.length);
-    Arrays.sort(
-        result, (DexEncodedMethod a, DexEncodedMethod b) -> a.method.slowCompareTo(b.method));
-    return result;
+  public List<DexEncodedMethod> allMethodsSorted() {
+    return methodCollection.allMethodsSorted();
   }
 
   public void virtualizeMethods(Set<DexEncodedMethod> privateInstanceMethods) {
-    int vLen = virtualMethods.length;
-    int dLen = directMethods.length;
-    int mLen = privateInstanceMethods.size();
-    assert mLen <= dLen;
-
-    DexEncodedMethod[] newDirectMethods = new DexEncodedMethod[dLen - mLen];
-    int index = 0;
-    for (int i = 0; i < dLen; i++) {
-      DexEncodedMethod encodedMethod = directMethods[i];
-      if (!privateInstanceMethods.contains(encodedMethod)) {
-        newDirectMethods[index++] = encodedMethod;
-      }
-    }
-    assert index == dLen - mLen;
-    setDirectMethods(newDirectMethods);
-
-    DexEncodedMethod[] newVirtualMethods = new DexEncodedMethod[vLen + mLen];
-    System.arraycopy(virtualMethods, 0, newVirtualMethods, 0, vLen);
-    index = vLen;
-    for (DexEncodedMethod encodedMethod : privateInstanceMethods) {
-      newVirtualMethods[index++] = encodedMethod;
-    }
-    setVirtualMethods(newVirtualMethods);
+    methodCollection.virtualizeMethods(privateInstanceMethods);
   }
 
   /**
@@ -561,28 +410,27 @@
 
   /** Find direct method in this class matching {@param method}. */
   public DexEncodedMethod lookupDirectMethod(DexMethod method) {
-    return lookupTarget(directMethods, method);
+    return methodCollection.getDirectMethod(method);
   }
 
   /** Find direct method in this class matching {@param predicate}. */
   public DexEncodedMethod lookupDirectMethod(Predicate<DexEncodedMethod> predicate) {
-    return PredicateUtils.findFirst(directMethods, predicate);
+    return methodCollection.getDirectMethod(predicate);
   }
 
   /** Find virtual method in this class matching {@param method}. */
   public DexEncodedMethod lookupVirtualMethod(DexMethod method) {
-    return lookupTarget(virtualMethods, method);
+    return methodCollection.getVirtualMethod(method);
   }
 
   /** Find virtual method in this class matching {@param predicate}. */
   public DexEncodedMethod lookupVirtualMethod(Predicate<DexEncodedMethod> predicate) {
-    return PredicateUtils.findFirst(virtualMethods, predicate);
+    return methodCollection.getVirtualMethod(predicate);
   }
 
   /** Find method in this class matching {@param method}. */
   public DexEncodedMethod lookupMethod(DexMethod method) {
-    DexEncodedMethod result = lookupDirectMethod(method);
-    return result == null ? lookupVirtualMethod(method) : result;
+    return methodCollection.getMethod(method);
   }
 
   public DexEncodedMethod lookupSignaturePolymorphicMethod(
@@ -592,7 +440,7 @@
     }
     DexEncodedMethod matchingName = null;
     DexEncodedMethod signaturePolymorphicMethod = null;
-    for (DexEncodedMethod method : virtualMethods) {
+    for (DexEncodedMethod method : virtualMethods()) {
       if (method.method.name == methodName) {
         if (matchingName != null) {
           // The jvm spec, section 5.4.3.3 details that there must be exactly one method with the
@@ -701,16 +549,7 @@
   }
 
   public synchronized DexEncodedMethod getClassInitializer() {
-    if (cachedClassInitializer == null) {
-      cachedClassInitializer = Optional.empty();
-      for (DexEncodedMethod directMethod : directMethods) {
-        if (directMethod.isClassInitializer()) {
-          cachedClassInitializer = Optional.of(directMethod);
-          break;
-        }
-      }
-    }
-    return cachedClassInitializer.orElse(null);
+    return methodCollection.getClassInitializer();
   }
 
   public Origin getOrigin() {
@@ -1004,8 +843,7 @@
     assert !isInterface() || virtualMethods().stream().noneMatch(DexEncodedMethod::isFinal);
     assert verifyCorrectnessOfFieldHolders(fields());
     assert verifyNoDuplicateFields();
-    assert verifyCorrectnessOfMethodHolders(methods());
-    assert verifyNoDuplicateMethods();
+    assert methodCollection.verify();
     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 78a59c2..35f209a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -160,8 +160,9 @@
       }
       synchronizedCollectAll(indexedItems, staticFields);
       synchronizedCollectAll(indexedItems, instanceFields);
-      synchronizedCollectAll(indexedItems, directMethods);
-      synchronizedCollectAll(indexedItems, virtualMethods);
+      synchronized (methodCollection) {
+        methodCollection.forEachMethod(m -> m.collectIndexedItems(indexedItems));
+      }
     }
   }
 
@@ -196,8 +197,9 @@
       // The ordering of methods and fields may not be deterministic due to concurrency
       // (see b/116027780).
       sortMembers();
-      synchronizedCollectAll(collector, directMethods);
-      synchronizedCollectAll(collector, virtualMethods);
+      synchronized (methodCollection) {
+        methodCollection.forEachMethod(m -> m.collectMixedSectionItems(collector));
+      }
       synchronizedCollectAll(collector, staticFields);
       synchronizedCollectAll(collector, instanceFields);
     }
@@ -278,7 +280,7 @@
   }
 
   public boolean hasMethods() {
-    return directMethods.length + virtualMethods.length > 0;
+    return methodCollection.size() > 0;
   }
 
   public boolean hasMethodsOrFields() {
@@ -287,15 +289,13 @@
 
   public boolean hasAnnotations() {
     return !annotations().isEmpty()
-        || hasAnnotations(virtualMethods)
-        || hasAnnotations(directMethods)
+        || hasAnnotations(methodCollection)
         || hasAnnotations(staticFields)
         || hasAnnotations(instanceFields);
   }
 
   boolean hasOnlyInternalizableAnnotations() {
-    return !hasAnnotations(virtualMethods)
-        && !hasAnnotations(directMethods)
+    return !hasAnnotations(methodCollection)
         && !hasAnnotations(staticFields)
         && !hasAnnotations(instanceFields);
   }
@@ -306,9 +306,9 @@
     }
   }
 
-  private boolean hasAnnotations(DexEncodedMethod[] methods) {
+  private boolean hasAnnotations(MethodCollection methods) {
     synchronized (methods) {
-      return Arrays.stream(methods).anyMatch(DexEncodedMethod::hasAnnotation);
+      return methods.hasAnnotations();
     }
   }
 
@@ -346,10 +346,7 @@
   }
 
   public boolean isSorted() {
-    return isSorted(virtualMethods)
-        && isSorted(directMethods)
-        && isSorted(staticFields)
-        && isSorted(instanceFields);
+    return methodCollection.isSorted() && isSorted(staticFields) && isSorted(instanceFields);
   }
 
   private static <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> boolean isSorted(
@@ -360,6 +357,7 @@
       return Arrays.equals(items, sorted);
     }
   }
+
   public DexEncodedArray getStaticValues() {
     // The sentinel value is left over for classes that actually have no fields.
     if (staticValues == SENTINEL_NOT_YET_COMPUTED) {
@@ -370,35 +368,21 @@
   }
 
   public void addMethod(DexEncodedMethod method) {
-    if (method.accessFlags.isStatic()
-        || method.accessFlags.isPrivate()
-        || method.accessFlags.isConstructor()) {
-      addDirectMethod(method);
-    } else {
-      addVirtualMethod(method);
-    }
+    methodCollection.addMethod(method);
   }
 
   public void addVirtualMethod(DexEncodedMethod virtualMethod) {
-    assert !virtualMethod.accessFlags.isStatic();
-    assert !virtualMethod.accessFlags.isPrivate();
-    assert !virtualMethod.accessFlags.isConstructor();
-    virtualMethods = Arrays.copyOf(virtualMethods, virtualMethods.length + 1);
-    virtualMethods[virtualMethods.length - 1] = virtualMethod;
+    methodCollection.addVirtualMethod(virtualMethod);
   }
 
-  public void addDirectMethod(DexEncodedMethod staticMethod) {
-    assert staticMethod.accessFlags.isStatic() || staticMethod.accessFlags.isPrivate()
-        || staticMethod.accessFlags.isConstructor();
-    directMethods = Arrays.copyOf(directMethods, directMethods.length + 1);
-    directMethods[directMethods.length - 1] = staticMethod;
+  public void addDirectMethod(DexEncodedMethod directMethod) {
+    methodCollection.addDirectMethod(directMethod);
   }
 
   public void sortMembers() {
     sortEncodedFields(staticFields);
     sortEncodedFields(instanceFields);
-    sortEncodedMethods(directMethods);
-    sortEncodedMethods(virtualMethods);
+    methodCollection.sort();
   }
 
   private void sortEncodedFields(DexEncodedField[] fields) {
@@ -407,12 +391,6 @@
     }
   }
 
-  private void sortEncodedMethods(DexEncodedMethod[] methods) {
-    synchronized (methods) {
-      Arrays.sort(methods, Comparator.comparing(a -> a.method));
-    }
-  }
-
   @Override
   public DexProgramClass get() {
     return this;
diff --git a/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java b/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
new file mode 100644
index 0000000..4dc36b1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
@@ -0,0 +1,258 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.PredicateUtils;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Sets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public class MethodArrayBacking {
+
+  private DexEncodedMethod[] directMethods = DexEncodedMethod.EMPTY_ARRAY;
+  private DexEncodedMethod[] virtualMethods = DexEncodedMethod.EMPTY_ARRAY;
+
+  int size() {
+    return directMethods.length + virtualMethods.length;
+  }
+
+  void forEachMethod(Consumer<DexEncodedMethod> consumer) {
+    for (DexEncodedMethod method : directMethods()) {
+      consumer.accept(method);
+    }
+    for (DexEncodedMethod method : virtualMethods()) {
+      consumer.accept(method);
+    }
+  }
+
+  List<DexEncodedMethod> directMethods() {
+    assert directMethods != null;
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(directMethods));
+    }
+    return Arrays.asList(directMethods);
+  }
+
+  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 verifyNoDuplicateMethods();
+  }
+
+  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 verifyNoDuplicateMethods();
+  }
+
+  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;
+  }
+
+  void removeDirectMethod(DexMethod method) {
+    int index = -1;
+    for (int i = 0; i < directMethods.length; i++) {
+      if (method.match(directMethods[i])) {
+        index = i;
+        break;
+      }
+    }
+    if (index >= 0) {
+      removeDirectMethod(index);
+    }
+  }
+
+  void setDirectMethod(int index, DexEncodedMethod method) {
+    directMethods[index] = method;
+    assert verifyNoDuplicateMethods();
+  }
+
+  void setDirectMethods(DexEncodedMethod[] methods) {
+    directMethods = MoreObjects.firstNonNull(methods, DexEncodedMethod.EMPTY_ARRAY);
+    assert verifyNoDuplicateMethods();
+  }
+
+  List<DexEncodedMethod> virtualMethods() {
+    assert virtualMethods != null;
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(virtualMethods));
+    }
+    return Arrays.asList(virtualMethods);
+  }
+
+  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 verifyNoDuplicateMethods();
+  }
+
+  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 verifyNoDuplicateMethods();
+  }
+
+  void setVirtualMethod(int index, DexEncodedMethod method) {
+    virtualMethods[index] = method;
+    assert verifyNoDuplicateMethods();
+  }
+
+  void setVirtualMethods(DexEncodedMethod[] methods) {
+    virtualMethods = MoreObjects.firstNonNull(methods, DexEncodedMethod.EMPTY_ARRAY);
+    assert verifyNoDuplicateMethods();
+  }
+
+  void virtualizeMethods(Set<DexEncodedMethod> privateInstanceMethods) {
+    int vLen = virtualMethods.length;
+    int dLen = directMethods.length;
+    int mLen = privateInstanceMethods.size();
+    assert mLen <= dLen;
+
+    DexEncodedMethod[] newDirectMethods = new DexEncodedMethod[dLen - mLen];
+    int index = 0;
+    for (int i = 0; i < dLen; i++) {
+      DexEncodedMethod encodedMethod = directMethods[i];
+      if (!privateInstanceMethods.contains(encodedMethod)) {
+        newDirectMethods[index++] = encodedMethod;
+      }
+    }
+    assert index == dLen - mLen;
+    setDirectMethods(newDirectMethods);
+
+    DexEncodedMethod[] newVirtualMethods = new DexEncodedMethod[vLen + mLen];
+    System.arraycopy(virtualMethods, 0, newVirtualMethods, 0, vLen);
+    index = vLen;
+    for (DexEncodedMethod encodedMethod : privateInstanceMethods) {
+      newVirtualMethods[index++] = encodedMethod;
+    }
+    setVirtualMethods(newVirtualMethods);
+  }
+
+  DexEncodedMethod getDirectMethod(DexMethod method) {
+    for (DexEncodedMethod directMethod : directMethods) {
+      if (method.match(directMethod)) {
+        return directMethod;
+      }
+    }
+    return null;
+  }
+
+  DexEncodedMethod getDirectMethod(Predicate<DexEncodedMethod> predicate) {
+    return PredicateUtils.findFirst(directMethods, predicate);
+  }
+
+  DexEncodedMethod getVirtualMethod(DexMethod method) {
+    for (DexEncodedMethod virtualMethod : virtualMethods) {
+      if (method.match(virtualMethod)) {
+        return virtualMethod;
+      }
+    }
+    return null;
+  }
+
+  DexEncodedMethod getVirtualMethod(Predicate<DexEncodedMethod> predicate) {
+    return PredicateUtils.findFirst(virtualMethods, predicate);
+  }
+
+  DexEncodedMethod getMethod(DexMethod method) {
+    DexEncodedMethod result = getDirectMethod(method);
+    return result == null ? getVirtualMethod(method) : result;
+  }
+
+  void addMethod(DexEncodedMethod method) {
+    if (method.accessFlags.isStatic()
+        || method.accessFlags.isPrivate()
+        || method.accessFlags.isConstructor()) {
+      addDirectMethod(method);
+    } else {
+      addVirtualMethod(method);
+    }
+  }
+
+  void addVirtualMethod(DexEncodedMethod virtualMethod) {
+    assert !virtualMethod.accessFlags.isStatic();
+    assert !virtualMethod.accessFlags.isPrivate();
+    assert !virtualMethod.accessFlags.isConstructor();
+    virtualMethods = Arrays.copyOf(virtualMethods, virtualMethods.length + 1);
+    virtualMethods[virtualMethods.length - 1] = virtualMethod;
+  }
+
+  void addDirectMethod(DexEncodedMethod staticMethod) {
+    assert staticMethod.accessFlags.isStatic()
+        || staticMethod.accessFlags.isPrivate()
+        || staticMethod.accessFlags.isConstructor();
+    directMethods = Arrays.copyOf(directMethods, directMethods.length + 1);
+    directMethods[directMethods.length - 1] = staticMethod;
+  }
+
+  boolean verifyNoDuplicateMethods() {
+    Set<DexMethod> unique = Sets.newIdentityHashSet();
+    forEachMethod(
+        method -> {
+          boolean changed = unique.add(method.method);
+          assert changed : "Duplicate method `" + method.method.toSourceString() + "`";
+        });
+    return true;
+  }
+
+  boolean hasAnnotations() {
+    for (DexEncodedMethod method : virtualMethods) {
+      if (method.hasAnnotation()) {
+        return true;
+      }
+    }
+    for (DexEncodedMethod method : directMethods) {
+      if (method.hasAnnotation()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  synchronized boolean isSorted() {
+    return isSorted(virtualMethods) && isSorted(directMethods);
+  }
+
+  private static boolean isSorted(DexEncodedMethod[] items) {
+    DexEncodedMethod[] sorted = items.clone();
+    Arrays.sort(sorted, Comparator.comparing(DexEncodedMember::toReference));
+    return Arrays.equals(items, sorted);
+  }
+
+  synchronized void sort() {
+    synchronized (virtualMethods) {
+      Arrays.sort(virtualMethods, Comparator.comparing(a -> a.method));
+    }
+    synchronized (directMethods) {
+      Arrays.sort(directMethods, Comparator.comparing(a -> a.method));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/MethodCollection.java b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
new file mode 100644
index 0000000..4d4fb2a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
@@ -0,0 +1,191 @@
+package com.android.tools.r8.graph;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public class MethodCollection {
+
+  private final DexClass holder;
+  private final MethodArrayBacking backing = new MethodArrayBacking();
+  private Optional<DexEncodedMethod> cachedClassInitializer = null;
+
+  public MethodCollection(DexClass holder) {
+    this.holder = holder;
+  }
+
+  public int size() {
+    return backing.size();
+  }
+
+  public void forEachMethod(Consumer<DexEncodedMethod> consumer) {
+    backing.forEachMethod(consumer);
+  }
+
+  public List<DexEncodedMethod> allMethodsSorted() {
+    List<DexEncodedMethod> sorted = new ArrayList<>(size());
+    forEachMethod(sorted::add);
+    sorted.sort((a, b) -> a.method.slowCompareTo(b.method));
+    return sorted;
+  }
+
+  public List<DexEncodedMethod> directMethods() {
+    return backing.directMethods();
+  }
+
+  public List<DexEncodedMethod> virtualMethods() {
+    return backing.virtualMethods();
+  }
+
+  public DexEncodedMethod getMethod(DexMethod method) {
+    return backing.getMethod(method);
+  }
+
+  public DexEncodedMethod getDirectMethod(DexMethod method) {
+    return backing.getDirectMethod(method);
+  }
+
+  public DexEncodedMethod getDirectMethod(Predicate<DexEncodedMethod> predicate) {
+    return backing.getDirectMethod(predicate);
+  }
+
+  public DexEncodedMethod getVirtualMethod(DexMethod method) {
+    return backing.getVirtualMethod(method);
+  }
+
+  public DexEncodedMethod getVirtualMethod(Predicate<DexEncodedMethod> predicate) {
+    return backing.getVirtualMethod(predicate);
+  }
+
+  public DexEncodedMethod getClassInitializer() {
+    if (cachedClassInitializer == null) {
+      cachedClassInitializer = Optional.empty();
+      for (DexEncodedMethod directMethod : directMethods()) {
+        if (directMethod.isClassInitializer()) {
+          cachedClassInitializer = Optional.of(directMethod);
+          break;
+        }
+      }
+    }
+    return cachedClassInitializer.orElse(null);
+  }
+
+  public void addMethod(DexEncodedMethod method) {
+    backing.addMethod(method);
+  }
+
+  public void addVirtualMethod(DexEncodedMethod virtualMethod) {
+    backing.addVirtualMethod(virtualMethod);
+  }
+
+  public void addDirectMethod(DexEncodedMethod directMethod) {
+    backing.addDirectMethod(directMethod);
+  }
+
+  public void appendDirectMethod(DexEncodedMethod method) {
+    assert verifyCorrectnessOfMethodHolder(method);
+    cachedClassInitializer = null;
+    backing.appendDirectMethod(method);
+  }
+
+  public void appendDirectMethods(Collection<DexEncodedMethod> methods) {
+    assert verifyCorrectnessOfMethodHolders(methods);
+    cachedClassInitializer = null;
+    backing.appendDirectMethods(methods);
+  }
+
+  public void removeDirectMethod(int index) {
+    cachedClassInitializer = null;
+    backing.removeDirectMethod(index);
+  }
+
+  public void removeDirectMethod(DexMethod method) {
+    backing.removeDirectMethod(method);
+  }
+
+  public void setDirectMethod(int index, DexEncodedMethod method) {
+    assert verifyCorrectnessOfMethodHolder(method);
+    cachedClassInitializer = null;
+    backing.setDirectMethod(index, method);
+  }
+
+  public void setDirectMethods(DexEncodedMethod[] methods) {
+    assert verifyCorrectnessOfMethodHolders(methods);
+    cachedClassInitializer = null;
+    backing.setDirectMethods(methods);
+  }
+
+  public void appendVirtualMethod(DexEncodedMethod method) {
+    assert verifyCorrectnessOfMethodHolder(method);
+    backing.appendVirtualMethod(method);
+  }
+
+  public void appendVirtualMethods(Collection<DexEncodedMethod> methods) {
+    assert verifyCorrectnessOfMethodHolders(methods);
+    backing.appendVirtualMethods(methods);
+  }
+
+  public void setVirtualMethod(int index, DexEncodedMethod method) {
+    assert verifyCorrectnessOfMethodHolder(method);
+    backing.setVirtualMethod(index, method);
+  }
+
+  public void setVirtualMethods(DexEncodedMethod[] methods) {
+    assert verifyCorrectnessOfMethodHolders(methods);
+    backing.setVirtualMethods(methods);
+  }
+
+  public void virtualizeMethods(Set<DexEncodedMethod> privateInstanceMethods) {
+    backing.virtualizeMethods(privateInstanceMethods);
+  }
+
+  public boolean hasAnnotations() {
+    return backing.hasAnnotations();
+  }
+
+  public boolean isSorted() {
+    return backing.isSorted();
+  }
+
+  public void sort() {
+    backing.sort();
+  }
+
+  public boolean verify() {
+    forEachMethod(
+        method -> {
+          assert verifyCorrectnessOfMethodHolder(method);
+        });
+    assert backing.verifyNoDuplicateMethods();
+    return true;
+  }
+
+  private boolean verifyCorrectnessOfMethodHolder(DexEncodedMethod method) {
+    assert method.method.holder == holder.type
+        : "Expected method `"
+            + method.method.toSourceString()
+            + "` to have holder `"
+            + holder.type.toSourceString()
+            + "`";
+    return true;
+  }
+
+  private boolean verifyCorrectnessOfMethodHolders(DexEncodedMethod[] methods) {
+    if (methods == null) {
+      return true;
+    }
+    return verifyCorrectnessOfMethodHolders(Arrays.asList(methods));
+  }
+
+  private boolean verifyCorrectnessOfMethodHolders(Iterable<DexEncodedMethod> methods) {
+    for (DexEncodedMethod method : methods) {
+      assert verifyCorrectnessOfMethodHolder(method);
+    }
+    return true;
+  }
+}