Java Stream - group by when key appears in a list - java-8

I am trying to group by a collection by a value that appears in my object as a list.
This is the model that I have
public class Student {
String stud_id;
String stud_name;
List<String> stud_location = new ArrayList<>();
public Student(String stud_id, String stud_name, String... stud_location) {
this.stud_id = stud_id;
this.stud_name = stud_name;
this.stud_location.addAll(Arrays.asList(stud_location));
}
}
When I initialize it with the following :
List<Student> studlist = new ArrayList<Student>();
studlist.add(new Student("1726", "John", "New York","California"));
studlist.add(new Student("4321", "Max", "California"));
studlist.add(new Student("2234", "Andrew", "Los Angeles","California"));
studlist.add(new Student("5223", "Michael", "New York"));
studlist.add(new Student("7765", "Sam", "California"));
studlist.add(new Student("3442", "Mark", "New York"));
I want to get the following :
California -> Student(1726),Student(4321),Student(2234),Student(7765)
New York -> Student(1726),Student(5223),Student(3442)
Los Angeles => Student(2234)
I try to write the following
Map<Student, List<String>> x = studlist.stream()
.flatMap(student -> student.getStud_location().stream().map(loc -> new Tuple(loc, student)))
.collect(Collectors.groupingBy(y->y.getLocation(), mapping(Entry::getValue, toList())));
But I am having trouble completing it - how do I keep the original student after the mapping?

Summing up the comments above, the collected wisdom would suggest:
Map<String, List<Student>> x = studlist.stream()
.flatMap(student -> student.getStud_location().stream().map(loc -> new AbstractMap.SimpleEntry<>(loc, student)))
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, toList())));
As an alternate approach, if you don't mind the Students in each list containing only that location, you might consider flattening the Student list to Students with only one location:
Map<String, List<Student>> x = studlist.stream()
.flatMap( student ->
student.stud_location.stream().map( loc ->
new Student(student.stud_id, student.stud_name, loc))
).collect(Collectors.groupingBy( student -> student.stud_location.get(0)));

Related

Merge list of arrays in map with duplicate keys in java

I have multiple numbers[] arrays in the below request
I want to merge them into one map. If a key exists in multiple lists, in that case, I should merge arrays.
For example, the Map should be like:
{60075=[100,200,500,600], 60076=[700,600]}
Can someone help me with a way to complete my requirement?
Request:
"details": [
{
"productSku": "60075",
"numbers": [
"100",
"200"
]
},
{
"productSku": "60075",
"numbers": [
"500",
"600"
]
},
{
"productSku": "60076",
"numbers": [
"700",
"600"
]
}
]
I tried the below code but it's not working:
Map<String, List<String>> map1 = new HashMap<String, List<String>>();
for (Details details: detailsList) {
List<String> numbers = (map1.get(details.getProductSku()));
map1.put(details.getProductSku(), (numbers== null) ? numbers
: numbers.addAll(diohShippingDetails.getSerialNumbers()));
}
Your conditional (numbers== null) ? numbers: numbers.addAll(diohShippingDetails.getSerialNumbers()) has two problems.
When numbers is null, which is the initial state as the map is empty, it uses number as result, which has been proven to be null, so the result would always be null.
addAll does not return a list at all, but a boolean. You have to separate the expression to produce the map’s value from the addAll operation.
You can use something like
List<String> numbers = map1.get(details.getProductSku());
if(numbers == null) {
numbers = new ArrayList<>();
map1.put(details.getProductSku(), numbers);
}
numbers.addAll(diohShippingDetails.getSerialNumbers());
When map1.get(…) returned a non-null List, we don’t need to put it again, but can add new elements to it. Only when numbers is null, we need to create a new list and put it into the map.
Alternatively, you can use
Map<String, List<String>> map1 = new HashMap<>();
for(Details details: detailsList) {
map1.computeIfAbsent(details.getProductSku(), key -> new ArrayList<>())
.addAll(diohShippingDetails.getSerialNumbers());
}
computeIfAbsent will use the specified function (key -> new ArrayList<>()) to create and store a new value if none exists. It will return an already existing list or the list just created and stored. Therefore, you can unconditionally add the elements to the returned list.
Try this:
Map<Integer, List<Integer>> map =
details.stream()
.collect(
toMap(Details::productSku,
d -> d.getNumbers() == null ? Collections.emptyList() : d.getNumbers(),
(a, b) -> Stream.concat(a.stream(), b.stream()).collect(toList())
)
);

