flatten Map in Map structure with stream [duplicate] - java

This question already has answers here:
How does recursion work with Java 8 Stream?
(2 answers)
Closed 4 years ago.
I have the following structure in HashMaps (not all the values are maps (see id) and there are more levels embedded):
{
"id":"0",
"name":{
"title":"Mr.",
"personName": {
"firstName":"Boot",
"familyName":"Strap"
}
}
}
Can I flatten it with Java 8 stream like
{
"id":"0",
"name.title":"Mr.",
"name.personName.firstName":"Boot",
"name.personName.familyName":"Strap"
}
?

Streams does not do recursive computation, you may use classic way like
static Map<String, String> flatten(Map<String, Object> map, String prefix) {
prefix = prefix.isEmpty() ? prefix : prefix + ".";
Map<String, String> res = new HashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Map) {
res.putAll(flatten((Map) entry.getValue(), prefix + entry.getKey()));
} else {
res.put(prefix + entry.getKey(), entry.getValue().toString());
}
}
return res;
}
Stream way will look like
static Map<String, String> flatten(Map<String, Object> map, String p) {
final String prefix = p.isEmpty() ? p : p + ".";
return map.entrySet()
.stream()
.map(e -> {
if (e.getValue() instanceof Map) {
return (Set<Map.Entry<String, String>>) flatten((Map) e.getValue(), prefix + e.getKey()).entrySet();
} else {
return Map.of(prefix + e.getKey(), e.getValue().toString()).entrySet();
}
})
.flatMap(Set::stream)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
And use as
public static void main(String[] args) {
Map<String, Object> map = Map.of("id", 0, "name", Map.of("title", "Mr.", "personName", Map.of("firstName", "Boot", "familyName", "Strap")));
Map<String, String> res = flatten(map, "");
res.entrySet().forEach(System.out::println);
}
name.title=Mr.
name.personName.familyName=Strap
name.personName.firstName=Boot
id=0

It is pretty simple not using Streams, but use recursion:
public static Map<String, String> flatten(Map<String, Object> map) {
return flatten("", map, new LinkedHashMap<>());
}
private static Map<String, String> flatten(String parent, Map<String, Object> map, Map<String, String> res) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = parent.isEmpty() ? entry.getKey() : parent + '.' + entry.getKey();
if (entry.getValue() instanceof Map)
flatten(key, (Map<String, Object>)entry.getValue(), res);
else
res.put(key, String.valueOf(entry.getValue()));
}
return res;
}
P.S. I know that string concatenation in loop is not good (it is better to use StringBuilder or even Deque), but I use String to make this example more clear.

Related

Loop through a list of map effectively

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)) ;
}
}));
}

Java8 Stream grouping by with toMap issue

I have a class named ConfigKey
public class ConfigKey {
String code;
String key;
String value;
//omit setter and getter
}
I want to convert List<ConfigKey> to Map<String, Map<String, Object>>, here is my method definition
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode,
Collectors.toMap(ConfigKey::getKey, ConfigKey::getValue)));
}
however I want to do some changes, for each ConfigKey put another key to the map, e.g.
{ "code": "code1","key", "key1", "value": "value1"}
to Map
{"code1": {"key1":"value1", "prefix_key1": "value1" }
is there any API to do it like bellow:
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode,
Collectors.toMap("prefix_" + ConfigKey::getKey, ConfigKey::getValue))
Collectors.toMap(ConfigKey::getKey, ConfigKey::getValue)));
}
You can make use of the Collector.of() factory method, which allows you to create your own collector:
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode, Collector.of(
HashMap::new, (m, c) -> {
m.put(c.getKey(), c.getValue());
m.put("prefix_" + c.getKey(), c.getValue());
}, (a, b) -> {
a.putAll(b);
return b;
}
)));
}
But honestly that seems a bit messy, and maybe a normal loop would've been better. The streams intention was to provide an api which does things in a more readable manner, but when you have to hackaround that construct, by introducing some extremely unreadable logic then it is almost always the better option to just do it the old way:
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
Map<String, Map<String, Object>> map = new HashMap<>();
for (ConfigKey ck : list) {
Map<String, Object> inner = map.computeIfAbsent(ck.getCode(), k -> new HashMap<>());
inner.put(ck.getKey(), ck.getValue());
inner.put("prefix_" + ck.getKey(), ck.getValue());
}
return map;
}
You can first add the new entries to the map and then group them:
private Map<String, Map<String, Object>> convert(List<ConfigKey> list) {
new ArrayList<>(list).stream().map(configKey -> new ConfigKey(configKey.getCode(), "prefix_" + configKey.getKey(), configKey.getValue())).forEachOrdered(list::add);
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode,
Collectors.toMap(ConfigKey::getKey, ConfigKey::getValue)));
}
I cloned the list (in order to prevent ConcurrentModificationException), then changed the keys to the "new" ones (with map) and added them to the original list - forEachOrdered(list::add).
Because the 'code' field was not changed, both entries will use it which results in 2 entries in the map

