I have the following code:
private Map<String, Object> flatten(Map<String, Object> source) {
Map<String, Object> result = new LinkedHashMap<>();
source.forEach((k, v) -> {
if (v instanceof Map) {
Map<String, Object> subMap = flatten((Map<String, Object>) v);
for (String subkey : subMap.keySet()) {
result.put(k + "." + subkey, subMap.get(subkey));
}
} else result.put(k, v);
});
return result;
}
The above code flattens a given Map
Ex:
{
"hello": {
"hi": {
"hola": 1
}
}
}
to
{
"hello.hi.hola": 1
}
I would like to use Java8 streams and implement the same logic, How can I do that?
I was going to post something similar to #John Bollinger's answer. Instead I'll offer a somewhat cleaner version using StreamEx:
static Map<String, Object> flatten(Map<?, ?> map) {
return flatten("", map)
.mapKeys(k -> k.substring(1))
.toMap();
}
static EntryStream<String, Object> flatten(String key, Object value) {
if (value instanceof Map) {
return EntryStream.of((Map<?, ?>)value)
.flatMapKeyValue((k, v) -> flatten(key + "." + k, v))
.chain(EntryStream::of);
} else {
return EntryStream.of(key, value);
}
}
Nope, the Stream API has not been designed to go with the recursion well. There are not much possibilities to "enhance" your solution using Stream API nor refactor. From the definition of Stream API from its package info is obvious that their usage has to be non-interfering, stateless and without side-effects - thus I can't imagine how you would achieve this using this API.
I find the only possible usage amending the inner block of the if-condition which brings no real benefit:
if (v instanceof Map) {
Map<String, Object> subMap = flatten((Map<String, Object>) v);
subMap.keySet().stream().forEach(subkey -> result.put(k + "." + subkey, subMap.get(subkey)));
} else result.put(k, v);
The task you present is not well suited to implementation via streams. What you already have is concise and clean. Recursion is a particular problem because lambdas cannot refer to themselves or directly recurse.
However, you don't have to express stream operations in terms of lambdas. You can express them in terms of instances of ordinary classes, too, and such instances can recurse. Thus, you might rewrite your flatten() method like so:
private Map<String, Object> flatten(Map<String, Object> source) {
Function<Entry<?, ?>, Stream<Entry<String, Object>>> flattener
= new Function<Entry<?, ?>, Stream<Entry<String, Object>>>() {
public Stream<Entry<String, Object>> apply(Entry<?, ?> e) {
String k = e.getKey().toString();
Object v = e.getValue();
if (v instanceof Map) {
return ((Map<?, ?>) v).entrySet()
.stream()
.flatMap(this)
.map(n -> new SimpleEntry<>(k + "." + n.getKey(), n.getValue()));
} else {
return Stream.of(new SimpleEntry<>(k, v));
}
}
};
return source.entrySet()
.stream()
.flatMap(flattener)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
The main body of that, at the end, is a pretty simple stream pipeline. The magic happens in the flatMap() step, with the provided flattening Function. It, too, is written in terms of stream operations, and it recurses by passing itself to its own invocation of Stream.flatMap().
But I cannot imagine why anyone would prefer this mess to the code you started with.
Related
Basically I have a List<Map<String, Object>> listOfValueand I need to check if the object is instance of byte then encode it to String as shown below:
private void convertByteToBase64(List<Map<String, Object>> listOfValue) {
Object value = null;
if (!CollectionUtils.isEmpty(listOfValue)) {
for (Map<String, Object> map : listOfValue) {
if (!map.isEmpty()) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
value = entry.getValue();
if (value instanceof byte[]) {
entry.setValue(Base64.getEncoder().encodeToString((byte[]) value));
}
}
}
}
}
}
I am using java 8 and it is working as expected but is it the correct way of doing it or any better way in term of performance?
Current implementation seems to be ok, however, checking for emptiness of the list and the nested maps seems to be redundant.
Some performance improvement may be possibly achieved if parallel streams are used to iterate the list/maps.
private void convertByteToBase64(List<Map<String, Object>> listOfValue) {
Base64.Encoder base64encoder = Base64.getEncoder();
listOfValue.parallelStream()
.flatMap(map -> map.entrySet().parallelStream())
.filter(entry -> entry.getValue() instanceof byte[])
.forEach(entry -> entry.setValue(
base64encoder.encodeToString((byte[]) entry.getValue())
));
}
Base64.Encoder is thread-safe: Instances of Base64.Encoder class are safe for use by multiple concurrent threads..
Alternatively, you can use parallelStream and filter as below.
private void convertByteToBase64(List<Map<String, Object>> listOfValue) {
listOfValue.parallelStream().forEach(map -> {
map.entrySet().parallelStream().filter(entry->entry.getValue() instanceof byte[]).forEach(entry -> {
entry.setValue(Base64.getEncoder().encodeToString((byte[]) entry.getValue()));
});
});`
}
public static void convertByteToBase64(List<Map<String, Object>> listOfValue) {
listOfValue.stream().parallel()
.forEach(map -> map.forEach((key,value)->{
if(value instanceof byte[]) {
map.put(key, Base64.getEncoder().encodeToString((byte[])value)) ;
}
}));
}
I have through various reasons ended up in a situation where I need to deserialize a json object from Fable F# in android studio with java.
The string is as follows:
{"MainForm":{"OriginMerd":{"id":"onMggagerd","number":5,"tests":{"Blod":{"blodid":"1","glucose":52}}}}}
the code:
Stream<Map.Entry<String, String>> flatten(Map<String, Object> map)
{
return map.entrySet()
.stream()
.flatMap(this::extractValue);
}
Stream<Map.Entry<String, String>> extractValue(Map.Entry<String, Object> entry) {
if (entry.getValue() instanceof String) {
return Stream.of(new AbstractMap.SimpleEntry(entry.getKey(), (String) entry.getValue()));
} else if (entry.getValue() instanceof Map) {
return flatten((Map<String, Object>) entry.getValue());
}
return null;
}
#ReactMethod
public void testFunc(String jsonString, Callback cb){
Map<String,Object> map = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
try {
//convert JSON string to Map
map = mapper.readValue(String.valueOf(jsonString), new TypeReference<Map<String, Object>>() {
});
Map<String, String> flattenedMap = flatten(map)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
for (Map.Entry<String, String> entry : flattenedMap.entrySet()) {
Log.e("flatmap",entry.getKey() + "/" + entry.getValue());
//System.out.println(entry.getKey() + "/" + entry.getValue());
}
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Log.e("JSONSTRING", jsonString);
cb.invoke("OK");
}
First I figured I'd make it into a map with object mapper as such I used the object mapper to get a map of then I followed this approach
How to Flatten a HashMap?
However the issue with this is that the result only gives me the orginMerd id and the blodid, not the number or glucose fields. Is there a elegant way to achieve this? I am unfortunately not very well versed in Java.
Paste the json you need to deserialize here. Select source-type JSON, deselect 'allow additional properties', input your package name and your class name. It's gonna generate Java classes (source code, not the compiled .class files) for you that fit your json.
Download generated sources, put them into your project and then just do: objectMapper.readValue(string, YourClassName.class);. YourClassName is the class name you input into the site (not MainForm class, be careful, I fell into that trap while testing this just now).
In your current version you are only allowing String values. You should change that to allow other simple types. To determine that you can use this method:
private static boolean isSimpleType(Class<?> type) {
return type.isPrimitive() ||
Boolean.class == type ||
Character.class == type ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Enum.class.isAssignableFrom(type);
}
Or look here for more details on how to determine if a class is simple. You also can simply adjust that methods to fit your needs.
With this you can use the following to flatten your map:
public static Stream<Map.Entry<String, Object>> flatten(Map<String, Object> map) {
return map.entrySet()
.stream()
.flatMap(YourClass::extractValue);
}
private static Stream<Map.Entry<String, Object>> extractValue(Map.Entry<String, Object> entry) {
if (isSimpleType(entry.getValue().getClass())) {
return Stream.of(new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue()));
} else if (entry.getValue() instanceof Map) {
return flatten((Map<String, Object>) entry.getValue());
}
return null;
}
And call it like this as before:
Map<String, Object> flattenedMap = flatten(map)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
flattenedMap.forEach((key, value) -> System.out.println(key + ": " + value));
You handle only the cases for values of type string or map of your json :
if (entry.getValue() instanceof String) {
return Stream.of(new AbstractMap.SimpleEntry(entry.getKey(), (String) entry.getValue()));
} else if (entry.getValue() instanceof Map) {
return flatten((Map<String, Object>) entry.getValue());
}
To get number or glucose entries, you should also handle the Number type with Long or Integer according to what you get from the JSON deserialization.
The problem with this approach is that Integer and Long are not String and actually you map the json entries to Stream<Map.Entry<String, String>>.
Putting numbers in String variables is possible with toString() :
For example to handle both number and string :
final String value = entry.getValue();
if (value instanceof String || value instanceof Number) {
return Stream.of(new AbstractMap.SimpleEntry(entry.getKey(),
value.toString()));
}
else if (value instanceof Map) {
return flatten((Map<String, Object>) value);
}
// you may return null but you will at least log something
else {
LOGGER.warn("field with key {} and value {} not mapped", entry.getKey(), value);
return null;
}
But it will also make the content of your Map unclear requiring some checks before using it.
Similarly using a generic type like Stream<Map.Entry<String, Object>> will create a similar issue.
So I think that you could consider using a specific class to represent your model and use it during deserialization.
Foo foo = mapper.readValue(String.valueOf(jsonString), Foo.class);
where Foo is a class describing the expected structure.
I am trying to modify a Map's keys based on conditional logic and struggling. I'm new to Java 8 streams API. Let's say I have a map like this:
Map<String, String> map = new HashMap<>();
map.put("PLACEHOLDER", "some_data1");
map.put("Google", "some_data2");
map.put("Facebook", "some_data3");
map.put("Microsoft", "some_data4");
When I would like to do is find the references of PLACEHOLDER and conditionally change that key to something else based on a boolean condition. I feel like it should be something like the below, but this doesn't even compile of course.
boolean condition = foo();
map = map.entrySet().stream().filter(entry -> "PLACEHOLDER".equals(entry.getKey()))
.map(key -> {
if (condition) {
return "Apple";
} else {
return "Netflix";
}
}).collect(Collectors.toMap(e -> e.getKey(), Map.Entry::getValue));
I found this question which kind of makes me think maybe I can't do this with Java 8 stream APIs. Hopefully someone better at this than me knows how to do this. Ideone link if you want to play with it.
You've filtered out all elements that aren't PLACEHOLDER. You need to add that filter logic to your map operation:
final Map<String, String> output = input.entrySet().stream()
.map(e -> {
if (!e.getKey().equals("PLACEHOLDER")) {
return e;
}
if (condition) {
return new AbstractMap.SimpleImmutableEntry<>("Apple", e.getValue());
}
return new AbstractMap.SimpleImmutableEntry<>("Netflix", e.getValue());
}).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
But as you are guaranteed to only have a single instance of PLACEHOLDER in the Map, you can just do
String placeholderData = input.remove("PLACEHOLDER");
if (placeholderData != null) {
input.put(condition ? "Apple" : "Netflix", placeholderData);
}
If you really want to do it using Streams, you just need to move the conditional logic to the collection phase, like that:
boolean condition = true;
map.entrySet().stream().collect(Collectors.toMap(
entry -> mapKey(entry.getKey(), condition), Map.Entry::getValue
));
where:
private static String mapKey(String key, boolean condition) {
if (!"PLACEHOLDER".equals(key)) {
return key;
}
if (condition) {
return "Apple";
} else {
return "Netflix";
}
}
However, the second part of Boris the Spider's answer using Map.remove and Map.put seems the best way to go.
I have a nested HashMap<String,Object> and I want to create a HashMap<String,String> by flattening the Hashmap. I have tried the solution from Recursively Flatten values of nested maps in Java 8. But I am unable to use the class FlatMap as mentioned in the answer.
I have also tried the solution in the question itself, still I am missing something. Then I found a similar use case and came up with the following solution. But it seems like I am missing something as a parameter for the lambda function flatMap .
public static void main(String[] args) {
Map<String,Object> stringObjectMap= new HashMap<String,Object>();
stringObjectMap.put("key1","value1");
stringObjectMap.put("key2","value2");
Map<String,Object> innerStringObjectMap = new HashMap<>();
innerStringObjectMap.put("i1key3","value3");
innerStringObjectMap.put("i1key4","value4");
innerStringObjectMap.put("i1key5","value5");
stringObjectMap.put("map1",innerStringObjectMap);
Map<String,Object> innerStringObjectMap2 = new HashMap<>();
innerStringObjectMap.put("i2key6","value6");
innerStringObjectMap2.put("i2key7","value7");
innerStringObjectMap.put("i1map2",innerStringObjectMap2);
Map<String,Object> collect =
stringObjectMap.entrySet().stream()
.map(x -> x.getValue())
.flatMap(x -> x) //I aint sure what should be give here
.distinct(); //there was a collect as List which i removed.
//collect.forEach(x -> System.out.println(x));
}
What is a better solution for flattening a nested map? I am not just interested in the values, but also the keys in the map. That is the reason why I decided to flatten the map to get another map (I am not sure if this is even possible)
EDIT - Expected Output
key1 - value1
key2-value2
map1 ="" //this is something i will get later for my purpose
i1key3=value3
.
.
i1map2=""
.
.
i2key7=value7
I modified the class from the mentioned answer according to your needs:
public class FlatMap {
public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
if (e.getValue() instanceof Map<?, ?>) {
return Stream.concat(Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), "")),
((Map<?, ?>) e.getValue()).entrySet().stream().flatMap(FlatMap::flatten));
}
return Stream.of(e);
}
}
Usage:
Map<?, ?> collect = stringObjectMap.entrySet()
.stream()
.flatMap(FlatMap::flatten)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(u, v) -> throw new IllegalStateException(String.format("Duplicate key %s", u)),
LinkedHashMap::new));
Attention:
Be sure to use the provided collect with a LinkedHashMap, otherwise the order will be screwed up.
I have used the function from https://stackoverflow.com/a/48578105/5243291. But I used the function in a different way.
Map<Object, Object> collect = new HashMap<>();
stringObjectMap.entrySet()
.stream()
.flatMap(FlatMap::flatten).forEach(it -> {
collect.put(it.getKey(), it.getValue());
});
Function again
public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
if (e.getValue() instanceof Map<?, ?>) {
return Stream.concat(Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), "")),
((Map<?, ?>) e.getValue()).entrySet().stream().flatMap(FlatMap::flatten));
}
return Stream.of(e);
}
I have a list of map objects. These map objects have properties/keys like id, condition_1, condition_2 etc. A sample map looks like so,
List<Map<String, Object>> allItems = Lists.newArrayList();
Map<String, Object> paramsMap = Maps.newHashMap();
paramsMap.put("id", "a");
paramsMap.put("condition_1", false);
paramsMap.put("condition_2", true);
paramsMap.put("condition_3", false);
allItems.add(paramsMap);
So, I need to filter the allItems object such that it has only those map objects which have condition_1 = true & condition_2 = false, & so on & so forth.
I thought about using apache commons CollectionUtils.filter but that doesn't seem to solve my problem because I have no way of specifying map entries as filter conditions.
I am not averse to using Google Guava as well, but I was unable to find a good solution.
Basically I am trying to mimic the _.where functionality found in the excellent JavaScript library underscore.js.
One Guava solution:
Iterables.filter(allItems, new Predicate<Map<String, Object>>() {
#Override public boolean apply(Map<String, Object> map) {
return Boolean.TRUE.equals(map.get("condition_1"))
&& Boolean.FALSE.equals(map.get("condition_2"));
}
});
I think you will have to do this the long way. Unfortunately Java Maps are not of type Iterable which means most common libraries don't have filter functions for them. Try something like this (I believe strings are implicitly true in java in case you are worried about your first key-value pair):
```
boolean result = true;
boolean test = true;
List<Map<String, Object> resultList;
for(Map<String, Object> map : allitems) {
for(String key : map.keySet()) {
result = result && test && map.get(key);
test = !test;
}
if(result) {
resultList.add(map);
}
}
return resultList;
```
Another possibility is to turn your Map into a List of KeyValues and using apache mapping and list functions. Most likely, while you are using java 7, you are not going to get that pretty one liner. Hope this helped.
I think the best solution is to actually do the same what Kotlin does in method Map<K, V>.filter.
Let's create predicate:
public interface Predicate<T> {
boolean apply(T t);
}
Predicate is an interface useful for functional-like programming. Method apply returns true when a condition is fulfilled.
Then, create class like CollectionUtils and put a static method there:
public static <K, V> Map<K, V> filter(
#NonNull Map<K, V> collection,
#NonNull Predicate<Map.Entry<K, V>> predicate
) {
Map<K, V> result = new HashMap<>();
for (Map.Entry<K, V> entry : collection.entrySet()) {
if (predicate.apply(entry) {
result.put(entry.getKey(), entry.getValue();
}
}
return result;
}
This way we can use this method in following way:
Map<String, Object> map = ...;
Map<String, Object> filtered = CollectionUtils.filter(map, new Predicate<Map.Entry<String, Object>>() {
#Override
public boolean apply(Map.Entry<String, Object> entry) {
return isEntryOK(entry);
}
};
If you in fact can use Java 8, but because of some reasons you cannot use Java's streams (like Android Development supporting older version of Android not compatible with Java 8) you can remove syntax's sugger and write it in better form:
Map<String, Object> map = ...;
Map<String, Object> filtered = CollectionUtils.filter(
map,
entry -> isEntryOK(entry)
);
Or, the best solution in my opinion -> Switch to Kotlin which gives you all those features out of the box! :D
in Java 8 you could use streams:
allItems.stream()
.filter(map->
(map.get("condition_1")==true)&&(map.get("condition_2")==false))
.forEach(System.out::println); //Example output