Introduce a field collection with array and map-based backings.

Bug: b/265148324
Change-Id: I5155084088ce04e389073314d763b4a952610add
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 85f928d..59bff83 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.TraversalContinuation;
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -32,7 +31,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
@@ -59,10 +57,7 @@
   private OptionalBool isResolvable = OptionalBool.unknown();
 
   /** Access has to be synchronized during concurrent collection/writing phase. */
-  protected DexEncodedField[] staticFields = DexEncodedField.EMPTY_ARRAY;
-
-  /** Access has to be synchronized during concurrent collection/writing phase. */
-  protected DexEncodedField[] instanceFields = DexEncodedField.EMPTY_ARRAY;
+  protected final FieldCollection fieldCollection;
 
   /** Access has to be synchronized during concurrent collection/writing phase. */
   protected final MethodCollection methodCollection;
@@ -112,8 +107,7 @@
     this.accessFlags = accessFlags;
     this.superType = superType;
     this.type = type;
-    setStaticFields(staticFields);
-    setInstanceFields(instanceFields);
+    this.fieldCollection = FieldCollection.create(this, staticFields, instanceFields);
     this.methodCollection = methodCollectionFactory.create(this);
     this.nestHost = nestHost;
     this.nestMembers = nestMembers;
@@ -222,9 +216,7 @@
   }
 
   public Iterable<DexEncodedField> fields(final Predicate<? super DexEncodedField> predicate) {
-    return Iterables.concat(
-        Iterables.filter(Arrays.asList(instanceFields), predicate::test),
-        Iterables.filter(Arrays.asList(staticFields), predicate::test));
+    return fieldCollection.fields(predicate);
   }
 
   public Iterable<DexEncodedMember<?, ?>> members() {
@@ -235,6 +227,10 @@
     return Iterables.concat(fields(predicate), methods(predicate));
   }
 
+  public FieldCollection getFieldCollection() {
+    return fieldCollection;
+  }
+
   @Override
   public MethodCollection getMethodCollection() {
     return methodCollection;
@@ -326,6 +322,10 @@
     methodCollection.forEachMethod(consumer);
   }
 
+  public List<DexEncodedField> allFieldsSorted() {
+    return fieldCollection.allFieldsSorted();
+  }
+
   public List<DexEncodedMethod> allMethodsSorted() {
     return methodCollection.allMethodsSorted();
   }
@@ -387,11 +387,7 @@
   }
 
   public List<DexEncodedField> staticFields() {
-    assert staticFields != null;
-    if (InternalOptions.assertionsEnabled()) {
-      return Collections.unmodifiableList(Arrays.asList(staticFields));
-    }
-    return Arrays.asList(staticFields);
+    return fieldCollection.staticFieldsAsList();
   }
 
   public Iterable<DexEncodedField> staticFields(Predicate<DexEncodedField> predicate) {
@@ -399,159 +395,71 @@
   }
 
   public void appendStaticField(DexEncodedField field) {
-    DexEncodedField[] newFields = new DexEncodedField[staticFields.length + 1];
-    System.arraycopy(staticFields, 0, newFields, 0, staticFields.length);
-    newFields[staticFields.length] = field;
-    staticFields = newFields;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendStaticField(field);
   }
 
   public void appendStaticFields(Collection<DexEncodedField> fields) {
-    DexEncodedField[] newFields = new DexEncodedField[staticFields.length + fields.size()];
-    System.arraycopy(staticFields, 0, newFields, 0, staticFields.length);
-    int i = staticFields.length;
-    for (DexEncodedField field : fields) {
-      newFields[i] = field;
-      i++;
-    }
-    staticFields = newFields;
-    assert verifyCorrectnessOfFieldHolders(fields);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendStaticFields(fields);
   }
 
   public DexEncodedField[] clearStaticFields() {
-    DexEncodedField[] previousFields = staticFields;
-    setStaticFields(DexEncodedField.EMPTY_ARRAY);
-    return previousFields;
-  }
-
-  public void removeStaticField(int index) {
-    DexEncodedField[] newFields = new DexEncodedField[staticFields.length - 1];
-    System.arraycopy(staticFields, 0, newFields, 0, index);
-    System.arraycopy(staticFields, index + 1, newFields, index, staticFields.length - index - 1);
-    staticFields = newFields;
-  }
-
-  public void setStaticField(int index, DexEncodedField field) {
-    staticFields[index] = field;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    List<DexEncodedField> previousFields = staticFields();
+    fieldCollection.clearStaticFields();
+    return previousFields.toArray(DexEncodedField.EMPTY_ARRAY);
   }
 
   public void setStaticFields(DexEncodedField[] fields) {
-    staticFields = MoreObjects.firstNonNull(fields, DexEncodedField.EMPTY_ARRAY);
-    assert verifyCorrectnessOfFieldHolders(staticFields());
-    assert verifyNoDuplicateFields();
+    fieldCollection.setStaticFields(fields);
   }
 
   public void setStaticFields(Collection<DexEncodedField> fields) {
     setStaticFields(fields.toArray(DexEncodedField.EMPTY_ARRAY));
   }
 
