28

Suppose I have a map of given name, surname pairs and I want to find the given name of the first entry in that map that has the surname matching a certain value. How would we do this in a java 8 fashion.

In my test case example below I put two ways that would do it.

However the first one (looking for the given name of the first person with a surname of "Donkey") will throw java.util.NoSuchElementException: No value present so it is not safe.

The second one works but it is not only harder to read but it it is a bit not quite functional.

Just wondering if someone here would suggest me an easier clearer way of achieving this using either stream() or forEach() or both.

@Test
public void shouldBeAbleToReturnTheKeyOfTheFirstMatchingValue() throws Exception {
    Map<String, String> names = new LinkedHashMap<>();
    names.put("John", "Doe");
    names.put("Fred", "Flintstone");
    names.put("Jane", "Doe");
    String keyOfTheFirst = names.entrySet().stream().filter(e -> e.getValue().equals("Doe")).findFirst().get().getKey();
    assertEquals("John", keyOfTheFirst);

    try {
        names.entrySet().stream().filter(e -> e.getValue().equals("Donkey")).findFirst().get();
    } catch (NoSuchElementException e){
        // Expected
    }

    Optional<Map.Entry<String, String>> optionalEntry = names.entrySet().stream().filter(e -> e.getValue().equals("Donkey")).findFirst();
    keyOfTheFirst = optionalEntry.isPresent() ? optionalEntry.get().getKey() : null;

    assertNull(keyOfTheFirst);
}

Thank you in advance.

Misha
  • 27,433
  • 6
  • 62
  • 78
Julian
  • 3,678
  • 7
  • 40
  • 72
  • 1
    Obviously using a bi-directional map would be more efficient. – Peter Lawrey Jun 02 '15 at 23:52
  • Hi patryk. I totally agree with you. This is why I posted the question. Are you able to provide a solution? – Julian Jun 03 '15 at 01:04
  • To me, some of the smell comes from the use of `null`. The answers posted have all provided `null` since that's what you asked for. But since you've bought into `Optional`, why use `null` at all? Make `keyOfTheFirst` be `Optional` and do `assertTrue(keyOfTheFirst.isEmpty())` or whatever. – Stuart Marks Jun 03 '15 at 03:30
  • 1
    @Stuart. Of course using the `java.util.Optional` would be the natural way but this was not what I was after. What I really wanted was to have a pure functional solution without the need for any extra checks/iterations etc. It was more of a theoretical question **"Can java 8 do it?"** and because I could not find the answer myself I posted it here rather than concluding it could not. The unit test was just something to help the posters easily exercise their solution and nothing else. – Julian Jun 04 '15 at 03:29
  • 1
    @Julian Fair enough. I was responding to the comment above, "This code smells. Poor design." The comment may be true, but it isn't particularly helpful. Although it's not directly related to the issue you were after, one issue I did perceive with the code was its use of `null`, so I suggested an alternative that avoids `null`. – Stuart Marks Jun 04 '15 at 18:43

6 Answers6

79

To return a default value if there is no match, use Optional#orElse

names.entrySet().stream()
  .filter(e -> e.getValue().equals("Donkey"))
  .map(Map.Entry::getKey)
  .findFirst()
  .orElse(null);
