Java generalize approach to validate the null in the object parameters - java

I am trying to implement a logic where I have a POJO class which has 7 attributes.
I have added these POJO classes into the map depends upon the value of the attributes.
Below is the implementation
Map<String,List<PriceClass>> map = new HashMap();
for (PriceClass price : prices) {
if (price.getAttribute1() !=null) {
if (map.get("attribute1") !=null) {
map.get("attribute1").add(price);
} else {
map.set("attibute1",Collections.singletonList(price))
}
} else if(price.getAttribute2()!=null) {
if (map.get("attribute12") !=null) {
map.get("attribute2").add(price);
} else {
map.set("attibute2",Collections.singletonList(price))
}
} else if (price.getAttribute3() !=null) {
.
.
.
} else if (price.getAttribute7() !=null) {
//update the map
}
}
My question is rather than writing these many if loops are there any generalize implementations I can try here.

You may use
Map<String,List<PriceClass>> map = new HashMap<>();
for(PriceClass price: prices) {
HashMap<String,Object> options = new HashMap<>();
options.put("attibute1", price.getAttribute1());
options.put("attibute2", price.getAttribute2());
options.put("attibute3", price.getAttribute3());
options.put("attibute4", price.getAttribute4());
options.put("attibute5", price.getAttribute5());
options.put("attibute6", price.getAttribute6());
options.put("attibute7", price.getAttribute7());
options.values().removeIf(Objects::isNull);
options.keySet().forEach(attr -> map.computeIfAbsent(attr, x -> new ArrayList<>())
.add(price));
}
or generalizing the process:
Prepare a unmodifiable map once
static final Map<String, Function<PriceClass,Object>> ATTR;
static {
Map<String, Function<PriceClass,Object>> a = new HashMap<>();
a.put("attibute1", PriceClass::getAttribute1);
a.put("attibute2", PriceClass::getAttribute2);
a.put("attibute3", PriceClass::getAttribute3);
a.put("attibute4", PriceClass::getAttribute4);
a.put("attibute5", PriceClass::getAttribute5);
a.put("attibute6", PriceClass::getAttribute6);
a.put("attibute7", PriceClass::getAttribute7);
ATTR = Collections.unmodifiableMap(a);
}
and use either
Map<String,List<PriceClass>> map = new HashMap<>();
for(PriceClass price: prices) {
HashMap<String,Object> options = new HashMap<>();
ATTR.forEach((attr,func) -> options.put(attr, func.apply(price)));
options.values().removeIf(Objects::isNull);
options.keySet().forEach(attr -> map.computeIfAbsent(attr, x -> new ArrayList<>())
.add(price));
}
or
Map<String,List<PriceClass>> map = prices.stream()
.flatMap(price -> ATTR.entrySet().stream()
.filter(e -> e.getValue().apply(price) != null)
.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), price)))
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));

A likely optimal solution would be similar to one I have suggested earlier today.
Use the Map<String, Optional<?>> to store the Optional values of the checked attributes with a key of the future output map key.
Map<String, Optional<?>> options = new HashMap<>();
options.put("attribute1", Optional.ofNullable(price.getAttribute1()));
// ...
options.put("attribute3", Optional.ofNullable(price.getAttribute2()));
// ...
Using the iteration of the indices would let you perform the update of a map.
Map<String,List<Price>> map = new HashMap();
for (int i=1; i<7; i++) { // attributes 1..7
String attribute = "attribute" + i; // attribute1...attribute7
options.get(attribute).ifPresent(any -> // for non-nulls
map.put( // put to the map
attribute, // attribute as key remains
Optional.ofNullable(map.get(attribute)) // gets the existing list
.orElse(new ArrayList<>()) // or creates empty
.add(price))); // adds the current Price
}
Moreover, I bet your intention was a bit different. There is no method Map::set
map.set("attibute1",Collections.singletonList(price))
Didn't you mean to put a List<Price> with one item to the very same key instead?
map.put("attibute1", Collections.singletonList(price))
For this reason you can use the way I posted above.