-  public boolean definesStaticField(DexField field) {
-    for (DexEncodedField encodedField : staticFields()) {
-      if (encodedField.getReference() == field) {
-        return true;
-      }
-    }
-    return false;
-  }
-
   public List<DexEncodedField> instanceFields() {
-    assert instanceFields != null;
-    if (InternalOptions.assertionsEnabled()) {
-      return Collections.unmodifiableList(Arrays.asList(instanceFields));
-    }
-    return Arrays.asList(instanceFields);
+    return fieldCollection.instanceFieldsAsList();
   }
 
   public Iterable<DexEncodedField> instanceFields(Predicate<? super DexEncodedField> predicate) {
-    return Iterables.filter(Arrays.asList(instanceFields), predicate::test);
+    return Iterables.filter(instanceFields(), predicate::test);
   }
 
   public void appendInstanceField(DexEncodedField field) {
-    DexEncodedField[] newFields = new DexEncodedField[instanceFields.length + 1];
-    System.arraycopy(instanceFields, 0, newFields, 0, instanceFields.length);
-    newFields[instanceFields.length] = field;
-    instanceFields = newFields;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendInstanceField(field);
   }
 
   public void appendInstanceFields(Collection<DexEncodedField> fields) {
-    DexEncodedField[] newFields = new DexEncodedField[instanceFields.length + fields.size()];
-    System.arraycopy(instanceFields, 0, newFields, 0, instanceFields.length);
-    int i = instanceFields.length;
-    for (DexEncodedField field : fields) {
-      newFields[i] = field;
-      i++;
-    }
-    instanceFields = newFields;
-    assert verifyCorrectnessOfFieldHolders(fields);
-    assert verifyNoDuplicateFields();
-  }
-
-  public void removeInstanceField(int index) {
-    DexEncodedField[] newFields = new DexEncodedField[instanceFields.length - 1];
-    System.arraycopy(instanceFields, 0, newFields, 0, index);
-    System.arraycopy(
-        instanceFields, index + 1, newFields, index, instanceFields.length - index - 1);
-    instanceFields = newFields;
-  }
-
-  public void setInstanceField(int index, DexEncodedField field) {
-    instanceFields[index] = field;
-    assert verifyCorrectnessOfFieldHolder(field);
-    assert verifyNoDuplicateFields();
+    fieldCollection.appendInstanceFields(fields);
   }
 
   public void setInstanceFields(DexEncodedField[] fields) {
-    instanceFields = MoreObjects.firstNonNull(fields, DexEncodedField.EMPTY_ARRAY);
-    assert verifyCorrectnessOfFieldHolders(instanceFields());
-    assert verifyNoDuplicateFields();
+    fieldCollection.setInstanceFields(fields);
   }
 
   public DexEncodedField[] clearInstanceFields() {
-    DexEncodedField[] previousFields = instanceFields;
-    instanceFields = DexEncodedField.EMPTY_ARRAY;
-    return previousFields;
+    List<DexEncodedField> previousFields = instanceFields();
+    fieldCollection.clearInstanceFields();
+    return previousFields.toArray(DexEncodedField.EMPTY_ARRAY);
   }
 