Matching two MultivaluedMaps and retrieving values

MultivaluedMap<String, Pair<String, Float>> dataToCheck;
MultivaluedMap<String, String> inputData;
I need to match the key of dataToCheck with inputData.
Followed by matching the values of dataToCheck with inputData.
If the values match, I retrieve the Float value. I want to exit at the first match.
Can exit with one key match.
float compareMap(MultivaluedMap<String, Pair<String, Float>> dataToCheck, MultivaluedMap<String, String> inputData) { }
EDIT:
MultivaluedMap is from Jax-Rs (https://docs.oracle.com/javaee/7/api/javax/ws/rs/core/MultivaluedMap.html)
This is what I came up with. I know I can use filter, but I am not sure how to make it work.
float sampleValue = new float[1];
dataToCheck.keySet().stream().forEach(field -> {
List<String> inputValues = inputData.get(field);
List<Pair<String, Float>> checkValues = dataToCheck.get(field);
if (checkValues != null && inputValues != null) {
checkValues.stream().forEach(value -> {
for (String inp : inputValues) {
if (value.getLeft().equalsIgnoreCase(inp)) {
sampleValue[0] = value.getRight();
break;
}
}
});
}
});
A slightly cleaner way of performing that would be:
private static Float findFirstMatchingRight(MultivaluedMap<String, Pair<String, Float>> dataToCheck,
MultivaluedMap<String, String> inputData) {
for (Map.Entry<String, List<Pair<String, Float>>> entry : dataToCheck.entrySet()) {
Optional<Float> sampleValue = entry.getValue().stream()
.filter(cv -> inputData.get(entry.getKey())
.stream()
.anyMatch(inp -> cv.getLeft().equalsIgnoreCase(inp)))
.findFirst()
.map(Pair::getRight);
if (sampleValue.isPresent()) {
return sampleValue.get();
}
}
return Float.NaN; // when the condition doesn't match anywhere in the maps
}

How to filter a key in a Map with only 3 string length?

I have the LinkHashMap here.
Map<String, String> map
{011A=HongKong, 012B=London, 015Y=NewYork, 312=China, 272=Canada}
I would like to filter or arrange the map to be this,
Only the key with a 3 digit or length = 3 kept in the map.
{312=China, 272=Canada}
What kind of method could I use?
Thank you very much!
You can use the Iterator
Iterator<String> it = map.keySet().iterator();
while (it.hasNext())
{
String s = it.next();
if(s.length() != 3){
it.remove();
}
}
If you are using Java 8 (or higher) there is a convenient feature called Lambda that provides a nice api to work with streams.
To create a second map with only the filtered keys use this below code:
Map<String, String> originMap;
Map<String, String> filteredmap = originMap.entrySet().stream()
.filter(x -> x.getKey().length() == 3)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
If you want to remove the elements from your map itself :
Map<String, String> map;
map.entrySet().removeIf(entry -> entry.getKey().length != 3);
You can write a method like this to filter,
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>() {{
put("011A", "HongKong");
put("012B", "London");
put("015Y", "NewYork");
put("312", "China");
put("272", "Canada");
}};
Map<String, String> filteredMap = filterMap(map);
}
static Map<String, String> filterMap (Map<String, String> map) {
HashMap<String, String> filteredMap = new HashMap<>();
for (String key: map.keySet()) {
if (key.length() == 3) {
filteredMap.put(key, map.get(key));
}
}
return filteredMap;
}
}
For Java >= 8:
public static Map<String, String> filterMap(Map<String, String> map) {
return map.entrySet().stream()
.filter(entry -> entry.getKey().length() == 3)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
For Java < 8:
public static Map<String, String> filterMap(Map<String, String> map) {
Map<String, String> res = new HashMap<>();
for (Map.Entry<String, String> entry : map.entrySet())
if (entry.getKey().length() == 3)
res.put(entry.getKey(), entry.getValue());
return res;
}

how to transform Map<String,String> to List<String> using google collections

I have a map with strings, I want to transform it to a list of strings with " " as a key value separator. Is it possible using google collections?
Code example that I want to do using google collections:
public static List<String> addLstOfSetEnvVariables(Map<String, String> env)
{
ArrayList<String> result = Lists.newArrayList();
for (Entry<String, String> entry : env.entrySet())
{
result.add(entry.getKey() + " " + entry.getValue());
}
return result;
}
Here you go:
private static final Joiner JOINER = Joiner.on(' ');
public List<String> mapToList(final Map<String, String> input){
return Lists.newArrayList(
Iterables.transform(
input.entrySet(), new Function<Map.Entry<String, String>, String>(){
#Override
public String apply(final Map.Entry<String, String> input){
return JOINER.join(input.getKey(), input.getValue());
}
}));
}
Update: optimized code. Using a Joiner constant should be much faster than String.concat()
These days, I would of course do this with Java 8 streams. No external lib needed.
public List<String> mapToList(final Map<String, String> input) {
return input.entrySet()
.stream()
.map(e -> new StringBuilder(
e.getKey().length()
+ e.getValue().length()
+ 1
).append(e.getKey())
.append(' ')
.append(e.getValue())
.toString()
)
.collect(Collectors.toList());
}
Functional programming is cool, but in Java often adds complexity you really shouldn't be adding (as Java doesn't support it very well) I would suggest you use a simple loop which is much shorter, more efficient and eaiser to maintain and doesn't require an additional library to learn.
public static List<String> mapToList(Map<String, String> env) {
List<String> result = new ArrayList<String>();
for (Entry<String, String> entry : env.entrySet())
result.add(entry.getKey() + " " + entry.getValue());
return result;
}
A simple test of code complexity it to count the number of symbols used. i.e. < ( , { = : . + #
Not counting close brackets.
plain loop 22 symbols.
functional approach 30 symbols.
Here's a functional approach using Java 8 streams:
List<String> kv = map.entrySet().stream()
.map(e -> e.getKey() + " " + e.getValue()) //or String.format if you prefer
.collect(Collectors.toList());
If you're not wedded to the functional style, here's a more concise variant of the obvious for loop:
List<String> kv = new ArrayList<>();
map.forEach((k, v) -> kv.add(k + " " + v));
More Current solution
public static void main(final String[] args)
{
final Map<String, String> m = ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3");
final Collection<String> c = Maps.transformEntries(m, new Maps.EntryTransformer<String, String, String>()
{
#Override public String transformEntry(#Nullable final String key, #Nullable final String value)
{
return Joiner.on(' ').join(key, value);
}
}).values();
System.out.println(c);
}
Outputs
[k1 v1, k2 v2, k3 v3]

Categories