Matching 2 ArrayLists of objects based on their member variables - java

Let's say I have 2 objects, Object1 and Object2. Their basic structure is as follows:
Object 1
int id
String email
Object 2
int id
ArrayList<String> emails
Now I have 2 ArrayLists, one of Object1 and Object2. What's an efficient way to find matches where Object 1's email is contained inside Object 2's emails ArrayList and then store their ids in a HashMap (or any other data structure that holds 2 ints)?
I know the obvious and basic solution is to brute force it with 2 for loops, like this:
ArrayList<Object1> obj1List;
ArrayList<Object2> obj2List;
HashMap<Integer, Integer> idMapping = new HashMap()<>;
for (Object1 obj1 : obj1List){
String obj1Email = obj1.getEmail();
for (Object2 obj2 : obj2List){
ArrayList<String> obj2EmailList = obj2.getEmails();
if(obj2EmailList.contains(obj1Email)){
int obj1Id = obj1.getId();
int obj2Id = obj2.getId();
idMapping.put(obj1Id, obj2Id);
}
}
}
Each ArrayList has around a thousand objects, so performance really isn't that big of an issue. However, I'm sure there's much more elegant ways of solving this problems. I'm guessing it might be possible using streams, but I'm not familiar enough with them to do it. Any suggestions?

I think the way you are doing is absolutely fine. However, if you want to use streams, you can try this approach.
Since you are going to traverse across the List of Object2 pretty frequently for every Object1, I would recommend creating a Map for Object2 List so that your retrieval is faster.
Based on your code, I think I can map each email to it's individual id assuming an email in Object2 List can never have more than 1 id which I think is true based on your implementation.
If my assumption is wrong, there is nothing much we can do.
Here is the code:
Map<String, Integer> obj2Map = new HashMap<>();
for (Object2 obj2 : obj2List) {
int id = obj2.getId();
obj2Map.putAll(
obj2.getEmails()
.stream()
.collect(Collectors.toMap(String::toString, email -> id))
);
}
Map<Integer, Integer> idMapping = new HashMap();
for (Object1 obj1 : obj1List) {
if (obj2Map.containsKey(obj1.getEmail())) {
idMapping.put(obj1.getId(), obj2Map.get(obj1.getEmail()));
}
}

Map<Integer, Integer> collect = object1List.stream()
.flatMap(ob1 -> object2List.stream().filter(ob2 -> ob2.getEmails().contains(ob1.getEmail())).map(ob2 -> {
int [] arr = new int[2];
arr[0] = ob1.getId();
arr[1] = ob2.getId();
return arr;
}))
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
you could use flatMap to flatten the list of emails in the second Object and compare to the first object using ArrayList contains method.
the above code is the stream version of your code however there could possibly be duplicate keys in the map.

As idMapping I would recommend use Map<Integer, List<Integer>>. There could be more ids in obj2List witch the same email. For example with this data:
List<Object1> obj1List = Arrays.asList(
new Object1(1, "a"),
new Object1(2, "b"),
new Object1(3, "c"));
List<Object2> obj2List = Arrays.asList(
new Object2(11, "a"),
new Object2(12, "a", "b"),
new Object2(14, "c", "d"),
new Object2(15, "e", "f")
);
When HashMap<Integer, Integer> idMapping you get:
{1=12, 2=12, 3=14}
But for Map<Integer, List<Integer>> idMapping it will be:
{1=[11, 12], 2=[12], 3=[14]}
The main solution could be like below. Create map (grouping by email) and then use it to create idMapping :
Map<String, List<Integer>> idsForEmailFromObj2List = obj2List.stream().
flatMap(obj2 -> obj2.getEmails().stream().map(
email -> new Object1(obj2.getId(), email)
))
.collect(Collectors.groupingBy(
Object1::getEmail,
Collectors.mapping(Object1::getId, Collectors.toList())));
Map<Integer, List<Integer>> idMapping = obj1List.stream()
.filter(obj1 -> idsForEmailFromObj2List.containsKey(obj1.getEmail()))
.collect(Collectors.toMap(
Object1::getId,
obj1 -> idsForEmailFromObj2List.get(obj1.getEmail())));

Related

Java group a map by value where value is a List

