Streams and Functional Interfaces

Streams

1) Main methods:
- Filter
- Map
- forEach (Terminal)
- generate
- iterate
- flat map
- count (Terminal)
- reduce (Terminal)


2) Terminal Operations
Operations that once used, the stream cannot be used again
The use Eager evaluation.
(Read more)

3) Stateful Operation
The operation that maintain state to accomplish the operation
E.g
- distinct
- sorted
- limit
- count
When the operation on a particular variable in the stream relies on the previous operation of the previous variable
Uses lazy evaluation

For example,
in Count, the number of variables depends on how many variables were counted before the current variables

Stateful Operation is best avoided during parallelism

4) Stateless Operation
The opposite of stateful. They do not store any state across passes.
e.g
-filter
-map
-findAny

Is useful in parallel


5) Method references
When using methods such as forEach or sort, we can use method signatures for comparison.
e.g When wanting to print out values using for each


IntStream.forEach(System.out::println)


Another example:

int max = marksList.stream().map(x-> x.getMark()).max(Integer::compare).get();
//marksList is a list of marks
//getMark() is a non-static function in the class Mark


There are four kinds of method references:
KindExample
Reference to a static methodContainingClass::staticMethodName
Reference to an instance method of a particular objectcontainingObject::instanceMethodName
Reference to an instance method of an arbitrary object of a particular typeContainingType::methodName



6) Lambda
All local variables referenced from a lambda expression must be effectively final or final.
Meaning that the value of variable or parameter is never changed after initialisation

  1. public static void Main(String[] args){
  2. int count =0;
  3. IntStream.range(0,10)
  4. .forEach(x->{
  5. count++; //this is illegal
  6. })
  7. }
A Cheat to counter:
Assume we cannot use the .count() method
  1. public static void Main(String[] args){
  2. IntStream.range(0,10)
  3. .map(x->1)
  4. .reduce(0,(x,y) -> x+ y;);
  5. }



7) Peek()
Useful for parallel, they do not respect encounter order and are mostly used for debugging purposes
It acts like a for each but is not a terminal operation

public class PeekExample {
 public static void main (String[] args) {
     IntStream.range(0, 5).parallel().peek(System.out::println).
                                                            count();
    }
}

8) Generate
Generate uses get() method from the supplier and returns supplier.get()

9) Iterate
Similar to generate excepts requires a seed.
Uses a function instead of a supplier to generate the method


Functional Interfaces

1) Consumer
Has a single method

void accept(T t)

Example usage:
Note: Streams do not like to have state changes especially forEach
  1. void doSomething(Consumer<?> super Circle> action, Circle... circles{
  2. for(Circle c: circles){
  3. action.accept(circle); //perform the consumer on circle
  4. }
  5. }
Consumer can be a lambda or a method signature


2) Supplier

T get()

Supplies and generates the next variable.

Example use 
  1. List<Circle> getCircles(Supplier<Circle> supplier,int n ){
  2. List<Circle> outList = new ArrayList<>();
  3. for(int i=0;i<n;i++){
  4. outList.add(supplier.get());
  5. }
  6. return outList
  7. }
The supplier can be a lambda for example
()-> new Circle(Math.random());


3) UnaryOperator

R apply(T t)

Accepts a type T argument and produces a type R result

Example use:

  1. Stream.of(circles)
  2. .map( new Function<Circle,Double>(){
  3. @Override //annon class override
  4. public Double apply(Circle c){
  5. return c.getArea(); //inside circle class
  6. })
  7. .forEach(System.out::println);
Here we using an annon class to override the apply method, appling to each circle input and returning the area of each circle.

Function with multiple Arguments:
Using the one input apply method, can we make it such that it takes in two inputs?




  1. Function <Integer,UnaryOPerator<Integer>> g = new Function<>(){
  2. UnaryOperator<Integer> f = new UnaryOperator<>(){
  3. @Override
  4. public Integer apply(Integer y){
  5. return x+y;
  6. }
  7. };
  8. return f;
  9. };
  10. }
We can use g.apply(1).apply(2) to call this method

4) Bi-function/ BinaryOperator
Similar to unary but instead takes in two inputs and produce an outpit
R apply(T t, U u )

More: https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

Order of Operation

Given the example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> twoEvenSquares = numbers.stream().filter(n -> {
    System.out.println("filtering " + n);
    return n % 2 == 0;
}).map(n -> {
    System.out.println("mapping " + n);
    return n * n;
}).limit(2).collect(Collectors.toList());


  1. The only terminal operation collect() starts the evaluation of the chain.
  2. limit() starts the evaluation of its ancestor
  3. map() starts the evaluation of its ancestor
  4. filter() starts consuming values from the source stream
  5. 1 is evaluated, 2 is evaluated and the first value is produced
  6. map() consumes the first value returned by its ancestor and produce a value too
  7. limit() consume that value
  8. collect() collect the first value
  9. limit() requires another value from the map() source
  10. map() requires another value from it's ancestor
  11. filter() resume the evaluation to produce another result and after evaluating 3 and 4produce the new value 4
  12. map() consumes it and produce a new value
  13. limit() consume the new value and returns it
  14. collect() collects the last value.


<Prev Runtime compile time                Next Enum>

No comments:

Post a Comment