How to join two lists by property in java
Overview
Occasionally, we need to establish associations between objects in two lists or collections based on a certain property, similar to one-to-one or one-to-many relationships in a database. While using SQL statements can directly handle such associations, there are situations where we prefer to retrieve two lists and manually establish the relationships using Java code. However, this often leads to repetitive code. Is it possible to create a relatively generic method for this purpose? That is the goal of this article.
Scenario Description
Assuming there are two tables: user table and order table, with the following data:
In Java code, we can define User and Order as entity classes for the tables like following.
// getter and setter omitted
// constructor omitted
public class User {
private int id;
private String name;
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public class Order {
private int id;
private int userId;
private String product;
@Override
public String toString() {
return "Order{" +
"id=" + id +
", userId=" + userId +
", product='" + product + '\'' +
'}';
}
}
Reading data from db is beyond the the scope of this article, so just assuming we have already retrieved the data from the user table and the order table into the 'users' and 'orders' lists respectively, now we need to establish association between the objects in these two lists.
List<User> users = new ArrayList<>();
// simulate read data from user table
users.add(new User(1, "lucy"));
users.add(new User(2, "john"));
// simulate red data from order table
List<Order> orders = new ArrayList<>();
orders.add(new Order(1, 2, "laptop"));
orders.add(new Order(2, 1, "mouse"));
// we need implement a join method which match each user with his orders
Join implementation is just simple, so we give it directly.
// if the relationship between user and order is one to one, we can map user id to order using java stream
Map<Integer, Order> userId2OrderMap = orders.stream().collect(Collectors.toMap(Order::getUserId, e -> e));
for (User user : users) {
int userId = user.getId();
// for each user, we can get the related order by user id
Order order = userId2OrderMap.get(userId);
}
Although it quite simple, but quite repective. We can do it better by writing a util method to handle the join.
One to one join
import java.util.Collection;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
class JoinUtil {
/**
* Join two collections which has one to one relationship.
*
* @param main the first collection
* @param sub the second collection to be joined with first collection
* @param mainKeyMapper the mapper to the first collection to get the join value
* @param subKeyMapper the mapper to convert the second collection to a map
* @param pairHandler callback method to receive a pair of matched elements
* @param <T> the element type of the first collection
* @param <E> the element type of the second collection
* @param <K> the type of the join key
*/
public static <T, E, K> void join(Collection<T> main,
Collection<E> sub,
Function<? super T, K> mainKeyMapper,
Function<? super E, K> subKeyMapper,
BiConsumer<? super T, ? super E> pairHandler) {
Map<K, E> map = sub.stream().collect(Collectors.toMap(subKeyMapper, e -> e, (a, b) -> a));
for (T t : main) {
K k = mainKeyMapper.apply(t);
E e = map.get(k);
pairHandler.accept(t, e);
}
}
}
With the join method in hand, we can join the users and orders with one line of code.
JoinUtil.join(users, orders, User::getId, Order::getUserId, (user, order) -> {
System.out.println(user + "matched order is: " + order);
});
// the output will be
User{id=1, name='lucy'}matched order is: Order{id=2, userId=1, product='mouse'}
User{id=2, name='john'}matched order is: Order{id=1, userId=2, product='laptop'}
One to many join
One to one join is easy, one two many join is no much harder, let's implement it.
// if the relationship between user and order is one to many, we can map user id to orders using java stream
Map<Integer, List<Order>> userId2OrdersMap = orders.stream().collect(Collectors.groupingBy(Order::getUserId));
for (User user : users) {
int userId = user.getId();
// for each user, we can get the related orders by user id
List<Order> orders = userId2OrdersMap.get(userId);
}
Now we can define a util method named joinM to handle one to many join.
/**
* Join two collections which has one to many relationship.
*
* @param main the first collection
* @param sub the second collection to be joined with first collection
* @param mainKeyMapper the mapper to the first collection to get the join value
* @param subKeyMapper the mapper to convert the second collection to a map
* @param pairHandler callback method to receive a pair of matched elements. the second element of each pair will be a List or null.
* @param <T> the element type of the first collection
* @param <E> the element type of the second collection
* @param <K> the type of the join key
*/
public static <T, E, K> void joinM(Collection<T> main,
Collection<E> sub,
Function<? super T, K> mainKeyMapper,
Function<? super E, K> subKeyMapper,
BiConsumer<? super T, List<? super E>> pairHandler) {
Map<K, List<E>> map = sub.stream().collect(Collectors.groupingBy(subKeyMapper));
for (T t : main) {
K k = mainKeyMapper.apply(t);
List<E> e = map.get(k);
pairHandler.accept(t, e);
}
}
With joinM above, let's join the users and orders
JoinUtil.joinM(users, orders, User::getId, Order::getUserId, (user, matchOrders) -> {
System.out.println(user + "matched orders is: " + matchOrders);
});
// 输出如下
User{id=1, name='lucy'} matched orders is: [Order{id=2, userId=1, product='mouse'}]
User{id=2, name='john'} matched orders is: [Order{id=1, userId=2, product='laptop'}]
Conclusion
I've searched everywhere to find utility methods for joining two collections, but with no luck, so i rolled my own. If there are any problems, please let me know. You can read more my articles here.
温馨提示:反馈需要登录