I have a
Map<String,List<User>>map = new HashMap<>();
map.put("projectA",Arrays.asList(new User(1,"Bob"),new User(2,"John"),new User(3,"Mo")));
map.put("projectB",Arrays.asList(new User(2,"John"),new User(3,"Mo")));
map.put("projectC",Arrays.asList(new User(3,"Mo")));
Can use String instead of User.
String is a project Name but the same users can relate to different projects.
I would like to get sth like Map<User, List<String>> where
the key will represent a distinct user and a value as a list of projects' names to which he/she relates.
Bob = [projectA]
John = [projectA, projectB]
Mo = [projectA, projectB, projectC]
TQ in advance for any piece of advice.
Just loop over the map's entries and the List inside of them:
public static void main(String[] args) {
Map<String, List<User>> map = new HashMap<>();
map.put("projectA", Arrays.asList(new User(1,"Bob"),new User(2,"John"),new User(3,"Mo")));
map.put("projectB",Arrays.asList(new User(2,"John"),new User(3,"Mo")));
map.put("projectC",Arrays.asList(new User(3,"Mo")));
Map<User, List<String>> result = new HashMap<>();
for(Map.Entry<String, List<User>> e:map.entrySet()) {
for(User u:e.getValue()) {
result.putIfAbsent(u, new ArrayList<>());
result.get(u).add(e.getKey());
}
}
System.out.println(result);
}
public static record User(int id, String name) {}
prints
{User[id=1, name=Bob]=[projectA], User[id=2, name=John]=[projectB, projectA], User[id=3, name=Mo]=[projectB, projectA, projectC]}
To reverse this Map, you need to iterate over its entries and for each distinct user create an entry containing a list of projects as a value in the resulting Map.
Java 8 computeIfAbsent()
This logic can be implemented using Java 8 methods Map.computeIfAbsent() and Map.forEach().
Map<String, List<User>> usersByProject = // initilizing the source map
Map<User, List<String>> projectsByUser = new HashMap<>();
usersByProject.forEach((project, users) ->
users.forEach(user -> projectsByUser.computeIfAbsent(user, k -> new ArrayList<>())
.add(project))
);
Stream API
Stream-based implementation would require a bit more effort.
The core logic remains the same. But there's one important peculiarity: we would need to generate from each entry of the source Map a sequence of new elements, containing references to a particular user and a project.
To carry this data we would need an auxiliary type, and a Java 16 record fits into this role very well. And quick and dirty alternative would be to use Map.Entry, but it's better to avoid resorting to this option because methods getKey()/getValue() are faceless, and it requires more effort to reason about the code. You can also define a regular class if you're using an earlier version of JDK.
public record UserProject(User user, String project) {}
That's how a stream-based solution might look like:
Map<String, List<User>> usersByProject = Map.of(
"projectA", List.of(new User(1, "Bob"), new User(2, "John"), new User(3, "Mo")),
"projectB", List.of(new User(2, "John"), new User(3, "Mo")),
"projectC", List.of(new User(3, "Mo"))
);
Map<User, List<String>> projectByUsers = usersByProject.entrySet().stream()
.flatMap(entry -> entry.getValue().stream().
map(user -> new UserProject(user, entry.getKey()))
)
.collect(Collectors.groupingBy(
UserProject::user,
Collectors.mapping(UserProject::project,
Collectors.toList())
));
projectsByUser.forEach((k, v) -> System.out.println(k + " -> " + v));
Output:
User[id=1, name=Bob] -> [projectA]
User[id=2, name=John] -> [projectA, projectB]
User[id=3, name=Mo] -> [projectA, projectC, projectB]

get an ordered list of objects from 2 maps sharing same properties in JAVA