-  private boolean verifyCorrectnessOfFieldHolder(DexEncodedField field) {
-    assert field.getHolderType() == type
-        : "Expected field `"
-            + field.getReference().toSourceString()
-            + "` to have holder `"
-            + type.toSourceString()
-            + "`";
-    return true;
+  /** Find method in this class matching {@param method}. */
+  public DexClassAndField lookupClassField(DexField field) {
+    return toClassFieldOrNull(lookupField(field));
   }
 
-  private boolean verifyCorrectnessOfFieldHolders(Iterable<DexEncodedField> fields) {
-    for (DexEncodedField field : fields) {
-      assert verifyCorrectnessOfFieldHolder(field);
-    }
-    return true;
-  }
-
-  private boolean verifyNoDuplicateFields() {
-    Set<DexField> unique = Sets.newIdentityHashSet();
-    for (DexEncodedField field : fields()) {
-      boolean changed = unique.add(field.getReference());
-      assert changed : "Duplicate field `" + field.getReference().toSourceString() + "`";
-    }
-    return true;
+  /** Find field in this class matching {@param field}. */
+  public DexEncodedField lookupField(DexField field) {
+    return fieldCollection.lookupField(field);
   }
 
   /** Find static field in this class matching {@param field}. */
   public DexEncodedField lookupStaticField(DexField field) {
-    return lookupTarget(staticFields, field);
+    return fieldCollection.lookupStaticField(field);
   }
 
   /** Find instance field in this class matching {@param field}. */
   public DexEncodedField lookupInstanceField(DexField field) {
-    return lookupTarget(instanceFields, field);
+    return fieldCollection.lookupInstanceField(field);
   }
 
   public DexEncodedField lookupUniqueInstanceFieldWithName(DexString name) {
@@ -576,21 +484,10 @@
     return result;
   }
 
-  /** Find method in this class matching {@param method}. */
-  public DexClassAndField lookupClassField(DexField field) {
-    return toClassFieldOrNull(lookupField(field));
-  }
-
   private DexClassAndField toClassFieldOrNull(DexEncodedField field) {
     return field != null ? DexClassAndField.create(this, field) : null;
   }
 
-  /** Find field in this class matching {@param field}. */
-  public DexEncodedField lookupField(DexField field) {
-    DexEncodedField result = lookupInstanceField(field);
-    return result == null ? lookupStaticField(field) : result;
-  }
-
   /** Find direct method in this class matching {@param method}. */
   public DexEncodedMethod lookupDirectMethod(DexMethod method) {
     return methodCollection.getDirectMethod(method);
@@ -676,16 +573,6 @@
         && method.getReference().proto.parameters.values[0] == factory.objectArrayType;
   }
 
-  private <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> D lookupTarget(
-      D[] items, R descriptor) {
-    for (D entry : items) {
-      if (descriptor.match(entry)) {
-        return entry;
-      }
-    }
-    return null;
-  }
-
   public boolean canBeInstantiatedByNewInstance() {
     return !isAbstract() && !isAnnotation() && !isInterface();
   }
@@ -1238,22 +1125,11 @@
   }
 
   public boolean hasStaticFields() {
-    return staticFields.length > 0;
+    return fieldCollection.hasStaticFields();
   }
 
   public boolean hasInstanceFields() {
-    return instanceFields.length > 0;
-  }
-
-  public boolean hasInstanceFieldsDirectlyOrIndirectly(AppView<?> appView) {
-    if (superType == null || type == appView.dexItemFactory().objectType) {
-      return false;
-    }
-    if (hasInstanceFields()) {
-      return true;
-    }
-    DexClass superClass = appView.definitionFor(superType);
-    return superClass == null || superClass.hasInstanceFieldsDirectlyOrIndirectly(appView);
+    return fieldCollection.hasInstanceFields();
   }
 
   public List<DexEncodedField> getDirectAndIndirectInstanceFields(AppView<?> appView) {
@@ -1269,8 +1145,7 @@
   public boolean isValid(InternalOptions options) {
     assert verifyNoAbstractMethodsOnNonAbstractClasses(virtualMethods(), options);
     assert !isInterface() || !getMethodCollection().hasVirtualMethods(DexEncodedMethod::isFinal);
-    assert verifyCorrectnessOfFieldHolders(fields());
-    assert verifyNoDuplicateFields();
+    assert fieldCollection.verify();
     assert methodCollection.verify();
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
index 43c2532..aca2a14 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
@@ -161,8 +161,7 @@
         .withItem(DexDefinition::annotations)
         // TODO(b/158159959): Make signatures structural.
         .withAssert(c -> c.classSignature == ClassSignature.noSignature())
-        .withItemArray(c -> c.staticFields)
-        .withItemArray(c -> c.instanceFields)
+        .withItemCollection(DexClass::allFieldsSorted)
         .withItemCollection(DexClass::allMethodsSorted);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexFieldSignature.java b/src/main/java/com/android/tools/r8/graph/DexFieldSignature.java
new file mode 100644
index 0000000..2f22c1a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DexFieldSignature.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2023, 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.structural.Equatable;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
+import java.util.Objects;
+
+public class DexFieldSignature implements StructuralItem<DexFieldSignature> {
+
+  private final DexString name;
+  private final DexType type;
+
+  private static void specify(StructuralSpecification<DexFieldSignature, ?> spec) {
+    spec.withItem(DexFieldSignature::getName).withItem(DexFieldSignature::getType);
+  }
+
+  public static DexFieldSignature fromField(DexField field) {
+    return new DexFieldSignature(field.getName(), field.getType());
+  }
+
+  private DexFieldSignature(DexString name, DexType type) {
+    this.name = name;
+    this.type = type;
+  }
+
+  public DexString getName() {
+    return name;
+  }
+
+  public DexType getType() {
+    return type;
+  }
+
+  public boolean match(DexField field) {
+    return getName().equals(field.getName()) && getType().equals(field.getType());
+  }
+
+  @Override
+  public DexFieldSignature self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<DexFieldSignature> getStructuralMapping() {
+    return DexFieldSignature::specify;
+  }
+
+  @Override
+  public boolean isEqualTo(DexFieldSignature other) {
+    return getName() == other.getName() && getType() == other.getType();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return Equatable.equalsImpl(this, o);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, type);
+  }
+}
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 34d5646..0cb6881 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -244,8 +244,7 @@
         .withItem(DexDefinition::annotations)
         // TODO(b/158159959): Make signatures structural.
         .withAssert(c -> c.classSignature == ClassSignature.noSignature())
-        .withItemArray(c -> c.staticFields)
-        .withItemArray(c -> c.instanceFields)
+        .withItemCollection(DexClass::allFieldsSorted)
         .withItemCollection(DexClass::allMethodsSorted);
   }
 
@@ -475,8 +474,7 @@
     if (hasMethodsOrFields()) {
       collector.add(this);
       methodCollection.forEachMethod(m -> m.collectMixedSectionItems(collector));
-      collectAll(collector, staticFields);
-      collectAll(collector, instanceFields);
+      fieldCollection.forEachField(f -> f.collectMixedSectionItems(collector));
     }
     annotations().collectMixedSectionItems(collector);
     if (interfaces != null) {
@@ -643,7 +641,7 @@
   }
 
   public boolean hasFields() {
-    return instanceFields.length + staticFields.length > 0;
+    return fieldCollection.size() > 0;
   }
 
   public boolean hasMethods() {
@@ -658,19 +656,16 @@
   public boolean hasClassOrMemberAnnotations() {
     return !annotations().isEmpty()
         || hasAnnotations(methodCollection)
-        || hasAnnotations(staticFields)
-        || hasAnnotations(instanceFields);
+        || hasAnnotations(fieldCollection);
   }
 
   boolean hasOnlyInternalizableAnnotations() {
-    return !hasAnnotations(methodCollection)
-        && !hasAnnotations(staticFields)
-        && !hasAnnotations(instanceFields);
+    return !hasAnnotations(methodCollection) && !hasAnnotations(fieldCollection);
   }
 
-  private boolean hasAnnotations(DexEncodedField[] fields) {
+  private boolean hasAnnotations(FieldCollection fields) {
     synchronized (fields) {
-      return Arrays.stream(fields).anyMatch(DexEncodedField::hasAnnotations);
+      return fields.hasAnnotations();
     }
   }
 
@@ -685,13 +680,12 @@
     if (!hasNonDefaultStaticFieldValues()) {
       return null;
     }
-    DexEncodedField[] fields = staticFields;
-    Arrays.sort(
-        fields, (a, b) -> a.getReference().compareToWithNamingLens(b.getReference(), namingLens));
+    List<DexEncodedField> fields = new ArrayList<>(staticFields());
+    fields.sort((a, b) -> a.getReference().compareToWithNamingLens(b.getReference(), namingLens));
     int length = 0;
-    List<DexValue> values = new ArrayList<>(fields.length);
-    for (int i = 0; i < fields.length; i++) {
-      DexEncodedField field = fields[i];
+    List<DexValue> values = new ArrayList<>(fields.size());
+    for (int i = 0; i < fields.size(); i++) {
+      DexEncodedField field = fields.get(i);
       DexValue staticValue = field.getStaticValue();
       assert staticValue != null;
       values.add(staticValue);
@@ -705,7 +699,7 @@
   }
 
   private boolean hasNonDefaultStaticFieldValues() {
-    for (DexEncodedField field : staticFields) {
+    for (DexEncodedField field : staticFields()) {
       DexValue value = field.getStaticValue();
       if (value != null && !value.isDefault(field.getReference().type)) {
         return true;
diff --git a/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java b/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java
new file mode 100644
index 0000000..4b782b3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldArrayBacking.java
@@ -0,0 +1,243 @@
+// Copyright (c) 2023, 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 static com.google.common.base.Predicates.alwaysTrue;
+
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.google.common.collect.Iterables;
+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;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class FieldArrayBacking extends FieldCollectionBacking {
+
+  private DexEncodedField[] staticFields;
+  private DexEncodedField[] instanceFields;
+
+  public static FieldCollectionBacking fromArrays(
+      DexEncodedField[] staticFields, DexEncodedField[] instanceFields) {
+    return new FieldArrayBacking(staticFields, instanceFields);
+  }
+
+  private FieldArrayBacking(DexEncodedField[] staticFields, DexEncodedField[] instanceFields) {
+    assert staticFields != null;
+    assert instanceFields != null;
+    this.staticFields = staticFields;
+    this.instanceFields = instanceFields;
+  }
+
+  @Override
+  boolean verify() {
+    assert verifyNoDuplicateFields();
+    return true;
+  }
+
+  private boolean verifyNoDuplicateFields() {
+    Set<DexField> unique = Sets.newIdentityHashSet();
+    for (DexEncodedField field : fields(alwaysTrue())) {
+      boolean changed = unique.add(field.getReference());
+      assert changed : "Duplicate field `" + field.getReference().toSourceString() + "`";
+    }
+    return true;
+  }
+
+  @Override
+  int numberOfStaticFields() {
+    return staticFields.length;
+  }
+
+  @Override
+  int numberOfInstanceFields() {
+    return instanceFields.length;
+  }
+
+  @Override
+  int size() {
+    return staticFields.length + instanceFields.length;
+  }
+
+  @Override
+  TraversalContinuation<?, ?> traverse(Function<DexEncodedField, TraversalContinuation<?, ?>> fn) {
+    for (int i = 0; i < staticFields.length; i++) {
+      if (fn.apply(staticFields[i]).shouldBreak()) {
+        return TraversalContinuation.doBreak();
+      }
+    }
+    for (int i = 0; i < instanceFields.length; i++) {
+      if (fn.apply(instanceFields[i]).shouldBreak()) {
+        return TraversalContinuation.doBreak();
+      }
+    }
+    return TraversalContinuation.doContinue();
+  }
+
+  @Override
+  Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate) {
+    return Iterables.concat(
+        Iterables.filter(Arrays.asList(instanceFields), predicate::test),
+        Iterables.filter(Arrays.asList(staticFields), predicate::test));
+  }
+
+  @Override
+  List<DexEncodedField> staticFieldsAsList() {
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(staticFields));
+    }
+    return Arrays.asList(staticFields);
+  }
+
+  @Override
+  void appendStaticField(DexEncodedField field) {
+    staticFields = appendFieldHelper(staticFields, field);
+  }
+
+  @Override
+  void appendStaticFields(Collection<DexEncodedField> fields) {
+    staticFields = appendFieldsHelper(staticFields, fields);
+  }
+
+  @Override
+  void clearStaticFields() {
+    staticFields = DexEncodedField.EMPTY_ARRAY;
+  }
+
+  @Override
+  public void setStaticFields(DexEncodedField[] fields) {
+    assert fields != null;
+    staticFields = fields;
+  }
+
+  @Override
+  List<DexEncodedField> instanceFieldsAsList() {
+    if (InternalOptions.assertionsEnabled()) {
+      return Collections.unmodifiableList(Arrays.asList(instanceFields));
+    }
+    return Arrays.asList(instanceFields);
+  }
+
+  @Override
+  void appendInstanceField(DexEncodedField field) {
+    instanceFields = appendFieldHelper(instanceFields, field);
+  }
+
+  @Override
+  void appendInstanceFields(Collection<DexEncodedField> fields) {
+    instanceFields = appendFieldsHelper(instanceFields, fields);
+  }
+
+  @Override
+  void clearInstanceFields() {
+    instanceFields = DexEncodedField.EMPTY_ARRAY;
+  }
+
+  @Override
+  void setInstanceFields(DexEncodedField[] fields) {
+    assert fields != null;
+    instanceFields = fields;
+  }
+
+  @Override
+  DexEncodedField lookupField(DexField field) {
+    DexEncodedField result = lookupInstanceField(field);
+    return result == null ? lookupStaticField(field) : result;
+  }
+
+  @Override
+  DexEncodedField lookupStaticField(DexField field) {
+    return lookupFieldHelper(staticFields, field);
+  }
+
+  @Override
+  DexEncodedField lookupInstanceField(DexField field) {
+    return lookupFieldHelper(instanceFields, field);
+  }
+
+  @Override
+  void replaceFields(Function<DexEncodedField, DexEncodedField> replacement) {
+    staticFields =
+        replaceFieldsHelper(
+            staticFields,
+            replacement,
+            FieldCollectionBacking::belongsInStaticPool,
+            this::appendInstanceFields);
+    instanceFields =
+        replaceFieldsHelper(
+            instanceFields,
+            replacement,
+            FieldCollectionBacking::belongsInInstancePool,
+            this::appendStaticFields);
+  }
+
+  private static DexEncodedField[] appendFieldHelper(
+      DexEncodedField[] existingItems, DexEncodedField itemToAppend) {
+    DexEncodedField[] newFields = new DexEncodedField[existingItems.length + 1];
+    System.arraycopy(existingItems, 0, newFields, 0, existingItems.length);
+    newFields[existingItems.length] = itemToAppend;
+    return newFields;
+  }
+
+  private static DexEncodedField[] appendFieldsHelper(
+      DexEncodedField[] existingItems, Collection<DexEncodedField> itemsToAppend) {
+    DexEncodedField[] newFields = new DexEncodedField[existingItems.length + itemsToAppend.size()];
+    System.arraycopy(existingItems, 0, newFields, 0, existingItems.length);
+    int i = existingItems.length;
+    for (DexEncodedField field : itemsToAppend) {
+      newFields[i] = field;
+      i++;
+    }
+    return newFields;
+  }
+
+  private static DexEncodedField lookupFieldHelper(DexEncodedField[] items, DexField reference) {
+    for (int i = 0; i < items.length; i++) {
+      DexEncodedField item = items[i];
+      if (reference.match(item)) {
+        return item;
+      }
+    }
+    return null;
+  }
+
+  private static DexEncodedField[] replaceFieldsHelper(
+      DexEncodedField[] fields,
+      Function<DexEncodedField, DexEncodedField> replacement,
+      Predicate<DexEncodedField> inThisPool,
+      Consumer<List<DexEncodedField>> onMovedToOtherPool) {
+    List<DexEncodedField> movedToOtherPool = new ArrayList<>();
+    for (int i = 0; i < fields.length; i++) {
+      DexEncodedField existingField = fields[i];
+      assert inThisPool.test(existingField);
+      DexEncodedField newField = replacement.apply(existingField);
+      assert newField != null;
+      if (existingField != newField) {
+        if (inThisPool.test(newField)) {
+          fields[i] = newField;
+        } else {
+          fields[i] = null;
+          movedToOtherPool.add(newField);
+        }
+      }
+    }
+    if (movedToOtherPool.isEmpty()) {
+      return fields;
+    }
+    onMovedToOtherPool.accept(movedToOtherPool);
+    return ArrayUtils.filter(
+        fields,
+        Objects::nonNull,
+        DexEncodedField.EMPTY_ARRAY,
+        fields.length - movedToOtherPool.size());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/FieldCollection.java b/src/main/java/com/android/tools/r8/graph/FieldCollection.java
new file mode 100644
index 0000000..e2cf122
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldCollection.java
@@ -0,0 +1,180 @@
+// Copyright (c) 2023, 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.TraversalContinuation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class FieldCollection {
+
+  // Threshold between using an array and a map for the backing store.
+  // The choice of 30 is just a copy from the method backing threshold.
+  private static final int ARRAY_BACKING_THRESHOLD = 30;
+
+  private final DexClass holder;
+  private FieldCollectionBacking backing;
+
+  private FieldCollection(DexClass holder, FieldCollectionBacking backing) {
+    this.holder = holder;
+    this.backing = backing;
+  }
+
+  public static FieldCollection create(
+      DexClass holder, DexEncodedField[] staticFields, DexEncodedField[] instanceFields) {
+    int fieldCount = staticFields.length + instanceFields.length;
+    FieldCollectionBacking backing;
+    if (fieldCount > ARRAY_BACKING_THRESHOLD) {
+      backing = FieldMapBacking.createLinked(fieldCount);
+      backing.setStaticFields(staticFields);
+      backing.setInstanceFields(instanceFields);
+    } else {
+      backing = FieldArrayBacking.fromArrays(staticFields, instanceFields);
+    }
+    return createInternal(holder, backing);
+  }
+
+  private static FieldCollection createInternal(DexClass holder, FieldCollectionBacking backing) {
+    // Internal create mirrors MethodCollection in case of adding a concurrency checker.
+    return new FieldCollection(holder, backing);
+  }
+
+  public int size() {
+    return backing.size();
+  }
+
+  public void forEachField(Consumer<DexEncodedField> fn) {
+    backing.traverse(
+        field -> {
+          fn.accept(field);
+          return TraversalContinuation.doContinue();
+        });
+  }
+
+  public Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate) {
+    return backing.fields(predicate);
+  }
+
+  public boolean verify() {
+    forEachField(
+        field -> {
+          assert verifyCorrectnessOfFieldHolder(field);
+        });
+    assert backing.verify();
+    return true;
+  }
+
+  private boolean verifyCorrectnessOfFieldHolder(DexEncodedField field) {
+    assert field.getHolderType() == holder.type
+        : "Expected field `"
+            + field.getReference().toSourceString()
+            + "` to have holder `"
+            + holder.type.toSourceString()
+            + "`";
+    return true;
+  }
+
+  private boolean verifyCorrectnessOfFieldHolders(Iterable<DexEncodedField> fields) {
+    for (DexEncodedField field : fields) {
+      assert verifyCorrectnessOfFieldHolder(field);
+    }
+    return true;
+  }
+
+  public boolean hasStaticFields() {
+    return backing.numberOfStaticFields() > 0;
+  }
+
+  public List<DexEncodedField> staticFieldsAsList() {
+    return backing.staticFieldsAsList();
+  }
+
+  public void appendStaticField(DexEncodedField field) {
+    assert verifyCorrectnessOfFieldHolder(field);
+    backing.appendStaticField(field);
+    assert backing.verify();
+  }
+
+  public void appendStaticFields(Collection<DexEncodedField> fields) {
+    assert verifyCorrectnessOfFieldHolders(fields);
+    backing.appendStaticFields(fields);
+    assert backing.verify();
+  }
+
+  public void clearStaticFields() {
+    backing.clearStaticFields();
+  }
+
+  public void setStaticFields(DexEncodedField[] fields) {
+    backing.setStaticFields(fields);
+    assert backing.verify();
+  }
+
+  public boolean hasInstanceFields() {
+    return backing.numberOfInstanceFields() > 0;
+  }
+
+  public List<DexEncodedField> instanceFieldsAsList() {
+    return backing.instanceFieldsAsList();
+  }
+
+  public void appendInstanceField(DexEncodedField field) {
+    assert verifyCorrectnessOfFieldHolder(field);
+    backing.appendInstanceField(field);
+    assert backing.verify();
+  }
+
+  public void appendInstanceFields(Collection<DexEncodedField> fields) {
+    assert verifyCorrectnessOfFieldHolders(fields);
+    backing.appendInstanceFields(fields);
+    assert backing.verify();
+  }
+
+  public void clearInstanceFields() {
+    backing.clearInstanceFields();
+  }
+
+  public void setInstanceFields(DexEncodedField[] fields) {
+    backing.setInstanceFields(fields);
+    assert backing.verify();
+  }
+
+  public DexEncodedField lookupField(DexField field) {
+    return backing.lookupField(field);
+  }
+
+  public DexEncodedField lookupStaticField(DexField field) {
+    return backing.lookupStaticField(field);
+  }
+
+  public DexEncodedField lookupInstanceField(DexField field) {
+    return backing.lookupInstanceField(field);
+  }
+
+  public void replaceFields(Function<DexEncodedField, DexEncodedField> replacement) {
+    backing.replaceFields(replacement);
+  }
+
+  public List<DexEncodedField> allFieldsSorted() {
+    List<DexEncodedField> sorted = new ArrayList<>(size());
+    forEachField(sorted::add);
+    sorted.sort(Comparator.comparing(DexEncodedMember::getReference));
+    return sorted;
+  }
+
+  public boolean hasAnnotations() {
+    return backing
+        .traverse(
+            field ->
+                field.hasAnnotations()
+                    ? TraversalContinuation.doBreak()
+                    : TraversalContinuation.doContinue())
+        .shouldBreak();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java b/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java
new file mode 100644
index 0000000..570996f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldCollectionBacking.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2023, 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.TraversalContinuation;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public abstract class FieldCollectionBacking {
+
+  // Internal consistency.
+
+  static boolean belongsInStaticPool(DexEncodedField field) {
+    return field.isStatic();
+  }
+
+  static boolean belongsInInstancePool(DexEncodedField field) {
+    return !belongsInStaticPool(field);
+  }
+
+  abstract boolean verify();
+
+  // Traversal methods.
+
+  abstract TraversalContinuation<?, ?> traverse(
+      Function<DexEncodedField, TraversalContinuation<?, ?>> fn);
+
+  // Collection methods.
+
+  abstract int size();
+
+  abstract Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate);
+
+  // Specialized to static fields.
+
+  abstract int numberOfStaticFields();
+
+  abstract List<DexEncodedField> staticFieldsAsList();
+
+  abstract void appendStaticField(DexEncodedField field);
+
+  abstract void appendStaticFields(Collection<DexEncodedField> fields);
+
+  abstract void clearStaticFields();
+
+  abstract void setStaticFields(DexEncodedField[] fields);
+
+  // Specialized to instance fields.
+
+  abstract int numberOfInstanceFields();
+
+  abstract List<DexEncodedField> instanceFieldsAsList();
+
+  abstract void appendInstanceField(DexEncodedField field);
+
+  abstract void appendInstanceFields(Collection<DexEncodedField> fields);
+
+  abstract void clearInstanceFields();
+
+  abstract void setInstanceFields(DexEncodedField[] fields);
+
+  abstract DexEncodedField lookupField(DexField field);
+
+  abstract DexEncodedField lookupStaticField(DexField field);
+
+  abstract DexEncodedField lookupInstanceField(DexField field);
+
+  abstract void replaceFields(Function<DexEncodedField, DexEncodedField> replacement);
+}
diff --git a/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java b/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java
new file mode 100644
index 0000000..3bac4e0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/FieldMapBacking.java
@@ -0,0 +1,219 @@
+// Copyright (c) 2023, 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.IterableUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
+import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class FieldMapBacking extends FieldCollectionBacking {
+
+  private SortedMap<DexFieldSignature, DexEncodedField> fieldMap;
+
+  public static FieldMapBacking createLinked(int capacity) {
+    return new FieldMapBacking(createdLinkedMap(capacity));
+  }
+
+  private static SortedMap<DexFieldSignature, DexEncodedField> createdLinkedMap(int capacity) {
+    return new Object2ReferenceLinkedOpenHashMap<>(capacity);
+  }
+
+  private FieldMapBacking(SortedMap<DexFieldSignature, DexEncodedField> fieldMap) {
+    this.fieldMap = fieldMap;
+  }
+
+  // Internal map allocation that shall preserve the map-backing type.
+  // Only the linked map exists for fields currently.
+  private SortedMap<DexFieldSignature, DexEncodedField> internalCreateMap(int capacity) {
+    return createdLinkedMap(capacity);
+  }
+
+  @Override
+  boolean verify() {
+    fieldMap.forEach(
+        (signature, field) -> {
+          assert signature.match(field.getReference());
+        });
+    return true;
+  }
+
+  @Override
+  TraversalContinuation<?, ?> traverse(Function<DexEncodedField, TraversalContinuation<?, ?>> fn) {
+    for (DexEncodedField field : fieldMap.values()) {
+      TraversalContinuation<?, ?> result = fn.apply(field);
+      if (result.shouldBreak()) {
+        return result;
+      }
+    }
+    return TraversalContinuation.doContinue();
+  }
+
+  @Override
+  int size() {
+    return fieldMap.size();
+  }
+
+  @Override
+  Iterable<DexEncodedField> fields(Predicate<? super DexEncodedField> predicate) {
+    return IterableUtils.filter(fieldMap.values(), predicate);
+  }
+
+  @Override
+  int numberOfStaticFields() {
+    return numberOfFieldsHelper(FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  List<DexEncodedField> staticFieldsAsList() {
+    return fieldsAsListHelper(FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  void appendStaticField(DexEncodedField field) {
+    assert belongsInStaticPool(field);
+    DexEncodedField old = fieldMap.put(getSignature(field), field);
+    assert old == null;
+  }
+
+  @Override
+  void appendStaticFields(Collection<DexEncodedField> fields) {
+    fields.forEach(this::appendStaticField);
+  }
+
+  @Override
+  void clearStaticFields() {
+    fieldMap.values().removeIf(FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  void setStaticFields(DexEncodedField[] fields) {
+    setFieldsInPoolHelper(fields, FieldCollectionBacking::belongsInStaticPool);
+  }
+
+  @Override
+  int numberOfInstanceFields() {
+    return numberOfFieldsHelper(FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  List<DexEncodedField> instanceFieldsAsList() {
+    return fieldsAsListHelper(FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  void appendInstanceField(DexEncodedField field) {
+    assert belongsInInstancePool(field);
+    DexEncodedField old = fieldMap.put(getSignature(field), field);
+    assert old == null;
+  }
+
+  @Override
+  void appendInstanceFields(Collection<DexEncodedField> fields) {
+    fields.forEach(this::appendInstanceField);
+  }
+
+  @Override
+  void clearInstanceFields() {
+    fieldMap.values().removeIf(FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  void setInstanceFields(DexEncodedField[] fields) {
+    setFieldsInPoolHelper(fields, FieldCollectionBacking::belongsInInstancePool);
+  }
+
+  @Override
+  DexEncodedField lookupField(DexField field) {
+    return fieldMap.get(getSignature(field));
+  }
+
+  @Override
+  DexEncodedField lookupStaticField(DexField field) {
+    DexEncodedField result = lookupField(field);
+    return result != null && belongsInStaticPool(result) ? result : null;
+  }
+
+  @Override
+  DexEncodedField lookupInstanceField(DexField field) {
+    DexEncodedField result = lookupField(field);
+    return result != null && belongsInInstancePool(result) ? result : null;
+  }
+
+  @Override
+  void replaceFields(Function<DexEncodedField, DexEncodedField> replacement) {
+    // The code assumes that when replacement.apply(field) is called, the map is up-to-date with
+    // the previously replaced fields. We therefore cannot postpone the map updates to the end of
+    // the replacement.
+    ArrayList<DexEncodedField> initialValues = new ArrayList<>(fieldMap.values());
+    for (DexEncodedField field : initialValues) {
+      DexEncodedField newField = replacement.apply(field);
+      if (newField != field) {
+        DexFieldSignature oldSignature = getSignature(field);
+        DexFieldSignature newSignature = getSignature(newField);
+        if (!newSignature.isEqualTo(oldSignature)) {
+          if (fieldMap.get(oldSignature) == field) {
+            fieldMap.remove(oldSignature);
+          }
+        }
+        fieldMap.put(newSignature, newField);
+      }
+    }
+  }
+
+  private DexFieldSignature getSignature(DexEncodedField field) {
+    return getSignature(field.getReference());
+  }
+
+  private DexFieldSignature getSignature(DexField field) {
+    return DexFieldSignature.fromField(field);
+  }
+
+  private int numberOfFieldsHelper(Predicate<DexEncodedField> predicate) {
+    int count = 0;
+    for (DexEncodedField field : fieldMap.values()) {
+      if (predicate.test(field)) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  private List<DexEncodedField> fieldsAsListHelper(Predicate<DexEncodedField> predicate) {
+    List<DexEncodedField> result = new ArrayList<>(fieldMap.size());
+    fieldMap.forEach(
+        (signature, field) -> {
+          if (predicate.test(field)) {
+            result.add(field);
+          }
+        });
+    return Collections.unmodifiableList(result);
+  }
+
+  private void setFieldsInPoolHelper(
+      DexEncodedField[] fields, Predicate<DexEncodedField> inThisPool) {
+    if (fields.length == 0 && fieldMap.isEmpty()) {
+      return;
+    }
+    SortedMap<DexFieldSignature, DexEncodedField> newMap =
+        internalCreateMap(size() + fields.length);
+    fieldMap.forEach(
+        (signature, field) -> {
+          if (!inThisPool.test(field)) {
+            newMap.put(signature, field);
+          }
+        });
+    for (DexEncodedField field : fields) {
+      assert inThisPool.test(field);
+      newMap.put(getSignature(field), field);
+    }
+    fieldMap = newMap;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 44eed32..77100b5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -126,8 +126,7 @@
         localUtilityClass.getDefinition().setStaticFields(localUtilityFields);
       } else {
         clazz.getMethodCollection().replaceMethods(this::fixupEncodedMethod);
-        fixupFields(clazz.staticFields(), clazz::setStaticField);
-        fixupFields(clazz.instanceFields(), clazz::setInstanceField);
+        clazz.getFieldCollection().replaceFields(this::fixupEncodedField);
       }
     }
 
@@ -613,32 +612,27 @@
     return newMethod;
   }
 
-  private void fixupFields(List<DexEncodedField> fields, DexClass.FieldSetter setter) {
-    if (fields == null) {
-      return;
+  private DexEncodedField fixupEncodedField(DexEncodedField encodedField) {
+    DexField field = encodedField.getReference();
+    DexType newType = fixupType(field.type);
+    if (newType == field.type) {
+      return encodedField;
     }
-    for (int i = 0; i < fields.size(); i++) {
-      DexEncodedField encodedField = fields.get(i);
-      DexField field = encodedField.getReference();
-      DexType newType = fixupType(field.type);
-      if (newType != field.type) {
-        DexField newField = field.withType(newType, factory);
-        lensBuilder.move(field, newField);
-        DexEncodedField newEncodedField =
-            encodedField.toTypeSubstitutedField(
-                appView,
-                newField,
-                builder ->
-                    builder.setAbstractValue(
-                        encodedField.getOptimizationInfo().getAbstractValue(), appView));
-        setter.setField(i, newEncodedField);
-        if (encodedField.isStatic() && encodedField.hasExplicitStaticValue()) {
-          assert encodedField.getStaticValue() == DexValue.DexValueNull.NULL;
-          newEncodedField.setStaticValue(DexValue.DexValueInt.DEFAULT);
-          // TODO(b/150593449): Support conversion from DexValueEnum to DexValueInt.
-        }
-      }
+    DexField newField = field.withType(newType, factory);
+    lensBuilder.move(field, newField);
+    DexEncodedField newEncodedField =
+        encodedField.toTypeSubstitutedField(
+            appView,
+            newField,
+            builder ->
+                builder.setAbstractValue(
+                    encodedField.getOptimizationInfo().getAbstractValue(), appView));
+    if (encodedField.isStatic() && encodedField.hasExplicitStaticValue()) {
+      assert encodedField.getStaticValue() == DexValue.DexValueNull.NULL;
+      newEncodedField.setStaticValue(DexValue.DexValueInt.DEFAULT);
+      // TODO(b/150593449): Support conversion from DexValueEnum to DexValueInt.
     }
+    return newEncodedField;
   }
 
   private DexProto fixupProto(DexProto proto) {