Cleaning a list of data in Java8 - java

For cleaning a list of data, I have created a method which accepts the list of data and list of cleaning operation to be performed.
public <T> List<T> cleanData(List<T> data, List<Function<T, T>> cleanOps) {
List<T>dataNew=data.stream().map((str) -> {
T cleanData = str;
for(Function<T,T> function:cleanOps) {
cleanData=function.apply(cleanData);
}
return cleanData;
}).collect(Collectors.toList());
return dataNew;
}
The issue here is that we are creating the whole list again as Collectors.toList() returns a new list.
Can we achieve the same result without using the extra space?
Below is the code for invocation:
public void processData() {
List<Function<String, String>> cleanOps = new ArrayList<>();
cleanOps.add(String::toLowerCase);
cleanOps.add(str -> str.replaceAll(" ", ""));
List<String> data = new ArrayList<>();
data.add("John Doe");
data.add("Jane Doe");
System.out.println(Arrays.toString(cleanData(data, cleanOps).toArray()));
}

If modifying the list in-place is allowed, you could use
public <T> List<T> cleanData(List<T> data, List<Function<T, T>> cleanOps) {
cleanOps.stream().reduce(Function::andThen).ifPresent(f -> data.replaceAll(f::apply));
return data;
}
andThen combines two Function instances and if at least one function was present, i.e. the cleanOps list is not empty, the resulting combined function will be applied to all list elements and the elements replaced by the result, using replaceAll.
Unfortunately, replaceAll requires a UnaryOperator<T> rather than a Function<T,T>, despite being functionally equivalent, so we have to use the adapter f::apply.
Since these function types are equivalent, we could change the list to List<UnaryOperator<T>>, but then, we have to face the fact that there is no specialized andThen implementation for UnaryOperator, so we would need:
public <T> List<T> cleanData(List<T> data, List<UnaryOperator<T>> cleanOps) {
cleanOps.stream()
.reduce((f1,f2) -> t -> f2.apply(f1.apply(t)))
.ifPresent(data::replaceAll);
return data;
}
The caller’s source changes to
List<UnaryOperator<String>> cleanOps = new ArrayList<>();
cleanOps.add(String::toLowerCase);
cleanOps.add(str -> str.replaceAll(" ", ""));
List<String> data = new ArrayList<>();
data.add("John Doe");
data.add("Jane Doe");
System.out.println(cleanData(data, cleanOps));
then.
As a side note, there is no need for a construct like
System.out.println(Arrays.toString(cleanData(data, cleanOps).toArray()));
as the toString() method of a List produces exactly the same output. Since the println(Object) method calls toString() implicitly, you can just use
System.out.println(cleanData(data, cleanOps));

It looks like you need to use List.replaceAll(), which replaces each element of this list with the result of applying the given operator to that element.
public <T> List<T> cleanString(List<T> data, List<Function<T, T>> cleanOps) {
data.replaceAll(str -> {
T cleanData = str;
for (Function<T,T> function : cleanOps) {
cleanData = function.apply(cleanData);
}
return cleanData;
});
return data;
}
I'd rename the method, though, since it's generic, so it doesn't necessarily process a List of Strings.

Related

How to avoid multiple Streams with Java 8