I have a map with order(rank) I expect
sortedMap{deviceId:rank}:
{0:"id0"},
{1,"id1"},
{2:"id2"},
{3:"id3"},
having another map that needs to be sorted
{"id3":object3},
{"id1":object1},
{"id0":object0},
{"id2":object2}
I expected having list like
{object0},
{object1},
{object2},
{object3}
i tried with code like below, but it seems wrong with the list adding part in my code, and i don't know any better solution for it:
private List<MyObject> getSortedResultList(Map<String, Integer> sortedMap, Map<String, Object> toSortMap) {
List<MyObject> sortedResultList = null;
for (Integer rank : sortedMap.keySet()) {
for (int i = 0; i < sortedMap.size(); i++) {
sortedResultList.add(i, (MyObject) toSortMap.get(sortedMap.get(rank)));
}
}
return sortedResultList;
}
Tried searched online for hours, got no clues to solve it because of my weak datastructure knowledge and decided asking for help from here, thanks advanced if any suggestions could be raised.
Here is one way to do it:
List<Object> sortedList = sortedMap.entrySet().stream()
.sorted(Comparator.comparingInt(Map.Entry::getValue))
.map(entry -> toSortMap.get(entry.getKey()))
.filter(Objects::nonNull)
.collect(Collectors.toList());
First the entries are sorted, then we get the corresponding object, then we filter out the null values and finally we collect everything in a list.
If sortedMap is sorted by key (e.g. it's a TreeMap) it should suffice to build the list sorted by the sortedMap's values:
private static List<MyObject> getSortedResultList(TreeMap<Integer, String> sortedMap, Map<String, MyObject> toSortMap) {
return sortedMap.values().stream()
.map(key -> toSortMap.get(key))
.collect(Collectors.toList());
}
If sortedMap needs to be sorted additionally:
private static List<MyObject> getSortedResultList2(Map<Integer, String> sortedMap, Map<String, MyObject> toSortMap) {
return sortedMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> toSortMap.get(entry.getValue()))
.collect(Collectors.toList());
}
Test:
Map<Integer, String> sorted = new TreeMap<>(Map.of(1, "id1", 0, "id0", 3, "id3", 2, "id2"));
Map<String, MyObject> toSort = Map.of(
"id3", new MyObject("object3"), "id1", new MyObject("object1"),
"id0", new MyObject("object0"), "id2", new MyObject("object2")
);
System.out.println(getSortedResultList(sorted, toSort));
System.out.println(getSortedResultList2(sorted, toSort));
Both outputs identical:
[MyObject(field=object0), MyObject(field=object1), MyObject(field=object2), MyObject(field=object3)]
[MyObject(field=object0), MyObject(field=object1), MyObject(field=object2), MyObject(field=object3)]

Pluck only relevent keys from a map

I have an int arry input, for example : [1,3,4].
I also have a fixed/constant map of :
1 -> A
2 -> B
3 -> C
4 -> D
5 -> E
I want my output to be a corresponding string of all the relevant keys.
For our example of input [1,3,4], the output should be : "A,C,D".
What's the most efficient way of achieving that?
My idea was to iterate over the whole map, each time.
The problem with that, is that I have a remote call in android that fetches a long list of data items, and doing that for each item in the list seems a bit.. inefficient. Maybe there's something more efficient and/or more elegant. Perhaps using Patterns
Assuming the array is defined as below along with the HashMap:
int arr[] = { 1, 3, 4 };
HashMap<Integer, String> hmap = new HashMap<>();
// data in the map
hmap.put(1, "A"); hmap.put(2, "B"); hmap.put(3, "C"); hmap.put(4, "D"); hmap.put(5, "E");
Instead of iterating over the entire map, you can iterate over the array
String[] array = Arrays.stream(arr).mapToObj(i -> hmap.get(i))
.filter(Objects::nonNull)
.toArray(String[]::new);
This gives the output :
A C D
As per your comment, to join it as one String you can use :
String str = Arrays.stream(arr).mapToObj(i -> hmap.get(i))
.filter(Objects::nonNull)
.collect(Collectors.joining("/"));
You can iterate over the list of input instead of a map. That's the benefit of using a Map by the way, it has a lookup time of O(1) with proper hashing implemented. It would somewhat like :
Map<Integer, String> data = new HashMap<>();
List<Integer> query = new ArrayList<>(); // your query such as "1,3,4"
List<String> information = new ArrayList<>();
for (Integer integer : query) {
String s = data.get(integer); // looking up here
information.add(s);
}
which with the help of java-stream can be changed to
List<String> information = query.stream()
.map(data::get) // looking up here
.collect(Collectors.toList()); // returns "A,C,D"
Note: I have used String and Integer for just a representation, you can use your actual data types there instead.

Converting List of object to Guava Table data structure using lambda