What about using e.g. Enum to define 7 different objects each of them is responsible for concrete attribute:
// this is client code, looks pretty easy
Map<String, List<PriceClass>> map = new HashMap<>();
for (PriceClass price : prices)
PriceAttribute.add(map, price);
// all logic is hidden within special Enum
enum PriceAttribute {
ATTRIBUTE1("attribute1", PriceClass::getAttribute1),
ATTRIBUTE2("attribute2", PriceClass::getAttribute2),
ATTRIBUTE3("attribute3", PriceClass::getAttribute3),
ATTRIBUTE4("attribute4", PriceClass::getAttribute4),
ATTRIBUTE5("attribute5", PriceClass::getAttribute5),
ATTRIBUTE6("attribute6", PriceClass::getAttribute6),
ATTRIBUTE7("attribute7", PriceClass::getAttribute7);
private final String key;
private final Function<PriceClass, ?> get;
PriceAttribute(String key, Function<PriceClass, ?> get) {
this.key = key;
this.get = get;
}
public static void add(Map<String, List<PriceClass>> map, PriceClass price) {
for (PriceAttribute attribute : values()) {
if (attribute.get.apply(price) != null) {
map.computeIfAbsent(attribute.key, key -> new ArrayList<>()).add(price);
break;
}
}
}
}

Following is repetitive code:
if(map.get("attribute1") !=null)
{
map.get("attribute1").add(price);
}
else
{
map.set("attibute1",Collections.singletonList(price))
}
It could be refactored into a method, and called from the parent method which could make this look a bit cleaner.
Additionally you can also try
prices.removeAll(Collections.singleton(null)) and then run loop through it, to avoid one "If" condition.

Following code snippet can be added as a method.
if(map.get("attribute1") !=null) {
map.get("attribute1").add(price);
} else {
map.set("attibute1",Collections.singletonList(price))
}
to
private static void addPrice(String attributeName, Price price){
if(map.get(attributeName) !=null) {
map.get(attributeName).add(price);
} else {
map.set(attributeName,Collections.singletonList(price))
}
}
Also, map should be created static to be used in this case.

That wasn't be strict answer to the question but I want to improve your code.
You call get twice. Instead of this:
if(map.get("attribute1") !=null) {
map.get("attribute1").add(price);
} else {
map.set("attibute1",Collections.singletonList(price))
}
Use this:
final List<PriceClass> attribute1 = map.get("attribute1");
if (attribute1 != null) {
attribute1.add(price);
} else {
map.set("attibute1", Collections.singletonList(price))
}
Second you use Collections.singletonList which create immutable list so if you try to add something to it (and you do it) you'll get exception. You should use
new ArrayList<PriceClass>(Arrays.asList(price)) or
Stream.of(price).collect(toList())

If this is the real code you have, so create an array of attribute
Attribute[] attributesOf(Price price){
Attribute[] a = new Attribute[7];
a[0] = price.getAttribute1();
a[1] = price.getAttribute2();
...
a[6] = price.getAttribute7();
}
when you have more attributes, just modify this method. Then your code can be refactor to
for(PriceClass price : prices){
Attribute[] attributes = attributesOf(price);
for(int i=0;i<attributes.length;i++){
String key = "attribute" + (i+1);
if(attributes[i] != null){
map.get(key).add(price);
}
else{
map.set(key, Collections.singletonList(price));
}
}
}
But if you code are different, like price.getGrossAmount(), price.getNetAmount(), price.getTax() you have to find the general contract to define the type of array.
Nevertheless, you have to understand that good data structure will make your code simple and perform well. Maybe you don't need to define attribute in the price class. Maybe you don't need the price class, but you can use BigDecimal as the price value instead.
I don't know your context well, I don't know what are you doing. I think you might have a better answer from someone else if you show the real code as well as its context.