I am having the below code
trainResponse.getIds().stream()
.filter(id -> id.getType().equalsIgnoreCase("Company"))
.findFirst()
.ifPresent(id -> {
domainResp.setId(id.getId());
});
trainResponse.getIds().stream()
.filter(id -> id.getType().equalsIgnoreCase("Private"))
.findFirst()
.ifPresent(id ->
domainResp.setPrivateId(id.getId())
);
Here I'm iterating/streaming the list of Id objects 2 times.
The only difference between the two streams is in the filter() operation.
How to achieve it in single iteration, and what is the best approach (in terms of time and space complexity) to do this?
You can achieve that with Stream IPA in one pass though the given set of data and without increasing memory consumption (i.e. the result will contain only ids having required attributes).
For that, you can create a custom Collector that will expect as its parameters a Collection attributes to look for and a Function responsible for extracting the attribute from the stream element.
That's how this generic collector could be implemented.
/** *
* #param <T> - the type of stream elements
* #param <F> - the type of the key (a field of the stream element)
*/
class CollectByKey<T, F> implements Collector<T, Map<F, T>, Map<F, T>> {
private final Set<F> keys;
private final Function<T, F> keyExtractor;
public CollectByKey(Collection<F> keys, Function<T, F> keyExtractor) {
this.keys = new HashSet<>(keys);
this.keyExtractor = keyExtractor;
}
#Override
public Supplier<Map<F, T>> supplier() {
return HashMap::new;
}
#Override
public BiConsumer<Map<F, T>, T> accumulator() {
return this::tryAdd;
}
private void tryAdd(Map<F, T> map, T item) {
F key = keyExtractor.apply(item);
if (keys.remove(key)) {
map.put(key, item);
}
}
#Override
public BinaryOperator<Map<F, T>> combiner() {
return this::tryCombine;
}
private Map<F, T> tryCombine(Map<F, T> left, Map<F, T> right) {
right.forEach(left::putIfAbsent);
return left;
}
#Override
public Function<Map<F, T>, Map<F, T>> finisher() {
return Function.identity();
}
#Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
main() - demo (dummy Id class is not shown)
public class CustomCollectorByGivenAttributes {
public static void main(String[] args) {
List<Id> ids = List.of(new Id(1, "Company"), new Id(2, "Fizz"),
new Id(3, "Private"), new Id(4, "Buzz"));
Map<String, Id> idByType = ids.stream()
.collect(new CollectByKey<>(List.of("Company", "Private"), Id::getType));
idByType.forEach((k, v) -> {
if (k.equalsIgnoreCase("Company")) domainResp.setId(v);
if (k.equalsIgnoreCase("Private")) domainResp.setPrivateId(v);
});
System.out.println(idByType.keySet()); // printing keys - added for demo purposes
}
}
Output
[Company, Private]
Note, after the set of keys becomes empty (i.e. all resulting data has been fetched) the further elements of the stream will get ignored, but still all remained data is required to be processed.
IMO, the two streams solution is the most readable. And it may even be the most efficient solution using streams.
IMO, the best way to avoid multiple streams is to use a classical loop. For example:
// There may be bugs ...
boolean seenCompany = false;
boolean seenPrivate = false;
for (Id id: getIds()) {
if (!seenCompany && id.getType().equalsIgnoreCase("Company")) {
domainResp.setId(id.getId());
seenCompany = true;
} else if (!seenPrivate && id.getType().equalsIgnoreCase("Private")) {
domainResp.setPrivateId(id.getId());
seenPrivate = true;
}
if (seenCompany && seenPrivate) {
break;
}
}
It is unclear whether that is more efficient to performing one iteration or two iterations. It will depend on the class returned by getIds() and the code of iteration.
The complicated stuff with two flags is how you replicate the short circuiting behavior of findFirst() in your 2 stream solution. I don't know if it is possible to do that at all using one stream. If you can, it will involve something pretty cunning code.
But as you can see your original solution with 2 stream is clearly easier to understand than the above.
The main point of using streams is to make your code simpler. It is not about efficiency. When you try to do complicated things to make the streams more efficient, you are probably defeating the (true) purpose of using streams in the first place.
For your list of ids, you could just use a map, then assign them after retrieving, if present.
Map<String, Integer> seen = new HashMap<>();
for (Id id : ids) {
if (seen.size() == 2) {
break;
}
seen.computeIfAbsent(id.getType().toLowerCase(), v->id.getId());
}
If you want to test it, you can use the following:
record Id(String getType, int getId) {
#Override
public String toString() {
return String.format("[%s,%s]", getType, getId);
}
}
Random r = new Random();
List<Id> ids = r.ints(20, 1, 100)
.mapToObj(id -> new Id(
r.nextBoolean() ? "Company" : "Private", id))
.toList();
Edited to allow only certain types to be checked
If you have more than two types but only want to check on certain ones, you can do it as follows.
the process is the same except you have a Set of allowed types.
You simply check to see that your are processing one of those types by using contains.
Map<String, Integer> seen = new HashMap<>();
Set<String> allowedTypes = Set.of("company", "private");
for (Id id : ids) {
String type = id.getType();
if (allowedTypes.contains(type.toLowerCase())) {
if (seen.size() == allowedTypes.size()) {
break;
}
seen.computeIfAbsent(type,
v -> id.getId());
}
}
Testing is similar except that additional types need to be included.
create a list of some types that could be present.
and build a list of them as before.
notice that the size of allowed types replaces the value 2 to permit more than two types to be checked before exiting the loop.
List<String> possibleTypes =
List.of("Company", "Type1", "Private", "Type2");
Random r = new Random();
List<Id> ids =
r.ints(30, 1, 100)
.mapToObj(id -> new Id(possibleTypes.get(
r.nextInt((possibleTypes.size()))),
id))
.toList();
You can group by type and check the resulting map.
I suppose the type of ids is IdType.
Map<String, List<IdType>> map = trainResponse.getIds()
.stream()
.collect(Collectors.groupingBy(
id -> id.getType().toLowerCase()));
Optional.ofNullable(map.get("company")).ifPresent(ids -> domainResp.setId(ids.get(0).getId()));
Optional.ofNullable(map.get("private")).ifPresent(ids -> domainResp.setPrivateId(ids.get(0).getId()));
I'd recommend a traditionnal for loop. In addition of being easily scalable, this prevents you from traversing the collection multiple times.
Your code looks like something that'll be generalised in the future, thus my generic approch.
Here's some pseudo code (with errors, just for the sake of illustration)
Set<String> matches = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for(id : trainResponse.getIds()) {
if (! matches.add(id.getType())) {
continue;
}
switch (id.getType().toLowerCase()) {
case "company":
domainResp.setId(id.getId());
break;
case "private":
...
}
}
Something along these lines can might work, it would go through the whole stream though, and won't stop at the first occurrence.
But assuming a small stream and only one Id for each type, why not?
Map<String, Consumer<String>> setters = new HashMap<>();
setters.put("Company", domainResp::setId);
setters.put("Private", domainResp::setPrivateId);
trainResponse.getIds().forEach(id -> {
if (setters.containsKey(id.getType())) {
setters.get(id.getType()).accept(id.getId());
}
});
We can use the Collectors.filtering from Java 9 onwards to collect the values based on condition.
For this scenario, I have changed code like below
final Map<String, String> results = trainResponse.getIds()
.stream()
.collect(Collectors.filtering(
id -> id.getType().equals("Company") || id.getIdContext().equals("Private"),
Collectors.toMap(Id::getType, Id::getId, (first, second) -> first)));
And getting the id from results Map.

Class type as a parameter in Java

I have a method:
private List<ApplicationForVacancy> createListOfApplicationsForVacancy(List<Document> documents) {
return documents.stream()
.filter(d -> d instanceof ApplicationForVacancy)
.map(d -> (ApplicationForVacancy) d)
.collect(Collectors.toList());
}
but I'd like to pass the name of the class/the type of the class in parameter and return type of the method like this:
private List<TypeOfSomeClass> createList (List<Document> documents, String or Type typeOfSomeClass) {
return documents.stream()
.filter(d -> d instanceof typeOfSomeClass)
.map(d -> (typeOfSomeClass) d)
.collect(Collectors.toList());
}
Is it possible? How can I do that?
You have to provide a Class object to declare the type and add a type parameter to the method, like this:
private static <T> List<T> filterType(List<?> input, Class<T> clazz) {
return input.stream()
.filter(clazz::isInstance)
.map(clazz::cast)
.collect(Collectors.toList());
}
You then call the method like this:
List<Object> stuff = List.of("String", Integer.valueOf(3), Double.valueOf(42), new Object());
List<String> strings = filterType(stuff, String.class);
List<Number> numbers = filterType(stuff, Number.class);
strings will only contain String and numbers will contain the 2 numeric values.
To my knowledge you can't perform a type conversion like this. But you could create a new List instance at the start of your method, containing the specified type. Then loop through your existing list, filtering out all instances that should be contained in the result. Add those, then return the new List. Here some pseudo code.
public <T> List<T> filterInstances(List<?> input, Class<T> type){
List<T> result = new ArrayList<>();
for(Object o : input){
if(type.isAssignableFrom(o.getClass()){
result.add((T)o);
}
}
return result;
}

Combine Consumers with different arguments or different arguments number

Let's say, our method receives input String and returns some List output. This output is the result of some number of generators, some of which depend on input and some of them not - they just add predefined values.
I want to implement these generators like a list of some function interfaces (Consumer for example), then combine them into one Consumer and just apply it to the input String.
So, I will able to change small generators easy and independently. But the problem is that not all my generators need input String as a parameter and I just pass this parameter there for only one reason - have an ability to combine such Consumers with others.
public class ConsumersTest {
private static <T, U> BiConsumer<T, U> combine(List<BiConsumer<T, U>> consumers) {
return consumers.stream().reduce((arg1, arg2) -> {}, BiConsumer::andThen);
}
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
combine(getGenerators()).accept(input, output);
return output;
}
private List<BiConsumer<String, List<String>>> getGenerators() {
return Arrays.asList(
this::addFirstDependent,
this::addSecondIndependent
);
}
private void addFirstDependent(String input, List<String> output) {
if (input.contains("some string")) {
output.add("First-Dependent");
}
}
private void addSecondIndependent(String input, List<String> output) {
output.add("Predefined Output");
}}
Is it possible to combine different consumers under one umbrella and apply them in one place? Or this is a bad idea and not the right way to do such things?
It is not an unusual pattern to have a common interface in a modular software and adapters, to make particular implementations fit. E.g.
public class ConsumersTest {
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
generators.accept(input, output);
return output;
}
private static <T, U> BiConsumer<T, U> ignoreFirstArg(Consumer<U> consumer) {
return (t, u) -> consumer.accept(u);
}
private final BiConsumer<String, List<String>> generators =
Stream.<BiConsumer<String, List<String>>>of(
this::addFirstDependent,
ignoreFirstArg(this::addSecondIndependent)
).reduce(BiConsumer::andThen).orElse((arg1, arg2) -> {});
private void addFirstDependent(String input, List<String> output) {
if (input.contains("some string")) {
output.add("First-Dependent");
}
}
private void addSecondIndependent(List<String> output) {
output.add("Predefined Output");
}
}
So ignoreFirstArg is the general adapter for methods not having that first parameter. There can be an arbitrary number of adapter methods. But note that if an adapter is very specific and thus, only use a single time, it’s also possible to write a lambda expression instead of a method reference, right in the combining code. Note that I changed that code, to not get repeatedly evaluated for every generate(String input) call, as otherwise, there would be no point in combining them when you don’t reuse the combined function, as you could also use
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
Stream.<BiConsumer<String, List<String>>>of(
this::addFirstDependent,
ignoreFirstArg(this::addSecondIndependent)
).forEach(g -> g.accept(input, output));
return output;
}
or even simpler
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
this.addFirstDependent(input, output);
this.addSecondIndependent(output);
return output;
}
which is not worse to maintain than the functional code, as still, every generator consists of a single line.

