Any way to make this stream more efficient? - java

How would one optimise this without adding value into the new ArrayList instead just having the original list updated?
String filterval = filter.toLowerCase();
ArrayList<String> filtredArr = new ArrayList<String>();
listArray.forEach(
valueText -> {
String val = valueText.toLowerCase();
if (val.startsWith(filterval) || val.contains(filterval))
filtredArr.add(valueText);
else {
Arrays.stream(valueText.split(" ")).forEach(
singleWord -> {
String word = singleWord.toLowerCase();
if(word.startsWith(filterval) || word.contains(filterval))
filtredArr.add(valueText);
}
);
}
});

When using streams, it is best not to modify the source of the stream while the latter iterates over the former. (See this for instance.)
Regarding readability and making idiomatic use of stream operations, your code is almost indistinguishable from a plain old for loop (and I would advise you to change it to that if you only use streams to do a forEach, but you could modify it to use a chain of shorter and more "atomic" stream operations, as in the following examples.
To get the list of strings in which at least one word contains filterval:
List<String> filtered = listArray.stream()
.filter(str -> str.toLowerCase().contains(filterval))
.collect(Collectors.toList());
To get the list of strings in which at least one word starts with filterval:
List<String> filtered =
listArray.stream()
.filter(str -> Arrays.stream(str.split(" "))
.map(String::toLowerCase)
.anyMatch(word -> word.startsWith(filterval)))
.collect(Collectors.toList());
To get the list of words (in any of the strings) that contain the filter value:
List<String> filteredWords = listArray.stream()
.map(String::toLowerCase)
.flatMap(str -> Arrays.stream(str.split(" ")))
.filter(word -> word.contains(filterval))
.collect(Collectors.toList());
(I'm assuming listArray, which you don't show in your code snippet, is a List<String>.)
Notes
The condition val.startsWith(filterval) || val.contains(filterval) is completely equivalent to val.contains(filterval). The reason is, if a string starts with filterval, it must also mean that it contains it; one implies the other.
There's no need to compute the lowercase versions of single words since you've already lowercased the whole string (so any words within it will also be lowercase).
Instead of treating single words and space-separated strings separately, we can apply split to all of them and then "concatenate" the sequences of words by using filterMap.
Minimal, complete, verifiable example
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
String filterval = "ba";
List<String> listArray = List.of("foo", "BAR", "spam EGGS ham bAt", "xxbazz");
List<String> filtered1 = listArray.stream()
.filter(str -> str.toLowerCase().contains(filterval))
.collect(Collectors.toList());
List<String> filtered2 =
listArray.stream()
.filter(str -> Arrays.stream(str.split(" "))
.map(String::toLowerCase)
.anyMatch(word -> word.startsWith(filterval)))
.collect(Collectors.toList());
List<String> filtered3 = listArray.stream()
.map(String::toLowerCase)
.flatMap(str -> Arrays.stream(str.split(" ")))
.filter(word -> word.contains(filterval))
.collect(Collectors.toList());
System.out.println(Arrays.toString(filtered1.toArray()));
System.out.println(Arrays.toString(filtered2.toArray()));
System.out.println(Arrays.toString(filtered3.toArray()));
}
}
Output:
[BAR, spam EGGS ham bAt, xxbazz]
[BAR, spam EGGS ham bAt]
[bar, bat, xxbazz]

Related

Java-Stream - Split, group and map the data from a String using a single Stream

