157

I very much want to use Map.computeIfAbsent but it has been too long since lambdas in undergrad.

Almost directly from the docs: it gives an example of the old way to do things:

Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
String key = "snoop";
if (whoLetDogsOut.get(key) == null) {
  Boolean isLetOut = tryToLetOut(key);
  if (isLetOut != null)
    map.putIfAbsent(key, isLetOut);
}

And the new way:

map.computeIfAbsent(key, k -> new Value(f(k)));

But in their example, I think I'm not quite "getting it." How would I transform the code to use the new lambda way of expressing this?

kasoban
  • 2,107
  • 17
  • 24
Benjamin H
  • 5,164
  • 6
  • 34
  • 42
  • I'm not sure what you don't understand from the example there? – Louis Wasserman Oct 09 '13 at 17:12
  • 2
    What is "k"? Is it a variable being defined? How about "new Value" - is that something from java 8, or representing an object I need to define or override? whoLetDogsOut.computeIfAbsent(key, k -> new Boolean(tryToLetOut(k))) doesn't compile, so I'm missing something... – Benjamin H Oct 09 '13 at 17:18
  • What exactly doesn't compile? What error does it produce? – axtavt Oct 09 '13 at 17:31
  • Temp.java:26: error: illegal start of expression whoLetDogsOut.computeIfAbsent(key, k -> new Boolean(tryToLetOut(k))); (pointing to the ">") – Benjamin H Oct 09 '13 at 17:48
  • Compiles fine for me. Make sure that you really use Java 8 compiler. Do other Java 8 features work? – axtavt Oct 09 '13 at 18:01
  • I *thought* I was: javac -version = javac 1.8.0-ea And netbeans had source set to 1.8 – Benjamin H Oct 09 '13 at 18:30

5 Answers5

143

Recently I was playing with this method too. I wrote a memoized algorithm to calcualte Fibonacci numbers which could serve as another illustration on how to use the method.

We can start by defining a map and putting the values in it for the base cases, namely, fibonnaci(0) and fibonacci(1):

private static Map<Integer,Long> memo = new HashMap<>();
static {
   memo.put(0,0L); //fibonacci(0)
   memo.put(1,1L); //fibonacci(1)
}

And for the inductive step all we have to do is redefine our Fibonacci function as follows:

public static long fibonacci(int x) {
   return memo.computeIfAbsent(x, n -> fibonacci(n-2) + fibonacci(n-1));
}

As you can see, the method computeIfAbsent will use the provided lambda expression to calculate the Fibonacci number when the number is not present in the map. This represents a significant improvement over the traditional, tree recursive algorithm.

Edwin Dalorzo
  • 76,803
  • 25
  • 144
  • 205
  • 33
    Nice, single-line conversion to dynamic programming. Very slick. – Benjamin H Oct 10 '13 at 16:32
  • 5
    You may get fewer recursive calls if you have the (n-2) call first? – Thorbjørn Ravn Andersen Apr 21 '16 at 09:02
  • 13
    You should be more cautious when you use computeIfAbsent recursively. For more details please check http://stackoverflow.com/questions/28840047/recursive-concurrenthashmap-computeifabsent-call-never-terminates-bug-or-fea – Ajit Kumar Apr 27 '16 at 23:43
  • is it still possible to set a cache lifetime? – Hernán Eche Aug 03 '16 at 14:56
  • 15
    This code results in `HashMap`'s internals being corrupted, just as in https://bugs.openjdk.java.net/browse/JDK-8172951 and will fail with `ConcurrentModificationException` in Java 9 (https://bugs.openjdk.java.net/browse/JDK-8071667) – Piotr Findeisen May 22 '17 at 07:27
  • 43
    The docs literally say that *the mapping function should not modify this map during computation*, so this answer is clearly wrong. – fps Jan 24 '18 at 14:08
  • @fps, Sorry, just trying to learn / understand. How is this modifying the map? Isn't it just adding value to a new key in the map which I assume is the purpose of computeIfAbsent? – Punter Vicky Dec 24 '22 at 00:06
  • 1
    @PunterVicky When you call `fibonaci(n-2)` and `fibonaci(n-1)` recursively, these calls result in new calls to `computeIfAbsent`, which internally modify the map, thus violating its contract – fps Dec 26 '22 at 15:50
