
IntStream factors(int n) {
return IntStream.rangeClosed(1, n)
.filter(x -> n % x == 0);
}
boolean isPrime(int n) {
return n > 1 && IntStream.range(2, n)
.noneMatch(x -> n % x == 0);
}
IntStream primeFactors(int n) {
return factors(n).filter(x -> isPrime(x));
}
int countPrimeFactors(int n) {
return primeFactors(n).reduce(0, (x, y) -> x + 1);
}
IntStream omega(int n) {
return IntStream.rangeClosed(1, n)
.map(x -> countPrimeFactors(x));
}

jshell> IntStream dot(int a, int b) {
...> return IntStream.rangeClosed(a, b)
...> .flatMap(i -> IntStream.rangeClosed(a, b)
...> .map(j -> i * j));
...> }
| replaced method dot(int,int)
Inner map creates 1, 2, 3 which is multiplied with i = 1 at round 1. Repeat this for i = 2 and i = 3. You create a [Stream(1,2,3), Stream(2,4,6), Stream(3,6,9)] Which you can then flatMap to give you [1, 2, 3, 2, 4, 6, 3, 6, 9]

jshell> Stream<IntPair> product(int a, int b) {
...> return IntStream.rangeClosed(a, b)
...> .boxed()
...> .flatMap(i -> IntStream.rangeClosed(a, b)
...> .mapToObj(j -> new IntPair(i, j)));
...> }
| created method product(int,int)
*.boxed() converts a primitive stream (IntStream, LongStream, DoubleStream) into an object stream (Stream<T> which can hold objects like Integer, String but only one type.
IntStream.of(1, 2, 3) // IntStream of primitives
.boxed() // Stream<Integer> of objects
*.mapToObj() allows you to map something to an object. If you use regular .map() on an IntStream, it expects you to return another int, keeping it as a IntStream.
IntStream.rangeClosed(1, 3)
.mapToObj(j -> new IntPair(i, j)) // int -> IntPair object
// Here, int is mapped as an argument to create a new IntPair object.

jshell> Stream<Integer> fibonacci(int n) {
...> return Stream.iterate(new IntPair(1, 1),
...> pr -> new IntPair(pr.snd(), pr.fst() + pr.snd()))
...> .map(pr -> pr.fst())
...> .limit(n);
...> }
| created method fibonacci(int)
jshell> fibonacci(10).toList()
$.. ==> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
.limit(n) stops the stream after n elements
pr -> new IntPair(pr.snd(), pr.fst() + pr.snd())
This is the function that generates the **next** pair from the current pair `pr`:
- new `fst` = old `snd`
- new `snd` = old `fst` + old `snd`
So the pairs evolve like:
(1, 1) -> (1, 2) -> (2, 3) -> (3, 5) -> (5, 8) ...