I am learning some cool stuff about Java StreamAPI and got stuck'd into one problem:
I have a use case where I want to return newly create hashmap using stream. I am using the traditional way of defining a HashMap in the function and adding up values to it.
I was more interested in knowing some better ways to achieve so
public Map<String,String> constructMap(List<CustomObject> lists){
Map<String,String> newMap = new HashMap<>();
lists.stream().filter(x->x!=null).forEach(map -> newMap.putAll(map.getSomeMapping(studentId));
return newMap;
}
Can I achieve this using reduceAPI or any other way without having to create a custom hashmap (directly return the stream one liner)?
Edit:
for Example:
CustomObject c1 = new CustomObject("bookId1", "book1");
CustomObject c2 = new CustomObject("bookId2", "book2");
List<CustomObject> lists = new ArrayList();
lists.add(c1); lists.add(c2);
The getter in class CustomObject is: getSomeMapping(input)
which return Map<BookID, Book>
Expected output:
{"bookId1" : "book1", "bookId2" : "book2"}
Edit2:
One more thing to clarify, the CustomObject class does not have any other getters defined. The only function I have access to is getSomeMapping(input) which returns a mapping
thank you for any help.
Assuming CustomObject has the following structure and getter getSomeMapping which returns a map:
class CustomObject {
private Map<String, String> someMapping;
public CustomObject(String key, String value) {
this.someMapping = new HashMap<>();
someMapping.put(key, value);
}
public Map<String, String> getSomeMapping() {
return someMapping;
}
}
Then constructMap will use already mentioned Collectors.toMap after flattening the entries in someMapping:
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.map(CustomObject::getSomeMapping)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v1, // merge function to handle possible duplicates
LinkedHashMap::new
));
}
Test
CustomObject c1 = new CustomObject("bookId1", "book1");
CustomObject c2 = new CustomObject("bookId2", "book2");
List<CustomObject> lists = Arrays.asList(c1, c2);
Map<String, String> result = constructMap(lists);
System.out.println(result);
Output:
{bookId1=book1, bookId2=book2}
You can use Collectors#toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) to create a LinkedHashMap using the bookId as the key, and bookName as the value.
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
class CustomObject {
private String bookId;
private String bookName;
public CustomObject(String bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
public String getBookId() {
return bookId;
}
public String getBookName() {
return bookName;
}
// Other stuff e.g. equals, hashCode etc.
}
public class Main {
public static void main(String[] args) {
List<CustomObject> list = List.of(new CustomObject("bookId1", "book1"), new CustomObject("bookId2", "book2"));
System.out.println(constructMap(list));
}
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getBookId, CustomObject::getBookName, (a, b) -> a, LinkedHashMap::new));
}
}
Output:
{bookId1=book1, bookId2=book2}
Note: The mergeFunction, (a, b) -> a resolves the collision between values associated with the same key e.g. in this case, we have defined it to select a out of a and b having the same key. If the order of elements does not matter, you can use Collectors#toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper) as shown below:
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getBookId, CustomObject::getBookName));
}
A sample output:
{bookId2=book2, bookId1=book1}
To turn a stream into a map you're better off using collect(). For instance:
public Map<String,String> toMap(List<Entry<String,String>> entries) {
return entries.stream().collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
Or if your keys are non-unique and you want the values to be combined as a list:
public Map<String,List<CustomObject>> toMap(List<CustomObject> entries) {
return entries.stream().collect(Collectors.groupingBy(CustomObject::getKey));
}
Look into [Collectors.toMap()] 1. This can return the items as a new Map.
lists.stream().filter(x->x!=null).collect(Collectors.toMap(CustomObject::getMapKey(), CustomObject::getMapValue()));
getMapKey and getMapValue are here methods returning the key and value of the CustomObject for the map. Instead of using simple getters it might also be necessary to execute some more advanced logic.
lists.stream().filter(x->x!=null).collect(Collectors.toMap(l -> {...; return key;}, l -> { ...; return value;}));
Let's assume your CustomObject class has getters to retrieve a school id with a name. You could do it like this. I declared it static as it does not appear to depend on instance fields.
public static Map<String,String> constructMap(List<CustomObject> lists){
return lists.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getName, CustomObject::getID));
}
This presumes that names and Id's are one-to-one, as this does not handle duplicate keys.
Related
I have a usecase where client is sending a List<Function>. Task is to iterate and execute this function and keep it in a TypedSafeMap.
pseudo client code:
Function<String, Integer> firstFn = x -> x.length();
Function<String, String> secondFn = x -> x.substring(0);
client.runTheseFunctions(Arrays.asList(firstFn, secondFn));
Inside runtTheseFunctions in the code, task is to execute these functions and keep it in a TypedSafeMap where the key is the datatype of the type of the result of the function and value is the return of functions.apply();
The code below
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
lst.stream().forEach( x -> {
typedSafeMap.put(????, x.apply(o));
//The key is nothing but the datatype of the x.apply(o).
//How do I add this in runtime here. Generics is all compile time safety.
});
}
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
lst.stream().collect(Collectors.toMap(f -> f.apply(o).getClass(), f -> f.apply(o)));
}
You can implement your "runTheseFunctions" method as shown below:
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
Map<Class<?>, Object> typedSafeMap = new HashMap<>();
lst.stream().forEach(x -> {
Object value = x.apply(o);
typedSafeMap.put(value.getClass(), value);
});
System.out.println(typedSafeMap);
}
In case the List of Functions contains two or more Functions with the same outputtype (for instance: String getFirstName, String getLastName, toMap will fail. So an alternative is:
var map = list.stream().collect(groupingBy(
f -> f.apply(e).getClass(),
mapping(f -> f.apply(e), toList())
));
Here is an example of what you want to achieve, and you can use for your tests. I assumed an trivial implementation of Employee class, just to give you an idea:
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
class Employee {
String name;
public Employee(String name) {
this.name = name;
}
public int length() {
return name.length();
}
public String substring(int index) {
return name.substring(index);
}
}
public class Test {
public static void main(String[] args) {
Employee e = new Employee("Marco");
Function<Employee, Integer> firstFn = x -> x.length();
Function<Employee, String> secondFn = x -> x.substring(0);
runTheseFunctions(Arrays.asList(firstFn, secondFn), e);
}
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
Map<Class, Object> typedSafeMap = new HashMap<>();
lst.stream().forEach(x -> {
Object result = x.apply(o);
typedSafeMap.put(x.apply(o).getClass(), x.apply(o));
// The key is nothing but the datatype of the x.apply(o).
// How do I add this in runtime here. Generics is all compile time safety.
});
typedSafeMap.entrySet().forEach(entry -> System.out.println(entry.getKey() + " - " + entry.getValue()));
}
}
And here is the output:
class java.lang.String - Marco
class java.lang.Integer - 5
Enhancing #Yonas answer:
private static Map<?, ? extends Object> runTheseFunctions(List<Function<String, ? extends Object>> list, String o) {
return list.stream()
.map(f -> f.apply(o))
.collect(Collectors.toMap(result -> result.getClass(), Function.identity()));
}
This will call the f.apply(o) only once.
Here is what I do to populate my static map
public static final Map<String, FooBar> mapEnum =
Arrays.stream(FooBarEnum.values())
.collect(Collectors.toMap(e-> StringUtils.upperCase(e.name), e -> e));
I want to add another single key-value to this map.
mapEnum.put("xx", FooBar.A);
Here is the enum
public enum FooBar {
A("a"), B("b"), C("c");
}
My static map will look like this after map is constructed
{"a":FooBar.A, "b": FooBar.B, "c": FooBar.C, "xx": Foobar.A}
Is it possible to include the explicit put call into Collectors.toMap()?
If you're open to using a third party library you can create a static ImmutableMap inline with a Stream using Eclipse Collections.
public static final ImmutableMap<String, FooBar> MAP_ENUM =
Arrays.stream(FooBar.values())
.collect(Collectors2.toMap(FooBar::getName, fooBar -> fooBar))
.withKeyValue("xx", FooBar.A)
.toImmutable();
public enum FooBar {
A("a"), B("b"), C("c");
private String name;
FooBar(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
You can also simplify the code slightly by using native Eclipse Collections APIs.
public static final ImmutableMap<String, FooBar> MAP_ENUM =
ArrayAdapter.adapt(FooBar.values())
.groupByUniqueKey(FooBar::getName)
.withKeyValue("xx", FooBar.A)
.toImmutable();
Note: I am a committer for Eclipse Collections
I actually don't see the need to use Java Streams for that. You simply can use the static block to initialize mapEnum and put additional values in it:
public static final Map<String, FooBar> mapEnum;
static {
mapEnum = Arrays.stream(FooBar.values())
.collect(Collectors.toMap(FooBar::getName, Function.identity()));
mapEnum.put("xx", FooBar.A);
// ...
}
Collectors.toMap(): There are no guarantees on the type, mutability, serializability, or thread-safety of the {#code Map} returned.
To ensure the mutability of the Map returned by Collectors.toMap(), so you can use Map.put() afterwards better use this:
Arrays.stream(FooBar.values())
.collect(Collectors.toMap(Function.identity(), Function.identity(), (a, b) -> a, HashMap::new));
If you really want to use java streams you can use this:
public static final Map<String, FooBar> mapEnum = Stream.concat(
Stream.of(FooBar.values()).map(e -> Map.entry(e.getName(), e)),
Stream.of(Map.entry("xx", FooBar.A))
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Or if you also want to add all names to the enum value itself you can change your class like this:
public static enum FooBar {
A("a", "xx"), B("b"), C("c");
private String[] names;
FooBar(String... names) {
this.names = names;
}
public String[] getNames() {
return names;
}
}
And use this to create the map:
public static final Map<String, FooBar> mapEnum = Stream.of(FooBar.values())
.flatMap(e -> Arrays.stream(e.getNames()).map(n -> Map.entry(n, e)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Prior to Java 9 use new AbstractMap.SimpleEntry<>() instead of Map.entry(). If you need the map to be sorted use LinkedHashMap::new with Collectors.toMap().
you can use Collectors::collectAndThen to modify the resulted map
Arrays.stream(FooBarEnum.values())
.collect(Collectors.collectAndThen(
Collectors.toMap(e-> StringUtils.upperCase(e.name),
Function.identity()), FooBarEnum::addCustom));
the following method is in enum
static Map<String, FooBar> addCustom(Map<String, FooBarEnum> map) {
map.put("xx", FooBar.A);
return map;
}
You cannot directly pass it to Collectors.toMap(). You can see all the overrides available in the javadocs: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function- .
However, you can make sure your stream has all the pairs needed to construct the map, before you call toMap by using Stream.concat. You concat the pairs from the enum, and the manual pairs you want to add.
My standalone code has to define the Pair class, but since you used StringUtils, I imagine you have a library that already includes Pair so you don't need to define it.
Code:
import java.util.*;
import java.util.stream.*;
public class Main {
private static enum FooBar {
A("a"), B("b"), C("c");
private String name;
FooBar(String name) {
this.name = name;
}
}
public static class Pair {
String a;
FooBar b;
Pair(String a, FooBar b) {
this.a = a;
this.b = b;
}
}
public static void main(String [] args) {
System.out.println(
Stream.concat(
Arrays.stream(FooBar.values()).map(e -> new Pair(e.name.toUpperCase(), e)),
Stream.of(new Pair("xx", FooBar.A))
)
.collect(Collectors.toMap(pair -> pair.a, pair -> pair.b))
);
}
}
Output:
{xx=A, A=A, B=B, C=C}
I have list of students.
I want to return list of objects StudentResponse classes that has the course and the list of students for the course.
So I can write which gives me a map
Map<String, List<Student>> studentsMap = students.stream().
.collect(Collectors.groupingBy(Student::getCourse,
Collectors.mapping(s -> s, Collectors.toList()
)));
Now I have to iterate through the map again to create a list of objects of StudentResponse class which has the Course and List:
class StudentResponse {
String course;
Student student;
// getter and setter
}
Is there a way to combine these two iterations?
Not exactly what you've asked, but here's a compact way to accomplish what you want, just for completeness:
Map<String, StudentResponse> map = new LinkedHashMap<>();
students.forEach(s -> map.computeIfAbsent(
s.getCourse(),
k -> new StudentResponse(s.getCourse()))
.getStudents().add(s));
This assumes StudentResponse has a constructor that accepts the course as an argument and a getter for the student list, and that this list is mutable (i.e. ArrayList) so that we can add the current student to it.
While the above approach works, it clearly violates a fundamental OO principle, which is encapsulation. If you are OK with that, then you're done. If you want to honor encapsulation, then you could add a method to StudentResponse to add a Student instance:
public void addStudent(Student s) {
students.add(s);
}
Then, the solution would become:
Map<String, StudentResponse> map = new LinkedHashMap<>();
students.forEach(s -> map.computeIfAbsent(
s.getCourse(),
k -> new StudentResponse(s.getCourse()))
.addStudent(s));
This solution is clearly better than the previous one and would avoid a rejection from a serious code reviewer.
Both solutions rely on Map.computeIfAbsent, which either returns a StudentResponse for the provided course (if there exists an entry for that course in the map), or creates and returns a StudentResponse instance built with the course as an argument. Then, the student is being added to the internal list of students of the returned StudentResponse.
Finally, your StudentResponse instances are in the map values:
Collection<StudentResponse> result = map.values();
If you need a List instead of a Collection:
List<StudentResponse> result = new ArrayList<>(map.values());
Note: I'm using LinkedHashMap instead of HashMap to preserve insertion-order, i.e. the order of the students in the original list. If you don't have such requirement, just use HashMap.
Probably way overkill but it was a fun exercise :) You could implement your own Collector:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public class StudentResponseCollector implements Collector<Student, Map<String, List<Student>>, List<StudentResponse>> {
#Override
public Supplier<Map<String, List<Student>>> supplier() {
return () -> new ConcurrentHashMap<>();
}
#Override
public BiConsumer<Map<String, List<Student>>, Student> accumulator() {
return (store, student) -> store.merge(student.getCourse(),
new ArrayList<>(Arrays.asList(student)), combineLists());
}
#Override
public BinaryOperator<Map<String, List<Student>>> combiner() {
return (x, y) -> {
x.forEach((k, v) -> y.merge(k, v, combineLists()));
return y;
};
}
private <T> BiFunction<List<T>, List<T>, List<T>> combineLists() {
return (students, students2) -> {
students2.addAll(students);
return students2;
};
}
#Override
public Function<Map<String, List<Student>>, List<StudentResponse>> finisher() {
return (store) -> store
.keySet()
.stream()
.map(course -> new StudentResponse(course, store.get(course)))
.collect(Collectors.toList());
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
}
Given Student and StudentResponse:
public class Student {
private String name;
private String course;
public Student(String name, String course) {
this.name = name;
this.course = course;
}
public String getName() {
return name;
}
public String getCourse() {
return course;
}
public String toString() {
return name + ", " + course;
}
}
public class StudentResponse {
private String course;
private List<Student> studentList;
public StudentResponse(String course, List<Student> studentList) {
this.course = course;
this.studentList = studentList;
}
public String getCourse() {
return course;
}
public List<Student> getStudentList() {
return studentList;
}
public String toString() {
return course + ", " + studentList.toString();
}
}
Your code where you collect your StudentResponses can now be very short and elegant ;)
public class StudentResponseCollectorTest {
#Test
public void test() {
Student student1 = new Student("Student1", "foo");
Student student2 = new Student("Student2", "foo");
Student student3 = new Student("Student3", "bar");
List<Student> studentList = Arrays.asList(student1, student2, student3);
List<StudentResponse> studentResponseList = studentList
.stream()
.collect(new StudentResponseCollector());
assertEquals(2, studentResponseList.size());
}
}
Just iterate over the entry set and map each entry to a StudentResponse:
List<StudentResponse> responses = studentsMap.entrySet()
.stream()
.map(e -> new StudentResponse(e.getKey(), e.getValue()))
.collect(Collectors.toList());
First, your downstream collector (mapping) is redundant and hence you can simplify your code by using the groupingBy overload without a downstream collector.
Given a List<T> as the source, after using the groupingBy overload taking a classifier alone the result map is Map<K, List<T>> so the mapping operation can be avoided.
As for your question, you can use collectingAndThen:
students.stream()
.collect(collectingAndThen(groupingBy(Student::getCourse),
m -> m.entrySet()
.stream()
.map(a -> new StudentResponse(a.getKey(), a.getValue()))
.collect(Collectors.toList())));
collectingAndThen basically:
Adapts a Collector to perform an additional finishing transformation.
This can be done in a very concise manner using the jOOλ library and its Seq.grouped method:
List<StudentResponse> responses = Seq.seq(students)
.grouped(Student::getCourse, Collectors.toList())
.map(Tuple.function(StudentResponse::new))
.toList();
It assumes StudentResponse has a constructor StudentResponse(String course, List<Student> students), and forwards to this constructor using the following Tuple.function overload.
As you can see from my other answer as well as shmosel's answer, you'll eventually need to invoke studentsMap.entrySet() to map each Entry<String, List<String>> in the resulting map to StudentResponse objects.
Another approach you could take is the toMap way; i.e.
Collection<StudentResponse> result = students.stream()
.collect(toMap(Student::getCourse,
v -> new StudentResponse(v.getCourse(),
new ArrayList<>(singletonList(v))),
StudentResponse::merge)).values();
This essentially groups the Student object by their course (Student::getCourse) as with the groupingBy collector; then in the valueMapper function maps from Student to a StudentResponse and finally in the merge function utilises StudentResponse::merge in the case of a key collision.
The above has a dependency on the StudentResponse class having at least the following fields, constructor and methods:
class StudentResponse {
StudentResponse(String course, List<Student> students) {
this.course = course;
this.students = students;
}
private List<Student> getStudents() { return students; }
StudentResponse merge(StudentResponse another){
this.students.addAll(another.getStudents());
// maybe some addition merging logic in the future ...
return this;
}
private String course;
private List<Student> students;
}
I have a Map<String, List<String>> map and I want extract from it a List<String> that contains the strings of all the list of strings in the map. I'd like to use java8 streams syntax.
In old java I would do:
List<String> all = new LinkedList<String>();
for (String key: map.keySet()) {
all.addAll(map.get(key));
}
return all;
how to do that using streams?
You can do what you want using Stream.flatMap(Function).
public static List<String> collectValues(Map<String, List<String>> map) {
return map.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
A more generic version could look like:
public static <E> List<E> collectValues(Map<?, ? extends Collection<? extends E>> map) {
return map.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
And an even more generic version which allows you to specify the return type:
public static <C extends Collection<E>, E> C collectValues(
Map<?, ? extends Collection<? extends E>> map, Supplier<C> collectionFactory) {
return map.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toCollection(collectionFactory));
}
And finally, just for the fun of it, the most generic version I can think of:
public static <C, E> C collectValues(Map<?, ? extends Iterable<? extends E>> map,
Collector<E, ?, C> collector) {
return map.values().stream()
.flatMap(iterable -> StreamSupport.stream(iterable.spliterator(), false))
.collect(collector);
}
This one uses the StreamSupport class and Collector interface.
Using a new ArrayList and the addAll() method to get the same result.
public class MapTest {
public static void main(String[] args) {
Map<String, List<String>> infoMap = new HashMap<>();
infoMap.put("1", Arrays.asList("a","b","c"));
infoMap.put("2", Arrays.asList("d","e","f"));
infoMap.put("3", Arrays.asList("g","h","i"));
List<String> result = new ArrayList<>();
infoMap.values().stream().forEach(result::addAll);
result.forEach(System.out::println);
}
}
I'm reviewing an old code of an project and got a datastructure as bellow using Map of Map of Map(3-Layered Map):
// data structure
Map<String, Map<String, Map<String, List<String>>>> tagTree
= new HashMap<String, Map<String,Map<String,List<String>>>>();
And fetch the values from Map (I think this is the nice part)
// fetch at tag values
List<String> tagList1 = tagTree.get("Java").get("Active").get("Tags");
List<String> tagList2 = tagTree.get("Java").get("Latest").get("SubTags");
Put the values in Map (little bit complex and error-prone)
// put values
Map<String, Map<String, List<String>>> javaLangMap = new HashMap<String, Map<String, List<String>>>();
Map<String, List<String>> javaStatusMap = new HashMap<String, List<String>>();
List<String> javaTagList = new ArrayList<String>();
javaTagList.add("Java-OOP");
javaTagList.add("Java-Variables");
// put tag list
javaStatusMap.put("Tags", javaTagList);
// put status-wise tag
javaLangMap.put("Active", javaStatusMap);
// put language-wise tag
tagTree.put("Java", javaLangMap);
Currently this is serving to maintain following structure
TagLanguage -> TagStatus -> TagType -> TagList
I'm planning to refactor this Map because it's hard to read for other developers.
Please share your Idea How to do it by considering following cases:
All four layer may be changed during runtime.
All Level should
accessible
In-memory solution required i.e. dont use Database table hierarchy .
If you only ever wanted to access the last level of your data structure, you could use a Multimap<Triple<String,String,String>,String>. Multimap<K,V> is a data structure from Guava which basically is a nicer Map<K,Collection<V>>. Triple<L,M,R> is a 3-elements tuple data structure from Apache Commons Lang3 which is Comparable and implements equals.
You could declare your tag tree like this:
Multimap<Triple<String, String, String>, String> tagTree = HashMultimap.create();
And then fill it like this:
tagTree.put(Triple.of("Java", "Active", "Tags"), "Java-OOP");
tagTree.put(Triple.of("Java", "Active", "Tags"), "Java-Variables");
Or:
tagTree.putAll(Triple.of("Java", "Active", "Tags"), Arrays.asList("Java-OOP", "Java-Variables"));
And then get your values from it like this:
Set<String> values = tagTree.get(Triple.of("Java", "Active", "Tags"));
Here is another rough solution that may suit you which enables to get with 1, 2 or 3 keys:
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
public class ThreeLevelMap<K1, K2, K3, V> {
private Map<K1, Map<K2, Multimap<K3, V>>> firstLevelMap = new HashMap<>();
private Map<Pair<K1, K2>, Multimap<K3, V>> secondLevelMap = new HashMap<>();
private Multimap<Triple<K1, K2, K3>, V> thirdLevelMap = HashMultimap.create();
public void put(K1 key1, K2 key2, K3 key3, V value) {
thirdLevelMap.put(Triple.of(key1, key2, key3), value);
final Pair<K1, K2> secondLevelKey = Pair.of(key1, key2);
Multimap<K3, V> secondLevelContainer = secondLevelMap.get(secondLevelKey);
if (secondLevelContainer == null) {
secondLevelContainer = HashMultimap.create();
secondLevelMap.put(secondLevelKey, secondLevelContainer);
}
secondLevelContainer.put(key3, value);
Map<K2, Multimap<K3, V>> firstLevelContainer = firstLevelMap.get(key1);
if (firstLevelContainer == null) {
firstLevelContainer = new HashMap<>();
firstLevelMap.put(key1, firstLevelContainer);
}
firstLevelContainer.put(key2, secondLevelContainer);
}
public Collection<V> get(K1 key1, K2 key2, K3 key3) {
return thirdLevelMap.get(Triple.of(key1, key2, key3));
}
public Multimap<K3, V> get(K1 key1, K2 key2) {
return secondLevelMap.get(Pair.of(key1, key2));
}
public Map<K2, Multimap<K3, V>> get(K1 key1) {
return firstLevelMap.get(key1);
}
}
You can use it this way:
ThreeLevelMap<String, String, String, String> tlm = new ThreeLevelMap<>();
tlm.put("Java", "Active", "Tags", "Java-OOP");
tlm.put("Java", "Active", "Tags", "Java-Variables");
Map<String, Multimap<String, String>> firstLevelMap = tlm.get("Java");
Multimap<String, String> secondLevelMap = tlm.get("Java", "Active");
Collection<String> tags = tlm.get("Java", "Active", "Tags");
I say it is rough because:
the maps that the get methods return are modifiable
I didn't implement remove methods
I didn't test it a lot
I don't think that this is such a bad solution.
It's just a tree representation, for a tree where every leaf is at level 3.
If this was not the case (different leaf levels, etc.) you would have to build up a tree class structure.
But what I would change is to put everything in a class, with a get and set method, including null-checks.
In the following code, the add method takes care of the error-prone handling of the intermediate level maps, while get checks for null values in the intermediate levels:
public class TreeStructure {
Map<String, Map<String, Map<String, List<String>>>> tagTree
= new HashMap<String, Map<String,Map<String,List<String>>>>();
// ... Constructor ...
// This method adds all intermediate levels if not existing
public void add(String level1, String level2, String level3) {
String l1 = tagTree.get(level1);
if(l1 == null)
tagTree.put(level1, new HashMap<String, Map<String, List<String>>>());
l1 = tagTree.get(level1);
String l2 = l1.get(level2),
if(l2 == null)
tagTree.put(level2, new Map<String, List<String>>(););
l2 = l1.get(level2);
String l3 = l2.get(level3);
if(l3 == null) l2.add(level3, new ArrayList<>());
}
// This method checks, if every intermediate level existed
// Otherwise, get() returns null, and the next get() would fail
public String get(String level1, String level2, String level3) {
String l1 = tagTree.get(level1);
if(l1 == null)
return null;
String l2 = l1.get(level2),
if(l2 == null)
return null;
l2 = l1.get(level2);
String l3 = l2.get(level3);
return l3;
}
}
(Code untested)
You can create classes witch will hold the data structure
public class A {
Map<String, List<String>> map;
}
public class B {
Map<String, A> map;
}
Map<String, B> tagTree;
Use a Map of 3-Tuples:
class Tuple3<A,B,C> {
private A a;
private B b;
private C c;
// getters, setters, constructor
// make sure equals() and hashCode() are okay
}
Note, however, that the Map of Map of Maps can tell you in O(1) whether there are entries for some element by just looking in the outer map. Whereas, with the tuple solution, you can only work with full keys.
I think there is no reason for refactor this Map structure. But if possible, it is probably a good idea to encapsulate this Map in another class and give other developers a clean interface.
...
public void addTag(String language, String status, String tag)
public void removeTag(String language, String status, String tag)
public List<String> getTags(String language, String status)
...
I like most of the solutions suggested above. I think the simpler and efficient the design is - more supportable it will be.
Hence, I used basics - plain object composition to refactor your code.
package design;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class JavaTag{
private String tags;
JavaTag(String tags){
this.tags = tags;
}
}
class JavaTagStatusList{
private ArrayList<JavaTag> tagList = new ArrayList<JavaTag>();
JavaTagStatusList(){
}
public void addJavaTag (JavaTag tagObj){
if (tagObj != null){
tagList.add(tagObj);
}
}
}
class JavaTagStatusMap {
private HashMap<String, JavaTagStatusList> tagStatusMap = new HashMap<String, JavaTagStatusList>();
JavaTagStatusMap(){
}
public void addTagStatusEntry(String status, JavaTag obj){
if (tagStatusMap.containsKey(status)){
tagStatusMap.get(status).addJavaTag(obj);
}
else {
JavaTagStatusList statusList = new JavaTagStatusList();
statusList.addJavaTag(obj);
tagStatusMap.put(status, statusList);
}
}
}
Main:
public class MapofMapRefactor {
public static void main(String[] args) {
JavaTag tag1 = new JavaTag("Java-OOP");
JavaTag tag2 = new JavaTag("Java-Variables");
JavaTagStatusMap statusMap = new JavaTagStatusMap();
statusMap.addTagStatusEntry("Active", tag1);
statusMap.addTagStatusEntry("Active", tag2);
// HashMap of Java Lang Map
HashMap<String, JavaTagStatusMap> javaLanguageMap = new HashMap<String, JavaTagStatusMap>();
javaLanguageMap.put("Java", statusMap);
}
}