import static java.lang.Math.min;
import static java.util.Arrays.stream;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.*;
import java.util.Map;
import java.util.stream.*;
Static imports are a feature added in 2004 to make Java slightly less verbose. They allow us to import individual names from classes and interfaces so we don’t have to qualify them: we can use min instead of Math.min.
countPrimesCount the number of primes in an int[].
public int countPrimes(int[] ns) {
return (int) stream(ns).filter(this::isPrime).count();
}
Cast to int because that’s the signature in the problem but count() returns a long.
In this case we know there can’t be more than Integer.MAX_VALUE elements in the stream since it came from an int[].
countVowelsCount the number of vowels in a String.
public int countVowels(String s) {
return (int) s
.codePoints()
.mapToObj(Character::toString)
.filter(this::isVowel)
.count();
}
indexOfVowel (#1)Find the index of the first vowel in a String.
public int indexOfVowel(String s) {
return (int) IntStream.range(0, s.length())
.filter(i -> isVowel(s.substring(i, i + 1)))
.findFirst()
.orElse(-1);
}
This is okay but using an IntStream to produce indexes is a bit ugly.
This is basically a for loop in disguise.
indexOfVowel (#2)public int indexOfVowel(String s) {
return s
.codePoints()
.mapToObj(Character::toString)
.filter(this::isVowel)
.mapToInt(v -> s.indexOf(v))
.findFirst()
.orElse(-1);
}
This solution leans heavily into the laziness of streams. It will essentially iterate over the String until it finds the first vowel and then iterate one more time to find out where that vowel is using indexOf.
indexOfVowel (#3)public int indexOfVowel(String s) {
int consonants = (int) s
.codePoints()
.mapToObj(Character::toString)
.takeWhile(not(this::isVowel))
.count();
return (consonants + 1) % (s.length() + 1) - 1;
}
Remember we imported java.util.function.Predicate.not
not is a higher-order function that takes a function and returns a function.
(c + 1) % (s.length() + 1) - 1
Assume s.length() ⟹ 3
| c | c + 1 | (c + 1) % (3 + 1) | (c + 1) % (3 + 1) - 1 |
|---|---|---|---|
| 0 | 1 | 1 | 0 |
| 1 | 2 | 2 | 1 |
| 2 | 3 | 3 | 2 |
| 3 | 4 | 0 | -1 |
disemvowelpublic String disemvowel(String s) {
return s
.codePoints()
.mapToObj(Character::toString)
.filter(not(this::isVowel))
.collect(joining());
}
Notice how after statically importing joining from Collectors this is quite readable.
While it’s common to chain together stream methods you can always write methods that take or return streams, just like any other type.
public Stream<String> letters(String s) {
return s.codePoints().mapToObj(Character::toString);
}
As with any code, if we’re doing the same thing more than once, it’s worth thinking about putting it into a method with a meaningful name.
public int countVowels(String s) {
return (int) letters(s).filter(this::isVowel).count();
}
public String disemvowel(String s) {
return letters(s)
.filter(not(this::isVowel))
.collect(joining());
}
countStartsWithVowelpublic int countStartsWithVowel(String[] ss) {
return (int) stream(ss)
.filter(not(String::isEmpty))
.map(s -> s.substring(0, 1))
.filter(this::isVowel)
.count();
}
totalLength (#1)public int totalLength(String[] ss) {
return stream(ss).collect(summingInt(String::length));
}
summingInt is a method in Collectors that takes a function that returns an int and “collects” stream elements by adding up all the ints obtained by calling that function on the elements of the stream.
totalLength (#2)public int totalLength(String[] ss) {
return stream(ss).mapToInt(String::length).sum();
}
mapToInt turns the Stream<String> into an IntStream which has a sum terminal method.
xs (#1)public int[] xs(String[] ss) {
return stream(ss).mapToInt(s -> s.indexOf("x")).toArray();
}
xs (#2)Write a streamy indexOf:
private int indexOf(String s, int cp) {
var before = (int) s
.codePoints()
.takeWhile(n -> n != cp)
.count();
return (before + 1) % (s.length() + 1) - 1;
}
Then use it:
public int[] xs(String[] ss) {
return stream(ss).mapToInt(s -> indexOf(s, 'x')).toArray();
}
There’s no reason to do this, however.
countByLengthpublic int countByLength(String[] ss, int len) {
return (int) stream(ss)
.mapToInt(String::length)
.filter(n -> n == len)
.count();
}
collectFourLetterWordspublic String[] collectFourLetterWords(String[] ss) {
return stream(ss)
.filter(s -> s.length() == 4)
.toArray(String[]::new);
}
collectShoutingpublic String[] collectShouting(String[] ss) {
return stream(ss)
.map(String::toUpperCase)
.toArray(String[]::new);
}
private LongStream hs(int start) {
return LongStream.concat(
LongStream.iterate(
start,
n -> n != 1,
n -> n % 2 == 0 ? n / 2 : 3 * n + 1
),
LongStream.of(1)
);
}
Would be hailstone but that was taken.
If you can prove this is not an infinite stream you can collect your math PhD on the way out.
hailstoneLengthFind the length of the hailstone sequence starting at a given number.
public int hailstoneLength(int start) {
return (int) hs(start).count();
}
Trival matter of counting the items in the stream.
hailstoneMaxFind the maximum value occuring in the hailstone sequence starting at a given number.
public int hailstoneMax(int start) {
return (int) hs(start).max().getAsLong();
}
Also trivial. getAsLong necessary to account for the possibility that the stream was empty.
And again, cast to int because the way the problem was defined.
hailstoneMake an array of the values in the hailstone sequence starting at a given number.
public int[] hailstone(int start) {
return hs(start).mapToInt(n -> (int) n).toArray();
}
Once again, fairly trivial plus a bit of casting. (Which can go wrong in the sense that the numbers in the sequence can go out of the bounds of an int.
lengthHistogrampublic int[] lengthHistogram(String[] ss) {
var h = new int[11];
stream(ss)
.mapToInt(String::length)
.map(n -> min(n, 10))
.forEach(n -> h[n]++);
return h;
}
This is okay but not as cool as it could be because it’s not safe with parallel streams.
The forEach is a code smell, bringing us back into non-functional style.
What’s the right way to do this?
lengthHistogram (#1)public int[] lengthHistogram(String[] ss) {
return stream(ss)
.mapToInt(String::length)
.map(n -> min(n, 10))
.collect(
() -> new int[11], // make the thing we'll collect into
(h, n) -> h[n]++, // update it with a new element
(a, b) -> { // combine two things if needed
for (int i = 0; i < a.length; i++) {
a[i] += b[i];
}
}
);
}
The three lambdas passed into collect are used to create an object that actually handles collecting the results.
lengthHistogram (#2)public int[] lengthHistogram(String[] ss) {
Map<Integer, Long> m = stream(ss)
.map(String::length)
.collect(groupingBy(n -> min(10, n), counting()));
int[] h = new int[11];
m.keySet().stream().forEach(i -> {
h[i] = m.get(i).intValue();
});
return h;
}
groupingBy is the move here but it’s annoying that we then need to copy the values into an array.
lengthHistogram (#3)public int[] lengthHistogram(String[] ss) {
Map<Integer, Long> m = stream(ss)
.map(String::length)
.collect(groupingBy(n -> min(10, n), counting()));
int[] h = new int[11];
Arrays.setAll(h, i -> m.getOrDefault(i, 0L).intValue());
return h;
}
Same as before but using a different technique to copy into the final array.
lengthHistogram (#4)public int[] lengthHistogram(String[] ss) {
Map<Integer, Long> m = stream(ss)
.map(String::length)
.collect(groupingBy(n -> min(10, n), counting()));
return IntStream.range(0, 11)
.map(i -> m.getOrDefault(i, 0L).intValue())
.toArray();
}
Same as before but generating final array from an IntStream.