I have a string as below:
String data = "010$$fengtai,010$$chaoyang,010$$haidain,027$$wuchang,027$$hongshan,027$$caidan,021$$changnin,021$$xuhui,020$$tianhe";
And I want to convert it into a map of type Map<String,List<String>> (like shown below) by performing the following steps:
first split the string by , and then split by $$;
the substring before $$ would serve as a Key while grouping the data, and the substring after $$ needs to placed inside into a list, which would be a Value of the Map.
Example of the resulting Map:
{
027=[wuchang, hongshan, caidan],
020=[tianhe],
010=[fengtai, chaoyang, haidain],
021=[changnin, xuhui]
}
I've used a traditional way of achieving this:
private Map<String, List<String>> parseParametersByIterate(String sensors) {
List<String[]> dataList = Arrays.stream(sensors.split(","))
.map(s -> s.split("\\$\\$"))
.collect(Collectors.toList());
Map<String, List<String>> resultMap = new HashMap<>();
for (String[] d : dataList) {
List<String> list = resultMap.get(d[0]);
if (list == null) {
list = new ArrayList<>();
list.add(d[1]);
resultMap.put(d[0], list);
} else {
list.add(d[1]);
}
}
return resultMap;
}
But it seems more complicated and verbose. Thus, I want to implement this logic one-liner (i.e. a single stream statement).
What I have tried so far is below
Map<String, List<String>> result = Arrays.stream(data.split(","))
.collect(Collectors.groupingBy(s -> s.split("\\$\\$")[0]));
But the output doesn't match the one I want to have. How can I generate a Map structured as described above?
You simply need to map the values of the mapping. You can do that by specifying a second argument to Collectors.groupingBy:
Collectors.groupingBy(s -> s.split("\\$\\$")[0],
Collectors.mapping(s -> s.split("\\$\\$")[1],
Collectors.toList()
))
Instead of then splitting twice, you can split first and group afterwards:
Arrays.stream(data.split(","))
.map(s -> s.split("\\$\\$"))
.collect(Collectors.groupingBy(s -> s[0],
Collectors.mapping(s -> s[1],Collectors.toList())
));
Which now outputs:
{027=[wuchang, hongshan, caidan], 020=[tianhe], 021=[changnin, xuhui], 010=[fengtai, chaoyang, haidain]}
You can extract the required information from the string without allocating intermediate arrays and by iterating over the string only once and also employing the regex engine only once instead of doing multiple String.split() calls and splitting first by coma , then by $$. We can get all the needed data in one go.
Since you're already using regular expressions (because interpreting "\\s\\s" requires utilizing the regex engine), it would be wise to leverage them to the full power.
Matcher.results()
We can define the following Pattern that captures the pieces of you're interested in:
public static final Pattern DATA = // use the proper name to describe a piece of information (like "027$$hongshan") that the pattern captures
Pattern.compile("(\\d+)\\$\\$(\\w+)");
Using this pattern, we can produce an instance of Matcher and apply Java 9 method Matcher.result(), which produces a stream of MatchResults.
MatchResult is an object encapsulating information about the captured sequence of characters. We can access the groups using method MatchResult.group().
private static Map<String, List<String>> parseParametersByIterate(String sensors) {
return DATA.matcher(sensors).results() // Stream<MatchResult>
.collect(Collectors.groupingBy(
matchResult -> matchResult.group(1), // extracting "027" from "027$$hongshan"
Collectors.mapping(
matchResult -> matchResult.group(2), // extracting "hongshan" from "027$$hongshan"
Collectors.toList())
));
}
main()
public static void main(String[] args) {
String data = "010$$fengtai,010$$chaoyang,010$$haidain,027$$wuchang,027$$hongshan,027$$caidan,021$$changnin,021$$xuhui,020$$tianhe";
parseParametersByIterate(data)
.forEach((k, v) -> System.out.println(k + " -> " + v));
}
Output:
027 -> [wuchang, hongshan, caidan]
020 -> [tianhe]
021 -> [changnin, xuhui]
010 -> [fengtai, chaoyang, haidain]

Java 8 Stream filter using list as condition