Java 8 stream map custom function and convert it to Map

I have the following object:
public class Book {
private Long id;
private Long bookId;
private String bookName;
private String owner;
}
Represented from following table:
Basically, a book can be owned by multiple owners i.e. Owner "a" owns books 1 and 2.
I have a basic function that will when passed a book object, will give its owner(s) in a List.
private List<String> getBookToOwner(Book book) {
List<String> a = new ArrayList<>();
if (book.getOwner() != null && !book.getOwner().isEmpty()) {
a.addAll(Arrays.asList(book.getOwner().split("/")));
}
return a;
}
I want to use that to apply to each book, retrieve their owners and create the following Map.
Map<String, List<Long>> ownerToBookMap;
Like this:
How do I use streams here?
//books is List<Book>
Map<String, List<Long>> ownerToBookMap = books.stream().map(
// apply the above function to get its owners, flatten it and finally collect it to get the above Map object
// Need some help here..
);
You can get the owner list from the book, then flatten the owners and map as pair of bookId and owner using flatMap. Then grouping by owner using groupingBy and collect the list of bookId of owner.
Map<String, List<Long>> ownerToBookMap =
books.stream()
.flatMap(b -> getBookToOwner(b)
.stream()
.map(o -> new AbstractMap.SimpleEntry<>(o, b.getBookId())))
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
Flatmap the owners into a single one, create entries with key as an single owner and value as a bookId. Then group the structure by the key (owner). Finally use Collectors::mapping to get the List of bookIds instead of the actual entries:
List<Book> books = ...
Map<String, List<Long>> booksByOwner = books.stream()
.flatMap(book -> Arrays.stream(book.getOwner().split("/"))
.map(owner -> new AbstractMap.SimpleEntry<>(owner, book.getBookId())))
.collect(Collectors.groupingBy(
AbstractMap.SimpleEntry::getKey,
Collectors.mapping(AbstractMap.SimpleEntry::getValue, Collectors.toList())));
I use reduce instead of map.
Map<String, List<Long>> ownerToBookMap = books.stream().reduce(
HashMap::new,
(acc,b) -> {
getBookToOwner(b).stream().forEach( o -> {
if (!acc.containsKey(o))
acc.put(o, new ArrayList<Long>());
acc.get(o).put(b.bookId);
});
return acc;
}
).get();

Efficient way to group by a given list based on a key and collect in same list java 8

I have the below class:
class A{
String property1;
String property2;
Double property3;
Double property4;
}
So the property1 and property2 is the key.
class Key{
String property1;
String property2;
}
I already have a list of A like below:
List<A> list=new ArrayList<>();
I want to group by using the key and add to another list of A in order to avoid having multiple items with same key in the list:
Function<A, Key> keyFunction= r-> Key.valueOf(r.getProperty1(), r.getProperty2());
But then while doing group by I have to take a sum of property3 and average of property4.
I need an efficient way to do it.
Note: I have skipped the methods of the given classes.
Collecting to a Map is unavoidable since you want to group things. A brute-force way to do that would be :
yourListOfA
.stream()
.collect(Collectors.groupingBy(
x -> new Key(x.getProperty1(), x.getProperty2()),
Collectors.collectingAndThen(Collectors.toList(),
list -> {
double first = list.stream().mapToDouble(A::getProperty3).sum();
// or any other default
double second = list.stream().mapToDouble(A::getProperty4).average().orElse(0D);
A a = list.get(0);
return new A(a.getProperty1(), a.getProperty2(), first, second);
})))
.values();
This could be slightly improved for example in the Collectors.collectingAndThen to only iterate the List once, for that a custom collector would be required. Not that complicated to write one...
Try like this:
Map<A,List<A>> map = aList
.stream()
.collect(Collectors
.groupingBy(item->new A(item.property1,item.property2)));
List<A> result= map.entrySet().stream()
.map(list->new A(list.getValue().get(0).property1,list.getValue().get(0).property1)
.avgProperty4(list.getValue())
.sumProperty3(list.getValue()))
.collect(Collectors.toList());
and create avgProperty4 and sumProperty3 methods like to this
public A sumProperty3(List<A> a){
this.property3 = a.stream().mapToDouble(A::getProperty3).sum();
return this;
}
public A avgProperty4(List<A> a){
this.property4 = a.stream().mapToDouble(A::getProperty4).average().getAsDouble();
return this;
}
result = aList.stream().collect(Collectors
.groupingBy(item -> new A(item.property1, item.property2),
Collectors.collectingAndThen(Collectors.toList(), list ->
new A(list.get(0).property1, list.get(0).property1)
.avgProperty4(list).sumProperty3(list))
)
);

