Retrieving values from a map that contains sets of objects - set

Firstly, I'm not very good at this java lark, so bear with me...
I have a map where the keys are Strings representing the title of a film and the values are Sets of actors, represented by objects of class Actor which each have their own attributes.
What I want to do is iterate over the map, returning the film title and the actors in a meaningful way.
I've got the map iteration part but I can't figure out how to do the next loop which produces the values from the Set of Actors.
Here's what I have:
Map <String, Set> filmCollection = new HashMap<>();
Actor a1 = new Actor("Joe Smith", "20071977");
Actor a2 = new Actor("Kate Jones", "01011980");
Actor a3 = new Actor("Frank DeFrank", "02021945");
Set <Actor> s1 = new HashSet<>();
Set <Actor> s2 = new HashSet<>();
s1.add(a1);
s1.add(a2);
s2.add(a3);
s2.add(a1);
filmCollection.put("The Best Film", s1);
filmCollection.put("The Nearly Best Film", s2);
Set<String> collectionKeys = filmCollection.keySet();
for (String eachFilm : collectionKeys)
{
System.out.println(eachFilm + " stars the actors ");
}
I know this is basic stuff and is probably very simple, but i'm learning and I've been searching and trying things all day - my head feels like it might explode soon! Any guidance would be appreciated.

So what you did is looping over the set of keys of the map. Since you have the keys, now you need to get the value of each key stored in the map. To do so, you have to call filmCollection.get(eachFilm).
Full code
Map <String, Set<Actor>> filmCollection = new HashMap<String, Set<Actor>>();
Actor a1 = new Actor("Joe Smith", "20071977");
Actor a2 = new Actor("Kate Jones", "01011980");
Actor a3 = new Actor("Frank DeFrank", "02021945");
Set <Actor> s1 = new HashSet<>();
Set <Actor> s2 = new HashSet<>();
s1.add(a1);
s1.add(a2);
s2.add(a3);
s2.add(a1);
filmCollection.put("The Best Film", s1);
filmCollection.put("The Nearly Best Film", s2);
Set<String> collectionKeys = filmCollection.keySet();
for (String eachFilm : collectionKeys)
{
System.out.println(eachFilm + " stars the actors ");
for (Actor actor : filmCollection.get(eachFilm)) {
System.out.println(actor + ", ");
}
}

