Introducing ClassMap to represent a class collection.

This CL introduces a new abstraction ClassMap to represent
a collection of classes of a particular kind which supports
lazy loading and various class sources/providers.

Key changes:

** ResourceProvider renamed into ClassFileResourceProvider
   since DEX resource apparently does not fit very well into
   ResourceProvider API.

** ClassProvider interface is introduced to unify the way
   ClassMap gets its classes populated.

** Raw class maps are hidden behind ClassMap abstraction.
   Three separate class collection types are created to
   represent program, classpath and library collections.

** Program classes are always considered preloaded, but they are
   still represented by ProgramClassCollection rather than bare map.

** Some of the classes are still pre-loaded by ApplicationReader
   and are submitted into collections by using special ClassProvider
   for preloaded classes. This works for DEX files and resources
   submitted NOT via resource providers. We might improve this later.

BUG=

Change-Id: Ied59ce75e13793ff54b6bbcc1a799fc0991c9e34
diff --git a/src/main/java/com/android/tools/r8/ResourceProvider.java b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
similarity index 73%
rename from src/main/java/com/android/tools/r8/ResourceProvider.java
rename to src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
index bed36e3..7dbb420 100644
--- a/src/main/java/com/android/tools/r8/ResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
@@ -3,17 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import java.util.Set;
+
 /**
- * Represents a provider for application resources. All resources returned
- * via this provider should be class file resources, other resource kinds
- * are not yet supported.
+ * Represents a provider for application resources of class file kind.
  *
  * Note that the classes will only be created for resources provided by
  * resource providers on-demand when they are needed by the tool. If
  * never needed, the resource will never be loaded.
  */