java 8 list grouping with value mapping function producing list

I have a following Person class
public class Person {
public String name;
public List<Brand> brands;
//Getters
}
and a List<Person> persons(possibly with same names). I need to group in a map of <String, List<Brand>> with Person's name as Keys and lists of accumulated Brands as values.
Something like this
Map<String, List<List<String>>> collect = list.stream().collect(
groupingBy(Person::getName, mapping(Person::getBrands, toList()))
);
produces undesired result and I know why. If the values could be somehow flatten during grouping? Is there a way to do it right there with Streams api?
java 9 will add the flatMapping collector specifically for this type of task:
list.stream().collect(
groupingBy(
Person::getName,
flatMapping(
p -> p.getBrands().stream(),
toList()
)
)
Guessing what is the desired result, you can achieve it with just toMap collector:
Map<String, List<String>> collect = persons.stream().collect(
toMap(
Person::getName,
Person::getBrands,
(l1, l2) -> ImmutableList.<String /*Brand*/>builder().addAll(l1).addAll(l2).build())
);
You will need to merge brands into a single List:
list.stream().collect(Collectors.toMap(
Person::getName,
Person::getBrands,
(left, right) -> {
left.addAll(right);
return left;
},
HashMap::new));
You can create a custom collector for the downstream to your groupBy:
Collector.of(LinkedList::new,
(list, person) -> list.addAll(person.brands),
(lhs, rhs) -> { lhs.addAll(rhs); return rhs; })
There is MoreCollectors provided in open source library: StreamEx
list.stream().collect(
groupingBy(Person::getName, MoreCollectors.flatMapping(p -> p.getBrands().stream()));

Spring Data Neo4j : relationship in specific order

I'm using Spring Data Neo4j, I have two entities, Person and PhoneNumber. Person has a 1-N relationship with PhoneNumber. I'm looking for a way to always have this relationship in a specific order.
Here's the definition of my entities (those are Groovy classes) :
#NodeEntity
class Person {
#GraphId Long id
String name
#RelatedToVia
Set<NumberRel> numbers
}
#RelationshipEntity(type = "has_number")
class NumberRel {
#GraphId Long id
#StartNode Person person
#EndNode PhoneNumber number
int sequence
}
#NodeEntity
class PhoneNumber {
#GraphId Long id
String number
}
interface PhoneNumberRepository extends GraphRepository<PhoneNumber>, CypherDslRepository<PhoneNumber> {
/* Empty */
}
interface PersonRepository extends GraphRepository<Person>, CypherDslRepository<Person> {
Person findByName(String name)
}
Then I create and save some entities :
def num01 = new PhoneNumber(number: "0101")
phoneNumberRepository.save(num01)
def num02 = new PhoneNumber(number: "0202")
phoneNumberRepository.save(num02)
def alice = new Person(
name: "Alice",
numbers: new LinkedHashSet<NumberRel>()
)
alice.numbers << new NumberRel(
person: alice,
sequence: 10,
number: num01
)
alice.numbers << new NumberRel(
person: alice,
sequence: 20,
number: num02
);
personRepository.save(alice)
And that's where I'm stuck : I would like to tell Spring - or Neo4j - to load the NumberRel relationship ordered by the value of the sequence attribute. I cannot find a way to do that.
Right now, the order is random. The following code prooves it :
(1..3).each {
println "Looking for Alice, pass ${it}"
def p = personRepository.findByName("Alice")
p.numbers.each { number ->
print "${number.sequence} "
}
println()
}
that produces this output :
Looking for Alice, pass 1
10 20
Looking for Alice, pass 2
20 10
Looking for Alice, pass 3
10 20
So my question is : is there a way to have Person.numbers be ordered by NumberRel.sequence ?
if SDN can't sort the results, send the collection into a sort first -
(1..3).each {
println "Looking for Alice, pass ${it}"
def p = personRepository.findByName("Alice")
p.numbers.sort{it.sequence}
p.numbers.each { number ->
print "${number.sequence} "
}
println()
}

Resources