Skip to content
brextonho edited this page Nov 11, 2021 · 21 revisions

Declarative vs Imperative programming

The stream data structure was introduced in java 8 and is used in functional programming, where data is manipulated in a self contained data pipeline. Streams aim to replace the for loop and are considered a type of declarative programming as opposed to imperative programming, where we write code that tells the program what we want but not how to get it instead of telling the program step by step how to get what we want.

Thus, in streams we completely remove the need for a for loop and avoid needing state management which can cause mutable data structures, leading to bugs (that's why all streams are self-contained).

Lets take a look at some examples

// Imperative Approach
List<Integer> intList = List.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

/**
 * Here we have to take note of the "state" of evenList. 
 * Right now its empty, but it will change its state overtime*
 */
List<Integer> evenList = new ArrayList<>(); 

for (Integer i : intList) { // we instruct the program to loop through the list
    if (i % 2 == 0) { // if the number is even, add to evenList
        evenList.add(i);
    }
}

System.out.println(evenList.toString());

// Declarative Approach
List<Integer> intList = List.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

/**
 * Here we tell the program to give us everything that is even.
 * Note that the stream is self-contained, and there's no "state" to be found 
 */
List <Integer> evenList =  
    intList.
        stream().
        filter((x) -> x % 2 == 0).
        collect(Collectors.toList());

System.out.println(evenList.toString());
                              

Lazy Evaluation

Stream Operations

Here is a non-exhaustive table of some useful stream operations. Feel free to add more to the table and add examples of how these stream operations can be used!

Starting a Stream Intermediate Operations Terminal Operations
Stream.<T>of(...)
Stream.<T>of(T t)
map(Function<? super T, ? extends U> mapper)
.map((x) -> x.method())
collect(Collectors<? super T, U, V> collector)
.collect(Collectors.toList())
Stream.<T>concat(Stream<? extends T> a, Stream<? extends T> b) filter(Predicate<? super T> pred)
.filter((x) -> { if x % 2 == 0 return true;})
forEach(Consumer<? super T> action)
.forEach((x) -> System.out.println(x))
.forEach(System.out::println)
Stream.<T>iterate(T a, Predicate limiter, Function increment)
Stream.<T>iterate(T seed, UnaryOperator<T> next)
flatMap(Function<? super T, ? extends Stream<? extends U>> mapper)
.flatMap((x) -> x.stream())
noneMatch(Predicate<? super T> pred)
IntStream.range(1, n)
IntStream.rangeClosed(1, n)
peek(Consumer<? super T> action>
.peek((x) -> System.print.ln(x))
allMatch(Predicate<? super T? pred)
Stream.<T>generate(Supplier<? extends T> s) limit(long maximumSize)
.limit(5)
anyMatch(Predicate<? super T> pred)
min(Comparator<? super T> comparator)
max(Comparator<? super T> comparator)
.sum()
.count()
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
.reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)

Iterate

To start of an empty stream, we can use Stream.iterate to return an infinite sequential ordered Stream by applying a function f to an initial element seed, creating a stream which consists of an initial seed Ex: f(seed), f(f(seed)), etc...

There are 2 types of Stream.iterate implementations, the first one starts off with an initial seed, and a function to apply to that seed for the next value and so on

/**
 * Initial Seed/starting value is 0
 * Subsequently + 1 to each value
 **/
Stream<Integer> integers = Stream.iterate(0, (x) -> x + 1);
// 0, 1, 2, 3 ...

// demostrate using IntStream
IntStream.iterate(1, (x) -> x + 1)

The second one starts off with an initial seed, a predicate that decides whether the stream will continue applying the function f, and the function to apply to the seed and subsequent values. Do note if the seed does not pass the predicate, the stream will be empty. If the predicate check is false, the stream will end.

Stream.iterate(1, (x) -> x <= 20, (x) -> x * 2)
    .forEach((x) -> System.print.ln(x));
// stream will end when 20 is reached
// 1, 2, 4, 8, 16

Stream.iterate(5, (x) -> x <= 0, (x) -> x - 1)
    .forEach((x) -> System.print.ln(x));
// no output since there the first seed would not pass the predicate.
// returns an empty stream

Generate

Another way to start a stream is to use generate along with a Supplier function. This will create an infinite stream based on the values returned from the Supplier's get() method. For example, generating an infinite stream of 5s:

Stream<Integer> stream = Stream.<Integer>.generate(() -> 5);

Or generating an infinite stream of random integers from 1 to 10:

Stream<Integer> stream = Stream.<Integer>generate(() -> (int) Math.ceil(Math.random() * 10));

Since the stream generated is infinite, limit() should be used when terminating the stream:

stream.limit(20).forEach(System.out::println)

Note that the method signature is Stream<T> generate(Supplier<? extends T> s), meaning the supplier can return any subclass of T. For instance, the following methods are valid:

Stream<Number> stream = Stream.<Number>generate(() -> 5);
Stream<Object> stream = Stream.<Object>generate(() -> "s");

Peek

Peek is an intermediate operation, which means that its best used together with a terminal operation for it to work. Generally, the peek operation is usually used to debug the stream, by printing out the elements past a certain point in the data pipeline.

An example of using peek() wrongly would be put the peek() at the end of the stream pipeline without a terminal operation like so:

Stream<String> nameStream = Stream.<String>of("Russell", "Henry", "Alvin");
nameStream.peek((x) -> System.out.println(x));

The above code will not output anything. To actually print something, you would need the terminal operation forEach()

Here's an example of using peek to debug a stream pipeline:

Stream.of("one", "two", "three", "four")
  .filter(e -> e.length() > 3)
  .peek(e -> System.out.println("Filtered value: " + e))
  .map(String::toUpperCase)
  .peek(e -> System.out.println("Mapped value: " + e))
  .collect(Collectors.toList());

peek() can also be used to alter the inner state of the element without completely replacing the element like map(). For example:

Stream<User> userStream = Stream.of(new User("Russell"), new User("Henry"), new User("Alvin"));
userStream.peek(u -> u.setName(u.getName().toLowerCase())) // useful for HashMaps too!
  .forEach((user) -> System.out.println(user));

keep in mind that ultimately peek() was mainly designed for debugging and should be used as such.

Filter

Returns a stream consisting of the elements of this stream passes the predicate. Uses the SAM test on each element

IntStream
    .rangeClosed(1, 10)
    .filter(x -> x % 2 == 0)
    .forEach((x) -> System.out.println(x));
// 2, 4, 6, 8, 10

Map

Applys the given function on each element, expects the returned type to be the same. Uses the SAM apply on each element

IntStream
    .rangeClose(1, 5)
    .map((x) -> x + 1)
    .forEach((x) -> System.out.println(x));
// 2, 3, 4, 5, 6

FlatMap

Stream flatMap(Function mapper) returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element. flatMap() is the combination of a map and a flat operation i.e, it applies a function to elements as well as flatten them.

IntStream
    .rangeClosed(1, 3)
    .flatMap((x) -> IntStream. // must return a Stream
        rangeClosed(1, x))
    .forEach((x) -> System.out.println(x));
// 1, 1, 2, 1, 2, 3

Limit

Returns a stream consisting of the elements of this stream, where size of the stream will be no larger than the inputed MaxSize in limit().

IntStream
    .iterate(2, x -> x + 2)
    .limit(5)
    .forEach((x) -> System.out.println(x));
// 2, 4, 6, 8, 10 -> 5 elements

Converting Between Streams, Arrays, ArrayLists and List

Feel free to add in more methods on how to convert list and arrays to streams so that stream operations can be applied on it

X -> ArrayList

  • Array -> ArrayList
int[] intArray = {0, 1, 2, 3}; // primitive
ArrayList<Integer> intArrayList = IntStream.of(intArray)
    .boxed()  // boxed method converts int -> into Integer
    .collect(Collectors.toCollection(() -> new ArrayList<Integer>()));

Integer[] integerArray = {0, 1, 2, 3};
ArrayList<Integer> integerArrayList= new ArrayList<>(Arrays.asList(integerArray));
  • Array -> List
int[] intArray = {0, 1, 2, 3}; // primitive
ArrayList<Integer> intArrayList = IntStream.of(intArray)
    .boxed()  // boxed method converts int -> into Integer
    .collect(Collectors.toList());

Integer[] integerArray = {0, 1, 2, 3};
List<Integer> integerArrayList= new ArrayList<>(Arrays.asList(integerArray));
  • Stream -> List
Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
List<Integer> integerList = stream.collect(Collectors.toList());
  • Stream -> ArrayList
Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
List<Integer> integerArrayList = stream.collect(Collectors.toCollection(() -> new ArrayList<Integer>()));

List<Integer> integerArrayList = stream.collect(Collectors.toCollection(ArrayList::new)); // alternative

X -> Array

  • ArrayList -> Array
List<Double> floatingNumbers = List.<Double>of(1.00, 2.00, 3.00, 4.00);
double[] doubleArray = floatingNumbers.stream().mapToDouble((x) -> x.doubleValue()).toArray();
  • Stream -> Array
Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
stream.toArray(); // ==> Object[4] {1, 2, 3, 4}

Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
stream.toArray(Integer[]::new); // ==> Integer[4]{1, 2, 3, 4}

X -> Stream

  • ArrayList -> Stream
List<Integer> intArrayList = List.<Integer>of(1, 2, 3, 4, 5);
Stream<Integer> intStream = intArrayList.stream();
  • Array -> Stream
int[] intArray = new int[] {1, 2, 3, 4, 5};
Stream<Integer> intStream = Arrays.stream(intArray);
Clone this wiki locally