122

Suppose you have the following code:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] s) {
        Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
    }
    static boolean f(String s) {
        System.out.println("creating a value for \""+s+'"');
        return s.isEmpty();
    }
}

Then you will see the message creating a value for "snoop" exactly once as on the second invocation of computeIfAbsent there is already a value for that key. The k in the lambda expression k -> f(k) is just a placeolder (parameter) for the key which the map will pass to your lambda for computing the value. So in the example the key is passed to the function invocation.

Alternatively you could write: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty()); to achieve the same result without a helper method (but you won’t see the debugging output then). And even simpler, as it is a simple delegation to an existing method you could write: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty); This delegation does not need any parameters to be written.

To be closer to the example in your question, you could write it as whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key)); (it doesn’t matter whether you name the parameter k or key). Or write it as whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut); if tryToLetOut is static or whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut); if tryToLetOut is an instance method.

Holger
  • 285,553
  • 42
  • 434
  • 765
56

Another example. When building a complex map of maps, the computeIfAbsent() method is a replacement for map's get() method. Through chaining of computeIfAbsent() calls together, missing containers are constructed on-the-fly by provided lambda expressions:

  // Stores regional movie ratings
  Map<String, Map<Integer, Set<String>>> regionalMovieRatings = new TreeMap<>();

  // This will throw NullPointerException!
  regionalMovieRatings.get("New York").get(5).add("Boyhood");

  // This will work
  regionalMovieRatings
    .computeIfAbsent("New York", region -> new TreeMap<>())
    .computeIfAbsent(5, rating -> new TreeSet<>())
    .add("Boyhood");
hexabc
  • 557
  • 4
  • 5
40

multi-map

This is really helpful if you want to create a multimap without resorting to the Google Guava library for its implementation of MultiMap.

For example, suppose you want to store a list of students who enrolled for a particular subject.

The normal solution for this using JDK library is:

Map<String,List<String>> studentListSubjectWise = new TreeMap<>();
List<String>lis = studentListSubjectWise.get("a");
if(lis == null) {
    lis = new ArrayList<>();
}
lis.add("John");

//continue....

Since it have some boilerplate code, people tend to use Guava Mutltimap.

Using Map.computeIfAbsent, we can write in a single line without guava Multimap as follows.

studentListSubjectWise.computeIfAbsent("a", (x -> new ArrayList<>())).add("John");

Stuart Marks & Brian Goetz did a good talk about this https://www.youtube.com/watch?v=9uTVXxJjuco

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
nantitv
  • 3,539
  • 4
  • 38
  • 61
  • Another way to make a multimap in Java 8 (and more concise) is to just do `studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());` This produces a multi-map of type `Map` in JDK only more concisely imho. – Zombies Dec 18 '19 at 17:35
  • Adding to the answer; the part about `computeIfAbsent` starts at 25:30 – alegria Jul 05 '21 at 12:18
0

Came up with this comparison example (old vs new) which demonstrates both the approaches;

static Map<String, Set<String>> playerSkills = new HashMap<>();
public static void main(String[] args) {
    //desired output
    //player1, cricket, baseball
    //player2, swimming

    //old way
    add("Player1","cricket");
    add("Player2","swimming");
    add("Player1","baseball");
    
    System.out.println(playerSkills);

    //clear
    playerSkills.clear();
    
    //new
    addNew("Player1","cricket");
    addNew("Player2","swimming");
    addNew("Player1","baseball");
    System.out.println(playerSkills);
    
}

private static void add(String name, String skill) {
    Set<String> skills = playerSkills.get(name);
    if(skills==null) {
        skills= new HashSet<>();
        playerSkills.put(name, skills);
    }
    skills.add(skill);
}

private static void addNew(String name, String skill) {
    playerSkills
            .computeIfAbsent(name, set -> new HashSet<>())
            .add(skill);
}
Nrj
  • 6,723
  • 7
  • 46
  • 58