-public interface ResourceProvider {
-  // TODO: Consider adding support for DEX resources.
+public interface ClassFileResourceProvider {
+  /** Returns all class descriptors. */
+  Set<String> getClassDescriptors();
 
   /**
    * Get the class resource associated with the descriptor, or null if
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index ceefac5..c06438f 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.android.tools.r8.utils.OutputMode;
-import com.android.tools.r8.utils.PreloadedResourceProvider;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -53,7 +53,7 @@
     public Builder addClasspathFiles(Collection<Path> files) throws IOException {
       for (Path file : files) {
         if (isArchive(file)) {
-          addClasspathResourceProvider(PreloadedResourceProvider.fromArchive(file));
+          addClasspathResourceProvider(PreloadedClassFileProvider.fromArchive(file));
         } else {
           super.addClasspathFiles(file);
         }
@@ -70,7 +70,7 @@
     public Builder addLibraryFiles(Collection<Path> files) throws IOException {
       for (Path file : files) {
         if (isArchive(file)) {
-          addLibraryResourceProvider(PreloadedResourceProvider.fromArchive(file));
+          addLibraryResourceProvider(PreloadedClassFileProvider.fromArchive(file));
         } else {
           super.addLibraryFiles(file);
         }
@@ -78,12 +78,12 @@
       return this;
     }
 
-    public Builder addClasspathResourceProvider(ResourceProvider provider) {
+    public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
       getAppBuilder().addClasspathResourceProvider(provider);
       return this;
     }
 
-    public Builder addLibraryResourceProvider(ResourceProvider provider) {
+    public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
       getAppBuilder().addLibraryResourceProvider(provider);
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/bisect/BisectState.java b/src/main/java/com/android/tools/r8/bisect/BisectState.java
index 183e513..148c167 100644
--- a/src/main/java/com/android/tools/r8/bisect/BisectState.java
+++ b/src/main/java/com/android/tools/r8/bisect/BisectState.java
@@ -8,8 +8,6 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexApplication.Builder;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.NamingLens;
@@ -24,7 +22,6 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -261,23 +258,20 @@
     System.out.println("Next bisection range: " + nextRange);
     int goodClasses = 0;
     int badClasses = 0;
-    Map<DexType, DexClass> classes = new HashMap<>();
-    for (DexLibraryClass clazz : badApp.libraryClasses()) {
-      classes.put(clazz.type, clazz);
-    }
+    List<DexProgramClass> programClasses = new ArrayList<>();
     for (DexProgramClass clazz : badApp.classes()) {
       DexProgramClass goodClass = getGoodClass(clazz);
       if (goodClass != null) {
-        classes.put(goodClass.type, goodClass);
+        programClasses.add(goodClass);
         ++goodClasses;
       } else {
-        classes.put(clazz.type, clazz);
+        programClasses.add(clazz);
         assert !nextRange.isEmpty();
         ++badClasses;
       }
     }
     System.out.println("Class split is good: " + goodClasses + ", bad: " + badClasses);
-    return new Builder(badApp, classes).build();
+    return new Builder(badApp).replaceProgramClasses(programClasses).build();
   }
 
   private DexProgramClass getGoodClass(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index e7bf9db..c7a66c2 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -8,20 +8,29 @@
 import static com.android.tools.r8.dex.Constants.ANDROID_O_API;
 import static com.android.tools.r8.dex.Constants.ANDROID_O_DEX_VERSION;
 import static com.android.tools.r8.dex.Constants.DEFAULT_ANDROID_API;
+import static com.android.tools.r8.graph.ClassKind.CLASSPATH;
+import static com.android.tools.r8.graph.ClassKind.LIBRARY;
+import static com.android.tools.r8.graph.ClassKind.PROGRAM;
 import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.graph.JarClassFileReader;
 import com.android.tools.r8.naming.ProguardMapReader;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ClassProvider;
+import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
 import com.android.tools.r8.utils.MainDexList;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -30,6 +39,8 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -64,82 +75,25 @@
     final DexApplication.Builder builder = new DexApplication.Builder(itemFactory, timing);
     try (Closer closer = Closer.create()) {
       List<Future<?>> futures = new ArrayList<>();
+      // Still preload some of the classes, primarily for two reasons:
+      // (a) class lazy loading is not supported for DEX files
+      //     now and current implementation of parallel DEX file
+      //     loading will be lost with on-demand class loading.
+      // (b) some of the class file resources don't provide information
+      //     about class descriptor.
+      // TODO: try and preload less classes.
       readProguardMap(builder, executorService, futures, closer);
       readMainDexList(builder, executorService, futures, closer);
-      readDexSources(builder, executorService, futures, closer);
-      readClassSources(builder, closer);
-      initializeLazyClassCollection(builder);
+      ClassReader classReader = new ClassReader(executorService, futures, closer);
+      classReader.readSources();
       ThreadUtils.awaitFutures(futures);
+      classReader.initializeLazyClassCollection(builder);
     } finally {
       timing.end();
     }
     return builder.build();
   }
 
-  private void readClassSources(DexApplication.Builder builder, Closer closer)
-      throws IOException, ExecutionException {
-    JarApplicationReader application = new JarApplicationReader(options);
-    JarClassFileReader reader = new JarClassFileReader(
-        application, builder::addClassIgnoringLibraryDuplicates);
-    for (Resource input : inputApp.getClassProgramResources()) {
-      reader.read(DEFAULT_DEX_FILENAME, ClassKind.PROGRAM, input.getStream(closer));
-    }
-    for (Resource input : inputApp.getClassClasspathResources()) {
-      reader.read(DEFAULT_DEX_FILENAME, ClassKind.CLASSPATH, input.getStream(closer));
-    }
-    for (Resource input : inputApp.getClassLibraryResources()) {
-      reader.read(DEFAULT_DEX_FILENAME, ClassKind.LIBRARY, input.getStream(closer));
-    }
-  }
-
-  private void initializeLazyClassCollection(DexApplication.Builder builder) {
-    List<ResourceProvider> classpathProviders = inputApp.getClasspathResourceProviders();
-    List<ResourceProvider> libraryProviders = inputApp.getLibraryResourceProviders();
-    if (!classpathProviders.isEmpty() || !libraryProviders.isEmpty()) {
-      builder.setLazyClassCollection(new LazyClassCollection(
-          new JarApplicationReader(options), classpathProviders, libraryProviders));
-    }
-  }
-
-  private void readDexSources(DexApplication.Builder builder, ExecutorService executorService,
-      List<Future<?>> futures, Closer closer)
-      throws IOException, ExecutionException {
-    List<Resource> dexProgramSources = inputApp.getDexProgramResources();
-    List<Resource> dexClasspathSources = inputApp.getDexClasspathResources();
-    List<Resource> dexLibrarySources = inputApp.getDexLibraryResources();
-    int numberOfFiles = dexProgramSources.size()
-        + dexLibrarySources.size() + dexClasspathSources.size();
-    if (numberOfFiles > 0) {
-      List<DexFileReader> fileReaders = new ArrayList<>(numberOfFiles);
-      int computedMinApiLevel = options.minApiLevel;
-      for (Resource input : dexProgramSources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
-        fileReaders.add(new DexFileReader(file, ClassKind.PROGRAM, itemFactory));
-      }
-      for (Resource input : dexClasspathSources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        fileReaders.add(new DexFileReader(file, ClassKind.CLASSPATH, itemFactory));
-      }
-      for (Resource input : dexLibrarySources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
-        fileReaders.add(new DexFileReader(file, ClassKind.LIBRARY, itemFactory));
-      }
-      options.minApiLevel = computedMinApiLevel;
-      for (DexFileReader reader : fileReaders) {
-        DexFileReader.populateIndexTables(reader);
-      }
-      // Read the DexCode items and DexProgramClass items in parallel.
-      for (DexFileReader reader : fileReaders) {
-        futures.add(executorService.submit(() -> {
-          reader.addCodeItemsTo();  // Depends on Everything for parsing.
-          reader.addClassDefsTo(builder::addClass);  // Depends on Methods, Code items etc.
-        }));
-      }
-    }
-  }
-
   private int verifyOrComputeMinApiLevel(int computedMinApiLevel, DexFile file) {
     int version = file.getDexVersion();
     if (options.minApiLevel == DEFAULT_ANDROID_API) {
@@ -203,4 +157,111 @@
       }));
     }
   }
+
+  private final class ClassReader {
+    private final ExecutorService executorService;
+    private final List<Future<?>> futures;
+    private final Closer closer;
+
+    // We use concurrent queues to collect classes
+    // since the classes can be collected concurrently.
+    private final Queue<DexProgramClass> programClasses = new ConcurrentLinkedQueue<>();
+    private final Queue<DexClasspathClass> classpathClasses = new ConcurrentLinkedQueue<>();
+    private final Queue<DexLibraryClass> libraryClasses = new ConcurrentLinkedQueue<>();
+    // Jar application reader to share across all class readers.
+    private final JarApplicationReader application = new JarApplicationReader(options);
+
+    ClassReader(ExecutorService executorService, List<Future<?>> futures, Closer closer) {
+      this.executorService = executorService;
+      this.futures = futures;
+      this.closer = closer;
+    }
+
+    private <T extends DexClass> void readDexSources(List<Resource> dexSources,
+        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+      if (dexSources.size() > 0) {
+        List<DexFileReader> fileReaders = new ArrayList<>(dexSources.size());
+        int computedMinApiLevel = options.minApiLevel;
+        for (Resource input : dexSources) {
+          DexFile file = new DexFile(input.getStream(closer));
+          computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
+          fileReaders.add(new DexFileReader(file, classKind, itemFactory));
+        }
+        options.minApiLevel = computedMinApiLevel;
+        for (DexFileReader reader : fileReaders) {
+          DexFileReader.populateIndexTables(reader);
+        }
+        // Read the DexCode items and DexProgramClass items in parallel.
+        for (DexFileReader reader : fileReaders) {
+          futures.add(executorService.submit(() -> {
+            reader.addCodeItemsTo();  // Depends on Everything for parsing.
+            reader.addClassDefsTo(
+                classKind.bridgeConsumer(classes::add)); // Depends on Methods, Code items etc.
+          }));
+        }
+      }
+    }
+
+    private <T extends DexClass> void readClassSources(List<Resource> classSources,
+        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+      JarClassFileReader reader = new JarClassFileReader(
+          application, classKind.bridgeConsumer(classes::add));
+      for (Resource input : classSources) {
+        reader.read(DEFAULT_DEX_FILENAME, classKind, input.getStream(closer));
+      }
+    }
+
+    void readSources() throws IOException, ExecutionException {
+      readDexSources(inputApp.getDexProgramResources(), PROGRAM, programClasses);
+      readDexSources(inputApp.getDexClasspathResources(), CLASSPATH, classpathClasses);
+      readDexSources(inputApp.getDexLibraryResources(), LIBRARY, libraryClasses);
+      readClassSources(inputApp.getClassProgramResources(), PROGRAM, programClasses);
+      readClassSources(inputApp.getClassClasspathResources(), CLASSPATH, classpathClasses);
+      readClassSources(inputApp.getClassLibraryResources(), LIBRARY, libraryClasses);
+    }
+
+    private <T extends DexClass> ClassProvider<T> buildClassProvider(ClassKind classKind,
+        Queue<T> preloadedClasses, List<ClassFileResourceProvider> resourceProviders,
+        JarApplicationReader reader) {
+      List<ClassProvider<T>> providers = new ArrayList<>();
+
+      // Preloaded classes.
+      if (!preloadedClasses.isEmpty()) {
+        providers.add(ClassProvider.forPreloadedClasses(classKind, preloadedClasses));
+      }
+
+      // Class file resource providers.
+      for (ClassFileResourceProvider provider : resourceProviders) {
+        providers.add(ClassProvider.forClassFileResources(classKind, provider, reader));
+      }
+
+      // Combine if needed.
+      if (providers.isEmpty()) {
+        return null;
+      }
+      return providers.size() == 1 ? providers.get(0)
+          : ClassProvider.combine(classKind, providers);
+    }
+
+    void initializeLazyClassCollection(DexApplication.Builder builder) {
+      // Add all program classes to the builder.
+      for (DexProgramClass clazz : programClasses) {
+        builder.addProgramClass(clazz.asProgramClass());
+      }
+
+      // Create classpath class collection if needed.
+      ClassProvider<DexClasspathClass> classpathClassProvider = buildClassProvider(CLASSPATH,
+          classpathClasses, inputApp.getClasspathResourceProviders(), application);
+      if (classpathClassProvider != null) {
+        builder.setClasspathClassCollection(new ClasspathClassCollection(classpathClassProvider));
+      }
+
+      // Create library class collection if needed.
+      ClassProvider<DexLibraryClass> libraryClassProvider = buildClassProvider(LIBRARY,
+          libraryClasses, inputApp.getLibraryResourceProviders(), application);
+      if (libraryClassProvider != null) {
+        builder.setLibraryClassCollection(new LibraryClassCollection(libraryClassProvider));
+      }
+    }
+  }
 }
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 73c0533..9e3e118 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -21,7 +21,7 @@
 
   public AppInfoWithSubtyping(DexApplication application) {
     super(application);
-    populateSubtypeMap(application.getClassMap(), application.dexItemFactory);
+    populateSubtypeMap(application.getFullClassMap(), application.dexItemFactory);
   }
 
   protected AppInfoWithSubtyping(AppInfoWithSubtyping previous) {
@@ -33,7 +33,7 @@
   protected AppInfoWithSubtyping(AppInfoWithSubtyping previous, GraphLense lense) {
     super(previous, lense);
     // Recompute subtype map if we have modified the graph.
-    populateSubtypeMap(previous.app.getClassMap(), dexItemFactory);
+    populateSubtypeMap(previous.app.getFullClassMap(), dexItemFactory);
   }
 
   public Set<DexType> getMissingClasses() {
diff --git a/src/main/java/com/android/tools/r8/graph/ClassKind.java b/src/main/java/com/android/tools/r8/graph/ClassKind.java
index 6f53a40..9298ba5 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassKind.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassKind.java
@@ -1,12 +1,14 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.Resource;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** Kind of the application class. Can be program, classpath or library. */
 public enum ClassKind {
-  PROGRAM(DexProgramClass::new),
-  CLASSPATH(DexClasspathClass::new),
-  LIBRARY(DexLibraryClass::new);
+  PROGRAM(DexProgramClass::new, DexClass::isProgramClass),
+  CLASSPATH(DexClasspathClass::new, DexClass::isClasspathClass),
+  LIBRARY(DexLibraryClass::new, DexClass::isLibraryClass);
 
   private interface Factory {
     DexClass create(DexType type, Resource.Kind origin, DexAccessFlags accessFlags,
@@ -17,9 +19,11 @@
   }
 
   private final Factory factory;
+  private final Predicate<DexClass> check;
 
-  ClassKind(Factory factory) {
+  ClassKind(Factory factory, Predicate<DexClass> check) {
     this.factory = factory;
+    this.check = check;
   }
 
   public DexClass create(
@@ -30,4 +34,16 @@
     return factory.create(type, origin, accessFlags, superType, interfaces, sourceFile,
         annotations, staticFields, instanceFields, directMethods, virtualMethods);
   }
+
+  public boolean isOfKind(DexClass clazz) {
+    return check.test(clazz);
+  }
+
+  public <T extends DexClass> Consumer<DexClass> bridgeConsumer(Consumer<T> consumer) {
+    return clazz -> {
+      assert isOfKind(clazz);
+      @SuppressWarnings("unchecked") T specialized = (T) clazz;
+      consumer.accept(specialized);
+    };
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index f472085..e7485aa 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -6,16 +6,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.ir.desugar.LambdaRewriter;
-import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
+import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.io.ByteArrayOutputStream;
@@ -29,6 +26,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Hashtable;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -36,16 +34,9 @@
 public class DexApplication {
 
   // Maps type into class, may be used concurrently.
-  private final ImmutableMap<DexType, DexClass> classMap;
-
-  // Lazily loaded classes.
-  //
-  // Note that this collection is autonomous and may be used in several
-  // different applications. Particularly, it is the case when one
-  // application is being build based on another one. Among others,
-  // it will have an important side-effect: class conflict resolution,
-  // generated errors in particular, may be different in lazy scenario.
-  private final LazyClassCollection lazyClassCollection;
+  private ProgramClassCollection programClasses;
+  private ClasspathClassCollection classpathClasses;
+  private LibraryClassCollection libraryClasses;
 
   public final ImmutableSet<DexType> mainDexList;
 
@@ -61,24 +52,27 @@
   /** Constructor should only be invoked by the DexApplication.Builder. */
   private DexApplication(
       ClassNameMapper proguardMap,
-      ImmutableMap<DexType, DexClass> classMap,
-      LazyClassCollection lazyClassCollection,
+      ProgramClassCollection programClasses,
+      ClasspathClassCollection classpathClasses,
+      LibraryClassCollection libraryClasses,
       ImmutableSet<DexType> mainDexList,
       DexItemFactory dexItemFactory,
       DexString highestSortingString,
       Timing timing) {
+    assert programClasses != null;
     this.proguardMap = proguardMap;
-    this.lazyClassCollection = lazyClassCollection;
+    this.programClasses = programClasses;
+    this.classpathClasses = classpathClasses;
+    this.libraryClasses = libraryClasses;
     this.mainDexList = mainDexList;
-    this.classMap = classMap;
     this.dexItemFactory = dexItemFactory;
     this.highestSortingString = highestSortingString;
     this.timing = timing;
   }
 
-  ImmutableMap<DexType, DexClass> getClassMap() {
-    assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
-    return classMap;
+  /** Force load all classes and return type -> class map containing all the classes */
+  public Map<DexType, DexClass> getFullClassMap() {
+    return forceLoadAllClasses();
   }
 
   // Reorder classes randomly. Note that the order of classes in program or library
@@ -92,49 +86,63 @@
     return true;
   }
 
-  public Iterable<DexProgramClass> classes() {
-    List<DexProgramClass> result = new ArrayList<>();
-    // Note: we ignore lazy class collection because it
-    // is never supposed to be used for program classes.
-    for (DexClass clazz : classMap.values()) {
-      if (clazz.isProgramClass()) {
-        result.add(clazz.asProgramClass());
-      }
-    }
-    assert reorderClasses(result);
-    return result;
+  public List<DexProgramClass> classes() {
+    List<DexProgramClass> classes = programClasses.collectLoadedClasses();
+    assert reorderClasses(classes);
+    return classes;
   }
 
-  public Iterable<DexLibraryClass> libraryClasses() {
-    assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
-    List<DexLibraryClass> result = new ArrayList<>();
+  public List<DexLibraryClass> libraryClasses() {
+    assert classpathClasses == null : "Operation is not supported.";
+    Map<DexType, DexClass> classMap = forceLoadAllClasses();
+    List<DexLibraryClass> classes = new ArrayList<>();
     for (DexClass clazz : classMap.values()) {
       if (clazz.isLibraryClass()) {
-        result.add(clazz.asLibraryClass());
+        classes.add(clazz.asLibraryClass());
       }
     }
-    assert reorderClasses(result);
-    return result;
+    assert reorderClasses(classes);
+    return classes;
+  }
+
+  private Map<DexType, DexClass> forceLoadAllClasses() {
+    Map<DexType, DexClass> loaded = new IdentityHashMap<>();
+
+    // program classes are supposed to be loaded, but force-loading them is no-op.
+    programClasses.forceLoad(type -> true);
+    programClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+
+    if (classpathClasses != null) {
+      classpathClasses.forceLoad(type -> !loaded.containsKey(type));
+      classpathClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+    }
+
+    if (libraryClasses != null) {
+      libraryClasses.forceLoad(type -> !loaded.containsKey(type));
+      libraryClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+    }
+
+    return loaded;
   }
 
   public DexClass definitionFor(DexType type) {
-    DexClass clazz = classMap.get(type);
-    // In presence of lazy class collection we also reach out to it
-    // as well unless the class found is already a program class.
-    if (lazyClassCollection != null && (clazz == null || !clazz.isProgramClass())) {
-      clazz = lazyClassCollection.get(type, clazz);
+    DexClass clazz = programClasses.get(type);
+    if (clazz == null && classpathClasses != null) {
+      clazz = classpathClasses.get(type);
+    }
+    if (clazz == null && libraryClasses != null) {
+      clazz = libraryClasses.get(type);
     }
     return clazz;
   }
 
   public DexProgramClass programDefinitionFor(DexType type) {
-    DexClass clazz = classMap.get(type);
-    // Don't bother about lazy class collection, it should never load program classes.
-    return (clazz == null || !clazz.isProgramClass()) ? null : clazz.asProgramClass();
+    DexClass clazz = programClasses.get(type);
+    return clazz == null ? null : clazz.asProgramClass();
   }
 
   public String toString() {
-    return "Application (classes #" + classMap.size() + ")";
+    return "Application (" + programClasses + "; " + classpathClasses + "; " + libraryClasses + ")";
   }
 
   public ClassNameMapper getProguardMap() {
@@ -184,7 +192,7 @@
    * <p>If no directory is provided everything is written to System.out.
    */
   public void disassemble(Path outputDir, InternalOptions options) {
-    for (DexClass clazz : classes()) {
+    for (DexProgramClass clazz : programClasses.collectLoadedClasses()) {
       for (DexEncodedMethod method : clazz.virtualMethods()) {
         if (options.methodMatchesFilter(method)) {
           disassemble(method, getProguardMap(), outputDir);
@@ -238,7 +246,7 @@
    * Write smali source for the application code on the provided PrintStream.
    */
   public void smali(InternalOptions options, PrintStream ps) {
-    List<DexProgramClass> classes = (List<DexProgramClass>) classes();
+    List<DexProgramClass> classes = programClasses.collectLoadedClasses();
     classes.sort(Comparator.comparing(DexProgramClass::toSourceString));
     boolean firstClass = true;
     for (DexClass clazz : classes) {
@@ -287,9 +295,16 @@
   }
 
   public static class Builder {
+    // We handle program class collection separately from classpath
+    // and library class collections. Since while we assume program
+    // class collection should always be fully loaded and thus fully
+    // represented by the map (making it easy, for example, adding
+    // new or removing existing classes), classpath and library
+    // collections will be considered monolithic collections.
 
-    private final Hashtable<DexType, DexClass> classMap = new Hashtable<>();
-    private LazyClassCollection lazyClassCollection;
+    private final List<DexProgramClass> programClasses;
+    private ClasspathClassCollection classpathClasses;
+    private LibraryClassCollection libraryClasses;
 
     public final Hashtable<DexCode, DexCode> codeItems = new Hashtable<>();
 
@@ -301,18 +316,17 @@
     private final Set<DexType> mainDexList = Sets.newIdentityHashSet();
 
     public Builder(DexItemFactory dexItemFactory, Timing timing) {
+      this.programClasses = new ArrayList<>();
       this.dexItemFactory = dexItemFactory;
       this.timing = timing;
-      this.lazyClassCollection = null;
+      this.classpathClasses = null;
+      this.libraryClasses = null;
     }
 
     public Builder(DexApplication application) {
-      this(application, application.classMap);
-    }
-
-    public Builder(DexApplication application, Map<DexType, DexClass> classMap) {
-      this.classMap.putAll(classMap);
-      this.lazyClassCollection = application.lazyClassCollection;
+      programClasses = application.programClasses.collectLoadedClasses();
+      classpathClasses = application.classpathClasses;
+      libraryClasses = application.libraryClasses;
       proguardMap = application.proguardMap;
       timing = application.timing;
       highestSortingString = application.highestSortingString;
@@ -326,58 +340,45 @@
       return this;
     }
 
+    public synchronized Builder replaceProgramClasses(List<DexProgramClass> newProgramClasses) {
+      assert newProgramClasses != null;
+      this.programClasses.clear();
+      this.programClasses.addAll(newProgramClasses);
+      return this;
+    }
+
     public synchronized Builder setHighestSortingString(DexString value) {
       highestSortingString = value;
       return this;
     }
 
-    public Builder addClass(DexClass clazz) {
-      addClass(clazz, false);
+    public synchronized Builder addProgramClass(DexProgramClass clazz) {
+      programClasses.add(clazz);
       return this;
     }
 
-    public Builder setLazyClassCollection(LazyClassCollection lazyClassMap) {
-      this.lazyClassCollection = lazyClassMap;
+    public Builder setClasspathClassCollection(ClasspathClassCollection classes) {
+      this.classpathClasses = classes;
       return this;
     }
 
-    public Builder addClassIgnoringLibraryDuplicates(DexClass clazz) {
-      addClass(clazz, true);
+    public Builder setLibraryClassCollection(LibraryClassCollection classes) {
+      this.libraryClasses = classes;
       return this;
     }
 
-    public Builder addSynthesizedClass(DexProgramClass synthesizedClass, boolean addToMainDexList) {
+    public synchronized Builder addSynthesizedClass(
+        DexProgramClass synthesizedClass, boolean addToMainDexList) {
       assert synthesizedClass.isProgramClass() : "All synthesized classes must be program classes";
-      addClass(synthesizedClass);
+      addProgramClass(synthesizedClass);
       if (addToMainDexList && !mainDexList.isEmpty()) {
         mainDexList.add(synthesizedClass.type);
       }
       return this;
     }
 
-    public List<DexProgramClass> getProgramClasses() {
-      List<DexProgramClass> result = new ArrayList<>();
-      // Note: we ignore lazy class collection because it
-      // is never supposed to be used for program classes.
-      for (DexClass clazz : classMap.values()) {
-        if (clazz.isProgramClass()) {
-          result.add(clazz.asProgramClass());
-        }
-      }
-      return result;
-    }
-
-    // Callback from FileReader when parsing a DexProgramClass (multi-threaded).
-    public synchronized void addClass(DexClass newClass, boolean skipLibDups) {
-      assert newClass != null;
-      DexType type = newClass.type;
-      DexClass oldClass = classMap.get(type);
-      if (oldClass != null) {
-        newClass = chooseClass(newClass, oldClass, skipLibDups);
-      }
-      if (oldClass != newClass) {
-        classMap.put(type, newClass);
-      }
+    public Collection<DexProgramClass> getProgramClasses() {
+      return programClasses;
     }
 
     public Builder addToMainDexList(Collection<DexType> mainDexList) {
@@ -388,83 +389,13 @@
     public DexApplication build() {
       return new DexApplication(
           proguardMap,
-          ImmutableMap.copyOf(classMap),
-          lazyClassCollection,
+          ProgramClassCollection.create(programClasses),
+          classpathClasses,
+          libraryClasses,
           ImmutableSet.copyOf(mainDexList),
           dexItemFactory,
           highestSortingString,
           timing);
     }
   }
-
-  public static DexClass chooseClass(DexClass a, DexClass b, boolean skipLibDups) {
-    // NOTE: We assume that there should not be any conflicting names in user defined
-    // classes and/or linked jars. If we ever want to allow 'keep first'-like policy
-    // to resolve this kind of conflict between program and/or classpath classes, we'll
-    // need to make sure we choose the class we keep deterministically.
-    if (a.isProgramClass() && b.isProgramClass()) {
-      if (allowProgramClassConflict(a.asProgramClass(), b.asProgramClass())) {
-        return a;
-      }
-      throw new CompilationError("Program type already present: " + a.type.toSourceString());
-    }
-    if (a.isProgramClass()) {
-      return chooseClass(a.asProgramClass(), b);
-    }
-    if (b.isProgramClass()) {
-      return chooseClass(b.asProgramClass(), a);
-    }
-
-    if (a.isClasspathClass() && b.isClasspathClass()) {
-      throw new CompilationError("Classpath type already present: " + a.type.toSourceString());
-    }
-    if (a.isClasspathClass()) {
-      return chooseClass(a.asClasspathClass(), b.asLibraryClass());
-    }
-    if (b.isClasspathClass()) {
-      return chooseClass(b.asClasspathClass(), a.asLibraryClass());
-    }
-
-    return chooseClasses(b.asLibraryClass(), a.asLibraryClass(), skipLibDups);
-  }
-
-  private static boolean allowProgramClassConflict(DexProgramClass a, DexProgramClass b) {
-    // Currently only allow collapsing synthetic lambda classes.
-    return a.getOrigin() == Resource.Kind.DEX
-        && b.getOrigin() == Resource.Kind.DEX
-        && a.accessFlags.isSynthetic()
-        && b.accessFlags.isSynthetic()
-        && LambdaRewriter.hasLambdaClassPrefix(a.type)
-        && LambdaRewriter.hasLambdaClassPrefix(b.type);
-  }
-
-  private static DexClass chooseClass(DexProgramClass selected, DexClass ignored) {
-    assert !ignored.isProgramClass();
-    if (ignored.isLibraryClass()) {
-      logIgnoredClass(ignored, "Class `%s` was specified as library and program type.");
-    }
-    // We don't log program/classpath class conflict since it is expected case.
-    return selected;
-  }
-
-  private static DexClass chooseClass(DexClasspathClass selected, DexLibraryClass ignored) {
-    logIgnoredClass(ignored, "Class `%s` was specified as library and classpath type.");
-    return selected;
-  }
-
-  private static DexClass chooseClasses(
-      DexLibraryClass selected, DexLibraryClass ignored, boolean skipDups) {
-    if (!skipDups) {
-      throw new CompilationError(
-          "Library type already present: " + selected.type.toSourceString());
-    }
-    logIgnoredClass(ignored, "Class `%s` was specified twice as a library type.");
-    return selected;
-  }
-
-  private static void logIgnoredClass(DexClass ignored, String message) {
-    if (Log.ENABLED) {
-      Log.warn(DexApplication.class, message, ignored.type.toSourceString());
-    }
-  }
 }
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 21ab0d7..04f6786 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -7,17 +7,14 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.KeyedDexItem;
 import com.android.tools.r8.graph.PresortedComparable;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.Set;
 
 public class TreePruner {
@@ -51,22 +48,20 @@
   }
 
   private DexApplication.Builder removeUnused(DexApplication application) {
-    return new DexApplication.Builder(application, removeUnusedClassStructure(application));
+    return new DexApplication.Builder(application)
+        .replaceProgramClasses(getNewProgramClasses(application.classes()));
   }
 
-  private Map<DexType, DexClass> removeUnusedClassStructure(DexApplication application) {
-    Map<DexType, DexClass> classMap = new IdentityHashMap<>();
-    for (DexLibraryClass clazz : application.libraryClasses()) {
-      classMap.put(clazz.type, clazz);
-    }
-    for (DexProgramClass clazz : application.classes()) {
+  private List<DexProgramClass> getNewProgramClasses(List<DexProgramClass> classes) {
+    List<DexProgramClass> newClasses = new ArrayList<>();
+    for (DexProgramClass clazz : classes) {
       if (!appInfo.liveTypes.contains(clazz.type) && !options.debugKeepRules) {
         // The class is completely unused and we can remove it.
         if (Log.ENABLED) {
           Log.debug(getClass(), "Removing class: " + clazz);
         }
       } else {
-        classMap.put(clazz.type, clazz);
+        newClasses.add(clazz);
         if (!appInfo.instantiatedTypes.contains(clazz.type) && !options.debugKeepRules) {
           // The class is only needed as a type but never instantiated. Make it abstract to reflect
           // this.
@@ -88,7 +83,7 @@
         clazz.staticFields = reachableFields(clazz.staticFields());
       }
     }
-    return classMap;
+    return newClasses;
   }
 
   private <S extends PresortedComparable<S>, T extends KeyedDexItem<S>> int firstUnreachableIndex(
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 7ea2367..e4c0de3 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -7,8 +7,8 @@
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.ClassKind;
@@ -53,8 +53,8 @@
   private final ImmutableList<Resource> programResources;
   private final ImmutableList<Resource> classpathResources;
   private final ImmutableList<Resource> libraryResources;
-  private final ImmutableList<ResourceProvider> classpathResourceProviders;
-  private final ImmutableList<ResourceProvider> libraryResourceProviders;
+  private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
+  private final ImmutableList<ClassFileResourceProvider> libraryResourceProviders;
   private final Resource proguardMap;
   private final Resource proguardSeeds;
   private final Resource packageDistribution;
@@ -65,8 +65,8 @@
       ImmutableList<Resource> programResources,
       ImmutableList<Resource> classpathResources,
       ImmutableList<Resource> libraryResources,
-      ImmutableList<ResourceProvider> classpathResourceProviders,
-      ImmutableList<ResourceProvider> libraryResourceProviders,
+      ImmutableList<ClassFileResourceProvider> classpathResourceProviders,
+      ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
       Resource proguardMap,
       Resource proguardSeeds,
       Resource packageDistribution,
@@ -176,12 +176,12 @@
   }
 
   /** Get classpath resource providers. */
-  public List<ResourceProvider> getClasspathResourceProviders() {
+  public List<ClassFileResourceProvider> getClasspathResourceProviders() {
     return classpathResourceProviders;
   }
 
   /** Get library resource providers. */
-  public List<ResourceProvider> getLibraryResourceProviders() {
+  public List<ClassFileResourceProvider> getLibraryResourceProviders() {
     return libraryResourceProviders;
   }
 
@@ -366,8 +366,8 @@
     private final List<Resource> programResources = new ArrayList<>();
     private final List<Resource> classpathResources = new ArrayList<>();
     private final List<Resource> libraryResources = new ArrayList<>();
-    private final List<ResourceProvider> classpathResourceProviders = new ArrayList<>();
-    private final List<ResourceProvider> libraryResourceProviders = new ArrayList<>();
+    private final List<ClassFileResourceProvider> classpathResourceProviders = new ArrayList<>();
+    private final List<ClassFileResourceProvider> libraryResourceProviders = new ArrayList<>();
     private Resource proguardMap;
     private Resource proguardSeeds;
     private Resource packageDistribution;
@@ -450,7 +450,7 @@
     /**
      * Add classpath resource provider.
      */
-    public Builder addClasspathResourceProvider(ResourceProvider provider) {
+    public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
       classpathResourceProviders.add(provider);
       return this;
     }
@@ -475,7 +475,7 @@
     /**
      * Add library resource provider.
      */
-    public Builder addLibraryResourceProvider(ResourceProvider provider) {
+    public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
       libraryResourceProviders.add(provider);
       return this;
     }
@@ -627,7 +627,7 @@
                 Resource.Kind.DEX, ByteStreams.toByteArray(stream)));
           } else if (isClassFile(name)) {
             containsClassData = true;
-            String descriptor = PreloadedResourceProvider.guessTypeDescriptor(name);
+            String descriptor = PreloadedClassFileProvider.guessTypeDescriptor(name);
             resources(classKind).add(Resource.fromBytes(Resource.Kind.CLASSFILE,
                 ByteStreams.toByteArray(stream), Collections.singleton(descriptor)));
           }
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java
new file mode 100644
index 0000000..d0f2cda
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Represents a collection of classes. Collection can be fully loaded,
+ * lazy loaded or have preloaded classes along with lazy loaded content.
+ */
+public abstract class ClassMap<T extends DexClass> {
+  // For each type which has ever been queried stores one class loaded from
+  // resources provided by different resource providers.
+  //
+  // NOTE: all access must be synchronized on `classes`.
+  private final Map<DexType, Value<T>> classes;
+
+  // Class provider if available. In case it's `null`, all classes of
+  // the collection must be pre-populated in `classes`.
+  private final ClassProvider<T> classProvider;
+
+  ClassMap(Map<DexType, Value<T>> classes, ClassProvider<T> classProvider) {
+    this.classes = classes == null ? new IdentityHashMap<>() : classes;
+    this.classProvider = classProvider;
+    assert this.classProvider == null || this.classProvider.getClassKind() == getClassKind();
+  }
+
+  /** Resolves a class conflict by selecting a class, may generate compilation error. */
+  abstract T resolveClassConflict(T a, T b);
+
+  /** Kind of the classes supported by this collection. */
+  abstract ClassKind getClassKind();
+
+  @Override
+  public String toString() {
+    synchronized (classes) {
+      return classes.size() + " loaded, provider: " +
+          (classProvider == null ? "none" : classProvider.toString());
+    }
+  }
+
+  /** Returns a definition for a class or `null` if there is no such class in the collection. */
+  public T get(DexType type) {
+    final Value<T> value = getOrCreateValue(type);
+
+    if (!value.ready) {
+      // Load the value in a context synchronized on value instance. This way
+      // we avoid locking the whole collection during expensive resource loading
+      // and classes construction operations.
+      synchronized (value) {
+        if (!value.ready && classProvider != null) {
+          classProvider.collectClass(type, clazz -> {
+            assert clazz != null;
+            assert getClassKind().isOfKind(clazz);
+            assert !value.ready;
+
+            if (clazz.type != type) {
+              throw new CompilationError("Class content provided for type descriptor "
+                  + type.toSourceString() + " actually defines class " + clazz.type
+                  .toSourceString());
+            }
+
+            if (value.clazz == null) {
+              value.clazz = clazz;
+            } else {
+              // The class resolution *may* generate a compilation error as one of
+              // possible resolutions. In this case we leave `value` in (false, null)
+              // state so in rare case of another thread trying to get the same class
+              // before this error is propagated it will get the same conflict.
+              T oldClass = value.clazz;
+              value.clazz = null;
+              value.clazz = resolveClassConflict(oldClass, clazz);
+            }
+          });
+        }
+        value.ready = true;
+      }
+    }
+
+    assert value.ready;
+    return value.clazz;
+  }
+
+  private Value<T> getOrCreateValue(DexType type) {
+    synchronized (classes) {
+      return classes.computeIfAbsent(type, k -> new Value<>());
+    }
+  }
+
+  /** Returns currently loaded classes */
+  public List<T> collectLoadedClasses() {
+    List<T> loadedClasses = new ArrayList<>();
+    synchronized (classes) {
+      for (Value<T> value : classes.values()) {
+        // Method collectLoadedClasses() must always be called when there
+        // is no risk of concurrent loading of the classes, otherwise the
+        // behaviour of this method is undefined. Note that value mutations
+        // are NOT synchronized in `classes`, so the assertion below does
+        // not enforce this requirement, but may help detect wrong behaviour.
+        assert value.ready : "";
+        if (value.clazz != null) {
+          loadedClasses.add(value.clazz);
+        }
+      }
+    }
+    return loadedClasses;
+  }
+
+  /** Forces loading of all the classes satisfying the criteria specified. */
+  public void forceLoad(Predicate<DexType> load) {
+    if (classProvider != null) {
+      Set<DexType> loaded;
+      synchronized (classes) {
+        loaded = classes.keySet();
+      }
+      Collection<DexType> types = classProvider.collectTypes();
+      for (DexType type : types) {
+        if (load.test(type) && !loaded.contains(type)) {
+          get(type); // force-load type.
+        }
+      }
+    }
+  }
+
+  // Represents a value in the class map.
+  final static class Value<T> {
+    volatile boolean ready;
+    T clazz;
+
+    Value() {
+      ready = false;
+      clazz = null;
+    }
+
+    Value(T clazz) {
+      this.clazz = clazz;
+      this.ready = true;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClassProvider.java b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
new file mode 100644
index 0000000..25abdce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
@@ -0,0 +1,187 @@
+// Copyright (c) 2017, 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.utils;
+
+import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.JarApplicationReader;
+import com.android.tools.r8.graph.JarClassFileReader;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.io.Closer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/** Represents a provider for classes loaded from different sources. */
+public abstract class ClassProvider<T extends DexClass> {
+  private final ClassKind classKind;
+
+  ClassProvider(ClassKind classKind) {
+    this.classKind = classKind;
+  }
+
+  /** The kind of the classes created by the provider. */
+  final ClassKind getClassKind() {
+    return classKind;
+  }
+
+  /**
+   * The provider uses the callback to return all the classes that might
+   * be associated with the descriptor asked for.
+   *
+   * NOTE: the provider is not required to cache created classes and this
+   * method may create a new class instance in case it is called twice for
+   * the same type. For this reason it is recommended that the provider
+   * user only calls this method once per any given type.
+   *
+   * NOTE: thread-safe.
+   */
+  public abstract void collectClass(DexType type, Consumer<T> classConsumer);
+
+  /**
+   * Returns all the types of classes that might be produced by this provider.
+   *
+   * NOTE: thread-safe.
+   */
+  public abstract Collection<DexType> collectTypes();
+
+  /** Create class provider for java class resource provider. */
+  public static <T extends DexClass> ClassProvider<T> forClassFileResources(
+      ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+    return new ClassFileResourceReader<>(classKind, provider, reader);
+  }
+
+  /** Create class provider for preloaded classes, classes may have conflicting names. */
+  public static <T extends DexClass> ClassProvider<T> forPreloadedClasses(
+      ClassKind classKind, Collection<T> classes) {
+    ImmutableListMultimap.Builder<DexType, T> builder = ImmutableListMultimap.builder();
+    for (T clazz : classes) {
+      builder.put(clazz.type, clazz);
+    }
+    return new PreloadedClassProvider<>(classKind, builder.build());
+  }
+
+  /** Create class provider for preloaded classes. */
+  public static <T extends DexClass> ClassProvider<T> combine(
+      ClassKind classKind, List<ClassProvider<T>> providers) {
+    return new CombinedClassProvider<>(classKind, providers);
+  }
+
+  private static class ClassFileResourceReader<T extends DexClass> extends ClassProvider<T> {
+    private final ClassKind classKind;
+    private final ClassFileResourceProvider provider;
+    private final JarApplicationReader reader;
+
+    private ClassFileResourceReader(
+        ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+      super(classKind);
+      this.classKind = classKind;
+      this.provider = provider;
+      this.reader = reader;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      String descriptor = type.descriptor.toString();
+      Resource resource = provider.getResource(descriptor);
+      if (resource != null) {
+        try (Closer closer = Closer.create()) {
+          JarClassFileReader classReader =
+              new JarClassFileReader(reader, classKind.bridgeConsumer(classConsumer));
+          classReader.read(DEFAULT_DEX_FILENAME, classKind, resource.getStream(closer));
+        } catch (IOException e) {
+          throw new CompilationError("Failed to load class: " + descriptor, e);
+        }
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      List<DexType> types = new ArrayList<>();
+      for (String descriptor : provider.getClassDescriptors()) {
+        types.add(reader.options.itemFactory.createType(descriptor));
+      }
+      return types;
+    }
+
+    @Override
+    public String toString() {
+      return "class-resource-provider(" + provider.toString() + ")";
+    }
+  }
+
+  private static class PreloadedClassProvider<T extends DexClass> extends ClassProvider<T> {
+    private final Multimap<DexType, T> classes;
+
+    private PreloadedClassProvider(ClassKind classKind, Multimap<DexType, T> classes) {
+      super(classKind);
+      this.classes = classes;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      for (T clazz : classes.get(type)) {
+        classConsumer.accept(clazz);
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      return classes.keys();
+    }
+
+    @Override
+    public String toString() {
+      return "preloaded(" + classes.size() + ")";
+    }
+  }
+
+  private static class CombinedClassProvider<T extends DexClass> extends ClassProvider<T> {
+    private final List<ClassProvider<T>> providers;
+
+    private CombinedClassProvider(ClassKind classKind, List<ClassProvider<T>> providers) {
+      super(classKind);
+      this.providers = providers;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      for (ClassProvider<T> provider : providers) {
+        provider.collectClass(type, classConsumer);
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      Set<DexType> types = Sets.newIdentityHashSet();
+      for (ClassProvider<T> provider : providers) {
+        types.addAll(provider.collectTypes());
+      }
+      return types;
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder builder = new StringBuilder();
+      String prefix = "combined(";
+      for (ClassProvider<T> provider : providers) {
+        builder.append(prefix);
+        prefix = ", ";
+        builder.append(provider);
+      }
+      return builder.append(")").toString();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
new file mode 100644
index 0000000..ee00c39
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClasspathClass;
+
+/** Represents a collection of classpath classes. */
+public class ClasspathClassCollection extends ClassMap<DexClasspathClass> {
+  public ClasspathClassCollection(ClassProvider<DexClasspathClass> classProvider) {
+    super(null, classProvider);
+  }
+
+  @Override
+  DexClasspathClass resolveClassConflict(DexClasspathClass a, DexClasspathClass b) {
+    throw new CompilationError("Classpath type already present: " + a.type.toSourceString());
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.CLASSPATH;
+  }
+
+  @Override
+  public String toString() {
+    return "classpath classes: " + super.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
new file mode 100644
index 0000000..c060ead
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
@@ -0,0 +1,76 @@
+// Copyright (c) 2017, 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.utils;
+
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.google.common.collect.Sets;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Lazy resource provider returning class file resources based
+ * on filesystem directory content.
+ */
+public final class DirectoryClassFileProvider implements ClassFileResourceProvider {
+  private final Path root;
+
+  private DirectoryClassFileProvider(Path root) {
+    this.root = root;
+  }
+
+  @Override
+  public Set<String> getClassDescriptors() {
+    HashSet<String> result = Sets.newHashSet();
+    collectClassDescriptors(root, result);
+    return result;
+  }
+
+  private void collectClassDescriptors(Path dir, Set<String> result) {
+    File file = dir.toFile();
+    if (file.exists()) {
+      File[] files = file.listFiles();
+      if (files != null) {
+        for (File child : files) {
+          if (child.isDirectory()) {
+            collectClassDescriptors(child.toPath(), result);
+          } else {
+            String relative = root.relativize(child.toPath()).toString();
+            if (relative.endsWith(CLASS_EXTENSION)) {
+              result.add("L" + relative.substring(
+                  0, relative.length() - CLASS_EXTENSION.length()) + ";");
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public Resource getResource(String descriptor) {
+    assert DescriptorUtils.isClassDescriptor(descriptor);
+
+    // Build expected file path based on type descriptor.
+    String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
+    Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
+    File file = filePath.toFile();
+
+    return (file.exists() && !file.isDirectory())
+        ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
+  }
+
+  /** Create resource provider from directory path. */
+  public static ClassFileResourceProvider fromDirectory(Path dir) {
+    return new DirectoryClassFileProvider(dir.toAbsolutePath());
+  }
+
+  @Override
+  public String toString() {
+    return "directory(" + root + ")";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
deleted file mode 100644
index b003d86..0000000
--- a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2017, 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.utils;
-
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import java.io.File;
-import java.nio.file.Path;
-
-/**
- * Lazy resource provider based on filesystem directory content.
- *
- * NOTE: only handles classfile resources.
- */
-public final class DirectoryResourceProvider implements ResourceProvider {
-  private final Path root;
-
-  private DirectoryResourceProvider(Path root) {
-    this.root = root;
-  }
-
-  @Override
-  public Resource getResource(String descriptor) {
-    assert DescriptorUtils.isClassDescriptor(descriptor);
-
-    // Build expected file path based on type descriptor.
-    String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
-    Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
-    File file = filePath.toFile();
-
-    return (file.exists() && !file.isDirectory())
-        ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
-  }
-
-  /** Create resource provider from directory path. */
-  public static ResourceProvider fromDirectory(Path dir) {
-    return new DirectoryResourceProvider(dir.toAbsolutePath());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java b/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
deleted file mode 100644
index d7379ef..0000000
--- a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (c) 2017, 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.utils;
-
-import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.graph.ClassKind;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.JarApplicationReader;
-import com.android.tools.r8.graph.JarClassFileReader;
-import com.google.common.collect.ImmutableList;
-import com.google.common.io.Closer;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Represents a collection of classes loaded lazily from a set of lazy resource
- * providers. The collection is autonomous, it lazily collects classes but
- * does not choose the classes in cases of conflicts, delaying it until
- * the class is asked for.
- *
- * NOTE: only java class resources are allowed to be lazy loaded.
- */
-public final class LazyClassCollection {
-  // For each type which has ever been queried stores one or several classes loaded
-  // from resources provided by different resource providers. In majority of the
-  // cases there will only be one class per type. We store classes for all the
-  // resource providers and resolve the classes at the time it is queried.
-  //
-  // Must be synchronized on `classes`.
-  private final Map<DexType, DexClass[]> classes = new IdentityHashMap<>();
-
-  // Available lazy resource providers.
-  private final List<ResourceProvider> classpathProviders;
-  private final List<ResourceProvider> libraryProviders;
-
-  // Application reader to be used. Note that the reader may be reused in
-  // many loaders and may be used concurrently, it is considered to be
-  // thread-safe since its only state is internal options which we
-  // consider immutable after they are initialized (barring dex factory
-  // which is thread-safe).
-  private final JarApplicationReader reader;
-
-  public LazyClassCollection(JarApplicationReader reader,
-      List<ResourceProvider> classpathProviders, List<ResourceProvider> libraryProviders) {
-    this.classpathProviders = ImmutableList.copyOf(classpathProviders);
-    this.libraryProviders = ImmutableList.copyOf(libraryProviders);
-    this.reader = reader;
-  }
-
-  /**
-   * Returns a definition for a class or `null` if there is no such class.
-   * Parameter `dominator` represents a class that is considered
-   * to be already loaded, it may be null but if specified it may affect
-   * the conflict resolution. For example non-lazy loaded classpath class
-   * provided as `dominator` will conflict with lazy-loaded classpath classes.
-   */
-  public DexClass get(DexType type, DexClass dominator) {
-    DexClass[] candidates;
-    synchronized (classes) {
-      candidates = classes.get(type);
-    }
-
-    if (candidates == null) {
-      String descriptor = type.descriptor.toString();
-
-      // Loading resources and constructing classes may be time consuming, we do it
-      // outside the global lock so others don't have to wait.
-      List<Resource> classpathResources = collectResources(classpathProviders, descriptor);
-      List<Resource> libraryResources = collectResources(libraryProviders, descriptor);
-
-      candidates = new DexClass[classpathResources.size() + libraryResources.size()];
-
-      // Check if someone else has already added the array for this type.
-      synchronized (classes) {
-        DexClass[] existing = classes.get(type);
-        if (existing != null) {
-          assert candidates.length == existing.length;
-          candidates = existing;
-        } else {
-          classes.put(type, candidates);
-        }
-      }
-
-      if (candidates.length > 0) {
-        // Load classes in synchronized content unique for the type.
-        synchronized (candidates) {
-          // Either all or none of the array classes will be loaded, so we use this
-          // as a criteria for checking if we need to load classes.
-          if (candidates[0] == null) {
-            new ClassLoader(type, candidates, reader, classpathResources, libraryResources).load();
-          }
-        }
-      }
-    }
-
-    // Choose class in case there are conflicts.
-    DexClass candidate = dominator;
-    for (DexClass clazz : candidates) {
-      candidate = (candidate == null) ? clazz
-          : DexApplication.chooseClass(candidate, clazz, /* skipLibDups: */ true);
-    }
-    return candidate;
-  }
-
-  private List<Resource> collectResources(List<ResourceProvider> providers, String descriptor) {
-    List<Resource> resources = new ArrayList<>();
-    for (ResourceProvider provider : providers) {
-      Resource resource = provider.getResource(descriptor);
-      if (resource != null) {
-        resources.add(resource);
-      }
-    }
-    return resources;
-  }
-
-  private static final class ClassLoader {
-    int index = 0;
-    final DexType type;
-    final DexClass[] classes;
-    final JarApplicationReader reader;
-    final List<Resource> classpathResources;
-    final List<Resource> libraryResources;
-
-    ClassLoader(DexType type, DexClass[] classes, JarApplicationReader reader,
-        List<Resource> classpathResources, List<Resource> libraryResources) {
-      this.type = type;
-      this.classes = classes;
-      this.reader = reader;
-      this.classpathResources = classpathResources;
-      this.libraryResources = libraryResources;
-    }
-
-    void addClass(DexClass clazz) {
-      assert index < classes.length;
-      assert clazz != null;
-      if (clazz.type != type) {
-        throw new CompilationError("Class content provided for type descriptor "
-            + type.toSourceString() + " actually defines class " + clazz.type
-            .toSourceString());
-      }
-      classes[index++] = clazz;
-    }
-
-    void load() {
-      try (Closer closer = Closer.create()) {
-        for (Resource resource : classpathResources) {
-          JarClassFileReader classReader = new JarClassFileReader(reader, this::addClass);
-          classReader.read(DEFAULT_DEX_FILENAME, ClassKind.CLASSPATH, resource.getStream(closer));
-        }
-        for (Resource resource : libraryResources) {
-          JarClassFileReader classReader = new JarClassFileReader(reader, this::addClass);
-          classReader.read(DEFAULT_DEX_FILENAME, ClassKind.LIBRARY, resource.getStream(closer));
-        }
-      } catch (IOException e) {
-        throw new CompilationError("Failed to load class: " + type.toSourceString(), e);
-      }
-      assert index == classes.length;
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
new file mode 100644
index 0000000..e11e660
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.logging.Log;
+
+/** Represents a collection of library classes. */
+public class LibraryClassCollection extends ClassMap<DexLibraryClass> {
+  public LibraryClassCollection(ClassProvider<DexLibraryClass> classProvider) {
+    super(null, classProvider);
+  }
+
+  @Override
+  DexLibraryClass resolveClassConflict(DexLibraryClass a, DexLibraryClass b) {
+    if (a.origin != Resource.Kind.CLASSFILE || b.origin != Resource.Kind.CLASSFILE) {
+      // We only support conflicts for classes both coming from jar files.
+      throw new CompilationError(
+          "Library type already present: " + a.type.toSourceString());
+    }
+    if (Log.ENABLED) {
+      Log.warn(DexApplication.class,
+          "Class `%s` was specified twice as a library type.", a.type.toSourceString());
+    }
+    return a;
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.LIBRARY;
+  }
+
+  @Override
+  public String toString() {
+    return "library classes: " + super.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
rename to src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
index 9601ff6..07c136d 100644
--- a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
@@ -7,9 +7,10 @@
 import static com.android.tools.r8.utils.FileUtils.isArchive;
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
+import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
 
 import java.io.File;
@@ -20,23 +21,25 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
 import java.util.zip.ZipInputStream;
 
-/**
- * Lazy resource provider based on preloaded/prebuilt context.
- *
- * NOTE: only handles classfile resources.
- */
-public final class PreloadedResourceProvider implements ResourceProvider {
+/** Lazy Java class file resource provider based on preloaded/prebuilt context. */
+public final class PreloadedClassFileProvider implements ClassFileResourceProvider {
   private final Map<String, byte[]> content;
 
-  private PreloadedResourceProvider(Map<String, byte[]> content) {
+  private PreloadedClassFileProvider(Map<String, byte[]> content) {
     this.content = content;
   }
 
   @Override
+  public Set<String> getClassDescriptors() {
+    return Sets.newHashSet(content.keySet());
+  }
+
+  @Override
   public Resource getResource(String descriptor) {
     byte[] bytes = content.get(descriptor);
     if (bytes == null) {
@@ -46,7 +49,7 @@
   }
 
   /** Create preloaded content resource provider from archive file. */
-  public static ResourceProvider fromArchive(Path archive) throws IOException {
+  public static ClassFileResourceProvider fromArchive(Path archive) throws IOException {
     assert isArchive(archive);
     Builder builder = builder();
     try (ZipInputStream stream = new ZipInputStream(new FileInputStream(archive.toFile()))) {
@@ -85,6 +88,11 @@
     return 'L' + descriptor + ';';
   }
 
+  @Override
+  public String toString() {
+    return content.size() + " preloaded resources";
+  }
+
   /** Create a new empty builder. */
   public static Builder builder() {
     return new Builder();
@@ -105,9 +113,9 @@
       return this;
     }
 
-    public PreloadedResourceProvider build() {
+    public PreloadedClassFileProvider build() {
       assert content != null;
-      PreloadedResourceProvider provider = new PreloadedResourceProvider(content);
+      PreloadedClassFileProvider provider = new PreloadedClassFileProvider(content);
       content = null;
       return provider;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
new file mode 100644
index 0000000..a37eb92
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/** Represents a collection of library classes. */
+public class ProgramClassCollection extends ClassMap<DexProgramClass> {
+  public static ProgramClassCollection create(List<DexProgramClass> classes) {
+    // We have all classes preloaded, but not necessarily without conflicts.
+    IdentityHashMap<DexType, Value<DexProgramClass>> map = new IdentityHashMap<>();
+    for (DexProgramClass clazz : classes) {
+      Value<DexProgramClass> value = map.get(clazz.type);
+      if (value == null) {
+        value = new Value<>(clazz);
+        map.put(clazz.type, value);
+      } else {
+        value.clazz = resolveClassConflictImpl(value.clazz, clazz);
+      }
+    }
+    return new ProgramClassCollection(map);
+  }
+
+  private ProgramClassCollection(IdentityHashMap<DexType, Value<DexProgramClass>> classes) {
+    super(classes, null);
+  }
+
+  @Override
+  public String toString() {
+    return "program classes: " + super.toString();
+  }
+
+  @Override
+  DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b) {
+    return resolveClassConflictImpl(a, b);
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.PROGRAM;
+  }
+
+  private static DexProgramClass resolveClassConflictImpl(DexProgramClass a, DexProgramClass b) {
+    // Currently only allow collapsing synthetic lambda classes.
+    if (a.getOrigin() == Resource.Kind.DEX
+        && b.getOrigin() == Resource.Kind.DEX
+        && a.accessFlags.isSynthetic()
+        && b.accessFlags.isSynthetic()
+        && LambdaRewriter.hasLambdaClassPrefix(a.type)
+        && LambdaRewriter.hasLambdaClassPrefix(b.type)) {
+      return a;
+    }
+    throw new CompilationError("Program type already present: " + a.type.toSourceString());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
index 8449bd9..34cd83c 100644
--- a/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8;
 
-import com.android.tools.r8.utils.DirectoryResourceProvider;
-import com.android.tools.r8.utils.PreloadedResourceProvider;
+import com.android.tools.r8.utils.DirectoryClassFileProvider;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import java.io.IOException;
 import java.nio.file.Path;
 
@@ -25,13 +25,13 @@
 
     private void addClasspathPath(Path location, D8Command.Builder builder) {
       builder.addClasspathResourceProvider(
-          DirectoryResourceProvider.fromDirectory(location.resolve("..")));
+          DirectoryClassFileProvider.fromDirectory(location.resolve("..")));
     }
 
     @Override
     void addLibraryReference(D8Command.Builder builder, Path location) throws IOException {
       builder.addLibraryResourceProvider(
-          PreloadedResourceProvider.fromArchive(location));
+          PreloadedClassFileProvider.fromArchive(location));
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index dc49caf..d05c800 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -287,7 +287,7 @@
         method.setCode(ir, allocator, factory);
         virtualMethods[i] = method;
       }
-      builder.addClass(
+      builder.addProgramClass(
           new DexProgramClass(
               type,
               null,