Search code examples
javaconcurrencyconcurrenthashmapjava-17

ConcurrentHashMap computeIfAbsent tell if first time or not


It's complicated for me to articulate a proper title for this. But an example should make it far simpler. Suppose I have this:

final class Cache {
   private static final ConcurrentHashMap<String, List<String>> CACHE = ...

   static List<String> byName(String name) {
      return CACHE.computeIfAbsent(name, x -> // some expensive operation)
   }

}

The idea is probably trivial, this acts as a LoadingCache, much like guava or caffeine (in reality it is more complicated, but that is irrelevant to the question).

I would like to be able to tell if this was the first load into the CACHE, or it was a read of an existing mapping. Currently, I do this:

final class Cache {
   private static final ConcurrentHashMap<String, List<String>> CACHE = ...

   static List<String> byName(String name) {
      boolean b[] = new boolean[1];
      List<String> result = CACHE.computeIfAbsent(name, x -> {
            b[0] = true;
            // some expensive operation)
      });

      if(b[0]) {
         // first load into the cache, do X
      } else {
         // do Y
      }

      return result;
   }

}

This works, but I am afraid I am missing something that ConcurrentHashMap can offer for me that would allow me to do the same. Thank you.


Solution

  • If you want to avoid your single-element array to pass data out of the lambda (which I would rather do with an AtomicReference or AtomicBoolean), you could use a stateful callback object. It doesn't change the behavior or design of your code, but could be considered a little bit cleaner and more OOP-y.

    class LoadingAction<K, V> {
      private boolean called = false;
    
      public V load(final K key) {
        called = true;
        // load data
        return ...;
      }
    
      public void executePostLoad() {
        if (called) {
          // loaded into cache, do X
        } else {
          // do Y
        }
      }
    }
    
    final class Cache {
       private static final ConcurrentHashMap<String, List<String>> CACHE = new ConcurrentHashMap<>();
    
       static List<String> byName(String name) {
          final LoadingAction<String, List<String>> loader = new LoadingAction<>();
          final List<String> result = CACHE.computeIfAbsent(name, loader::load);
    
          loader.executePostLoad();
    
          return result;
       }
    
    }
    

    Or turn it inside-out:

    class Loader<K, V> {
      private boolean called = false;
    
      public V load(final Map<K, V> map, final K key) {
        final V result = map.computeIfAbsent(key, this::load);
        this.executePostLoad();
        return result;
      }
    
      private V load(final K key) {
        called = true;
        // load data
        return ...;
      }
    
      private void executePostLoad() {
        if (called) {
          // loaded into cache, do X
        } else {
          // do Y
        }
      }
    }
    
    final class Cache {
       private static final ConcurrentHashMap<String, List<String>> CACHE = new ConcurrentHashMap<>();
    
       static List<String> byName(String name) {
          final Loader<String, List<String>> loader = new Loader<>();
          return loader.load(CACHE, name);
       }
    
    }
    

    Construction and loading could be encapsulated in a static method:

    class Loader<K, V> {
      private boolean called = false;
    
      public static <K, V> V load(final Map<K, V> map, final K key) {
          final Loader<K, V> loader = new Loader<>();
          return loader.doLoad(map, key);
      }
    
      private V doLoad(final Map<K, V> map, final K key) {
        final V result = map.computeIfAbsent(key, this::load);
        this.executePostLoad();
        return result;
      }
    
      private V load(final K key) {
        called = true;
        // load data
        return ...;
      }
    
      private void executePostLoad() {
        if (called) {
          // loaded into cache, do X
        } else {
          // do Y
        }
      }
    }
    
    final class Cache {
       private static final ConcurrentHashMap<String, List<String>> CACHE = new ConcurrentHashMap<>();
    
       static List<String> byName(String name) {
          return Loader.load(CACHE, name);
       }
    
    }