Misha
  • 27,433
  • 6
  • 62
  • 78
  • 1
    Thanks Misha and Doon. Both your answers helped me to figure out the solution I wanted. I am choosing Misha's as being the closest to it. To return a `java.lang.String` but not an `java.util.Optional` all I had to do was to invoke `orElseGet(new Supplier() { @Override public String get() { return null; } })` – Julian Jun 03 '15 at 01:55
  • 3
    If you really wish to use `orElseGet` instead of `orElse`, you can use a lambda expression instead of the anonymous class: `.orElseGet(() -> null)`. – Misha Jun 03 '15 at 02:01
  • Thanks Misha. This is even better and was exactly what I was after: a pure functional solution to this problem. **Java 8 rocks!** – Julian Jun 03 '15 at 03:11
  • 1
    With [this helper](http://stackoverflow.com/a/29254246/2711488), it would be `PairStream.from(names).filterValue("Donkey"::equals).keys().findFirst()`… – Holger Jun 03 '15 at 07:45
  • in some cases `.orElse(null)` can be changed to `.orElseGet(() -> Iterables.getLast(yourCollection))` – Augustas Oct 17 '16 at 13:32
  • What if it is a variable x instead of "Donkey"? – fastcodejava Jun 19 '17 at 21:46
1

From a similar question:

public static <T, E> Set<T> getKeysByValue(Map<T, E> map, E value) {
    return map.entrySet()
              .stream()
              .filter(entry -> Objects.equals(entry.getValue(), value))
              .map(Map.Entry::getKey)
              .collect(Collectors.toSet());
}

Then you can select the first, if you want to. Remember that the key is unique, the value is not.

Edit: The whole code (thanks @Peter Lawrey)

package test;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public class Main {

    public static void main(String[] args) {
        Map<String, String> names = new LinkedHashMap<>();
        names.put("John", "Doe");
        names.put("Fred", "Flintstone");
        names.put("Jane", "Doe");

        Optional<String> firstKey = names.entrySet().stream()
                .filter(entry -> Objects.equals(entry.getValue(), "Doe"))
                .map(Map.Entry::getKey).findFirst();

        if (firstKey.isPresent()) {
            System.out.println(firstKey.get());
        }
    }
}
Community
  • 1
  • 1
Doon
  • 3,339
  • 5
  • 25
  • 31
  • 2
    Instead of `collect` you can use `findAny` to get the first one found. – Peter Lawrey Jun 02 '15 at 23:52
  • @PeterLawrey Well reminded! But it would be `findFirst`, wouldn't it? – Doon Jun 02 '15 at 23:54
  • 1
    It would be first unless you use parallelStream. – Peter Lawrey Jun 02 '15 at 23:57
  • But this is similar with my second solution. You will still have to iterate to the collected set or to check if the optional value returned by the findFirst is present in order to get the actual key. I am after something happening inside the lambda that would return the key I want if any or a null value otherwise. – Julian Jun 03 '15 at 00:06
  • @Julian I'm afraid you can't go further. You have no guarantee that you'll always find a key. – Doon Jun 03 '15 at 00:10
  • @Doon This will do it but it is still not nice you have to use an array: `final String[] keyOfTheFirst = new String[1]; names.entrySet().stream().filter(e -> e.getValue().equals("Doe")).map(Map.Entry::getKey) .findFirst().ifPresent(new Consumer() { @Override public void accept(String s) { keyOfTheFirst[0] = s; } });` – Julian Jun 03 '15 at 01:08
1

The solution provided by @Misha is the best one if you don't want to use the third-party code. My library has the special shortcut method ofKeys for such cases as I discovered that it's quite common task:

StreamEx.ofKeys(names, "Donkey"::equals).findFirst().orElse(null);
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • Won't that filter for a key named donkey, rather than the key corresponding to a value named donkey? – flup Nov 18 '15 at 10:32
  • @flup, no, it's filter for the key corresponding to a value named `"Donkey"`. To filter a key you can use usual filter method like `StreamEx.ofKeys(names).filter("Donkey"::equals)`. – Tagir Valeev Nov 18 '15 at 10:45
0

I like old fashioned:

static <K, V> K findFirstKeyByValue(Map<K, V> map, String value) {
    for (Entry<K, V> e : map.entrySet())
        if (e.getValue().equals(value))
            return e.getKey();
    return null;
}
0

Below is my code snippet to get key from map,

Map<String,String> pageDetails = new HashMap<String,String>();

public String getAssociatedKey(){
pageDetails.entrySet().stream().filter( e -> e.getValue().contains("John").findFirst().get().getKey();
}
Pavan T
  • 716
  • 9
  • 12
0

In order to avoid null pointer exception if map entry exist with null value:

private String getValueByKey(Map<String, String> map, String key)
{
        return map.entrySet().stream().filter(e -> 
        StringUtils.equalsIgnoreCase(e.getKey(), key)).findFirst().get()
                .getValue();
}