Hello I'm beginner when it comes to Java 8 so please be patient for me :)
I have a method that returns custom list of objects. What I need to do: I have got a list of disabledPaymentTypesStrings - and I don't know how many elements it has got. How can I change my code in order to not write every condition like !paymentType.getName().equalsIgnoreCase(disabledPaymentTypesStrings.get(1))? I would like to have somehow my whole list "disabledPaymentTypesStrings" placed here as a condition but I have no idea how to do that. Please give me some hints or advices :)
private List<PaymentType> listOfPaymentTypesForChangePayment(OrderPaymentTypeParameters paymentTypeParameters) {
List<String> disabledPaymentTypesStrings = newArrayList(Splitter.on(COMMA).split(systemUtils.getChangePaymentTypeDisabled()));
return paymentTypeSelector.availablePaymentTypesForChangePayment(paymentTypeParameters).stream()
.filter(paymentType ->
!paymentType.getName().equalsIgnoreCase(disabledPaymentTypesStrings.get(0)) &&
!paymentType.getName().equalsIgnoreCase(disabledPaymentTypesStrings.get(1)) &&
!paymentType.getName().equalsIgnoreCase(disabledPaymentTypesStrings.get(2)))
.collect(toList());
}
A stream approach could consist in the filter() to stream the List of String and keep PaymentType elements where paymentType.getName() don't match with any elements of the List of String :
return paymentTypeSelector.availablePaymentTypesForChangePayment(paymentTypeParameters)
.stream()
.filter(paymentType -> disabledPaymentTypesStrings.stream()
.allMatch(ref -> !ref.equalsIgnoreCase(paymentType.getName())))
.collect(toList());
But you could also compare Strings by using the same case. For example lowercase. It will simplify the filtering.
You can convert the reference list elements to lowercase :
List<String> disabledPaymentTypesStrings = newArrayList(Splitter.on(COMMA).split(systemUtils.getChangePaymentTypeDisabled()))
.stream()
.map(String::toLowerCase)
.collect(toList());
And you can so use List.contains() in the filter() :
return paymentTypeSelector.availablePaymentTypesForChangePayment(paymentTypeParameters)
.stream()
.filter(paymentType -> !disabledPaymentTypesStrings.contains(paymentType.getName().toLowerCase()))
.collect(toList());
Note that for big lists, using a Set would be more efficient.
Use contains(). But you have to think about case sensitivity ignoring
private List<PaymentType> listOfPaymentTypesForChangePayment(OrderPaymentTypeParameters paymentTypeParameters) {
List<String> disabledPaymentTypesStrings = newArrayList(Splitter.on(COMMA).split(systemUtils.getChangePaymentTypeDisabled()));
return paymentTypeSelector.availablePaymentTypesForChangePayment(paymentTypeParameters).stream()
.filter(paymentType -> !disabledPaymentTypesStrings.contains(paymentType)
.collect(toList());
}
Both the steps need to have values in common case(either in uppercase or in lowercase, I preferred lowercase)
List<String> disabledPaymentTypesStringsLowerCase = newArrayList(Splitter.on(COMMA).split(systemUtils.getChangePaymentTypeDisabled()))
.stream()
.map(String::toLowerCase)
.collect(toList());
return paymentTypeSelector.availablePaymentTypesForChangePayment(paymentTypeParameters)
.stream()
.map(paymentType -> paymentType.getName())
.map(String::toLowerCase)
.filter(disabledPaymentTypesStrings::contains)
.collect(toList());
This code can further be refactored if paymentType class is known, assuming class of paymentType is PaymentType code would look like below,
return paymentTypeSelector.availablePaymentTypesForChangePayment(paymentTypeParameters)
.stream()
.map(PaymentType::getName)
.map(String::toLowerCase)
.filter(disabledPaymentTypesStrings::contains)
.collect(toList());

Efficiently joining text in nested lists

Suppose I have a text represented as a collection of lines of words. I want to join words in a line with a space, and join lines with a newline:
class Word {
String value;
}
public static String toString(List <List <Word>> lines) {
return lines.stream().map(
l -> l.stream().map(w -> w.value).collect(Collectors.joining(" "))
).collect(Collectors.joining("\n"));
}
This works fine, but I end up creating an intermediate String object for each line. Is there a nice concise way of doing the same without the overhead?
String s = List.of(
List.of(new Word("a"), new Word("b")),
List.of(new Word("c"), new Word("d")),
List.of(new Word("e"), new Word("f")))
.stream()
.collect(Collector.of(
() -> new StringJoiner(""),
(sj, list) -> {
list.forEach(x -> sj.add(x.getValue()).add(" "));
sj.add("\n");
},
StringJoiner::merge,
StringJoiner::toString));
EDIT
I can thing of this, but can't tell if you would agree for the extra verbosity vs creating that String:
.stream()
.collect(Collector.of(
() -> new StringJoiner(""),
(sj, list) -> {
int i;
for (i = 0; i < list.size() - 1; ++i) {
sj.add(list.get(i).getValue()).add(" ");
}
sj.add(list.get(i).getValue());
sj.add("\n");
},
StringJoiner::merge,
x -> {
String ss = x.toString();
return ss.substring(0, ss.length() - 1);
}));
You can use
public static String toString(List<List<Word>> lines) {
return lines.stream()
.map(l -> l.stream()
.map(w -> w.value)
.collect(() -> new StringJoiner(" "),
StringJoiner::add,
StringJoiner::merge))
.collect(() -> new StringJoiner("\n"),
StringJoiner::merge,
StringJoiner::merge).toString();
}
The inner collect basically does what Collectors.joining(" ") does, but omits the final StringJoiner.toString() step.
Then, the outer collect differs from an ordinary Collectors.joining("\n") in that it accepts StringJoiner as an input and combines them using merge. This relies on a documented behavior:
If the other StringJoiner is using a different delimiter, then elements from the other StringJoiner are concatenated with that delimiter and the result is appended to this StringJoiner as a single element.
This is done internally on the StringBuilder/character data level without creating a String instance while retaining the intended semantic.

Split a stream if it contains multiple comma-separated values

This code will add all elements from mylist to mylist2.
How can I change it to add elements if it finds a comma, and separates them?
ArrayList<String> mylist = new ArrayList<String>();
mylist.add("test");
mylist.add("test2, test3");
ArrayList<String> mylist2 = new ArrayList<String>();
mylist.stream()
.forEach(s -> mylist2.add(s));
mylist2.stream().forEach(System.out::println);
So, current output is:
test
test2, test3
and I need the second arraylist to contain and then print:
test
test2
test3
I know in Java 8 there are filters. Could I do something like
.filter(p -> p.contains(",")...
then something to split it?
You could do a flatMap including a Pattern::splitAsStream like
Pattern splitAtComma = Pattern.compile("\\s*,\\s*");
List<String> myList2 = mylist.stream().flatMap(splitAtComma::splitAsStream)
.collect(Collectors.toList());
You could split each string and flatMap the resulting array:
mylist.stream()
.flatMap(x -> Arrays.stream(x.split(", ")))
.forEach(System.out::println);
You can use :
mylist.forEach(str -> mylist2.addAll(Arrays.asList(str.split("\\s*,\\s*"))));
Output
test
test2
test3
and I need the second arraylist to contain and then print:
You could create two streams : one for creating the target List and another one for displaying it.
List<String> list2 = mylist.stream()
.flatMap(x -> Arrays.stream(x.split(", ")))
.collect(Collectors.toList());
list2.stream().forEach(System.out::println);
or with one stream by using peak() to output the element of the Stream before collecting into a List :
List<String> list2 = mylist.stream()
.flatMap(x -> Arrays.stream(x.split(", ")))
.peek(System.out::println)
.collect(Collectors.toList());

Java Lambda - finding if any String element of list matches partially with any element of another list

I've 2 lists of String
A = {"apple", "mango", "pineapple", "banana", ... }
B = {"app", "framework",...}
What I'm looking for is this: is any element of B at least a partial match ( substring/contains/startsWith ) with any element of A. e.g., 1st element of B 'app' matches partially with at least one element 'apple'.
Other closely matching topics on StackOverflow don't consider 2 lists.
Is there any elegant way to express a solution using Java lambda?
I feel it's a general problem in Search domain. So, if there is any helping or interesting read on this topic, I'd be glad to receive pointers.
Depends on what you mean by elegant, but try this:
List<String> r = list1
.parallelStream()
.filter( w1->{
return list2
.parallelStream()
.anyMatch( w2->w1.contains(w2) );
}
)
.collect(Collectors.toList());
anyMatch (and filter) can be short circuited and will abort the stream of the second list after finding the first match of w1.contains(w2) and return true if found, giving some efficiency. Use this to filter the first stream. Do it in parallel streams.
You could chain streams of the two List<String> and filter with String.contains() or any other condition you want to use (substring(), startsWith()) .
Then you could map couple of String that valid the condition into a String array :
List<String> listOne = Arrays.asList("apple", "mango", "pineapple", "framework");
List<String> listTwo = Arrays.asList("app", "frame");
List<String[]> values = listOne.stream()
.flatMap(x -> listTwo.stream()
.filter(y -> x.contains(y))
.map(y -> {
return new String[] { x, y };
}))
.collect(Collectors.toList());
for (String[] array : values) {
System.out.println(Arrays.toString(array));
}
Output :
[apple, app]
[pineapple, app]
[framework, frame]

Categories