I have a list of objects, and I need to group objects having status equal to my customizedStatusto a single customized one with count = sumOfSameObjectsCount .
We have class MyObject
class MyObject {
Integer id;
String name;
String status;
Long count;
//constructor with attributes
//getters
//setters
}
Suggested implementation :
List<MyObject> resultList = listOfObjects.stream()
.collect(Collectors.groupingBy(MyObject::getStatus))
.entrySet().stream()
.map(e -> e.getValue().stream()
.reduce((partialResult,nextElem) ->
{
LOGGER.info("ahaaaa! inside your reduce block ");
if(partialResult.getStatus().equals(customizedStatus)) {
LOGGER.info("equal to my customizedStatus");
return new MyObject(customizedId, customizedName, customizedStatus, partialResult.getCount()+nextElem.getCount());
} else {
LOGGER.info("not equal to my customizedStatus");
return new MyObject(partialResult.getId(), partialResult.getName(), partialResult.getStatus(), partialResult.getCount());
}
}
)
)
.map(f -> f.get())
.collect(Collectors.toList());
Things work like a charm in case there are multiple objects with status equal to my customizedStatus.
Input :
[
{
"id": XX,
"name": "nameXX",
"status": "statusXX",
"count": countXX
},
{
"id": YY,
"name": "nameYY",
"status": "statusYY",
"count": countYY
},
{
"id": ZZ,
"name": "nameZZ",
"status": "customizedStatus",
"count": countZZ
},
{
"id": ZZz,
"name": "nameZZz",
"status": "customizedStatus",
"count": countZZz
}
]
Output :
[
{
"id": XX,
"name": "nameXX",
"status": "statusXX",
"count": countXX
},
{
"id": YY,
"name": "nameYY",
"status": "statusYY",
"count": countYY
},
{
"id": customizedId,
"name": "customizedName",
"status": "customizedStatus",
"count": countZZ+countZZz
}
]
In case there is one object with status equal to my customizedStatus, need to be customized it too, unfortunately reduce block is being skipped !
Input :
[
{
"id": XX,
"name": "nameXX",
"status": "statusXX",
"count": countXX
},
{
"id": YY,
"name": "nameYY",
"status": "statusYY",
"count": countYY
},
{
"id": ZZ,
"name": "nameZZ",
"status": "customizedStatus",
"count": countZZ
}
]
Output :
[
{
"id": XX,
"name": "nameXX",
"status": "statusXX",
"count": countXX
},
{
"id": YY,
"name": "nameYY",
"status": "statusYY",
"count": countYY
},
{
"id": ZZ,
"name": "nameZZ",
"status": "customizedStatus",
"count": countZZ
}
]
Expected output :
[
{
"id": XX,
"name": "nameXX",
"status": "statusXX",
"count": countXX
},
{
"id": YY,
"name": "nameYY",
"status": "statusYY",
"count": countYY
},
{
"id": customizedId,
"name": "customizedName",
"status": "customizedStatus",
"count": countZZ
}
]
It seems like reduce is executed in case there is multiple objects with same status, if there isn't reduce not being executed at all ! Any thoughts to get the expected output using groupBy and reduce ?
Update
The resulting type is not correct. Because you didn't provide the identity within the reduce() it will return an Optional<Object>, but not an object.
For the same reason (because you are using a flavor of reduce() that doesn't expect identity), the accumulator will have no impact on a single element. A quote from the documentation:
Performs a reduction on the elements of this stream, using an
associative accumulation function, and returns an Optional describing
the reduced value, if any. This is equivalent to:
boolean foundAny = false;
T result = null;
for (T element : this stream) {
if (!foundAny) {
foundAny = true;
result = element;
}
else
result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();
The first encountered stream element would become a partial result and there's no more elements, it would be wrapped by the optional as is and returned.
A possible remedy is to introduce the identity:
public static final Integer customizedId = 99;
public static final String customizedName = "customizedName";
public static final String customizedStatus = "customizedStatus";
public static void main(String[] args) {
List<MyObject> listOfObjects =
List.of(new MyObject(1, "nameXX", "statusXX", 1L),
new MyObject(2, "nameYY", "statusYY", 1L),
new MyObject(3, "nameZZz", "customizedStatus", 3L));
List<MyObject> result =
listOfObjects.stream()
.collect(Collectors.groupingBy(MyObject::getStatus))
.entrySet().stream()
.map(e -> e.getValue().stream()
.reduce(getIdentity(e), (partialResult, nextElem) -> accumulate(partialResult, nextElem)) )
.collect(Collectors.toList());
result.forEach(System.out::println);
}
public static MyObject getIdentity(Map.Entry<String, List<MyObject>> entry) {
return entry.getKey().equals(customizedStatus) ?
new MyObject(customizedId, customizedName, customizedStatus, 0L) :
entry.getValue().iterator().next();
}
public static MyObject accumulate(MyObject result, MyObject next) {
return result.getStatus().equals(customizedStatus) ?
new MyObject(customizedId, customizedName, customizedStatus, result.getCount() + next.getCount()) :
new MyObject(result.getId(), result.getName(), result.getStatus(), result.getCount());
}
Output:
MyObject{id=2, name='nameYY', status='statusYY', count=1}
MyObject{id=1, name='nameXX', status='statusXX', count=1}
MyObject{id=99, name='customizedName', status='customizedStatus', count=3}
You can play around with this Online demo
But keep in mind that it's not the brightest idea to try to crap a lot of conditional logic into stream because it becomes more difficult to read.
Solutions provided below were written before the question was updated, and the problem was clarified. Although, they don't target this specific problem, someone might benefit from them and for that reason I'll preserve them.
Reducing the list into a single object
Is there any solution to make it pass by reduce even listOfObjects entries are different by status ?
In case if you want to reduce a list of objects into a single object with a predefined id, name and status, there's no need to create an intermediate map with Collectors.groupingBy().
If you want to utilize reduce() operation for that, you can accumulate count and then create a resulting object based on it:
That's how it might look like (the type of dummy object was changed to MyObject to avoid confusion with java.lang.Object):
final Integer customizedId = // intializing the resulting id
final String customizedName = // intializing the resulting name
final String customizedStatus = // intializing the resulting status
List<MyObject> listOfObjects = // intializing the source list
MyObject resultingObject = listOfObjects.stream()
.map(MyObject::getCount)
.reduce(Long::sum)
.map(count -> new MyObject(customizedId, customizedName, customizedStatus, 0L))
.orElseThrow(); // or .orElse(() -> new MyObject(customizedId, customizedName, customizedStatus, 0L));
Another way of achieving it is to make use of the fact that MyObject is mutable and utilize it as a container inside the collect() operation:
MyObject resultingObject = listOfObjects.stream()
.collect(() -> new MyObject(customizedId, customizedName, customizedStatus, 0L),
(MyObject result, MyObject next) -> result.setCount(result.getCount() + next.getCount()),
(left, right) -> left.setCount(left.getCount() + right.getCount()));
Related
#PostMapping("/countries")
public void createCountry(#Valid #RequestBody List<Country> countries) {
for(Country country: countries){
countryRepository.save(country);
}
Stream.of(countries)
.forEach(country -> countryRepository.save((Country)country));
}
I send this through postman
[
{
"name": "sweden",
"cities": [
{
"name": "trelleborg"
},
{
"name": "ystad"
}
]
},
{
"name": "japan",
"cities": [
{
"name": "osaka"
},
{
"name": "tokio"
}
]
}
]
With the for loop above the code works as intented, but with lambda expression below, I get this exception
java.lang.ClassCastException: java.util.ArrayList cannot be cast to com.qweqwe.Models.Country
What is the problem? Thanks
You can alsow write it in that way (List has forEach):
countries.forEach(country -> countryRepository.save(country));
or even with method references:
countries.forEach(countryRepository::save);
Stream.of is used to build a Stream of Objects.
Since you are passing one object of type list, you get error here (Country)country
Either change Stream like this
Stream.of(countries.get(0), countries.get(1))
.forEach(country -> countryRepository.save(country));
or
countries.stream().forEach(country -> countryRepository.save(country));
I tried below test data, adding just for Reference
List<UsersDTO> lstUser = new ArrayList<>();
UsersDTO userDto1 = new UsersDTO();
userDto1.setRecordsTotal(1);
userDto1.setRecordsFiltered(3);
UsersDTO userDto2 = new UsersDTO();
userDto2.setRecordsTotal(1);
userDto2.setRecordsFiltered(2);
lstUser.add(userDto1);
lstUser.add(userDto2);
Stream.of(lstUser.get(0), lstUser.get(1))
.forEach(user -> System.out.println(user.getRecordsFiltered() ));
lstUser.stream().forEach(user -> System.out.println(user.getRecordsFiltered() ));
OUTPUT:
3
2
3
2
I have these sample records from database which is a result of group by query:
[
{
"name": "Troy",
"acct": 1123,
"type": " Savings",
"total": 50
},
{
"name": "Larry",
"acct": 4233,
"type": " Savings",
"total": 200
},
{
"name": "Troy",
"acct": 1123,
"type": " Current",
"total": 120
},
{
"name": "Larry",
"acct": 4233,
"type": " Current",
"total": 220
}
]
Now, i need to create a report that looks like so:
[
{
"name": "Troy",
"acct": 1123,
"totalSavings": 50,
"totalCurrent": 120
},
{
"name": "Larry",
"acct": 4233,
"totalSavings": 200,
"totalCurrent": 220
}
]
.
public class DbTrans {
private String name;
private String acct;
private String type;
private double total;
// getters and setters
...
}
I have tried using some lambda techniques like the one below, but i'm still not getting close to the solution i desire.
Map<String, List<DbTrans>> collect = transList.stream().collect(
Collectors.groupingBy(f -> f.getType()));
First of all the response Dto is not the same as the request Dto, what I suggest is to create a new Response class lests call it:
public class DbTransResponse {
private String name;
private String acct;
private double totalSavings;
private double totalCurrent;
// getters and setters
}
then the result can be like so :
List<DbTransResponse> result = transList.stream()
.collect(Collectors.groupingBy(DbTrans::getAcct))
.values().stream()
.map(trans -> new DbTransResponse(
trans.get(0).getName(),
trans.get(0).getAcct(),
trans.get(0).getTotal(),
trans.get(1).getTotal()
)).collect(Collectors.toList());
I consider that the list should contain two entries of each name so the you can get totalSavings from the first entry trans.get(0).getTotal() and totalCurrent from the second trans.get(1).getTotal().
If you are not sure you can use conditions to fill your object for example you can check if there are two elements if not set a default value.
Ideone demo
Outputs
DbTransResponse{name='Larry', acct='4233', totalSavings=200.0, totalCurrent=220.0}
DbTransResponse{name='Troy', acct='1123', totalSavings=50.0, totalCurrent=120.0}
you can use Collectors::toMap for this purpose (with a single collect operation)
Map<Integer, DbTransResponse> collect = transList.stream()
.collect(Collectors.toMap(DbTrans::getAcct,
DbTransResponse::new,
DbTransResponse::merge));
Collection<DbTransResponse> result = collect.values();
here is merge method in DbTransResponse class
static DbTransResponse merge(DbTransResponse r1, DbTransResponse r2) {
return new DbTransResponse(
r1.name, r1.acct,
r1.totalSavings + r2.totalSavings,
r1.totalCurrent + r2.totalCurrent
);
}
and an additional constructor for DbTransResponse class, though you can move this logic to a method
DbTransResponse(DbTrans dbTrans) {
this.name = dbTrans.getName();
this.acct = dbTrans.getAcct();
this.totalSavings = "Savings".equals(dbTrans.getType()) ? dbTrans.getTotal() : 0;
this.totalCurrent = "Current".equals(dbTrans.getType()) ? dbTrans.getTotal() : 0;
}
demo
The goal is to start with a JSON string, parse it, and then recursively process every object property or array element. If the value is an array, and every element of the array is an object that contains a "Name" property, sort the array by the object.Name. The recursion is important, because the real-world JSON I am going to have to apply this to has three levels of nested arrays that need to be sorted.
Once the arrays are sorted into a deterministic order, finally, it should re-serialize the whole thing with properties in alphabetical order.
The purpose of this is to ensure consistency of the JSON, so I can do string comparison between the expected and the actual, and have it not be thrown off if things are serialized in a different order, or the array elements come back in a non-deterministic order (which is currently the case).
In pseudo-code, it would look something like this:
process_array( arr ) {
if ( arr.every( e => e.has("Name") ) ) {
arr.sort( byName )
}
arr.forEach( process )
}
process_object( obj ) {
for each key in obj {
process( obj[key] )
}
process( it ) {
if ( isArray( it ) {
process_arr( it )
} else {
process_object( it )
}
}
standardize( json ) {
generic_obj = parse( json )
process( generic_obj )
return serialize_with_sorted_keys( generic_obj )
}
To make it concrete, given this input:
[
{
"Id": "1",
"Name": "foo",
"Children": [
{
"Name": "two",
"Value": 2
},
{
"Value": 1,
"Name": "one"
}
],
"Other": [ 1, 3, 2 ]
},
{
"Name": "bar",
"Id": "2",
"Children": [
{
"Name": "Banana",
"Value": 3
},
{
"Value": 4,
"Name": "Cherry"
},
{
"Apples": "are tasty",
"Name": "Apple",
"Value": 5
}
]
}
]
The expected output would be:
[ // array elements are sorted by .Name
{ // object properties are sorted by key
"Children": [ // sorted by .Name
{
"Apples": "are tasty",
"Name": "Apple",
"Value": 5
},
{
"Name": "Banana",
"Value": 3
},
{ // properties sorted by name
"Name": "Cherry",
"Value": 4
}
],
"Id": "2",
"Name": "bar"
},
{
"Children": [
{
"Name": "two",
"Value": 2
},
{
"Name": "one",
"Value": 1
}
],
"Id": "1",
"Name": "foo",
"Other": [ 1, 2, 3 ] // NOTE: order has been changed!
}
]
How do I do this in Java in a generic way?
I have some problem. I get a Response from my RESTful WebServices with JSON Objects in a list. For example:
I get a List of these elements and I want just to get the id from the benutzerId.
[{
"benutzerId": {
"benutzername": "Nenzema",
"geschlecht": "m",
"id": 1,
"nachname": "Marinovic",
"passwort": "test1",
"vorname": "Nenad"
},
"hochzeitId": {
"id": 4
},
"id": 1,
"isbrautbenutzer": true
}, {
"benutzerId": {
"benutzername": "dnikolic",
"geschlecht": "m",
"id": 2,
"nachname": "Nikolic",
"passwort": "test2",
"vorname": "Djordje"
},
"hochzeitId": {
"id": 4
},
"id": 4,
"isbrautbenutzer": false
}]
I already made it, to get just the benutzer, but I dont know how to return a json Object where only the id is in this object, because I dont want to send the whole data to the requester.
[{
"benutzername": "Nenzema",
"geschlecht": "m",
"id": 1,
"nachname": "Marinovic",
"passwort": "test1",
"vorname": "Nenad"
}, {
"benutzername": "dnikolic",
"geschlecht": "m",
"id": 2,
"nachname": "Nikolic",
"passwort": "test2",
"vorname": "Djordje"
}]
So my aim would be to get something like this:
[{
"id": 1
}, {
"id": 2
}]
This is the query for the first list:
#NamedQuery(name = "Hochzeitsplan.findByBenutzerId",
query = "SELECT h FROM Hochzeitsplan h WHERE h.benutzerId.id = :benutzerId"),
These two queries I also tested, but its not working..
#NamedQuery(name = "Hochzeitsplan.getHochzeitsuserByHochzeitsId",
query = "SELECT DISTINCT h.benutzerId FROM Hochzeitsplan h WHERE h.hochzeitId.id = :hochzeitId"),
#NamedQuery(name = "Hochzeitsplan.findBenutzerIdByHochzeitsId",
query = "SELECT h.benutzerId.id FROM Hochzeitsplan h WHERE h.hochzeitId.id = :hochzeitId"),
This is the method I use for getting the Response with the List:
#GET
#Path("/hochzeitIDneu={hochzeitId}")
#Produces({MediaType.APPLICATION_JSON})
public Response getListByHochzeitID(#PathParam("hochzeitId") Integer hochzeitId) {
Query query=em.createNamedQuery("Hochzeitsplan.findByHochzeitId",Hochzeitsplan.class)
.setParameter("hochzeitId", hochzeitId);
List<Hochzeitsplan> a =query.getResultList();
a.sort(new Comparator<Hochzeitsplan>() {
#Override
public int compare(Hochzeitsplan o1, Hochzeitsplan o2)
{
return o1.getId().compareTo(o2.getId());
}
});
if(a.isEmpty()) {
ErrorManager error1 = new ErrorManager();
return Response.status(400).entity(error1.getError(Enums.ErrorResponses.NORESULTS)).build();
} else {
return Response.ok(a.toArray(new Hochzeitsplan[a.size()])).build();
}
}
Use a JsonPath Evaluator like Jayway Jsonpath.
https://mvnrepository.com/artifact/com.jayway.jsonpath/json-path
Your json path can be something like this
$[*]['benutzerId'].id
This would generate output something like
[
1,
2
]
U can try this online https://jsonpath.herokuapp.com
I have a schema like this (simplified):
{
"range": {
"offset": 0,
"limit": 1,
"total": 2
},
"items": [
{
"id": 11,
"name": "foo",
"children": [
{
"id": 112,
"name": "bar",
"children": [
{
"id": 113,
"name": "foobar",
"type": "file"
}
],
"type": "folder"
},
{
"id": 212,
"name": "foofoo",
"type": "file"
}
],
"type": "room"
},
{
"id": 21,
"name": "barbar",
"type": "room"
}
]
}
I need to read only specific values like "id" from the first room (item). For this I need to iterate trough all items on every level (n items for root, n items for n children) with type folder or file.
For now i have this code:
POJO
public static class Item {
public int id;
}
Jackson Tree Iteration
ObjectMapper mapper = new ObjectMapper();
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(JSON);
root = root.get("items").get(0);
TypeReference<List<Item>> typeRef = new TypeReference<List<Item>>(){};
List<Item> list = mapper.readValue(root.traverse(), typeRef);
for (Item f : list) {
System.out.println(f.id);
}
How can i get all id's of all children in all items with specific type?
How to avoid the "Unrecognized field" exception without defining the whole schema?
Thank you very much for your help!
Try using java8 functions it has lot to do it in lesser lines ,
ObjectMapper mapper = new ObjectMapper();
Pass your json value
Map obj = mapper.readValue(s, Map.class);
List<Object> items= (List<Object>) obj.get("items");
Object[] Ids= items
.stream()
.filter(items-> ((Map)items).get("type").equals("room"))
.toArray()
Use the readTree(...) method to parse the JSON without needing to define the entire schema and find Nodes called "id".
You can then use findValues("id") to get the List of values back.