0

My app gets some string from web service. It's look like this:

name=Raul&city=Paris&id=167136

I want to get map from this string:

{name=Raul, city=Paris, id=167136}

Code:

Arrays.stream(input.split("&"))
          .map(sub -> sub.split("="))
          .collect(Collectors.toMap(string-> string[0]), string -> string[1]));

It's okay and works in most cases, but app can get a string with duplicate keys, like this:

name=Raul&city=Paris&id=167136&city=Oslo

App will crash with following uncaught exception:

Exception in thread "main" java.lang.IllegalStateException: Duplicate key city (attempted merging values Paris and Oslo)

I tried to change collect method:

.collect(Collectors.toMap(tokens -> tokens[0], tokens -> tokens[1]), (r, strings) -> strings[0]);

But complier says no:

Cannot resolve method 'collect(java.util.stream.Collector<T,capture<?>,java.util.Map<K,U>>, <lambda expression>)'

And Array type expected; found: 'T'

I guess, it's because I have an array. How to fix it?

funnelCONN
  • 149
  • 1
  • 11
  • 2
    Why not use a library to parse query strings? Doing this by hand is a very bad idea as there a hundred corner cases your code misses! For example what about flags (properties without a value) or URL encoded data? – Boris the Spider Nov 12 '20 at 20:25
  • In the specific case though, simply use a custom merge strategy for your collector. – Boris the Spider Nov 12 '20 at 20:26
  • TL;DR: no, it doesn’t work in most cases are fixing this particular parsing issue leaves a multitude of others. – Boris the Spider Nov 12 '20 at 20:27
  • What kind of library to parse query? Can u provide a good example? – funnelCONN Nov 12 '20 at 20:30
  • Does this answer your question? [Parse a URI String into Name-Value Collection](https://stackoverflow.com/questions/13592236/parse-a-uri-string-into-name-value-collection) – Progman Nov 12 '20 at 20:34
  • This is not a real URI actually. It's just string with key/values similar to url query params. – funnelCONN Nov 12 '20 at 20:35
  • @ChokkiAST It doesn't have to be a real URI, check the answers for code example and ideas. – Progman Nov 12 '20 at 20:41

3 Answers3

2

You are misunderstanding the final argument of toMap (the merge operator). When it find a duplicate key it hands the current value in the map and the new value with the same key to the merge operator which produces the single value to store.

For example, if you want to just store the first value found then use (s1, s2) -> s1. If you want to comma separate them, use (s1, s2) -> s1 + ", " + s2.

sprinter
  • 27,148
  • 6
  • 47
  • 78
  • (although note that comma separation suggestion suffers from the string-concat-in-loop antipattern) – Boris the Spider Nov 12 '20 at 20:32
  • And the string-concat-in-loop antipattern suffers from the permature-optimisation antipattern :-) – sprinter Nov 12 '20 at 20:35
  • On that I don’t agree. I think that quote is often misused - and I’m certain that is the case here. Especially being as there is a joining collector specifically to address this common issue. – Boris the Spider Nov 12 '20 at 20:41
  • I agree using a `groupingBy` with a downstream `joining` collector would make more sense in this case. However I disagree that string-concat-in-loop is often an antipattern. I've seen a lot of dumb code over the years that uses `StringBuilder` for theoretical performance improvements with no evidence that it is required in that particular case - but a lot of evidence that it makes the code much less readable. – sprinter Nov 13 '20 at 00:29
  • Just to be clear - you have my upvote, this is the correct answer. We’re arguing over abstract theoreticals - and I totally agree that people can do stupid things with code regardless of how much the tooling tries to help. – Boris the Spider Nov 13 '20 at 21:46
0

If you want to add value of duplicated keys together and group them by key (since app can get a string with duplicate keys), instead of using Collectors.toMap() you can use a Collectors.groupingBy with custom collector (Collector.of(...)) :

String input = "name=Raul&city=Paris&city=Berlin&id=167136&id=03&id=505";

Map<String, Set<Object>> result = Arrays.stream(input.split("&"))
                .map(splitedString -> splitedString.split("="))
                .filter(keyValuePair -> keyValuePair.length() == 2)
                .collect(
                        Collectors.groupingBy(array -> array[0], Collector.of(
                                () -> new HashSet<>(), (set, array) -> set.add(array[1]),
                                (left, right) -> {
                                    if (left.size() < right.size()) {
                                        right.addAll(left);
                                        return right;
                                    } else {
                                        left.addAll(right);
                                        return left;
                                    }
                                }, Collector.Characteristics.UNORDERED)
                        )
                );

This way you'll get :

result => size = 3
 "city" -> size = 2 ["Berlin", "Paris"]
 "name" -> size = 1 ["Raul"]
 "id"   -> size = 3 ["167136","03","505"]
Abdelghani Roussi
  • 2,707
  • 2
  • 21
  • 39
  • Why not use a map collector with a duplicate key function? Isn’t the intent of that more obvious? – Boris the Spider Nov 12 '20 at 21:10
  • @BoristheSpider I prefer `Set` rather than concatenating string like `s1 + "," + s2`, after all it depends on the need of the question author ! – Abdelghani Roussi Nov 12 '20 at 21:22
  • Yes, but a custom collector as you suggest is not required in any of those cases. And the author specifically asked just to take a single value - which is solved trivially with a custom merge function. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function-java.util.function.BinaryOperator- – Boris the Spider Nov 12 '20 at 21:38
  • I didn't said it is required or it is the only solution, it could serve other people that are willing to have this behaviour instead of having a single value – Abdelghani Roussi Nov 13 '20 at 09:17
-1

You can achieve the same result using kotlin collections

val res = message
        .split("&")
        .map {
            val entry = it.split("=")
            Pair(entry[0], entry[1])
        }
println(res)
println(res.toMap()) //distinct by key

The result is

[(name, Raul), (city, Paris), (id, 167136), (city, Oslo)]

{name=Raul, city=Oslo, id=167136}

Vahe Gharibyan
  • 5,277
  • 4
  • 36
  • 47