There you go:
Your filmCollection should be a Map<String, Set<Actor>>
Map<String, Set<Actor>> filmCollection = new HashMap<>();
Actor a1 = new Actor("Joe Smith", "20071977");
Actor a2 = new Actor("Kate Jones", "01011980");
Actor a3 = new Actor("Frank DeFrank", "02021945");
Set<Actor> s1 = new HashSet<>();
Set<Actor> s2 = new HashSet<>();
s1.add(a1);
s1.add(a2);
s2.add(a3);
s2.add(a1);
filmCollection.put("The Best Film", s1);
filmCollection.put("The Nearly Best Film", s2);
Set<String> collectionKeys = filmCollection.keySet();
for (String eachFilm : collectionKeys) {
System.out.print(eachFilm + " stars the actors : ");
for (Actor actor : filmCollection.get(eachFilm)) {
System.out.print(" [ " + actor.name + " ] ");
}
System.out.println();
Sample output:
The Best Film stars the actors : [ Kate Jones ] [ Joe Smith ]
The Nearly Best Film stars the actors : [ Frank DeFrank ] [ Joe Smith ]

If I understand you properly, you just lack a loop: the first one loops over the films, and you should have another one for their actors. Use entrySet instead of keySet if you need both key and value at the same time. I tweaked your code a little bit to show you what I mean:
Map <String, Set<Actor>> filmCollection = new HashMap<>();
Actor a1 = new Actor("Joe Smith", "20071977");
Actor a2 = new Actor("Kate Jones", "01011980");
Actor a3 = new Actor("Frank DeFrank", "02021945");
Set <Actor> s1 = new HashSet<>();
Set <Actor> s2 = new HashSet<>();
s1.add(a1);
s1.add(a2);
s2.add(a3);
s2.add(a1);
filmCollection.put("The Best Film", s1);
filmCollection.put("The Nearly Best Film", s2);
for (Entry<String, Set<Actor>> eachFilm : filmCollection.entrySet()) {
// The key -> a movie
System.out.println(eachFilm.getKey() + " stars the actors ");
// The value -> the actors
for (Actor actor : eachFilm.getValue()) {
System.out.println(actor);
}
}

Since you are using both the film and actors then just iterate over the entries of your film-to-actors map, since the entries contains both the key and the value. Then, for each entry, print the name of the film (entry.getKey()) and get the set of actors (entry.getValue()) and print each one.
Map <String, Set<String>> filmCollection = new HashMap<>();
// ...
for (Map.Entry<String, Set<String>> entry : filmCollection.entrySet()){
System.out.println(entry.key() + " stars the actors " + entry.value());
// or
for(String actor : entry.value()){
System.out.println(actor);
}
}

Your problem is that the key set does not contain any information about the actors. The key set just contains the keys.
You can loop over the map entries which are key -> value pairs. Once you have the set of actors, you can concatenate their names with String.join
for (Map.Entry<String, Set<Actor>> film : filmCollection.entrySet())
{
// get all the names for this film (I would usually use streams for this, but
// they're a bit more advanced)
Set<String> actorsNames = new HashSet<>();
for (Actor actor : film.getValue())
{
actorsNames.add(actor.getName());
}
// concatenate the names with String.join
System.out.println(
film.getKey() + " stars the actors " + String.join(", ", actorsNames)
);
}
You will also need to change the definition of your map to:
Map<String, Set<Actor>> filmCollection = new HashMap<>();
because this states that the set is a set of actors. Without this, it's just a set of objects.
Little bonus for how I'd do this with streams:
filmCollection.entrySet().stream()
.map(film -> film.getKey() + " stars the actors "
+ film.getValue().stream().map(Actor::getName).collect(Collectors.joining(", "))
)
.forEach(System.out::println);

Related

Convert list of complex objects to Map using Java 8

I have an array of subjects
List<String> subjects = Arrays.asList(“physics”, “maths”);
I wanted to create a dummy list of users for each of these subjects and add them to a map with key as subject and value as List
Something like
Map<String,List<User>> userMap = new HashMap<>();
for(String subject: subjects){
List<User> users = new ArrayList<User>();
for(int i=0;i<10;i++){
User user = new User(“first name”+i+subject,”last name”+i+subject);
users.add(user);
}
userMap.put(subject,users);
}
I wanted to try this with the Java 8. Just tried something below, but doesn’t look like the right way.
subjects.stream().map((subjectName)->{
List<User> userList = new ArrayList<User>();
for(int i=0;i<10;i++){
User user = new User(“first name”+i+subject,”last name”+i+subject);
userList.add(user);
}
})
subjects.stream()
.map(subjectName -> {
List<User> users = .... create the users;
return new SimpleEntry<>(subjectName, users);
})
.collect(Collectors.toMap(Entry::getKey, Entry::getValue))
This would be one way if you really wanted to do it with java-8 and streams. One improvement would be to have a method that takes a String subjectName and create that Entry for example:
private static Entry<String, List<User>> createEntry(String subjectName) {
List<User> users = .... create the user;
return new SimpleEntry<>(subjectName, users);
}
And use it with:
subjects.stream()
.map(YourClass::createEntry)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue))
Just notice that your loop is the cleanest way to do it IMO
One way to do it with java 8:
Map<String,List<User>> userMap = new HashMap<>();
subjects.forEach(s -> {
for (int i = 0; i < 10; i++)
userMap.computeIfAbsent(s, k -> new ArrayList<>())
.add(new User("first name" + i + subject, "last name" + i + subject));
});
Let's do this one step at a time. First, the inner loop for creating 10 users can be written with streams as:
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
List<User> userList = IntStream.range(0, 10)
.mapToObj(i -> new User("first name" + i + subject, "last name" + i + subject)
.collect(toList());
And the outer loop can be written as
subjects.stream()
.collect(toMap(
subject -> subject, // key for the map is the subject
subject -> ... // whatever should be the value of the map
));
And now we can put it together:
Map<String, List<User>> userMap = subjects.stream()
.collect(toMap(
subject -> subject,
subject -> IntStream.range(0, 10)
.mapToObj(i -> new User("first name" + i + subject, "last name" + i + subject))
.collect(toList())
));

About Change a string list to Map List of each element and others

There is a list: ["aaa", "bbb", "ccc"]
I want to transform it to below structure
[{
main = aaa,
others = [bbb, ccc]
}, {
main = bbb,
others = [aaa, ccc]
}, {
main = ccc,
others = [aaa, bbb]
}]
Now my way is
List<String> list = newArrayList("aaa", "bbb", "ccc");
List<ImmutableMap<String, Object>> mainAndOthersList = list.stream().map(s -> ImmutableMap.of("main", s, "others", list.stream().filter(s2 -> !s2.equals(s)).collect(toList()))).collect(toList());
System.out.println(mainAndOthersList);
Are there some other manner to implement it?
Here is an alternative without streams, that is subjectively easier or harder to read.
List<String> list = Arrays.asList("aaa", "bbb", "ccc");
List<Map<String,Object>> result = new ArrayList<>();
for(int i = 0; i < list.size(); i++) {
Map<String,Object> map = new HashMap<>();
List<Object> innerList = new ArrayList<>(list);
map.put((String)innerList.remove(i), innerList);
result.add(map);
}
If your elements inside the List would not be String(s) and you could use java-9, you can basically do the same thing streaming on indexes:
List<Map<String, Object>> result = IntStream.range(0, list.size())
.mapToObj(x -> Map.of(
"main",
list.get(x),
"others",
IntStream.range(0, list.size()).filter(y -> y != x).mapToObj(list::get).collect(Collectors.toList())))
.collect(Collectors.toList());
But notice, this is close to the same thing you are already doing.

Map first element of stream differently than rest

Is there a way in Java's Stream API to map first element of stream differently than other?
Equivalent of this code:
List<Bar> barList = new ArrayList<>();
for (int i=0; i<fooList.size(); i++) {
Foo foo = fooList.get(i);
Foo modifiedFoo = foo.getModifiedFoo();
if (i == 0) {
barList.add(new Bar(modifiedFoo, false));
}else {
barList.add(new Bar(modifiedFoo, true));
}
}
Stream<Bar> = barList.stream();
Note: I already have a stream setup and I would want some operation after first mapping
fooList.stream()
.map(Foo::getModifiedFoo)
.(May be Some operation here to get different new Bar for first modifiedFoo)
.map(modifiedFoo -> new Bar(modifiedFoo, true));
I would get the first element, create a Stream out of it and apply the needed mappings. Then, I'd take the rest of the list, create a stream out of it and apply the different mappings. Then concat the streams. Something like this:
Stream<Bar> first = Stream.of(fooList.get(0))
.map(Foo::getModifiedFoo)
.map(modifiedFoo -> new Bar(modifiedFoo, false));
Stream<Bar> others = fooList.subList(1, fooList.size()).stream()
.map(Foo::getModifiedFoo)
.map(modifiedFoo -> new Bar(modifiedFoo, true));
Stream<Bar> bars = Stream.concat(first, others).flatMap(s -> s);
Another approach:
Stream<Bar> bars = IntStream.range(0, fooList.size())
.mapToObj(i -> new Bar(fooList.get(i).getModifiedFoo(), i > 0));
This way is succinct and does the job pretty well.
Use an IntStream to iterate over the indices, then mapToObj to create an object for that index, and finally collect into a list:
List<Bar> barList = IntStream.range(0, fooList.size())
.mapToObj(i -> (i == 0 ? new Bar (fooList.get(i), false) :
new Bar(fooList.get(i),true)))
.collect(Collectors.toList());
What would be more readable though, is doing the first item handling outside the loop, and using IntStream starting with 1.
Here is a demo using simple lists.
I can propose two ways but I find your way straighter.
With IntStream such as :
List<Bar> barList = new ArrayList<>();
IntStream.range(0, fooList.size())
.forEach(i->{
if (i == 0) {
barList.add(new Bar(foo, false));
}else {
barList.add(new Bar(foo, true));
}
}
);
It is not a real functional approach (forEach() use and no Collector) because it maintains the current index of the List.
As alternative, you could use a more functional approach but I don't find it straighter either :
List<Bar> barList = IntStream.range(0, fooList.size())
.mapToObj(i->{
Foo foo = fooList.get(i);
if (i == 0) {
return new Bar(foo, false);
}
return new Bar(foo, true));
})
.collect(Collectors.toList());
Although I think the accepted answer is better, here is an alternate approach.
int[] counter = {-1};
Stream<Bar> barListStream = fooList.stream().map(foo -> {
counter[0]++;
return new Bar(mfoo.getModifiedFoo(), counter[0]>0);
}).collect(Collectors.toList()).stream();
You can have an object to hold a flag e.g. AtomicBoolean or AtomicInteger - that you can reset on first or nth iteration (you would need something like AtomicInteger or some Integer holder to reset on nth iteration) e.g. following code using HashMap as the holder class - will print first line of the stream differently than the other lines:
Map<String, Boolean> firstTime = new HashMap<>(Map.of("firstTime", true)); // to make the map modifiable
try (Stream<String> lines = Files.lines(Paths.get(filename), Charset.defaultCharset())) {
lines.forEachOrdered(line -> System.out.println(firstTime.remove("firstTime") != null ? ("firstTime: " + line) : line));
}
Use an AtomicBoolean initially set to true to determine when is the first item.
final AtomicBoolean first = new AtomicBoolean(true);
System.out.println("** Print all numbers 1..10");
IntStream.range(1, 11).forEach(number -> {
System.out.print((first.get() ? "" : ",") + number);
first.set(false);
});
System.out.println();

