Firstly, I'd suggest avoiding the use of raw types and instead, use List<String[]> as the receiver type.
List<String[]> numbers = reader.lines()
.map(s -> s.split(delimiter)) // substitute with your deilimeter
.collect(Collectors.toList());
You mentioned that you wanted a LinkedList implementation. you should almost always favour ArrayList which toList returns by default currently although it is not guaranteed to persist thus, you can specify the list implementation explcitly with toCollection:
List<String[]> numbers = reader.lines()
.map(s -> s.split(delimiter)) // substitute with your deilimeter
.collect(Collectors.toCollection(ArrayList::new));
and likewise for LinkedList:
List<String[]> numbers = reader.lines()
.map(s -> s.split(delimiter)) // substitute with your deilimeter
.collect(Collectors.toCollection(LinkedList::new));
Answer from Ousmane D. on Stack OverflowFirstly, I'd suggest avoiding the use of raw types and instead, use List<String[]> as the receiver type.
List<String[]> numbers = reader.lines()
.map(s -> s.split(delimiter)) // substitute with your deilimeter
.collect(Collectors.toList());
You mentioned that you wanted a LinkedList implementation. you should almost always favour ArrayList which toList returns by default currently although it is not guaranteed to persist thus, you can specify the list implementation explcitly with toCollection:
List<String[]> numbers = reader.lines()
.map(s -> s.split(delimiter)) // substitute with your deilimeter
.collect(Collectors.toCollection(ArrayList::new));
and likewise for LinkedList:
List<String[]> numbers = reader.lines()
.map(s -> s.split(delimiter)) // substitute with your deilimeter
.collect(Collectors.toCollection(LinkedList::new));
You may do it like so,
Path path = Paths.get("src/main/resources", "data.txt");
try (Stream<String> lines = Files.lines(path)) {
List<String> strings = lines.flatMap(l -> Arrays.stream(l.split(","))).map(String::trim)
.collect(Collectors.toCollection(LinkedList::new));
}
Read each line of the file, then split it using the delimiter. After that trim it to eliminate any remaining white space characters. Finally collect it to a result container.
java - How to split a String into a Stream of Strings? - Stack Overflow
java - How to convert comma-separated String to List? - Stack Overflow
regex - Split list of words using streams in Java - Stack Overflow
java - Split List<String[]> into List<List<String[]>> - Code Review Stack Exchange
Arrays.stream/String.split
Since String.split returns an array String[], I always recommend Arrays.stream as the canonical idiom for streaming over an array.
String input = "dog,cat,bird";
Stream<String> stream = Arrays.stream(input.split( "," ));
stream.forEach(System.out::println);
Stream.of/String.split
Stream.of is a varargs method which just happens to accept an array, due to the fact that varargs methods are implemented via arrays and there were compatibility concerns when varargs were introduced to Java and existing methods retrofitted to accept variable arguments.
Stream<String> stream = Stream.of(input.split(",")); // works, but is non-idiomatic
Stream<String> stream = Stream.of("dog", "cat", "bird"); // intended use case
Pattern.splitAsStream
Pattern.compile(",").splitAsStream(string) has the advantage of streaming directly rather than creating an intermediate array. So for a large number of sub-strings, this can have a performance benefit. On the other hand, if the delimiter is trivial, i.e. a single literal character, the String.split implementation will go through a fast path instead of using the regex engine. So in this case, the answer is not trivial.
Stream<String> stream = Pattern.compile(",").splitAsStream(input);
If the streaming happens inside another stream, e.g. .flatMap(Pattern.compile(pattern) ::splitAsStream) there is the advantage that the pattern has to be analyzed only once, rather than for every string of the outer stream.
Stream<String> stream = Stream.of("a,b", "c,d,e", "f", "g,h,i,j")
.flatMap(Pattern.compile(",")::splitAsStream);
This is a property of method references of the form expression::name, which will evaluate the expression and capture the result when creating the instance of the functional interface, as explained in What is the equivalent lambda expression for System.out::println and java.lang.NullPointerException is thrown using a method-reference but not a lambda expression
Robustness
I can see no difference in the robustness of the three approaches.
Readability
I am not aware of any credible scientific studies on code readability involving experienced Java programmers, so readability is a matter of opinion. Even then, you never know if someone giving their opinion is making an objective distinction between actual readability, what they have been taught about readability, and their own personal taste.
So I will leave it to you to make your own judgements on readability ... noting that you do consider this to be a high priority.
FWIW, the only people whose opinions on this matter are you and your team.
Performance
I think that the answer to that is to carefully benchmark the three alternatives. Holger provides an analysis based on his study of some versions of Java. But:
- He was not able to come to a definite conclusion on which was fastest.
- Strictly speaking, his analysis only applies to the versions of Java he looked at. (Some aspects of his analysis could be different on (say) Android Java, or some future Oracle / OpenJDK version.)
- The relative performance is likely depend on the length of the string being split, the number of fields, and the complexity of the separator regex.
- In a real application, the relative performance may also depend what you do with the
Streamobject, what garbage collector you have selected (since the different versions apparently generate different amounts of garbage), and other issues.
So if you (or anyone else) are really concerned with the performance, you should write a micro-benchmark and run it on your production platform(s). Then do some application specific benchmarking. And you should consider looking at solutions that don't involve streams.
Convert comma separated String to List
List<String> items = Arrays.asList(str.split("\\s*,\\s*"));
The above code splits the string on a delimiter defined as: zero or more whitespace, a literal comma, zero or more whitespace which will place the words into the list and collapse any whitespace between the words and commas.
Please note that this returns simply a wrapper on an array: you CANNOT for example .remove() from the resulting List. For an actual ArrayList you must further use new ArrayList<String>.
Arrays.asList returns a fixed-size List backed by the array. If you want a normal mutable java.util.ArrayList you need to do this:
List<String> list = new ArrayList<String>(Arrays.asList(string.split(" , ")));
Or, using Guava:
List<String> list = Lists.newArrayList(Splitter.on(" , ").split(string));
Using a Splitter gives you more flexibility in how you split the string and gives you the ability to, for example, skip empty strings in the results and trim results. It also has less weird behavior than String.split as well as not requiring you to split by regex (that's just one option).
Having stream, you can simply "flatMap" further and return the result:
return combinedStream
.flatMap(str -> Arrays.stream(str.split("\\P{L}")))
.collect(Collectors.toList());
To put it altogether:
private List<String> getWordList(List<String>... lists) {
return Stream.of(lists)
.flatMap(Collection::stream)
.flatMap(str -> Arrays.stream(str.split("\\P{L}")))
.collect(Collectors.toList());
}
You don't need to introduce so many variables :
private List<String> getWordList(List<String>... lists) {
return Stream.of(lists) // Stream<Stream<String>>
.flatMap(Collection::stream) // Stream<String>
.flatMap(Pattern.compile("\\P{L}")::splitAsStream) //Stream<String>
.collect(toList()); // List<String>
}
As underlined by Holger, .flatMap(Pattern.compile("\\P{L}")::splitAsStream)
should be favored over .flatMap(s -> Arrays.stream(s.split("\\P{L}"))) to spare array allocation and pattern compilation performed for each element of the stream.
Motivations for improvement
I think "solving in 1 or 2 statements" is not a good target. Aiming for Java 8 features for the sake of using Java 8 features is also not a good target. A better target would be a combination of:
- correct and reasonably robust
- easy to understand
- reasonably efficient
Here's a bit shorter and more efficient, but pretty bad solution:
private List<List<String[]>> splitIntoBlocks(List<String[]> rows) {
List<List<String[]>> blocks = new ArrayList<>();
int start = 0;
for (int index = 0; index < rows.size(); index++) {
String marker = rows.get(index)[0];
if (marker.equals("3000")) {
start = index;
} else if (marker.equals("3900")) {
int end = index + 1;
blocks.add(rows.subList(start, end));
}
}
return blocks;
}
This is bad (as bad as the original code), because it doesn't validate the input. It will only work correctly with input in the assumed format, which it doesn't verify, and it doesn't detect or signal when something is wrong, which could lead to nasty bugs.
Validating inputs
The posted code assumes the input follows a certain format. It should at least document the assumptions, and state if the caller can be trusted to enforce those assumptions.
As you mentioned, the input comes from CSV files. Since the expected format is not captured by the List<String[]> type, I would be very cautious. A verification is needed somewhere to avoid bugs or undefined behavior, for example the following checks come to mind:
- There are the same number of START and END markers (you already did this one)
- The START and END markers don't overlap
- (optional?) There are no garbage records after an END and before the next START
- The rows are not empty (avoid crashing due to index out of bounds exceptions)
When any of these assumptions fail, I would either raise a custom exception that the caller can handle, or else throw IllegalArgumentException to fail fast.
So in fact, instead of making the posted code shorter, I'm suggesting to actually make it longer, by adding more validation logic.
Making the code easier to understand
Another answer already mentioned to improve the variable names. (I suggest following Java conventions with camelCase names, so startIndices and endIndices.)
The values "3000" and "3900" are magic strings. It would be better to use private static final constants for such values, so that they stand out, and with a descriptive name.
Building the lists of startIndices and endIndices use the same logic, duplicated. It would be good to extract to a helper method.
When returning a container type, it's usually more practical to return an empty container instead of null.
Low hanging fruit
l1 and l2 are very opaque variable names. start_indices and end_indices would be much more descriptive.
//get all position of 3000
List<Integer> l1 = IntStream.range(0,rows.size())
.filter(i->rows.get(i)[0].equals("3000"))
.boxed()
.collect(Collectors.toList());
This code takes int indices, boxes them into Integer indices, and collects them into a List<Integer>. This results in many small objects in memory.
We can avoid the boxing, by replacing .boxed().collect(Collectors.toList()) with .toArray() and changing the variable type to int[].
// get positions of all "3000"'s
int[] start_indices = IntStream.range(0,rows.size())
.filter(i->rows.get(i)[0].equals("3000"))
.toArray();
Apply similar change to the group end indices, and corresponding changes to the final res construction.
Single Pass
The above is still passing over the rows input 3 times; the first to collect the starting indices, the second to collect the ending indices, and the third time to assemble the result. Three passes means you cannot apply this grouping operation on an ephemeral stream; it requires a concrete collection that can be repeatedly iterated over.
Moreover, an IntStream is being used to generate indices into the rows list -- a hack -- which requires an ArrayList or similar \$O(1)\$-indexable collection to avoid quadratic time complexity.
It would be much better to perform a single pass over the stream of data, and collecting the required groups along the way:
private List<List<String[]>> splitIntoBlocks(List<String[]> rows) {
return rows.stream().collect(Block.collector());
}
Well, the above clearly solves the "looking for someone to solve this in perhaps 1 or 2 statements" request, but clearly additional work is still required. The Block.collector() class is required, writing of which (to handle possible splitting & parallel stream handling) is a non-trivial exercise. See Collector.of(...)
To make the code clearer, you could use simple foreach loop:
Stream<String> lines = File.lines(Paths.get("path-to-file");
lines.forEach(s -> s.split(" "));
/**
* Takes a stream, splits group by first part of the string:
*/
public Map<Integer, List<String>> split(Stream<String> a) {
Map<Integer, List<String>> result = new HashMap<>();
a.forEach(s -> {
String[] pair = s.split(" ");
Integer key = Integer.valueOf(pair[0]);
String value = pair[1];
// as 4castle suggested - to avoid unnecessary computation
result.computeIfAbsent(key, key -> new ArrayList<>());
result.get(key).add(value);
});
return result;
}
Or you can map your input directly in the stream processing:
a.map(s -> s.split(" "))
.forEach(pair -> {
Integer key = Integer.valueOf(pair[0]);
String value = pair[1];
result.putIfAbsent(key, new ArrayList<>());
result.get(key).add(value);
});
Use Collectors.groupingBy with a downstream collector which gets the second part of the split line before collecting to a list.
Map<Integer, List<String> table =
Files.lines(Paths.get("path-to-file"))
.map(line -> line.split(" ", 2))
.collect(Collectors.groupingBy(
parts -> Integer.valueOf(parts[0]),
Collectors.mapping(parts -> parts[1], Collectors.toList())
));
list
.stream()
.flatMap(s -> Arrays.stream(s.split(","))
.collect(Collectors.toList());
We basically use flatMap() which has the effect of applying a one-to-many transformation to the elements of the stream, and then flattening the resulting elements into a new stream.
Next to the already provided working answer from @Tom I propose the following which makes use of the java.util.regex.Pattern class and its method splitAsStream(String) which was freshly introduced in Java 8.
Pattern pattern = Pattern.compile(",");
List<String> split = list
.stream()
.flatMap(pattern::splitAsStream)
.collect(Collectors.toList());