Add prefix and suffix to Collectors.joining() only if there are multiple items present

I have a stream of strings:
Stream<String> stream = ...;
I want to construct a string which concatenates these items with , as a separator. I do this as following:
stream.collect(Collectors.joining(","));
Now I want add a prefix [ and a suffix ] to this output only if there were multiple items. For example:
a
[a,b]
[a,b,c]
Can this be done without first materializing the Stream<String> to a List<String> and then checking on List.size() == 1? In code:
public String format(Stream<String> stream) {
List<String> list = stream.collect(Collectors.toList());
if (list.size() == 1) {
return list.get(0);
}
return "[" + list.stream().collect(Collectors.joining(",")) + "]";
}
It feels odd to first convert the stream to a list and then again to a stream to be able to apply the Collectors.joining(","). I think it's suboptimal to loop through the whole stream (which is done during a Collectors.toList()) only to discover if there is one or more item(s) present.
I could implement my own Collector<String, String> which counts the number of given items and use that count afterwards. But I am wondering if there is a directer way.
This question intentionally ignores there case when the stream is empty.
Yes, this is possible using a custom Collector instance that will use an anonymous object with a count of items in the stream and an overloaded toString() method:
public String format(Stream<String> stream) {
return stream.collect(
() -> new Object() {
StringJoiner stringJoiner = new StringJoiner(",");
int count;
#Override
public String toString() {
return count == 1 ? stringJoiner.toString() : "[" + stringJoiner + "]";
}
},
(container, currentString) -> {
container.stringJoiner.add(currentString);
container.count++;
},
(accumulatingContainer, currentContainer) -> {
accumulatingContainer.stringJoiner.merge(currentContainer.stringJoiner);
accumulatingContainer.count += currentContainer.count;
}
).toString();
}
Explanation
Collector interface has the following methods:
public interface Collector<T,A,R> {
Supplier<A> supplier();
BiConsumer<A,T> accumulator();
BinaryOperator<A> combiner();
Function<A,R> finisher();
Set<Characteristics> characteristics();
}
I will omit the last method as it is not relevant for this example.
There is a collect() method with the following signature:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
and in our case it would resolve to:
<Object> Object collect(Supplier<Object> supplier,
BiConsumer<Object, ? super String> accumulator,
BiConsumer<Object, Object> combiner);
In the supplier, we are using an instance of StringJoiner (basically the same thing that Collectors.joining() is using).
In the accumulator, we are using StringJoiner::add() but we increment the count as well
In the combiner, we are using StringJoiner::merge() and add the count to the accumulator
Before returning from format() function, we need to call toString() method to wrap our accumulated StringJoiner instance in [] (or leave it as is is, in case of a single-element stream
The case for an empty case could also be added, I left it out in order not to make this collector more complicated.
There is already an accepted answer and I upvoted it too.
Still I would like to offer potentially another solution. Potentially because it has one requirement:
The stream.spliterator() of Stream<String> stream needs to be Spliterator.SIZED.
If that applies to your case, you could use also this solution:
public String format(Stream<String> stream) {
Spliterator<String> spliterator = stream.spliterator();
StringJoiner sj = spliterator.getExactSizeIfKnown() == 1 ?
new StringJoiner("") :
new StringJoiner(",", "[", "]");
spliterator.forEachRemaining(sj::add);
return sj.toString();
}
According to the JavaDoc Spliterator.getExactSizeIfKnown() "returns estimateSize() if this Spliterator is SIZED, else -1." If a Spliterator is SIZED then "estimateSize() prior to traversal or splitting represents a finite size that, in the absence of structural source modification, represents an exact count of the number of elements that would be encountered by a complete traversal."
Since "most Spliterators for Collections, that cover all elements of a Collection report this characteristic" (API Note in JavaDoc of SIZED) this could be the desired directer way.
EDIT:
If the Stream is empty we can return an empty String at once. If the Stream has only one String there is no need to create a StringJoiner and to copy the String to it. We return the single String directly.
public String format(Stream<String> stream) {
Spliterator<String> spliterator = stream.spliterator();
if (spliterator.getExactSizeIfKnown() == 0) {
return "";
}
if (spliterator.getExactSizeIfKnown() == 1) {
AtomicReference<String> result = new AtomicReference<String>();
spliterator.tryAdvance(result::set);
return result.get();
}
StringJoiner result = new StringJoiner(",", "[", "]");
spliterator.forEachRemaining(result::add);
return result.toString();
}
I know that I'm late to the party, but I too came across this very same problem recently, and I thought it might be worthwhile documenting how I solved it.
While the accepted solution does work, my opinion is that it is more complicated than it needs to be, making it both hard to read and understand. As opposed to defining an Object implementation and then separate accumulator and combiner functions that access and modify the state of said object, why not just define your own collector? In the problem presented above, this can be achieved as follows:
Collector<CharSequence, StringJoiner, String> collector = new Collector<>() {
int count = 0;
#Override
public Supplier<StringJoiner> supplier() {
return () -> new StringJoiner(",");
}
#Override
public BiConsumer<StringJoiner, CharSequence> accumulator() {
return (joiner, sequence) -> {
count++;
joiner.add(sequence);
};
}
#Override
public BinaryOperator<StringJoiner> combiner() {
return StringJoiner::merge;
}
#Override
public Function<StringJoiner, String> finisher() {
return (joiner) -> {
String joined = joiner.toString();
return (count > 1) ? "[" + joined + "]" : joined;
};
}
#Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
};
The principle is the same, you have a counter that is incremented whenever elements are added to the StringJoiner. Based on the total number of elements added, the finisher decides wheater the StringJoiner result should be wrapped in square brackets. It goes without saying, but you can even define a separate class specific to this implementation, allowing it to be used throughout your codebase. You can even go a step further by making it more generic to support a wider range of input types, adding custom prefix/suffix parameters, and so on. Speaking of use, just pass instances of the collector to the collect method of your Stream:
return list.stream().collect(collector);

How to convert lambda filters with dynamic values to method references

I have some Java code which filters a list based on some input. It currently uses a lambda, for example:
public List<ComplexObject> retrieveObjectsFilteredByTags(List<String> allowedTags) {
List<ComplexObject> complexObjects = retrieveAllComplexObjects();
return complexObjects
.stream()
.filter( compObject -> allowedTags.contains(compObject.getTag()))
.collect(Collectors.toList());
}
What I want to do is to move the filter logic to another method to make it re-usable and easily unit testable. So I wanted to use a method reference in place of the lambda passed to the filter method. Easy to do if the filter logic is fairly static (i.e. list of allowed tags is known at compile time) but I can't figure out how to do this with dynamic data in the filter.
What I wanted was some way to use a method reference and then pass the second dynamic param i.e.
public List<ComplexObject> retrieveObjectsFilteredByTags(List<String> allowedTags) {
List<ComplexObject> complexObjects = retrieveAllComplexObjects();
return complexObjects
.stream()
.filter(this::filterByAllowedTags, allowedTags)
.collect(Collectors.toList());
}
So is it possible to do what I want or am I possibly approaching this situation incorrectly?
I'd suggest passing in a Predicate as a parameter. That way the caller can filter based on any criteria it wants, including allowedTags or whatever:
public List<ComplexObject> retrieveObjectsFilteredBy(Predicate<ComplexObject> pred) {
List<ComplexObject> complexObjects = retrieveAllComplexObjects();
return complexObjects.stream()
.filter(pred)
.collect(Collectors.toList());
}
This would be called like so:
List<String> allowedTags = ... ;
List<ComplexObject> result =
retrieveObjectsFilteredBy(cobj -> allowedTags.contains(cobj.getTag()));
But you could go even further, depending on how much refactoring you're willing to do. Instead of "retrieve" returning a List, how about having it return a Stream? And instead of the retrieve-filter method returning a List, how about having it return a Stream too?
public Stream<ComplexObject> retrieveObjectsFilteredBy2(Predicate<ComplexObject> pred) {
Stream<ComplexObject> complexObjects = retrieveAllComplexObjects2();
return complexObjects.filter(pred);
}
And the calling side would look like this:
List<String> allowedTags = ... ;
List<ComplexObject> result =
retrieveObjectsFilteredBy2(cobj -> allowedTags.contains(cobj.getTag()))
.collect(toList());
Now if you look at it carefully, you can see that the retrieve-filter method isn't adding any value at all, so you might just as well inline it into the caller:
List<String> allowedTags = ... ;
List<ComplexObject> result =
retrieveAllComplexObjects2()
.filter(cobj -> allowedTags.contains(cobj.getTag()))
.collect(toList());
Of course, depending upon what the caller wants to do, it might not want to collect the results into a list; it might want to process the results with forEach(), or something else.
Now you can still factor out the filter into its own method, for testing/debugging, and you can use a method reference:
boolean cobjFilter(ComplexObject cobj) {
List<String> allowedTags = ... ;
return allowedTags.contains(cobj.getTag());
}
List<ComplexObject> result =
retrieveAllComplexObjects2()
.filter(this::cobjFilter)
.collect(toList());
If you don't want the filter to have the allowed tags built into it, you can change it from being a predicate into a higher-order function that returns a predicate instead:
Predicate<ComplexObject> cobjFilter(List<String> allowedTags) {
return cobj -> allowedTags.contains(cobj.getTag());
}
List<String> allowedTags = ... ;
List<ComplexObject> result =
retrieveAllComplexObjects2()
.filter(cobjFilter(allowedTags))
.collect(toList());
Which of these variations makes the most sense depends on what your application looks like and what kind of dynamicism you require in filtering.
How about the following? It extracts the predicate in a separate method to make it easily testable, and can be reused easily.
public Predicate<ComplexObject> tagAllowed(List<String> allowedTags) {
return (ComplexObject co) -> allowedTags.contains(co.getTag());
}
public List<ComplexObject> retrieveObjectsFilteredByTags(List<String> allowedTags) {
List<ComplexObject> complexObjects = retrieveAllComplexObjects();
return complexObjects
.stream()
.filter(tagAllowed(allowedTags))
.collect(Collectors.toList());
}
The problem with the method reference this::filterByAllowedTags is that it has the shape:
(ComplexObject, List<String>) -> boolean
But it's being passed into filter() which expects a lambda of the shape:
(ComplexObject) -> boolean
In other words, this::filterByAllowedTags can never be a Predicate, but it could be some alternate interface Predicate2. Then you'd also need an overload of filter which takes a Predicate2.
Eclipse Collections (formerly GS Collections) has the method select() which behaves just like filter(), and selectWith() which behaves like the overload I just described.
Using select():
public List<ComplexObject> retrieveObjectsFilteredByTags(List<String> allowedTags)
{
// retrieveAllComplexObjects returns a MutableList, possibly FastList
MutableList<ComplexObject> complexObjects = retrieveAllComplexObjects();
// select() returns MutableList here which extends List
return complexObjects.select(compObject -> allowedTags.contains(compObject.getTag()));
}
Using selectWith():
public List<ComplexObject> retrieveObjectsFilteredByTags(List<String> allowedTags)
{
// retrieveAllComplexObjects returns a MutableList, possibly FastList
MutableList<ComplexObject> complexObjects = retrieveAllComplexObjects();
// select() returns MutableList here which extends List
return complexObjects.selectWith(this::filterByAllowedTags, allowedTags);
}
private boolean filterByAllowedTags(ComplexObject complexObject, List<String> allowedTags)
{
return allowedTags.contains(complexObject.getTag());
}
Note: I am a committer for Eclipse Collections.

Categories