How to collect map from the Set of objects that has a list using Collectors.toMap

I have class Element with a list, my intended output is like this:
Map<String , List<Element>>
{
1 = [Element3, Element1],
2 = [Element2, Element1],
3 = [Element2, Element1], 4=[Element2]
}
And my input is set of element objects, I used forEach to get the desired outcome, but I'm looking for how to collect it using collectors.toMap. Any inputs are much appreciated
Set<Element> changes = new HashSet();
List<String> interesetList = new ArrayList();
interesetList.add("1");
interesetList.add("2");
interesetList.add("3");
Element element = new Element(interesetList);
changes.add(element);
interesetList = new ArrayList();
interesetList.add("2");
interesetList.add("3");
interesetList.add("4");
element = new Element(interesetList);
changes.add(element);
Map<String, List<Element>> collect2 = new HashMap();
changes.forEach(element -> {
element.getInterestedList().forEach(tracker -> {
collect2.compute(tracker, ( key , val) -> {
List<Element> elementList = val == null ? new ArrayList<Element>() : val;
elementList.add(Element);
return elementList;
});
});
});
class Element {
List<String> interestedList;
static AtomicInteger sequencer = new AtomicInteger(0);
String mName;
public Element(List<String> aList) {
interestedList = aList;
mName = "Element" + sequencer.incrementAndGet();
}
public List<String> getInterestedList() {
return interestedList;
}
#Override
public String toString() {
return mName;
}
}
You can do it by using Collectors.groupingBy instead of Collectors.toMap, along with Collectors.mapping, which adapts a collector to another collector:
Map<String, List<Element>> result = changes.stream()
.flatMap(e -> e.getInterestedList().stream().map(t -> Map.entry(t, e)))
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
You need to use the Stream.flatMap method first and then pair the elements of the inner lists with the current Element instance. I did this via the new Java 9's Map.entry(key, value) method. If you're not on Java 9 yet, you could change it to new AbstractMap.SimpleEntry<>(key, value).
After flatmapping, we need to collect instances of Map.Entry. So I'm using Collectors.groupingBy to classify entries by key (where we had previously stored each element of the inner lists, aka what you call tracker in your code). Then, as we don't want to have instances of List<Map.Entry<String, Element>> as the values of the map, we need to transform each Map.Entry<String, Element> of the stream to just Element (that's why I'm using Map.Entry::getValue as the first argument of Collectors.mapping). We also need to specify a downstream collector (here Collectors.toList()), so that the outer Collectors.groupingBy collector knows where to place all the adapted elements of the stream that belong to each group.
A shorter and surely more efficient way to do the same (similar to your attempt) could be:
Map<String, List<Element>> result = new HashMap<>();
changes.forEach(e ->
e.getInterestedList().forEach(t ->
result.computeIfAbsent(t, k -> new ArrayList<>()).add(e)));
This uses Map.computeIfAbsent, which is a perfect fit for your use case.