I have list of ImmutableTriple object where for first and middle there could collection of last values (first, middle and last are triple values).
Now in order to make it queryable, I need to convert it to Guava Table data structure. I am able to achieve this, with for loop as below but I am wondering if I can achieve this more functionally using lambda expression.
here is the piece code -
public static void main(String[] args) {
//In real world, this list is coming from various transformation of lamda
final List<ImmutableTriple<LocalDate, Integer, String>> list = ImmutableList.of(
ImmutableTriple.of(LocalDate.now(), 1, "something"),
ImmutableTriple.of(LocalDate.now(), 1, "anotherThing")
);
Table<LocalDate, Integer, List<String>> table = HashBasedTable.create();
//is it possible to avoid this forEach and use side effect free lambda.
list.forEach(s -> {
final List<String> strings = table.get(s.left, s.middle);
final List<String> slotList = strings == null ? new ArrayList<>() : strings;
slotList.add(s.right);
table.put(s.left, s.middle, slotList);
});
System.out.println(table);
}
There is a Tables class which contains a Collector to get your desired result.
Table<LocalDate, Integer, ImmutableList<String>> collect = list.stream()
.collect(Tables.toTable(
it -> it.left,
it -> it.middle,
it -> ImmutableList.of(it.right),
(l1, l2) -> ImmutableList.<String>builder()
.addAll(l1).addAll(l2).build(),
HashBasedTable::create));
If you really want a mutable List then you can use:
Table<LocalDate, Integer, List<String>> collect = list.stream()
.collect(Tables.toTable(
it -> it.left,
it -> it.middle,
it -> Lists.newArrayList(it.right),
(l1, l2) -> {l1.addAll(l2); return l1;},
HashBasedTable::create));

Java 8 lambda Expression for reading contents of the list and storing the counts of repeating values in list to a map [duplicate]

I want to collect the items in a stream into a map which groups equal objects together, and maps to the number of occurrences.
List<String> list = Arrays.asList("Hello", "Hello", "World");
Map<String, Long> wordToFrequency = // what goes here?
So in this case, I would like the map to consist of these entries:
Hello -> 2
World -> 1
How can I do that?
I think you're just looking for the overload which takes another Collector to specify what to do with each group... and then Collectors.counting() to do the counting:
import java.util.*;
import java.util.stream.*;
class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Hello");
list.add("Hello");
list.add("World");
Map<String, Long> counted = list.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(counted);
}
}
Result:
{Hello=2, World=1}
(There's also the possibility of using groupingByConcurrent for more efficiency. Something to bear in mind for your real code, if it would be safe in your context.)
Here is example for list of Objects
Map<String, Long> requirementCountMap = requirements.stream().collect(Collectors.groupingBy(Requirement::getRequirementType, Collectors.counting()));
Here are slightly different options to accomplish the task at hand.
using toMap:
list.stream()
.collect(Collectors.toMap(Function.identity(), e -> 1, Math::addExact));
using Map::merge:
Map<String, Integer> accumulator = new HashMap<>();
list.forEach(s -> accumulator.merge(s, 1, Math::addExact));
List<String> list = new ArrayList<>();
list.add("Hello");
list.add("Hello");
list.add("World");
Map<String, List<String>> collect = list.stream()
.collect(Collectors.groupingBy(o -> o));
collect.entrySet()
.forEach(e -> System.out.println(e.getKey() + " - " + e.getValue().size()));
Here is the simple solution by StreamEx:
StreamEx.of(list).groupingBy(Function.identity(), MoreCollectors.countingInt());
This has the advantage of reducing the Java stream boilerplate code: collect(Collectors.
If you're open to using a third-party library, you can use the Collectors2 class in Eclipse Collections to convert the List to a Bag using a Stream. A Bag is a data structure that is built for counting.
Bag<String> counted =
list.stream().collect(Collectors2.countBy(each -> each));
Assert.assertEquals(1, counted.occurrencesOf("World"));
Assert.assertEquals(2, counted.occurrencesOf("Hello"));
System.out.println(counted.toStringOfItemToCount());
Output:
{World=1, Hello=2}
In this particular case, you can simply collect the List directly into a Bag.
Bag<String> counted =
list.stream().collect(Collectors2.toBag());
You can also create the Bag without using a Stream by adapting the List with the Eclipse Collections protocols.
Bag<String> counted = Lists.adapt(list).countBy(each -> each);
or in this particular case:
Bag<String> counted = Lists.adapt(list).toBag();
You could also just create the Bag directly.
Bag<String> counted = Bags.mutable.with("Hello", "Hello", "World");
A Bag<String> is like a Map<String, Integer> in that it internally keeps track of keys and their counts. But, if you ask a Map for a key it doesn't contain, it will return null. If you ask a Bag for a key it doesn't contain using occurrencesOf, it will return 0.
Note: I am a committer for Eclipse Collections.

Categories

Resources