I have a List<Object[]>
where the Object[]
has the size of 3
and comes from a query. The result of the query is as follows:
| vehicleId | modelId | serviceId |
|------------------------------------|---------|-----------------|
|93bf3e92-7a37-4e23-836d-eed5a104341f| 214|80a7-ce5640b18879|
|b4066520-e127-44b7-bcc0-1d1187de559c| 214|80a7-ce5640b18879|
|ae6cb0fe-1501-4311-a2b4-cfb8b4f51ca4| 214|80a7-ce5640b18879|
|cf80ff11-6e23-4c19-8b6d-55d34d131566| 214|80a7-ce5640b18879|
It should be mapped in the List below. The second and last columns will be mapped to modelId and serviceId whilst the first column should become a list of vehicleIds.
I need to map it into a List<MyDTO>
where MyDTO
is as follows:
MyDTO{
// constructor
MyDTO(String modelId, String serviceId, List<String> vehicleIds){...}
String modelId;
String serviceId;
List<String> vehicleIds;
}
I am trying to figure out how to group by in a stream but nothing seems to come out. That's where I'm blocked...
listOfObjectArrays.stream()
.map(objects -> new MyDTO((String) objects[0], (String) objects[1], null));
Can't figure out how to apply a reduce operation that does the job, any help really appreciated!
Edit: Sorry I forgot to mention that I'm stuck with Java 8. Thank you all for the great answers.
You could map them all to Strings first and then group by the tuple (modelId, serviceId) and only then map the results of the grouping to the dtos.
import java.util.Arrays;
import java.util.stream.Collectors;
List<MyDto> myDtos = queryResult.stream()
// cast all to String and put into tuple
.map(objects -> Arrays.asList((String) objects[0], (String) objects[1], (String) objects[2]))
// group by tuple (modelid, serviceId)
.collect(Collectors.groupingBy(r -> Arrays.asList(r.get(1), r.get(2)),
Collectors.mapping(r -> r.get(0), Collectors.toList())))
.entrySet().stream()
.map(entry -> new MyDto(entry.getKey().get(0), entry.getKey().get(1), entry.getValue()))
.collect(Collectors.toList());
To increase readability instead of using lists we introduce records
import java.util.List;
import java.util.Map;
record MyDto(String vehicleId, String modelId, List<String> serviceIds) {
public MyDto(Map.Entry<AggregateKey, List<String>> vehiclesAggregate) {
this(vehiclesAggregate.getKey().modelId(), vehiclesAggregate.getKey().serviceId(),vehiclesAggregate.getValue());
}
}
record ResultRow(String vehicleId, String modelId, String serviceId) {
public ResultRow(Object[] entries) {
this((String) entries[0], (String) entries[1], (String) entries[2]);
}
public AggregateKey getKey() {
return new AggregateKey(modelId, serviceId);
}
}
record AggregateKey(String modelId, String serviceId) {}
Resulting in a much shorter and very readable streaming pipeline
import java.util.List;
import java.util.stream.Collectors;
List<MyDto> myDtos = queryResult.stream()
.map(ResultRow::new)
.collect(Collectors.groupingBy(ResultRow::getKey,
Collectors.mapping(ResultRow::vehicleId, Collectors.toList())))
.entrySet().stream()
.map(MyDto::new)
.toList(); // shorthand for collect(Collectors.toList) since Java 16
You can create a nested intermediate map by grouping your data by modelId
and then serviceId
using groupingBy()
and mapping()
collectors.
And then create a stream over entry set. And flatten each inner map creating new MyDTO
based on every combination of modelId
and serviceId
.
Map<String, Map<String, List<String>>> vehicleIdByModelIdAndServiceId =
listOfObjectArrays.stream()
.collect(Collectors.groupingBy(objects -> (String) objects[1],
Collectors.groupingBy(objects -> (String) objects[2],
Collectors.mapping(objects -> (String) objects[0],
Collectors.toList()))));
List<MyDTO> result = vehicleIdByModelIdAndServiceId.entrySet().stream()
.flatMap(baseEntry -> baseEntry.getValue().entrySet().stream()
.map(entry -> new MyDTO(baseEntry.getKey(), entry.getKey(), entry.getValue())))
.collect(Collectors.toList());
Another option is to use a Map.Entry
as a key in the intermediate map, and a value will be a list of vehicleId
.
List<MyDTO> result = listOfObjectArrays.stream()
.collect(Collectors.groupingBy(objects -> new AbstractMap.SimpleEntry<>((String) objects[1], (String) objects[2]),
Collectors.mapping(objects -> (String) objects[0],
Collectors.toList())))
.entrySet().stream()
.map(entry -> new MyDTO(entry.getKey().getKey(),
entry.getKey().getValue(),
entry.getValue()))
.collect(Collectors.toList());
If you don't necessarily need to use stream
, you can always use a standard cycle, for example:
Map<String, MyDTO> myDTOs = new HashMap<>();
for(Object[] a : listOfObjectArrays) {
String key = a[1] + " - " + a[2];
MyDTO myDTO = myDTOs.get(key);
if(myDTO == null) {
List<String> vehicleIds = new ArrayList<>();
vehicleIds.add((String) a[0]);
myDTO = new MyDTO((String) a[1], (String) a[2], vehicleIds);
myDTOs.put(key, myDTO);
} else {
myDTO.getVehicleIds().add((String) a[0]);
}
}
List<MyDTO> myDTOList = new ArrayList<>(myDTOs.values());
The idea is to
Object[]
as {String,Long,String}
{Object[1],Object[2]}
; Collect(Distinct Object[0]
)Map<List<Object[0]>,{Object[1],Object[2]}>
--> List<MyDto>
For this, I setup two supporting classes, one of which you provided, the other one needed as a Key for the groupingBuy
to work:
class MyDTO{
String modelId;
String serviceId;
List<String> vehicleIds;
MyDTO(String modelId, String serviceId, String vehicleId) {
// Need to put in a Modifiable ArrayList so the reduce can happen
this(modelId,serviceId,new ArrayList<>(List.of(vehicleId)));
}
MyDTO(String modelId, String serviceId, List<String> vehicleIds){
this.modelId = modelId;
this.serviceId = serviceId;
this.vehicleIds = vehicleIds;
}
public MyDTOKey getKey() {
return new MyDTOKey(modelId, serviceId);
}
public MyDTO reduce(MyDTO other) {
this.vehicleIds.addAll(other.vehicleIds);
return this;
}
public void dump() {
System.out.println("modelId: "+modelId+"; serviceId: "+serviceId+"; vehicleIds: "+vehicleIds.toString());
}
}
private class MyDTOKey<T1,T2> {
T1 v1;
T2 v2;
public MyDTOKey(T1 v1,T2 v2) {
this.v1 = v1;
this.v2 = v2;
}
@Override
public int hashCode() {
return Objects.hash(v1,v2);
}
/*
* Required for the groupby to work correctly as it
* doesnt automatically on an Object[]
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final MyDTOKey <?, ?> other = (MyDTOKey <?, ?>) obj;
if (!Objects.equals(this.v1, other.v1)) {
return false;
}
return Objects.equals(this.v2, other.v2);
}
}
And with a heads-on approach, I can build something like this:
List<Object []> data = List.of(
new Object[]{"93bf3e92-7a37-4e23-836d-eed5a104341f", "214", "80a7-ce5640b18879"},
new Object[]{"b4066520-e127-44b7-bcc0-1d1187de559c", "214", "80a7-ce5640b18879"},
new Object[]{"b4066520-e127-44b7-bcc0-dddddddddddd", "215", "80a7-ce5640b18879"}
);
public static void main(String [] pars) {
Map<Pair<String,String>,List<String>> map = data
.stream()
.collect(groupingBy(
o->new MyDTOKey((String)o[1],(String)o[2]), // group by last two only
mapping(
o->(String)o[0], // Collapse the vehicle id's to a list
toList() // Using set to take out duplicate vehicle id's
)
));
// Convert the map to a List.
List<MyDTO> dtos = map.entrySet().stream()
.map(e->new MyDTO(e.getKey().v1,e.getKey().v2,e.getValue()))
.collect(toList());
for (MyDTO dto:dtos) {
dto.dump();
}
}
The whole effort of creating a class to represent a Tuple ( MyDTOKey
) can be avoided by setting it up as a List<String>
as done in @Valerij Doblers answer.
But with some minor tweaking already done to the MyDTO
class, we can rewrite this also neatly as follows:
public static void main(String [] pars) {
List<MyDTO> data = List.of(
new MyDTO("214", "80a7-ce5640b18879","93bf3e92-7a37-4e23-836d-eed5a104341f"),
new MyDTO("214", "80a7-ce5640b18879","b4066520-e127-44b7-bcc0-1d1187de559c"),
new MyDTO("215", "80a7-ce5640b18879","b4066520-e127-44b7-bcc0-dddddddddddd")
);
List<MyDTO> dtos = data.stream()
.collect(
groupingBy(
MyDTO::getKey,
reducing(MyDTO::reduce)
)
) // Produces a Map<MyDTOKey,MyDTO>
.values().stream() // Streams Optional<MyDTO>
.flatMap(o->o.stream()) // expand to contained MyDTO
.collect(toList())
;
for (MyDTO dto:dtos) {
dto.dump();
}
}
Maybe with a little more effort we can collect directly in a List without going through the Map first.
This needs a Collectors.groupBy() function since you're taking a stream and grouping them together.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class F {
public static void main(String[] args) {
var in = List.of(new Object[]{"93bf3e92-7a37-4e23-836d-eed5a104341f", "214", "80a7-ce5640b18879"},
new Object[]{"b4066520-e127-44b7-bcc0-1d1187de559c", "214", "80a7-ce5640b18879"},
new Object[]{"b4066520-e127-44b7-bcc0-dddddddddddd", "215", "80a7-ce5640b18879"}
);
Map<Key, List<MyDTOOne>> p1 =
in.stream()
.map(objs -> new String[]{(String) objs[0], (String) objs[1], (String) objs[2]})
.map(objs -> new MyDTOOne(objs[1], objs[2], objs[0]))
.collect(Collectors.groupingBy(myDTO -> new Key(myDTO.modelId, myDTO.serviceId)));
List<MyDTO> p2 = p1.entrySet()
.stream()
.map(e -> new MyDTO(e.getKey().modelId, e.getKey().serviceId, e.getValue().stream().map(MyDTOOne::vehicleId).toList()))
.toList();
System.out.println(p2);
}
static record Key(String modelId, String serviceId) {
}
static record MyDTO(String modelId, String serviceId, List<String> vehicleIds) {
}
static record MyDTOOne(String modelId, String serviceId, String vehicleId) {
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.