Filter and Sort internal Map java

I have class Person
private String name;
private int age;
private Map<String, LocalDate> carsBoughWithDate;
You can ignore name and age. The important one here is carsBoughWithDate
Due to some reason I am saving person cars bough in a map with the date
Test Data
Map<String, LocalDate> carsbought = new HashMap<>();
carsbought.put("Toyota", LocalDate.of(2017, 2, 1));
carsbought.put("Corolla", LocalDate.of(2017, 2, 1));
Person john = new Person("John", 22, carsbought);
carsbought = new HashMap<>();
carsbought.put("Vauxhall", LocalDate.of(2017, 1, 1));
carsbought.put("BMW", LocalDate.of(2017, 1, 1));
carsbought.put("Toyota", LocalDate.of(2017, 1, 1));
Person michael = new Person("Michael", 44, carsbought);
List<Person> personList = new ArrayList<>();
personList.add(john);
personList.add(michael);
Output:
[Person{name='John', age=22, carsBoughWithDate={Toyota=2017-02-01, Corolla=2017-02-01}},
Person{name='Michael', age=44, carsBoughWithDate={Vauxhall=2017-01-01, Toyota=2017-01-01, BMW=2017-01-01}}]
Now, I have to find out the person which has bought cars but then sort the person who bought the car earliest on the top in the list
Example: search person who has cars "Toyota" or BMW
This is what I have done
**
System.out.println("Before sort >" + personList);
List<Person> sortedList = Lists.newArrayList();
HashMap<LocalDate, Person> collect = Maps.newHashMap();
for (Person person : personList) {
Map<String, LocalDate> docCarsBoughWithDate = person.getCarsBoughWithDate();
collect.putAll(docCarsBoughWithDate.entrySet().stream()
.filter(map -> Lists.newArrayList("Toyota", "BMW").contains(map.getKey()))
.collect(HashMap::new,
(m, v) -> m.put(
v.getValue(),
person),
HashMap::putAll
));
}
Map<String, List<Person>> collect1 = collect.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(m -> m.getValue()).collect(Collectors.groupingBy(Person::getName));
collect1.keySet().forEach(key -> sortedList.add(collect1.get(key).get(0)));
System.out.println("after sort > " + sortedList
);
This all works
Before sort >
[Person{name='John', age=22, carsBoughWithDate={Toyota=2017-02-01, Corolla=2017-02-01}}, Person{name='Michael', age=44, carsBoughWithDate={Vauxhall=2017-01-01, Toyota=2017-01-01, BMW=2017-01-01}}]
after sort >
[Person{name='Michael', age=44, carsBoughWithDate={Vauxhall=2017-01-01, Toyota=2017-01-01, BMW=2017-01-01}}, Person{name='John', age=22, carsBoughWithDate={Toyota=2017-02-01, Corolla=2017-02-01}}]
I feel this is bit cumbersome. Can I simplify the logic?
Here you go:
List<Person> sortedList = personList.stream() //
.flatMap(p -> p.getCarsBoughWithDate().entrySet().stream() //
.filter(e -> targetCarNames.contains(e.getKey())) // filter the bought cars which are in the target bought cars.
.sorted(Entry.comparingByValue()).limit(1) // sorted and only fetch the entry with earliest bought date.
.map(e -> new SimpleEntry<>(p, e.getValue()))) // composite a new entry with the person and the earliest bought date.
.sorted(Entry.comparingByValue()).map(e -> e.getKey()).collect(toList()); //
First of all, are you sure that "this all works"? I tried your code with your test data with the following additional person:
carsbought = new HashMap<>();
carsbought.put("BMW", LocalDate.of(2017, 2, 1));
Person sally = new Person("Sally", 25, carsbought);
and she overwrote John because she happened to have bought a car at the same date.
Second, the strategy to solve complex problems is to break them down into simpler problems. For example, I would first add a method which determines the first date at which a person bought one of a set of cars:
private Optional<LocalDate> firstDateOf(Person person, Collection<String> cars)
{
return person.getCarsBoughWithDate().entrySet().stream()
.filter(e -> cars.contains(e.getKey()))
.map(Map.Entry::getValue)
.min(Comparator.naturalOrder());
}
This will be the sort key of the people. Then use this method to map each person to the sort key and finally sort the list:
List<Person> sortCarOwners(Collection<Person> people, Collection<String> cars)
{
Map<Person, Optional<LocalDate>> personToDateMap = people.stream()
.collect(Collectors.toMap(p -> p, p -> firstDateOf(p, cars)));
return personToDateMap.entrySet().stream()
.filter(e -> e.getValue().isPresent())
.sorted(Comparator.comparing(e -> e.getValue().get()))
.map(e -> e.getKey())
.collect(Collectors.toList());
}
I don't know if you consider this "less cumbersome", but I hope it helps.

Resources