I have an Entity with multiple nested lists like this :
public class DataFile {
#Id
private int id;
private List<DataObject> datas = new ArrayList<>();
}
public class DataObject {
#Id
private String type;
private List<DataValue> values = new ArrayList<>();
}
public class DataValue {
#Id
private int id;
private String dataValue;
private LocalDateTime dataDate = LocalDateTime.now();
}
If I want a specific dataValue with DataObject.type = "speType" and DataValue.id = 2, I need this:
String value = dataFile.getDatas().forEach(t -> {
if(t.getType().equals("speType")){
t.getValues().forEach(v -> {
if(v.getId(2))
return v.getDataValue();
});
}
});
Is it possible to create a simple method ?
Thanks
For a pure "stream" solution :
dataFile.getDatas()
.stream()
.filter(t -> t.getType().equals("speType"))
.flatMap(t -> t.getValues().stream())
.filter(v -> v.getId()==2)
.map(DataValue::getDataValue)
.findFirst();
I assumed that by "if(v.getId(2))" you meant "if(v.getId()==2)", if not you can easily change the code above.
You could try something like that :
Optional<String> optValue =
dataFile.getDatas()
.stream()
.filter(t -> t.getType().equals("specType"))
.flatMap(t -> t.getValues().stream()
.filter(v -> v.getId() == 2)
.map(DataValue::getDataValue)
)
.findFirst();
Stream.findFirst() allows to exit of the processing early as soon as one element of it matches to the conditions. Similarly to what you did in your actual code.
Note that Stream.findFirst() returns an Optional.
Either unwrap the object after the terminal operation such as :
String value = optValue.orElse("default value");
Or do in this stream itself :
String value =
...
.findFirst();
.orElse("defaultValue")
Related
I have a problem about writing a java stream by filtering multiple condition, grouping by month of the date and calculating the sum of person.
I store the values as Map<String(pId),List<Person>>
Here is my Person class shown below
public class Person{
private String id;
private String name;
private String surname;
private Statement event; // JOIN , EXIT
private Object value;
private LocalDate eventDate;
}
Here is the list
ID,Info,Date (All values are stored in Person object defined in the List)
per1, JOIN, 10-01-2022
per2, JOIN, 10-01-2022
per3, EXIT, 10-01-2022
per3 EXIT, 10-02-2022
per4, JOIN, 10-03-2022
What I want to do is to get this result.
Month Info Total Number
1 JOIN 2
1 EXIT 1
2 EXIT 1
3 JOIN 1
Here is my dto shown below.
public class DTO {
private int month;
private State info; // State -> enum
private int totalEmployees;
}
Here is my GroupDto shown below.
public class GroupDto {
private int month;
private State info;
}
Here is the code snippet but I cannot complete it.
List<DTO > result = persons .values().stream()
.flatMap(List::stream)
.filter(person -> person .getInfo() == Value.ONBOARD || person .getInfo() == Value.EXIT)
.collect(Collectors.groupingBy(
p -> new GroupDto(p.getEventDate().getMonthValue(), p.getEvent()),
Collectors.counting()
))
.entrySet().stream()
.map(e -> new DTO(p.getKey().get, p.getKey(), (int) (long) e.getValue())) // -> ERROR Line
.sorted(Comparator.comparing(MonthWiseDto::getMonth))
.toList();
How can I do that?
Assuming that you want to get the total number of people for every combination of Month and State you need to group the data based on these two properties.
For that, you can define an object that would serve as a key in the intermediate Map. It can be implemented as a Java 16 record like that (you can use a class with earlier JDK version) :
public record MonthState(int mont, State info) {}
In the stream, we can make use of the Collector groupingBy() with Collector counting() passed as a downstream to generate the total number of Person objects having particular combination of month and state.
Map<String, List<Person>> personListById = // initializing the map
List<DTO> result = personListById.values().stream()
.flatMap(List::stream)
.filter(per -> per.getInfo() == State.EXIT || per.getInfo() == State.JOIN)
.collect(Collectors.groupingBy(
p -> new MonthState(p.getEventDate().getMonthValue(), p.getInfo()),
Collectors.counting()
))
.entrySet().stream()
.map(e -> new DTO(e.getKey().mont(), e.getKey().info(), (int) (long) e.getValue()))
.sorted(Comparator.comparing(DTO::getMonth))
.toList();
Link to online Demo
public class Main {
public static void main(String... args) {
List<Person> persons = List.of(
new Person("per1", Statement.JOIN, LocalDate.of(2022, 1, 10)),
new Person("per2", Statement.JOIN, LocalDate.of(2022, 1, 10)),
new Person("per3", Statement.EXIT, LocalDate.of(2022, 1, 10)),
new Person("per3", Statement.EXIT, LocalDate.of(2022, 2, 10)),
new Person("per4", Statement.JOIN, LocalDate.of(2022, 3, 10)));
List<Dto> groupResults = calculateGroupResults(persons);
groupResults.forEach(System.out::println);
}
public static List<Dto> calculateGroupResults(List<Person> persons) {
Map<GroupKey, Long> map = persons.stream()
.filter(person -> person.getEvent() == Statement.EXIT || person.getEvent() == Statement.JOIN)
.collect(Collectors.groupingBy(
person -> new GroupKey(person.getEventDate().getMonthValue(), person.getEvent()),
Collectors.counting()));
return map.entrySet().stream()
.map(entry -> new Dto(entry.getKey().getMonth(), entry.getKey().getInfo(), entry.getValue()))
.sorted(Dto.SORT_BY_MONTH_INFO)
.collect(Collectors.toList());
}
enum Statement {
JOIN,
EXIT
}
#Getter
#RequiredArgsConstructor
public static final class Person {
private final String id;
private final Statement event;
private final LocalDate eventDate;
}
#Getter
#EqualsAndHashCode
#RequiredArgsConstructor
public static final class GroupKey {
private final int month;
private final Statement info;
}
#Getter
#RequiredArgsConstructor
public static final class Dto {
public static final Comparator<Dto> SORT_BY_MONTH_INFO =
Comparator.comparing(Dto::getMonth)
.thenComparingInt(one -> one.getInfo().ordinal());
private final int month;
private final Statement info;
private final long totalEmployees;
#Override
public String toString() {
return month + "-" + info + '-' + totalEmployees;
}
}
}
Demo
1-JOIN-2
1-EXIT-1
2-EXIT-1
3-JOIN-1
I'm trying to filter a list of objects say BOLReference based on a key which is present in the inner object of one of the List of Objects of this BOLReference.
List<BOLReference> bolRef = complianceMongoTemplate.find(findQuery, BOLReference.class);
Optional<BOLReference> bref = bolRef.stream().filter(br -> br.getWorkflowExceptions().stream()
.filter(bk -> bk.getBusinessKeyValues().get(businessKey)
.equalsIgnoreCase("ABCD1234"))).findFirst();
While doing so I'm getting error as :
Cannot convert from Stream<WorkFlowExceptions> to boolean
My BOLReference looks like below:
private String ediTransmissionId;
private List<WorkflowExceptions> workflowExceptions;
And my WorkflowExceptions looks like:
private String category;
private Map<String,String> businessKeyValues;
Optional<BOLReference> bref = bolRef.stream()
.filter(br ->
br.getWorkflowExceptions().stream()
.anyMatch(bk ->
bk.getBusinessKeyValues().get(businessKey)
.equalsIgnoreCase("ABCD1234")))
.findFirst();
Failing anyMatch I think.
A filter clause does not return a boolean but a stream. This causes a problem because your first filter expects a boolean result, but gets a stream from the second filter. You can use anyMatch instead to achieve the desired result.
You can make stream expressions more readable by formatting them on more lines and extracting complex logic into methods.
You could fix & refactor it like this:
String businessKey = null;
String targetValue = "ABCD1234";
Optional<BOLReference> bref = bolReferences.stream()
.filter(br -> br. hasBusinessKey(businessKey, targetValue))
.findFirst();
And:
public class BOLReference {
// ...
public boolean hasBusinessKey(String key, String value) {
return getWorkflowExceptions().stream()
.anyMatch(wfe -> wfe.hasBusinessKey(key, value));
}
}
And:
public class WorkflowExceptions {
// ...
public boolean hasBusinessKey(String key, String value) {
return getBusinessKeyValues().get(key).equalsIgnoreCase(value);
}
}
Now I have an object:
public class Room{
private long roomId;
private long roomGroupId;
private String roomName;
... getter
... setter
}
I want sort list of rooms by 'roomId', but in the meantime while room objects has 'roomGroupId' greator than zero and has same value then make them close to each other.
Let me give you some example:
input:
[{"roomId":3,"roomGroupId":0},
{"roomId":6,"roomGroupId":0},
{"roomId":1,"roomGroupId":1},
{"roomId":2,"roomGroupId":0},
{"roomId":4,"roomGroupId":1}]
output:
[{"roomId":6,"roomGroupId":0},
{"roomId":4,"roomGroupId":1},
{"roomId":1,"roomGroupId":1},
{"roomId":3,"roomGroupId":0},
{"roomId":2,"roomGroupId":0}]
As shown above, the list sort by 'roomId', but 'roomId 4' and 'roomId 1' are close together, because they has the same roomGroupId.
This does not have easy nice solution (maybe I am wrong).
You can do this like this
TreeMap<Long, List<Room>> roomMap = new TreeMap<>();
rooms.stream()
.collect(Collectors.groupingBy(Room::getRoomGroupId))
.forEach((key, value) -> {
if (key.equals(0L)) {
value.forEach(room -> roomMap.put(room.getRoomId(), Arrays.asList(room)));
} else {
roomMap.put(
Collections.max(value, Comparator.comparing(Room::getRoomId))
.getRoomId(),
value
.stream()
.sorted(Comparator.comparing(Room::getRoomId)
.reversed())
.collect(Collectors.toList())
);
}
});
List<Room> result = roomMap.descendingMap()
.entrySet()
.stream()
.flatMap(entry -> entry.getValue()
.stream())
.collect(Collectors.toList());
If you're in Java 8, you can use code like this
Collections.sort(roomList, Comparator.comparing(Room::getRoomGroupId)
.thenComparing(Room::getRoomId));
If not, you should use a comparator
class SortRoom implements Comparator<Room>
{
public int compare(Room a, Room b)
{
if (a.getRoomGroupId().compareTo(b.getRoomGroupId()) == 0) {
return a.getRoomId().compareTo(b.getRoomId());
}
return a.getRoomGroupId().compareTo(b.getRoomGroupId();
}
}
and then use it like this
Collections.sort(roomList, new SortRoom());
I am trying to convert to Lambda function
So far I am able to convert the above code to lambda function like as shown below
Stream.of(acceptedDetails, rejectedDetails)
.filter(list -> !isNull(list) && list.length > 0)
.forEach(new Consumer<Object>() {
public void accept(Object acceptedOrRejected) {
String id;
if(acceptedOrRejected instanceof EmployeeValidationAccepted) {
id = ((EmployeeValidationAccepted) acceptedOrRejected).getId();
} else {
id = ((EmployeeValidationRejected) acceptedOrRejected).getAd().getId();
}
if(acceptedOrRejected instanceof EmployeeValidationAccepted) {
dates1.add(new Integer(id.split("something")[1]));
Integer empId = Integer.valueOf(id.split("something")[2]);
empIds1.add(empId);
} else {
dates2.add(new Integer(id.split("something")[1]));
Integer empId = Integer.valueOf(id.split("something")[2]);
empIds2.add(empId);
}
}
});
But still my goal was to avoid repeating the same logic and also to convert to Lambda function, still in my converted lambda function I feel its not clean and efficient.
This is just for my learning aspect I am doing this stuff by taking one existing code snippet.
Can anyone please tell me how can I improvise the converted Lambda function
Generally, when you try to refactor code, you should only focus on the necessary changes.
Just because you’re going to use the Stream API, there is no reason to clutter the code with checks for null or empty arrays which weren’t in the loop based code. Neither should you change BigInteger to Integer.
Then, you have two different inputs and want to get distinct results from each of them, in other words, you have two entirely different operations. While it is reasonable to consider sharing common code between them, once you identified identical code, there is no sense in trying to express two entirely different operations as a single operation.
First, let’s see how we would do this for a traditional loop:
static void addToLists(String id, List<Integer> empIdList, List<BigInteger> dateList) {
String[] array = id.split("-");
dateList.add(new BigInteger(array[1]));
empIdList.add(Integer.valueOf(array[2]));
}
List<Integer> empIdAccepted = new ArrayList<>();
List<BigInteger> dateAccepted = new ArrayList<>();
for(EmployeeValidationAccepted acceptedDetail : acceptedDetails) {
addToLists(acceptedDetail.getId(), empIdAccepted, dateAccepted);
}
List<Integer> empIdRejected = new ArrayList<>();
List<BigInteger> dateRejected = new ArrayList<>();
for(EmployeeValidationRejected rejectedDetail : rejectedDetails) {
addToLists(rejectedDetail.getAd().getId(), empIdRejected, dateRejected);
}
If we want to express the same as Stream operations, there’s the obstacle of having two results per operation. It truly took until JDK 12 to get a built-in solution:
static Collector<String,?,Map.Entry<List<Integer>,List<BigInteger>>> idAndDate() {
return Collectors.mapping(s -> s.split("-"),
Collectors.teeing(
Collectors.mapping(a -> Integer.valueOf(a[2]), Collectors.toList()),
Collectors.mapping(a -> new BigInteger(a[1]), Collectors.toList()),
Map::entry));
}
Map.Entry<List<Integer>, List<BigInteger>> e;
e = Arrays.stream(acceptedDetails)
.map(EmployeeValidationAccepted::getId)
.collect(idAndDate());
List<Integer> empIdAccepted = e.getKey();
List<BigInteger> dateAccepted = e.getValue();
e = Arrays.stream(rejectedDetails)
.map(r -> r.getAd().getId())
.collect(idAndDate());
List<Integer> empIdRejected = e.getKey();
List<BigInteger> dateRejected = e.getValue();
Since a method can’t return two values, this uses a Map.Entry to hold them.
To use this solution with Java versions before JDK 12, you can use the implementation posted at the end of this answer. You’d also have to replace Map::entry with AbstractMap.SimpleImmutableEntry::new then.
Or you use a custom collector written for this specific operation:
static Collector<String,?,Map.Entry<List<Integer>,List<BigInteger>>> idAndDate() {
return Collector.of(
() -> new AbstractMap.SimpleImmutableEntry<>(new ArrayList<>(), new ArrayList<>()),
(e,id) -> {
String[] array = id.split("-");
e.getValue().add(new BigInteger(array[1]));
e.getKey().add(Integer.valueOf(array[2]));
},
(e1, e2) -> {
e1.getKey().addAll(e2.getKey());
e1.getValue().addAll(e2.getValue());
return e1;
});
}
In other words, using the Stream API does not always make the code simpler.
As a final note, we don’t need to use the Stream API to utilize lambda expressions. We can also use them to move the loop into the common code.
static <T> void addToLists(T[] elements, Function<T,String> tToId,
List<Integer> empIdList, List<BigInteger> dateList) {
for(T t: elements) {
String[] array = tToId.apply(t).split("-");
dateList.add(new BigInteger(array[1]));
empIdList.add(Integer.valueOf(array[2]));
}
}
List<Integer> empIdAccepted = new ArrayList<>();
List<BigInteger> dateAccepted = new ArrayList<>();
addToLists(acceptedDetails, EmployeeValidationAccepted::getId, empIdAccepted, dateAccepted);
List<Integer> empIdRejected = new ArrayList<>();
List<BigInteger> dateRejected = new ArrayList<>();
addToLists(rejectedDetails, r -> r.getAd().getId(), empIdRejected, dateRejected);
A similar approach as #roookeee already posted with but maybe slightly more concise would be to store the mappings using mapping functions declared as :
Function<String, Integer> extractEmployeeId = empId -> Integer.valueOf(empId.split("-")[2]);
Function<String, BigInteger> extractDate = empId -> new BigInteger(empId.split("-")[1]);
then proceed with mapping as:
Map<Integer, BigInteger> acceptedDetailMapping = Arrays.stream(acceptedDetails)
.collect(Collectors.toMap(a -> extractEmployeeId.apply(a.getId()),
a -> extractDate.apply(a.getId())));
Map<Integer, BigInteger> rejectedDetailMapping = Arrays.stream(rejectedDetails)
.collect(Collectors.toMap(a -> extractEmployeeId.apply(a.getAd().getId()),
a -> extractDate.apply(a.getAd().getId())));
Hereafter you can also access the date of acceptance or rejection corresponding to the employeeId of the employee as well.
How about this:
class EmployeeValidationResult {
//constructor + getters omitted for brevity
private final BigInteger date;
private final Integer employeeId;
}
List<EmployeeValidationResult> accepted = Stream.of(acceptedDetails)
.filter(Objects:nonNull)
.map(this::extractValidationResult)
.collect(Collectors.toList());
List<EmployeeValidationResult> rejected = Stream.of(rejectedDetails)
.filter(Objects:nonNull)
.map(this::extractValidationResult)
.collect(Collectors.toList());
EmployeeValidationResult extractValidationResult(EmployeeValidationAccepted accepted) {
return extractValidationResult(accepted.getId());
}
EmployeeValidationResult extractValidationResult(EmployeeValidationRejected rejected) {
return extractValidationResult(rejected.getAd().getId());
}
EmployeeValidationResult extractValidationResult(String id) {
String[] empIdList = id.split("-");
BigInteger date = extractDate(empIdList[1])
Integer empId = extractId(empIdList[2]);
return new EmployeeValidationResult(date, employeeId);
}
Repeating the filter or map operations is good style and explicit about what is happening. Merging the two lists of objects into one and using instanceof clutters the implementation and makes it less readable / maintainable.
I'm new to java and love the Stream API.
I got this loop:
List<FileTree> expanded = new ArrayList<>();
for(FileTree item : tree){
if(item.getType().equals("tree")){
expanded.addAll(getTreeOfSubStudPackage(item.getName()));
}
else{
expanded.add(item);
}
}
And I wonder if this could be converted to a neat stream. My first guess was the following:
tree.stream().map(fileTree -> {
if(fileTree.getType().equals("tree")){
return getTreeOfSubStudPackage(fileTree.getName());
}
else{
return fileTree;
}
}).collect(Collectors.toList());
It compiles fine. But is this recommended, have I even implemented a No-Go or is there a even nicer way?
And last but not least: Is there a overhead in .stream() that would make this improvement worthless?
Appendix
List<FileTree> getTreeOfSubStudPackage(...){
//...
}
class FileTree {
private String name;
private String type;
private String mode;
private String id;
//... Public Getter And Setter ...
}
you need to do map and then flatMap.
Arrays.asList(item) is to add the element to List to be flat in the next flatMap
tree.stream()
.map(item -> item.getType().equals("tree") ? getTreeOfSubStudPackage(item.getName()) : Arrays.asList(item))
.flatMap(Collection::stream)
.collect(Collectors.toList());