OldCurmudgeon
OldCurmudgeon

Reputation: 65851

Lambdas and putIfAbsent

I posted an answer here where the code demonstrating use of the putIfAbsent method of ConcurrentMap read:

ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong> ();

public long addTo(String key, long value) {
  // The final value it became.
  long result = value;
  // Make a new one to put in the map.
  AtomicLong newValue = new AtomicLong(value);
  // Insert my new one or get me the old one.
  AtomicLong oldValue = map.putIfAbsent(key, newValue);
  // Was it already there? Note the deliberate use of '!='.
  if ( oldValue != newValue ) {
    // Update it.
    result = oldValue.addAndGet(value);
  }
  return result;
}

The main downside of this approach is that you have to create a new object to put into the map whether it will be used or not. This can have significant effect if the object is heavy.

It occurred to me that this would be an opportunity to use Lambdas. I have not downloaded Java 8 n'or will I be able to until it is official (company policy) so I cannot test this but would something like this be valid and effective?

public long addTo(String key, long value) {
  return map.putIfAbsent( key, () -> new AtomicLong(0) ).addAndGet(value);
}

I am hoping to use the lambda to delay the evaluation of the new AtomicLong(0) until it is actually determined that it should be created because it does not exist in the map.

As you can see this is much more succinct and functional.

Essentially I suppose my questions are:

  1. Will this work?
  2. Or have I completely misinterpreted lambdas?
  3. Might something like this work one day?

Upvotes: 7

Views: 5845

Answers (4)

Tagir Valeev
Tagir Valeev

Reputation: 100269

Note that using Java 8 ConcurrentHashMap it's completely unnecessary to have AtomicLong values. You can safely use ConcurrentHashMap.merge:

ConcurrentMap<String, Long> map = new ConcurrentHashMap<String, Long>();

public long addTo(String key, long value) {
    return map.merge(key, value, Long::sum);
}

It's much simpler and also significantly faster.

Upvotes: 2

Stuart Marks
Stuart Marks

Reputation: 132460

UPDATE 2015-08-01

The computeIfAbsent method as described below has indeed been added to Java SE 8. The semantics appear to be very close to the pre-release version.

In addition, computeIfAbsent, along with a whole pile of new default methods, has been added to the Map interface. Of course, maps in general can't support atomic updates, but the new methods add considerable convenience to the API.


What you're trying to do is quite reasonable, but unfortunately it doesn't work with the current version of ConcurrentMap. An enhancement is on the way, however. The new version of the concurrency library includes ConcurrentHashMapV8 which contains a new method computeIfAbsent. This pretty much allows you to do exactly what you're looking to do. Using this new method, your example could be rewritten as follows:

public long addTo(String key, long value) {
    return map.computeIfAbsent( key, () -> new AtomicLong(0) ).addAndGet(value);
}

For further information about the ConcurrentHashMapV8, see Doug Lea's initial announcement thread on the concurrency-interest mailing list. Several messages down the thread is a followup message that shows an example very similar to what you're trying to do. (Note however the old lambda syntax. That message was from August 2011 after all.) And here is recent javadoc for ConcurrentHashMapV8.

This work is intended to be integrated into Java 8, but it hasn't yet as far as I can see. Also, this is still a work in progress, names and specs may change, etc.

Upvotes: 8

jacobm
jacobm

Reputation: 14035

Unfortunately it's not as easy as that. There are two main problems with the approach you've sketched out: 1. The type of the map would need to change from Map<String, AtomicLong> to Map<String, AtomicLongFunction> (where AtomicLongFunction is some function interface that has a single method that takes no arguments and returns an AtomicLong). 2. When you retrieve the element from the map you'd need to apply the function each time to get the AtomicLong out of it. This would result in creating a new instance each time you retrieve it, which is not likely what you wanted.

The idea of having a map that runs a function on demand to fill up missing values is a good one, though, and in fact Google's Guava library has a map that does exactly that; see their MapMaker. In fact that code would benefit from Java 8 lambda expressions: instead of

   ConcurrentMap<Key, Graph> graphs = new MapMaker()
       .concurrencyLevel(4)
       .weakKeys()
       .makeComputingMap(
           new Function<Key, Graph>() {
             public Graph apply(Key key) {
               return createExpensiveGraph(key);
             }
           });

you'd be able to write

   ConcurrentMap<Key, Graph> graphs = new MapMaker()
       .concurrencyLevel(4)
       .weakKeys()
       .makeComputingMap((Key key) -> createExpensiveGraph(key));

or

   ConcurrentMap<Key, Graph> graphs = new MapMaker()
       .concurrencyLevel(4)
       .weakKeys()
       .makeComputingMap(this::createExpensiveGraph);

Upvotes: 2

Blank Chisui
Blank Chisui

Reputation: 1073

AtomicLong is not really a heavy object. For heavier objects I would consider a lazy proxy and provide a lambda to that one to create the object if needed.

class MyObject{
    void doSomething(){}
}

class MyLazyObject extends MyObject{
    Funktion create;
    MyLazyObject(Funktion create){
        this.create = create;
    }
    MyObject instance;
    MyObject getInstance(){
        if(instance == null)
            instance = create.apply();
        return instance;
    }
    @Override void doSomething(){getInstance().doSomething();}
}

public long addTo(String key, long value) {
  return map.putIfAbsent( key, new MyLazyObject( () -> new MyObject(0) ) );
}

Upvotes: 2

Related Questions