Acc. to your question, it seems like if attibute1 is present in a PriceClass object, it will go to the attibute1 key. This means if all objects in prices list has attibute1, the whole list will go the attibute1 key.
With the above assumption, here is a java-8 solution containing streams.
public static void main(String[] args){
Map<String,List<PriceClass>> map = new HashMap<>();
List<PriceClass> prices =new ArrayList<>();
prices.add(new PriceClass(1,2,3,4,5,6,7));
prices.add(new PriceClass(null,12,13,14,15,16,17));
map = prices.stream()
.map(priceClass -> Arrays
.stream(PriceClass.class.getDeclaredFields())
.map(field -> getFieldValue(field, priceClass))
.filter(Objects::nonNull)
.findFirst()
.orElse(null)
)
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(AbstractMap.SimpleEntry::getKey,
Collectors.mapping(AbstractMap.SimpleEntry::getValue,
Collectors.toList())));
System.out.println(map);
}
private static AbstractMap.SimpleEntry<String,PriceClass> getFieldValue(Field field, PriceClass priceClass){
Optional<Integer> value = Optional.empty();
try {
value = Optional.ofNullable((Integer)field.get(priceClass));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (value.isPresent()) {
return new AbstractMap.SimpleEntry<>(field.getName(), priceClass);
}
return null;
}
Output:
{attribute1=[PriceClass{attribute1=1, attribute2=2, attribute3=3, attribute4=4, attribute5=5, attribute6=6, attribute7=7}],
attribute2=[PriceClass{attribute1=null, attribute2=12, attribute3=13, attribute4=14, attribute5=15, attribute6=16, attribute7=17}]}

Related

using Java 8 How to filter by list and group by based on filter condition and convert to Map with their count

I want to convert below for loop to Java 8. But having problem with filtering list of status and grouping into one status and total count.
I tried but for each "LIVE", "DRAFT", "TEST" have to loop 3 times and get 3 different maps. Is it possible to get into one loop using Java-8?
Where "LIVE", "DRAFT" and "TEST" are again combination of status from workflowInstance like DRAFT = {"DRAFT_EDIT","DRAFT_SAVE"}. I want to categorize all status into 3 based on this combination defined.
Map<String, Integer> summaryMap = new HashMap<>();
int l = 0, d = 0, t = 0;
for (WorkflowInstance instance : workflowInstances) {
if (liveStatuses.contains(instance.getStatus())) {
summaryMap.put("LIVE", l++);
} else if (testStatuses.contains(instance.getStatus())) {
summaryMap.put("TEST", t++);
} else if (draftStatuses.contains(instance.getStatus())) {
summaryMap.put("DRAFT", d++);
}
}
Java-8 individually for "LIVE", "DRAFT" and "TEST":
map.put("DRAFT", workflowInstances.stream()
.filter(inst-> Constants.DRAFT_STATUS.contains(inst.getStatus()))
.collect(Collectors.groupingBy(WorkflowInstance::getStatus, Collectors.counting()))
.entrySet().stream().mapToLong(e-> e.getValue()).sum()
);
map.put("LIVE", workflowInstances.stream()
.filter(inst-> Constants.LIVE_STATUS.contains(inst.getStatus()))
.collect(Collectors.groupingBy(WorkflowInstance::getStatus, Collectors.counting()))
.entrySet().stream().mapToLong(e-> e.getValue()).sum()
);
// Similar for "TEST"
Instead of looping 3 times I want to do in 1 go and categorize them.
Any help would be appreciated.
You cannot avoid extracting the type of the status anyway. Create a dedicated method for it (I suppose the list of statuses as liveStatuses etc. are either static or instance variables. Note that you have forgotten to handle the case no one of the predefined statuses match the current one. In that case, let's use "UNDEFINED":
String extractStatus(WorkflowInstance workflowInstance) {
String status = workflowInstance.getStatus();
if (liveStatuses.contains(status)) {
return "LIVE";
} else if (testStatuses.contains(status)) {
return "TEST";
} else if (draftStatuses.contains(status)) {
return "DRAFT";
}
return "UNCATEGORIZED"; // in case nothing is matched
}
Then the collecting is fairly easy using Collectors.groupingBy with a combination of Collectors.counting:
Map<String, Long> map = workflowInstances.stream()
.collect(Collectors.groupingBy( // groups to Map
this::extractStatus, // extracted status is the key
Collectors.counting())); // value is a number of occurences
Note the result is Map<String, Long> if you insist on Map<String, Integer> you need an additional downstream collector using Collectors.collectingAndThen:
Map<String, Integer> map = workflowInstances.stream()
.collect(Collectors.groupingBy( // groups to Map
Foo::extractStatus, // extracted status is the key
Collectors.collectingAndThen( // value is collected ...
Collectors.counting(), // ... a number of occurences
count -> new BigDecimal(count) // ... as Integer from Long
.intValueExact()))); // ... but might throw an exception
Using The ArithmeticException is thrown if the number is outside bounds. Remember that Long has the way bigger range than Integer. There are many different ways of the conversion of Long->Integer but they follow the same principle.
... or use a simple trick using Collectors.summingInt(e -> 1) instead of Collectors.counting as #HadiJ suggested. It returns Integer instead:
Map<String, Integer> map = workflowInstances.stream()
.collect(Collectors.groupingBy(this::extractStatus, Collectors.summingInt(e -> 1)));
I think all you need to do is to create a more complex grouping function that would transform getStatus into one of the three types you want. You could try something like this:
Map<String, Long> summaryMap = workflowInstances.stream()
.groupingBy(a -> {
if (liveStatuses.contains(a.getStatus())) {
return "LIVE";
} else if (testStatuses.contains(a.getStatus())) {
return "TEST";
} else if (draftStatuses.contains(a.getStatus())) {
return "DRAFT";
}
}, Collectors.counting());
Check this code:
My solution add a enum to handle multiple status in different types.
public static void main(String[] args) {
List<Instance> instances = new ArrayList<>();
instances.add(new Instance("LIVE"));
instances.add(new Instance("TEST"));
instances.add(new Instance("TEST"));
instances.add(new Instance("TEST"));
instances.add(new Instance("DRAFT"));
instances.add(new Instance("DRAFT"));
Map<String, Long> counts = instances.stream()
.collect(Collectors.groupingBy(a -> TemplateStatus.getStatus(a.getStatus()),Collectors.counting());
System.out.println(counts); //output: {DRAFT=2, TEST=3, LIVE=1}
}
class Instance {
public Instance(String status) {
this.status = status;
}
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
enum TemplateStatus {
LIVE("LIVE,LIVE2"), DRAFT("DRAFT,DRAFT_2"), TEST("TEST_1,TEST");
private List<String> status;
TemplateStatus(String s) {
status = Arrays.asList(s.split(","));
}
public static String getStatus(String s) {
if (LIVE.status.contains(s)) return "LIVE";
else if (TEST.status.contains(s)) return "TEST";
else if (DRAFT.status.contains(s)) return "DRAFT";
return "UNKNOWN";
}
}
Hope this helps you

Converting array iteration to lambda function using Java8

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.

How to find a field of an object that is a value in a HashMap?

I'm trying to check if value of productId is in HashMap, but i dont understand how to adress it properly.
public static void main(String[] args) {
HashMap<Storage, HashSet<Product>> myMap = new HashMap<>();
Storage storage1 = new Storage("101", "1");
Storage storage2 = new Storage("102", "2");
HashSet<Product> myProduct = new HashSet<>();
Product product = new Product("120", "bread", "15");
myProduct.add(product);
myMap.put(storage1, myProduct);
System.out.println(myMap);
String in = "120";
List entry = new ArrayList(myMap.values());
if (entry.contains(in)) {
System.out.println("true");
}
}
Both storage and product classes have private fields, constructors, getters, setters, and hashcode and equals generated by IDEA.
Using java 8, you can do this:
String in = "120";
boolean contains = myMap
.values().stream()
.flatMap(Set::stream)
.anyMatch(p -> p.getId().equals(in)));
System.out.println("Contains? " + contains);
This basically "streams" through the values inside the map, calls stream on the subsets, then returns true when the id of any item matches the provided string, false otherwise
Using java 8:
myMap.forEach((k,v) -> {
for (Product p : v) {
if (p.getValue().equals(in))
System.out.println(true);
}
});
EDIT: fixed the answer

How to conditionally modify a Map in Java 8 stream API?

I am trying to modify a Map's keys based on conditional logic and struggling. I'm new to Java 8 streams API. Let's say I have a map like this:
Map<String, String> map = new HashMap<>();
map.put("PLACEHOLDER", "some_data1");
map.put("Google", "some_data2");
map.put("Facebook", "some_data3");
map.put("Microsoft", "some_data4");
When I would like to do is find the references of PLACEHOLDER and conditionally change that key to something else based on a boolean condition. I feel like it should be something like the below, but this doesn't even compile of course.
boolean condition = foo();
map = map.entrySet().stream().filter(entry -> "PLACEHOLDER".equals(entry.getKey()))
.map(key -> {
if (condition) {
return "Apple";
} else {
return "Netflix";
}
}).collect(Collectors.toMap(e -> e.getKey(), Map.Entry::getValue));
I found this question which kind of makes me think maybe I can't do this with Java 8 stream APIs. Hopefully someone better at this than me knows how to do this. Ideone link if you want to play with it.
You've filtered out all elements that aren't PLACEHOLDER. You need to add that filter logic to your map operation:
final Map<String, String> output = input.entrySet().stream()
.map(e -> {
if (!e.getKey().equals("PLACEHOLDER")) {
return e;
}
if (condition) {
return new AbstractMap.SimpleImmutableEntry<>("Apple", e.getValue());
}
return new AbstractMap.SimpleImmutableEntry<>("Netflix", e.getValue());
}).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
But as you are guaranteed to only have a single instance of PLACEHOLDER in the Map, you can just do
String placeholderData = input.remove("PLACEHOLDER");
if (placeholderData != null) {
input.put(condition ? "Apple" : "Netflix", placeholderData);
}
If you really want to do it using Streams, you just need to move the conditional logic to the collection phase, like that:
boolean condition = true;
map.entrySet().stream().collect(Collectors.toMap(
entry -> mapKey(entry.getKey(), condition), Map.Entry::getValue
));
where:
private static String mapKey(String key, boolean condition) {
if (!"PLACEHOLDER".equals(key)) {
return key;
}
if (condition) {
return "Apple";
} else {
return "Netflix";
}
}
However, the second part of Boris the Spider's answer using Map.remove and Map.put seems the best way to go.

How to perform filtering by Key on KeyValue objects using Lambda-Expressions?

Given i want to filter a List of Key-Value objects.
My (Document)-Object from the example below looks like this
{
"attributeEntityList" : [
{key: 'key1', value: 'somevalue1'},
{key: 'key2', value: 'somevalue2'},
{key: 'key3', value: 'somevalue3'}
]
}
When I pass in a list of the following keys ["key1", "key2", "key3"], I expect my function to return the whole given List of attributes.
When I pass in a list of the following keys ["key1", "key2"], I expect my function to return a list of Attributes with the given key-names.
When I pass in a list of the following keys ["key1", "key2", "faultyKey"], I expect my function to return an Empty list.
My imperative-style solution looks like this and it works okay:
private List<AttributeEntity> getAttributeEntities(List<String> keys, Document value) {
final List<AttributeEntity> documentAttributeList = value.getAttributeEntityList();
final List<AttributeEntity> resultList = new ArrayList<>();
for(String configKey: keys){
boolean keyInAttribute = false;
for(AttributeEntity documentAttribute : documentAttributeList){
if(configKey.equals(documentAttribute.getAttribute_key())){
keyInAttribute = true;
resultList.add(documentAttribute);
break;
}
}
if(!keyInAttribute){
resultList.clear();
break;
}
}
return resultList;
}
For education and fun (and maybe better scaling) I'd like to know how to convert this piece of Code into a solution using the new Java 8 streaming-api.
This is what I came up with, converting my pre-Java8-code to Java8.
To my eyes it looks much more concise and it's shorter. But it does not, what I expect it to do :/
I'm realy struggling implementing the third bulletpoint of my requirements.
It always returns all (found) Attributes, even when i pass in a not existant key.
private List<AttributeEntity> getAttributeEntities(List<String> keys, Document value) {
final List<AttributeEntity> documentAttributeList = value.getAttributeList();
return documentAttributeList.stream()
.filter(attribute ->
keys.contains(attribute.getAttribute_key())
).collect(Collectors.toList());
}
I'm thinking of implementing my own custom Collector.
Since my Collector should only return the List, when the collected results contain each given key at least once.
Any other Idea on how to achieve that?
This solution passes my tests.
But it feel's like i'm putting the cart before the horse.
It's neither concise nor short or elegant any more.
private List<AttributeEntity> getAttributeEntities(List<String> keys, Document value) {
final List<AttributeEntity> documentAttributeList = value.getAttributeList();
return documentAttributeList.stream()
.filter(attribute ->
keys.contains(attribute.getAttribute_key())
)
.collect(Collectors.collectingAndThen(Collectors.toList(), new Function<List<AttributeEntity>, List<AttributeEntity>>() {
#Override
public List<AttributeEntity> apply(List<AttributeEntity> o) {
System.out.println("in finisher code");
if (keys.stream().allMatch(key -> {
return o.stream().filter(attrbiute -> attrbiute.getAttribute_key().equals(key)).findAny().isPresent();
})) {
return o;
} else {
return new ArrayList<AttributeEntity>();
}
}
}));
}
First of all I must say that I'm also new at Java 8 features, so I'm not familiar with everything, and not very used to functional programming. I tried a different approach, dividing it all into some methods.
Here it is:
public class Main {
private static List<AttributeEntity> documentAttributeList;
static {
documentAttributeList = new ArrayList<>();
documentAttributeList.add(new AttributeEntity("key1", "value1"));
documentAttributeList.add(new AttributeEntity("key2", "value2"));
documentAttributeList.add(new AttributeEntity("key3", "value3"));
}
public static void main(String[] args) {
Main main = new Main();
List<AttributeEntity> attributeEntities = main.getAttributeEntities(Arrays.asList("key1", "key2"));
for (AttributeEntity attributeEntity : attributeEntities) {
System.out.println(attributeEntity.getKey());
}
}
private List<AttributeEntity> getAttributeEntities(List<String> keys) {
if(hasInvalidKey(keys)){
return new ArrayList<>();
} else {
return documentAttributeList.stream().filter(attribute -> keys.contains(attribute.getKey())).collect(toList());
}
}
private boolean hasInvalidKey(List<String> keys) {
List<String> attributeKeys = getAttributeKeys();
return keys.stream().anyMatch(key -> !attributeKeys.contains(key));
}
private List<String> getAttributeKeys() {
return documentAttributeList.stream().map(attribute -> attribute.getKey()).collect(toList());
}
}
If a document can never have multiple attributes with the same name, I think you can do it like this (don't have a compiler handy to try):
Map<String, AttributeEntity> filteredMap=value.getAttributeEntityList().stream()
.filter(at->keys.contains(at.getKey()))
.collect(toMap(at->at.getKey(), at->at));
return filteredMap.keySet().containsAll(keys)
? new ArrayList<>(filteredMap.values())
: new ArrayList<>();
If multiple attributes per name are allowed, you would have to use groupingBy instead of toMap. You can, of course, rewrite this with collectingAndThen but I think it would be less clear.
I came up with something.
I don't know if it it the most elegant solution but at least it works and i can reason about it.
private List<AttributeEntity> getAttributeEntities(List<String> keys, Document value) {
final List<AttributeEntity> documentAttributeList = value.getAttributeList();
boolean allKeysPresentInAnyAttribute = keys.stream()
.allMatch(key ->
documentAttributeList.stream()
.filter(attrbiute ->
attrbiute.getAttribute_key().equals(key)
)
.findAny()
.isPresent()
);
if (allKeysPresentInAnyAttribute) {
return documentAttributeList.stream()
.filter(attribute ->
keys.contains(attribute.getAttribute_key())
)
.collect(Collectors.toList());
}
return new ArrayList<>();
}
Any hints or comments greatly appreciated.

Categories