Starting with a bean class MyBean with a single relevant propterty:
#Data
class MyBean {
private String myProperty;
}
Now I have got a set of these beans Set<MyBean> mySet usually with 0, 1, or 2 elements.
The question is: How do I retrieve myProperty from this set if it is equal for all elements, or else null. Preferably in a single line with effort O(n).
I found several examples to determine the boolean if all properties are equal. But I want to know the corresponding property.
Is there something smarter than this?
String uniqueProperty = mySet.stream().map(MyBean::getMyProperty).distinct().count() == 1
? mySet.stream().map(MyBean::getMyProperty).findAny().orElse(null)
: null;
Your version is already O(n).
It's possible to do this with a one-liner (although yours is too depending on how you write it).
String uniqueProperty = mySet.stream()
.map(MyBean::getMyProperty)
.map(Optional::ofNullable)
.reduce((a, b) -> a.equals(b) ? a : Optional.empty()) // Note: equals compares 2 Optionals here
.get() // unwraps first Optional layer
.orElse(null); // unwraps second layer
The only case this doesn't work for is when all property values are null. You cannot distinguish the set (null, null) from (null, "A") for example, they both return null.
Just a single iteration without the use of streams looks much better for such a use case :
Iterator<MyBean> iterator = mySet.iterator();
String uniqueProperty = iterator.next().getMyProperty();
while (iterator.hasNext()) {
if (!iterator.next().getMyProperty().equals(uniqueProperty)) {
uniqueProperty = null; // some default value possibly
break;
}
}
You use the findAny() first and check mySet again with allMatch() to require all items to match the first one in a filter():
String uniqueProperty = mySet.stream().findAny().map(MyBean::getMyProperty)
.filter(s -> mySet.stream().map(MyBean::getMyProperty).allMatch(s::equals))
.orElse(null);
The advantage of this is, that allMatch() will only evaluate all elements if necessary (docs).
Related
I have a stream of data as shown below and I wish to collect the data based on a condition.
Stream of data:
452857;0;L100;csO;20220411;20220411;EUR;000101435;+; ;F;1;EUR;000100000;+;
452857;0;L120;csO;20220411;20220411;EUR;000101435;+; ;F;1;EUR;000100000;+;
452857;0;L121;csO;20220411;20220411;EUR;000101435;+; ;F;1;EUR;000100000;+;
452857;0;L126;csO;20220411;20220411;EUR;000101435;+; ;F;1;EUR;000100000;+;
452857;0;L100;csO;20220411;20220411;EUR;000101435;+; ;F;1;EUR;000100000;+;
452857;0;L122;csO;20220411;20220411;EUR;000101435;+; ;F;1;EUR;000100000;+;
I wish to collect the data based on the index = 2 (L100,L121 ...) and store it in different lists of L120,L121,L122 etc using Java 8 streams. Any suggestions?
Note: splittedLine array below is my stream of data.
For instance: I have tried the following but I think there's a shorter way:
List<String> L100_ENTITY_NAMES = Arrays.asList("L100", "L120", "L121", "L122", "L126");
List<List<String>> list= L100_ENTITY_NAMES.stream()
.map(entity -> Arrays.stream(splittedLine)
.filter(line -> {
String[] values = line.split(String.valueOf(DELIMITER));
if(values.length > 0){
return entity.equals(values[2]);
}
else{
return false;
}
}).collect(Collectors.toList())).collect(Collectors.toList());
I'd rather change the order and also collect the data into a Map<String, List<String>> where the key would be the entity name.
Assuming splittedLine is the array of lines, I'd probably do something like this:
Set<String> L100_ENTITY_NAMES = Set.of("L100", ...);
String delimiter = String.valueOf(DELIMITER);
Map<String, List<String>> result =
Arrays.stream(splittedLine)
.map(line -> {
String[] values = line.split(delimiter );
if( values.length < 3) {
return null;
}
return new AbstractMap.SimpleEntry<>(values[2], line);
})
.filter(Objects::nonNull)
.filter(tempLine -> L100_ENTITY_NAMES.contains(tempLine.getEntityName()))
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList());
Note that this isn't necessarily shorter but has a couple of other advantages:
It's not O(n*m) but rather O(n * log(m)), so it should be faster for non-trivial stream sizes
You get an entity name for each list rather than having to rely on the indices in both lists
It's easier to understand because you use distinct steps:
split and map the line
filter null values, i.e. lines that aren't valid in the first place
filter lines that don't have any of the L100 entity names
collect the filtered lines by entity name so you can easily access the sub lists
I would convert the semicolon-delimited lines to objects as soon as possible, instead of keeping them around as a serialized bunch of data.
First, I would create a model modelling our data:
public record LBasedEntity(long id, int zero, String lcode, …) { }
Then, create a method to parse the line. This can be as well an external parsing library, for this looks like CSV with semicolon as delimiter.
private static LBasedEntity parse(String line) {
String[] parts = line.split(";");
if (parts.length < 3) {
return null;
}
long id = Long.parseLong(parts[0]);
int zero = Integer.parseInt(parts[1]);
String lcode = parts[2];
…
return new LBasedEntity(id, zero, lcode, …);
}
Then the mapping is trivial:
Map<String, List<LBasedEntity>> result = Arrays.stream(lines)
.map(line -> parse(line))
.filter(Objects::nonNull)
.filter(lBasedEntity -> L100_ENTITY_NAMES.contains(lBasedEntity.lcode()))
.collect(Collectors.groupingBy(LBasedEntity::lcode));
map(line -> parse(line)) parses the line into an LBasedEntity object (or whatever you call it);
filter(Objects::nonNull) filters out all null values produced by the parse method;
The next filter selects all entities of which the lcode property is contained in the L100_ENTITY_NAMES list (I would turn this into a Set, to speed things up);
Then a Map is with key-value pairs of L100_ENTITY_NAME → List<LBasedEntity>.
You're effectively asking for what languages like Scala provide on collections: groupBy. In Scala you could write:
splitLines.groupBy(_(2)) // Map[String, List[String]]
Of course, you want this in Java, and in my opinion, not using streams here makes sense due to Java's lack of a fold or groupBy function.
HashMap<String, ArrayList<String>> map = new HashMap<>();
for (String[] line : splitLines) {
if (line.length < 2) continue;
ArrayList<String> xs = map.getOrDefault(line[2], new ArrayList<>());
xs.addAll(Arrays.asList(line));
map.put(line[2], xs);
}
As you can see, it's very easy to understand, and actually shorter than the stream based solution.
I'm leveraging two key methods on a HashMap.
The first is getOrDefault; basically if the value associate with our key doesn't exist, we can provide a default. In our case, an empty ArrayList.
The second is put, which actually acts like a putOrReplace because it lets us override the previous value associated with the key.
I hope that was helpful. :)
you're asking for a shorter way to achieve the same, actually your code is good. I guess the only part that makes it look lengthy is the if/else check in the stream.
if (values.length > 0) {
return entity.equals(values[2]);
} else {
return false;
}
I would suggest introduce two tiny private methods to improve the readability, like this:
List<List<String>> list = L100_ENTITY_NAMES.stream()
.map(entity -> getLinesByEntity(splittedLine, entity)).collect(Collectors.toList());
private List<String> getLinesByEntity(String[] splittedLine, String entity) {
return Arrays.stream(splittedLine).filter(line -> isLineMatched(entity, line)).collect(Collectors.toList());
}
private boolean isLineMatched(String entity, String line) {
String[] values = line.split(DELIMITER);
return values.length > 0 && entity.equals(values[2]);
}
This question already has answers here:
Filter Java Stream to 1 and only 1 element
(24 answers)
Closed 2 years ago.
I have this brainer.
I need to iterate over a Map<UUID, List<Items>> groups and return a single value!
Can I do this with Lambda and how? (how do I get the id of the group that has an item of TYPE_12? The item of TYPE_12 is going to be only one across all groups)
Thanks in advance this is my for code:
String theId = null;
for(Map.Entry<UUID, List<Item>> group : groupsOfItems) {
for (Item item : group.getValue()) {
if (item.getType() == Types.TYPE_12) {
theId = group.getKey().toString();
break;
}
}
}
If you want to use functional style, you can create a stream from you map entry set, then expand it to get a stream of each item in underlying lists :
Optional<String> result = groupOfItems.entrySet().stream()
.flatMap(entry -> entry.getValue().stream())
.filter(item -> Types.TYPE_12.equals(item.getType))
.map(Item::getId)
.findAny();
result.ifPresent(id -> System.out.println("A match has been extracted: "+id));
As is, the functional style way is not more performant than the imperative one, but is more easily adaptable. Let's say you want to know if there's more than one match, you can replace findAny by a collector with a limit :
List<String> matchedIds = groupOfItems.entrySet().stream()
.flatMap(entry -> entry.getValue().stream())
.filter(item -> Types.TYPE_12.equals(item.getType))
.map(Item::getId)
.limit(2)
.collect(Collectors.toList());
if (matched.isEmpty()) System.out.println("No match found");
else if (matched.size() == 1) System.out.println("Exactly one match found: "+matched.get(0));
else System.out.println("At least two matches exist");
Stream usage also allow parallelization if necessary, by simply adding parallel() step to your pipeline.
Here is a solution using lambdas. The difference is this one does not use flatMap and throws an exception if the required value is not found (based on the question stating that there should be one and only one TYPE_12 in the whole value set of the map).
UUID result = groupsOfItems.entrySet().stream()
.filter(e -> e.getValue().stream()
.anyMatch(item -> item.getType() == TYPE.TYPE_12))
.findAny().orElseThrow().getKey();
String theId = null;
find:
for (Map.Entry<UUID, List<Item>> group : groupsOfItems.entrySet()) {
for (Item item : group.getValue()) {
if (item.getType() == Types.TYPE_12) {
theId = group.getKey().toString();
break find;
}
}
}
No don't use lambda for this.
Just use java labels to break out of both for loops after you found your entry. (Notice the find: before the first for loop. That's a label and once you call break find; it will break out of the block marked with that label)
Also you need to use .entrySet in the first for loop. Just passing groupsOfItems won't be enough
I have if condition where I am checking for String equality, if they match I store them in Set. Then I am looping through Set to check if ENUM3 value is present, if yes I replace that particular string with value String Java . I am using Iterator to loop and check for equality. I am looking for same functionality with use of streams where
1. I can loop through Set
2. Check for String equality
3. If ENUM3 found then replace with Java
4. Save all the matched String
Here is my code
{
Set<String> only = new HashSet<String>();
Iterator<Mark> itr = Marks.iterator();
while (itr.hasNext()) {
Mark find = itr.next();
if (ENUM1.getData().equals(find.search())||ENUM3.getData().equals(find.search())) {
only.add(find.search());
only = only.stream()
.map(macro -> macro.equals(ENUM3.getData()) ? "Java" : macro).collect(Collectors.toSet());
}
}
}
Here is what I tried using Stream
only = only.stream()
.map(macro -> macro.equals(ENUM3.getData()) ? "Java" : macro)
.collect(Collectors.toSet());
This should perform your entire operation:
Set<String> only = Marks.stream()
.map(Mark::search)
.filter(mark -> ENUM1.getData().equals(mark)
|| ENUM2.getData().equals(mark)
|| ENUM3.getData().equals(mark))
.map(macro -> macro.equals(ENUM3.getData()) ? "Java" : macro)
.collect(Collectors.toSet());
You seem to be unnecessarily doing these 2 (both of which are avoided in the pipeline above)
iterating through only in each iteration to replace ENUM3.getData() with Java
repeatedly calling Mark.search()
In the following code, a local method is called on every element of a HashSet. If it returns a special value we halt the loop. Otherwise we add every return value to a new HashSet.
HashSet<Object> myHashSet=…;
HashSet<Object> mySecondHashSet=…;
for (Object s : myHashSet) {
Object value = my_method(s);
if(value==specialValue)
return value;
else
mySecondHashSet.add(value);
}
I’d like to parralelize this process. None of the objects in the HashSet have any objects in common (it’s a tree-like structure) so I know they can run without any synchonization issues. How do I modify the code such that each call of my_method(s) starts a new tread, and also that if one of the threads evaluates to the special values, all the threads halt without returning and the special value is returned?
Having in mind java 8, this could be relatively simple, while it won't preserve your initial code semantics:
In case all you need is to return special value once you hit it
if (myHashSet.parallelStream()
.map(x -> method(x))
.anyMatch(x -> x == specialValue)) {
return specialValue;
}
If you need to keep transformed values until you meet the special value, you already got an answer from #Elliot in comments, while need to mention that semantic is not the same as your original code, since no orderer will be preserved.
While it yet to be checked, but I would expect following to be optimized and stop once it will hit wanted special value:
if (myHashSet.parallelStream()
.anyMatch(x -> method(x) == specialValue)) {
return specialValue;
}
I would do that in two passes:
find if any of the transformed set elements matches the special value;
transform them to a Set.
Starting a new thread for each transformation is way too heavy, and will bring your machine to its knees (unless you have very few elements, in which case parallelizing is probably not worth the effort.
To avoid transforming the values twice with my_method, you can do the transformation lazily and memoize the result:
private class Memoized {
private Object value;
private Object transformed;
private Function<Object, Object> transform;
public Memoized(Object value, Function<Object, Object> transform) {
this.value = value;
}
public Object getTransformed() {
if (transformed == null) {
transformed = transform.apply(value);
}
return transformed;
}
}
And then you can use the following code:
Set<Memoized> memoizeds =
myHashSet.stream() // no need to go parallel here
.map(o -> new Memoized(o, this::my_method))
.collect(Collectors.toSet());
Optional<Memoized> matching = memoized.parallelStream()
.filter(m -> m.getTransformed().equals(specialValue))
.findAny();
if (matching.isPresent()) {
return matching.get().getTransformed();
}
Set<Object> allTransformed =
memoized.parallelStream()
.map(m -> m.getTransformed())
.collect(Collectors.toSet());
Following code was an existing code in my project, using the same concept, I am updating the project,
public static List<Option> filterNetworkSettingsOptionBySetting(NetworkSettingsGroup group,
String settingName) {
LOGGER.info("************ Filtering Option based on SettingName ************");
LOGGER.info("***** Setting Name "+settingName);
List<Option> newOptions = new ArrayList<Option>();
if (group == null || group.getOptions() == null || group.getOptions().isEmpty()
|| StringUtils.isBlank(settingName))
return newOptions;
List<Option> oldOptions = group.getOptions();
LOGGER.info("Before Filtering Options "+oldOptions);
Stream<Option> stream = oldOptions.stream();
LOGGER.info("Before Filtering Stream "+stream);
newOptions.addAll(group.getOptions().stream()
.filter(option -> option.getName().equalsIgnoreCase(settingName)).collect(Collectors.toList()));
return newOptions;
}
I am getting NullPointerException at
newOptions.addAll(group.getOptions().stream()
.filter(option -> option.getName().equalsIgnoreCase(settingName)).collect(Collectors.toList()));
this step..
I am not able to understanding that step, can anyone explain me the step clearly... what is happening in that step and how can I divide that step into multiple steps(because I want to check in method call null is coming).
The statement you have asked to be explained is:
newOptions.addAll(group.getOptions().stream()
.filter(option -> option.getName()
.equalsIgnoreCase(settingName))
.collect(Collectors.toList()));
addAll takes a collection so the statement inside returns a collection (a list, in fact).
group.getOptions() must be returning a collection. This should not be null.
.stream() turns the collection into a stream of items
.filter(option -> option.getName().equalsIgnoreCase(settingName)) filters the stream of options to only have options whose name equals settingName (ignoring case).
.collect(Collectors.toList()) turns the stream into a list to be added to newOptions.
In terms of checking for nulls, all the methods mentioned above are not able to return a null: they all return either a Stream or a List. However the getName method of options could be. So, realistically, it is either getOptions or getName that is returning a null value.
I would suggest changing to the following:
Collection<Option> options = group.getOptions();
assert(options != null);
assert(settingName != null);
newOptions.addAll(options.stream()
.filter(o -> settingName.equalsIgnoringCase(o.getName()))
.collect(Collectors.toList());
Then the failing assertion will alert you to the null value. By switching the equalsIgnoringCase the code will filter out null names rather than throwing an exception.