Functional Programming - Streams and Lambdas

I have already covered the basics in regards to streams and lambdas some of which I will cover again here but I will take it a bit further with some advanced features and more complex examples, in a functional programming way.

Below are the two previous introduction sections on Streams and Lambdas

A number of examples use the course class (nothing special) as per below

course class
public class Course {
    private String name;
    private String category;
    private int reviewScore;
    private int noOfStudents;

    public Course(String name, String category, int reviewScore, int noOfStudents) {
        this.name = name;
        this.category = category;
        this.reviewScore = reviewScore;
        this.noOfStudents = noOfStudents;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public int getReviewScore() {
        return reviewScore;
    }

    public void setReviewScore(int reviewScore) {
        this.reviewScore = reviewScore;
    }

    public int getNoOfStudents() {
        return noOfStudents;
    }

    public void setNoOfStudents(int noOfStudents) {
        this.noOfStudents = noOfStudents;
    }

    @Override
    public String toString() {
        return name + ":" + noOfStudents + ":" + reviewScore;
    }
}

Streams and Lambda's Refresh

Below are some basic examples of using streams and lambdas using numbers and strings, Intermediate Operations such as distinct, sorted, map, filter are all intermediate operations as they return a Stream of values. Terminal Operations sunch as forEach, collect, reduce (which we will see later) do not return a stream but could return a void, another object type (List, Set, etc), one element (reduce).

Using Numbers
public class list_numbers {

    public static void main(String[] args) {
        // traditional way
        //printAllNumbersTraditional(List.of(12,5,6,3,13,8,12,15));

        // functional way
        printAllNumbersFunctional(List.of(12,5,6,3,13,8,12,15));
    }

    private static void printAllNumbersTraditional(List<Integer> integers) {
        for(int i : integers){
            System.out.println(i);
        }
    }

    private static void printAllNumbersFunctional(List<Integer> integers) {
        // convert the list of numbers convert it into a stream (sequence of elements)
        // then use a method reference to print the number
        integers.stream()
                .forEach(System.out::println);

        // You can also call your own static functions
        integers.stream()
                .forEach(list_numbers::print);
    }

    // Not really needed but used for demo purposes
    private static void print(int number) {
        System.out.println(number);
    }
}
Numbers and filtering
public class list_numbers2 {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        // functional way
        printAllNumbersFunctional(numbers);
        System.out.println("------------------------------------");

        printEvenNumbers(numbers);
        System.out.println("------------------------------------");

        printOddNumbers(numbers);
    }

    private static void printAllNumbersFunctional(List<Integer> numbers) {
        // convert the list of numbers convert it into a stream
        // then use a method reference to print the number
        numbers.stream()
                .forEach(System.out::println);
    }

    private static void printEvenNumbers(List<Integer> numbers) {
        numbers.stream()
                .filter(num -> num % 2 == 0)        // lambda expression
                .forEach(System.out::println);
    }

    private static void printOddNumbers(List<Integer> numbers) {
        numbers.stream()
                .filter(num -> num % 2 == 1)        // lambda expression
                .forEach(System.out::println);
    }
}
Strings and filtering
public class list_strings {

    public static void main(String[] args) {
        List<String> courses = List.of("Spring", "Spring Boot", "API" , "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        printStrings(courses);
        System.out.println("--------------------------");

        printSpringCourses(courses);
        System.out.println("--------------------------");

        printCoursesWithAtLeast4Letters(courses);
        System.out.println("--------------------------");
    }

    private static void printStrings(List<String> courses) {
        courses.stream()
                .forEach(System.out::println);
    }

    private static void printSpringCourses(List<String> courses) {
        courses.stream()
                .filter(str -> str.toLowerCase().contains("spring"))
                .forEach(System.out::println);
    }

    private static void printCoursesWithAtLeast4Letters(List<String> courses) {
        courses.stream()
                .filter(str -> str.length() >= 4)
                .forEach(System.out::println);
    }
}
Using map
public class using_map {

    // a map lets you convert an object to something else
    // or in the case of a string you can append to it

    public static void main(String[] args) {

        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        List<String> courses = List.of("Spring", "Spring Boot", "API" , "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        printSquaresEvenNumbers(numbers);
        System.out.println("----------------------------");

        printCubesOddNumbers(numbers);
        System.out.println("----------------------------");

        printNumberOfLettersInCourse(courses);
    }

    private static void printSquaresEvenNumbers(List<Integer> numbers) {
        numbers.stream()
                .filter(num -> num % 2 == 0)
                .map(num -> num * num)          // calculated number is returned
                .forEach(System.out::println);
    }

    private static void printCubesOddNumbers(List<Integer> numbers) {
        numbers.stream()
                .filter(num -> num % 2 == 1)
                .map(num -> num * num * num)          // calculated number is returned
                .forEach(System.out::println);
    }

    private static void printNumberOfLettersInCourse(List<String> courses) {
        courses.stream()
                .map(course -> course + " " + course.length())       // number of letters is returned
                .forEach(System.out::println);
    }
}

Streams Methods

Streams has a number of useful methods that you can use

Using comparingInt, comparingDouble, comparingLong will provide better performance as autoboxing is not used (better for large lists)

Distinct and Sort
public class distinct_sorted {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);
        List<String> courses = List.of("Spring", "Spring Boot", "API" ,
                "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        getDistinctNumbers(numbers);
        System.out.println("\n-------------------------------");

        getSortedDistinctNumbers(numbers);
        System.out.println("\n-------------------------------");

        getSortedDistinctStrings(courses);
        System.out.println("\n-------------------------------");

        // Use generics
        sortedDistinctObjects(numbers);
        System.out.println("\n-------------------------------");

        sortedDistinctObjects(courses);
        System.out.println("\n-------------------------------");
    }

    private static void getDistinctNumbers(List<Integer> numbers) {

        numbers.stream()
                .distinct()
                .forEach(num -> System.out.print(num + ", "));  // 12 is only printed once
    }

    private static void getSortedDistinctNumbers(List<Integer> numbers) {

        numbers.stream()
                .sorted()
                .distinct()
                .forEach(num -> System.out.print(num + ", "));
    }

    private static void getSortedDistinctStrings(List<String> courses) {
        courses.stream()
               .distinct()
               .sorted()
               .forEach(course -> System.out.print(course + ", "));
    }

    // Using generics
    private static void sortedDistinctObjects(List<?> list) {
        System.out.print("Generics: ");
        list.stream()
                .distinct()
                .sorted()
                .forEach(ob -> System.out.print(ob + ", "));
    }
}
Reduce 1
public class reduce1 {

    // A reduction is a terminal operation that aggregates a stream
    // into a type or a primitive

    public static void main(String[] args) {
        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        addNumbers(numbers);

        // There are better ways to do this but good examples of reduce
        getMinValue(numbers);
        getMaxValue(numbers);
    }

    private static void addNumbers(List<Integer> numbers) {
        Integer sum1 = numbers.stream()
                // 0 start value (identity)
                // a is the aggregate number
                // b is the number to be added to the aggregated number
               .reduce(0, (a, b) -> a + b);

        // Same as above
        Integer sum2 = numbers.stream()
                .reduce(0, Integer::sum);   // use Integers sum static method

        System.out.println(sum1);
        System.out.println(sum2);
    }

    private static void getMinValue(List<Integer> numbers) {
        System.out.println("Min Value: " +
                numbers.stream()
                       .reduce(Integer.MAX_VALUE, (a,b) -> a>b ? b : a)
        );
    }

    private static void getMaxValue(List<Integer> numbers) {
        System.out.println("Max Value: " +
                numbers.stream()
                       .reduce(Integer.MIN_VALUE, (a,b) -> a>b ? a : b)
        );
    }
}
Reduce 2
public class reduce2 {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        squareAndSum(numbers);
        System.out.println("----------------------");

        cubeAndSum(numbers);
        System.out.println("----------------------");

        sumOddNumbers(numbers);
    }

    private static void squareAndSum(List<Integer> numbers) {

        int result = numbers.stream()
                .map(num -> num * num)
                .reduce(0, Integer::sum);

        printResult(result);
    }

    private static void cubeAndSum(List<Integer> numbers) {

        int result = numbers.stream()
                .map(num -> num * num * num)
                .reduce(0, Integer::sum);

        printResult(result);
    }

    private static void sumOddNumbers(List<Integer> numbers) {

        int result = numbers.stream()
                .filter( num -> num % 2 == 1)
                .reduce(0, Integer::sum);

        printResult(result);
    }

    private static void printResult(Integer num) {
        System.out.println("Result: " + num);
    }
}
Sort Comparators
public class sort_comparators {

    public static void main(String[] args) {

        List<String> courses = List.of("Spring", "Spring Boot", "API" ,
                "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        getSortedStrings(courses);
        System.out.println("\n-----------------------------");

        getSortedReverseOrderStrings(courses);
        System.out.println("\n-----------------------------");

        getSortedUsingOwnComparator(courses);
    }

    private static void getSortedStrings(List<String> courses) {
        courses.stream()
                .sorted()
                .forEach(course -> System.out.print(course + ", "));
    }

    private static void getSortedReverseOrderStrings(List<String> courses) {
        courses.stream()
                .sorted(Comparator.reverseOrder())
                .forEach(course -> System.out.print(course + ", "));
    }

    private static void getSortedUsingOwnComparator(List<String> courses) {
        courses.stream()
                .sorted(Comparator.comparing(str -> str.length()))      // use lambda
                .forEach(course -> System.out.print(course + ", "));
    }
}
Collect
public class using_collect {

    // using collect we can recreate another list
    // we can even change the type for example from List to Set

    public static void main(String[] args) {

        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        List<String> courses = List.of("Spring", "Spring Boot", "API" ,
                "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        List<Integer> doubledNumbers = doubleNumbers(numbers);
        System.out.println(doubledNumbers);

        List<String> lowerCaseList = lowerCaseList(courses);
        System.out.println(lowerCaseList);

        // Set does not have duplicates, so only one 12
        // also its sorted by natural ordering
        Set<Integer> numbersSet = convertListToSet(numbers);
        System.out.println(numbersSet);

        List<Integer> courseTitleLengths = getCourseTitleLengths(courses);
        System.out.println(courseTitleLengths);
    }

    private static List<Integer> doubleNumbers(List<Integer> numbers) {
        return numbers.stream()
                .map(num -> num * num)
                .collect(Collectors.toList());
    }

    private static List<String> lowerCaseList(List<String> courses) {
        return courses.stream()
                .map(course -> course.toLowerCase())
                .collect(Collectors.toList());
    }

    private static Set<Integer> convertListToSet(List<Integer> numbers) {
        return numbers.stream()
                .collect(Collectors.toSet());
    }

    private static List<Integer> getCourseTitleLengths(List<String> courses) {
        return courses.stream()
                .map(course -> course.length())
                .collect(Collectors.toList());
    }
}

Streams Utility Methods

There are a number of stream utility methods that you can use

Utility Methods 1
public class utility_methods1 {

    public static void main(String[] args) {

        List<Course> courses = List.of(
                new Course("Spring", "Framework", 98, 20000),
                new Course("Spring Boot", "Framework", 95, 18000),
                new Course("API", "Microservices", 97, 22000),
                new Course("Microservices", "Microservices", 96, 25000),
                new Course("FullStack", "FullStack", 91, 14000),
                new Course("AWS", "Cloud", 92, 21000),
                new Course("Azure", "Cloud", 99, 21000),
                new Course("Docker", "Cloud", 92, 20000),
                new Course("Kubernetes", "Cloud", 91, 20000));

        // allMatch, nonMatch, anyMatch
        Predicate<Course> scoreGreaterThan90Predicate = course -> course.getReviewScore() > 90;
        Predicate<Course> scoreGreaterThan95Predicate = course -> course.getReviewScore() > 95;
        Predicate<Course> scoreLessThan90Predicate = course -> course.getReviewScore() < 90;

        // all have to match to be true
        display("allMatch: " + courses.stream().allMatch(scoreGreaterThan90Predicate));
        display("allMatch: " + courses.stream().allMatch(scoreGreaterThan95Predicate));

        // none have to match to be true
        display("noneMatch: " + courses.stream().noneMatch(scoreGreaterThan90Predicate));
        display("noneMatch: " + courses.stream().noneMatch(scoreGreaterThan95Predicate));
        display("noneMatch: " + courses.stream().noneMatch(scoreLessThan90Predicate));

        // at least one has to match to be true
        display("anyMatch: " + courses.stream().anyMatch(scoreGreaterThan90Predicate));
        display("anyMatch: " + courses.stream().anyMatch(scoreGreaterThan95Predicate));
        display("anyMatch: " + courses.stream().anyMatch(scoreLessThan90Predicate));

        // Comparing elements
        Comparator<Course> comparingByNoOfStudentsASC = Comparator.comparing(Course::getNoOfStudents);
        Comparator<Course> comparingByNoOfStudentsDES = Comparator.comparing(Course::getNoOfStudents).reversed();
        Comparator<Course> comparingByNoOfStudentsDESAndReviews = 
Comparator.comparing(Course::getNoOfStudents).thenComparing(Course::getReviewScore).reversed(); // using comparingInt, better performance as autoboxing is not used (better for large lists) Comparator<Course> comparingIntByNoOfStudentsASC = Comparator.comparingInt(Course::getNoOfStudents); display("Sorting (asc): " + courses.stream().sorted(comparingByNoOfStudentsASC).collect(Collectors.toList())); display("Sorting (asc using int): " + courses.stream().sorted(comparingIntByNoOfStudentsASC).collect(Collectors.toList())); display("Sorting (des): " + courses.stream().sorted(comparingByNoOfStudentsDES).collect(Collectors.toList())); display("Sorting (des/review): " + courses.stream().sorted(comparingByNoOfStudentsDESAndReviews).collect(Collectors.toList())); // Limit the number of returned elements display("limit (2): " + courses.stream().limit(2).collect(Collectors.toList())); display("limit (5): " + courses.stream().limit(5).collect(Collectors.toList())); // Skip first # of elements display("skip (2): " + courses.stream().skip(2).collect(Collectors.toList())); display("skip (5): " + courses.stream().skip(5).collect(Collectors.toList())); // takeWhile will return all the elements of the list until to hit your predicate (opposite of dropWhile) display("takeWhile (>95): " + courses.stream().takeWhile(course -> course.getReviewScore() > 95).collect(Collectors.toList())); display("takeWhile (<99): " + courses.stream().takeWhile(course -> course.getReviewScore() < 99).collect(Collectors.toList())); // dropWhile will drop all the elements of the list until to hit your predicate (opposite of takeWhile) display("dropWhile (>95): " + courses.stream().dropWhile(course -> course.getReviewScore() > 95).collect(Collectors.toList())); display("dropWhile (<99): " + courses.stream().dropWhile(course -> course.getReviewScore() < 99).collect(Collectors.toList())); } private static void display(String display) { System.out.println(display); } }
Utility Methods 2
public class utility_methods2 {

    public static void main(String[] args) {

        List<Course> courses = List.of(
                new Course("Spring", "Framework", 98, 20000),
                new Course("Spring Boot", "Framework", 95, 18000),
                new Course("API", "Microservices", 97, 22000),
                new Course("Microservices", "Microservices", 96, 25000),
                new Course("FullStack", "FullStack", 91, 14000),
                new Course("AWS", "Cloud", 92, 21000),
                new Course("Azure", "Cloud", 99, 21000),
                new Course("Docker", "Cloud", 92, 20000),
                new Course("Kubernetes", "Cloud", 91, 20000));


        // Getting a single element for example max, min
        Comparator<Course> comparingIntByNoOfStudentsDESAndReviews = 
Comparator.comparingInt(Course::getNoOfStudents).thenComparingInt(Course::getReviewScore).reversed(); // max actually returns the last element in the list after sorting (nothing clever), optional is returned display("max: " + courses.stream().max(comparingIntByNoOfStudentsDESAndReviews).get()); // you can also get he min element based on the comparator, optional is returned display("min: " + courses.stream().min(comparingIntByNoOfStudentsDESAndReviews).get()); // because you are using an optional you can return a default course back Predicate<Course> scoreGreaterThan100Predicate = course -> course.getReviewScore() > 100; display("min: " + courses.stream() .filter(scoreGreaterThan100Predicate) // will filter everything .min(comparingIntByNoOfStudentsDESAndReviews) .orElse(new Course("Kubernetes", "Cloud", 91, 20000)) ); // findFirst will find the first match Predicate<Course> scoreGreaterThan95Predicate = course -> course.getReviewScore() > 95; display("findFirst: " + courses.stream().filter(scoreGreaterThan95Predicate).findFirst().get()); // findAny, this is a nondeterministic method display("findFirst: " + courses.stream().filter(scoreGreaterThan95Predicate).findAny().get()); } private static void display(String display) { System.out.println(display); } }
Utility Methods 3
public class utility_methods3 {

    public static void main(String[] args) {

        List<Course> courses = List.of(
                new Course("Spring", "Framework", 98, 20000),
                new Course("Spring Boot", "Framework", 95, 18000),
                new Course("API", "Microservices", 97, 22000),
                new Course("Microservices", "Microservices", 96, 25000),
                new Course("FullStack", "FullStack", 91, 14000),
                new Course("AWS", "Cloud", 92, 21000),
                new Course("Azure", "Cloud", 99, 21000),
                new Course("Docker", "Cloud", 92, 20000),
                new Course("Kubernetes", "Cloud", 91, 20000));

        Predicate<Course> scoreGreaterThan90Predicate = course -> course.getReviewScore() > 90;
        Predicate<Course> scoreGreaterThan95Predicate = course -> course.getReviewScore() > 95;
        Predicate<Course> scoreLessThan90Predicate = course -> course.getReviewScore() < 90;

        // sum is used to sum a list of values
        display("sum (students > 95 score): " + courses.stream()
                .filter(scoreGreaterThan95Predicate)
                .mapToInt(Course::getNoOfStudents)
                .sum());

        // average is used to find the average of a list
        display("average (students > 95 score): " + courses.stream()
                .filter(scoreGreaterThan95Predicate)
                .mapToInt(Course::getNoOfStudents)
                .average().getAsDouble());

        // count is used to count the elements of a list
        display("count (students > 95 score): " + courses.stream()
                .filter(scoreGreaterThan95Predicate)
                .mapToInt(Course::getNoOfStudents)
                .count());

        // groupingBy allows you to group elements
        display("groupingBy (category): " + courses.stream().collect(Collectors.groupingBy(Course::getCategory)));

        // we can even specify how the hash map is created
        display("groupingBy (category): " +
                courses.stream()
                .collect(Collectors
                        .groupingBy(Course::getCategory, Collectors.counting())));

        // we can even specify how the hash map is created
        display("groupingBy (category): " +
                courses.stream()
                .collect(Collectors
                        .groupingBy(Course::getCategory,
                                Collectors.maxBy(Comparator.comparing(Course::getReviewScore)))));

        // we can even specify how the hash map is created, just the course name for each category
        display("groupingBy (category): " +
                courses.stream()
                        .collect(Collectors
                                .groupingBy(Course::getCategory,
                                        Collectors.mapping(Course::getName, Collectors.toList()))));
    }

    private static void display(String display) {
        System.out.println(display);
    }
}
Utility Methods 4
public class utility_methods4 {

    public static void main(String[] args) {

        // Immutable
        List<String> courses = List.of("Spring", "Spring Boot", "API" ,
                "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        // you cannot do this
        // courses.add("Puppet");

        List<String> modifiableCourses = new ArrayList<>(courses);

        modifiableCourses.replaceAll(course -> course.toUpperCase());
        System.out.println( modifiableCourses );

        modifiableCourses.removeIf(course -> course.length() <  6);
        System.out.println( modifiableCourses );
    }
}

Streams Interfaces

At the heart of Java's functional programming are a number of functional interfaces, you can look in java.util.function package (java.base module) to see all the predicates, consumers, functions, suppliers, etc

Predicate represents a predicate (boolean-valued function) of one argument, it basically tests something, you pass one argument and it returns a boolean
Function represents a function that accepts one argument and produces a result, you send in one input and get one input back, you can change that input value
Consumer represents an operation that accepts a single input argument and returns no result
Supplier accepts no arguments but returns something

Below are a number of examples using the functional interfaces as described above

Predicate, Function and Consumer
public class basic1 {

    public static void main(String[] args) {

//      Predicate - Represents a predicate (boolean-valued function) of one argument,
//                  it basically tests something, you pass one argument and it returns a boolean
//
//      Function - Represents a function that accepts one argument and produces a result,
//                 you send in one input and get one input back, you can change that input value
//
//      Consumer - Represents an operation that accepts a single input argument and returns no result

        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        numbers.stream()
               .filter(num -> num % 2  == 0)
               .map(num -> num * num)
               .forEach(System.out::println);
        System.out.println("---------------------------");

        // The above lambda's expressions can be expanded to the below to show how Predicate,
        // Function and Consumer are used

        Predicate<Integer> isEvenPredicate = num -> num % 2 == 0;      // <input type>
        Function<Integer, Integer> squareFunction = num -> num * num;  // <input type, output type>
        Consumer<Integer> sysOutConsumer = System.out::println;        // <input type>

        numbers.stream()
                .filter(isEvenPredicate)
                .map(squareFunction)
                .forEach(sysOutConsumer);
        System.out.println("---------------------------");

        // Now we can take this one step further what is the compiler doing to
        // in the background, basically it creates three methods similar to below

        // in this case the compiler creates the logic similar to below
        Predicate<Integer> isEvenPredicate2 = new Predicate<Integer>() {
            @Override
            public boolean test(Integer num) {
                return num % 2 == 0;
            }
        };

        // in this case the compiler creates the logic similar to below
        Function<Integer, Integer> squareFunction2 = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer num) {
                return num * num;
            }
        };

        // in this case the compiler creates the logic similar to below
        Consumer<Integer> sysOutConsumer2 = new Consumer<Integer>() {
            @Override
            public void accept(Integer num) {
                System.out.println(num);
            }
        };

        numbers.stream()
                .filter(isEvenPredicate2)
                .map(squareFunction2)
                .forEach(sysOutConsumer2);
    }
}
BinaryOperator
public class binaryOperator {

    // When using Operator types (BinaryOperator, UnaryOperator, etc),
    // input and outputs will be the same type

    public static void main(String[] args) {
        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        int sum = numbers.stream().reduce(0, Integer::sum);
        System.out.println("SUM1: " + sum);

        // First convert Integer::sum into a local variable (use refactor)
        // this gives us the function interface type in this case BinaryOperator<Integer>

        // BinaryOperator<Integer> sum1 = Integer::sum;

        // Once have the function type we can then create our own
        BinaryOperator<Integer> sumBinaryOperator = new BinaryOperator<Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return integer + integer2;
            }
        };

        int sum2 = numbers.stream().reduce(0, sumBinaryOperator); // same as using Integer::sum
        System.out.println("SUM2: " + sum2);
    }
}

Note: 
    When using Operator types (BinaryOperator, UnaryOperator, etc), input and outputs will be the same type

    BinaryOperator - accepts two inputs and returns an output of the same type
    UnaryOperator -  accepts one input and returns an output of the same type
Behavior Parameterization
public class behavior_parameterization {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(12,5,6,3,13,8,12,15);

        // Pass the algorithm to the method (Predicate or Function)
        filterAndPrint(numbers, num -> num % 2 == 0);
        filterAndPrint(numbers, num -> num % 2 == 1);
        filterAndPrint(numbers, num -> num % 3 == 0);

        List<Integer> squareNumbers = doSomethingWithNumbers(numbers, num -> num * num);
        System.out.println(squareNumbers);

        List<Integer> cubeNumbers = doSomethingWithNumbers(numbers, num -> num * num * num);
        System.out.println(cubeNumbers);
    }

    // This method accepts a Predicate parameter (algorithm)
    private static void filterAndPrint(List<Integer> numbers, Predicate<Integer> predicate) {
        numbers.stream()
                .filter(predicate)
                .forEach(System.out::println);
        System.out.println("------------------------------------");
    }

    // This method accepts a Function parameter (algorithm)
    private static List<Integer> doSomethingWithNumbers(List<Integer> numbers, Function<Integer,Integer> predicate) {
        return numbers.stream()
                .map(predicate)
                .collect(Collectors.toList());
    }
}
Supplier and UnaryOperator
public class supplier_UnaryOperator {

    // Supplier - accepts no arguments but returns something

    // UnaryOperator -  accepts one input and returns an output of the same type

    // When using Operator types (BinaryOperator, UnaryOperator, etc),
    // input and outputs will be the same type

    public static void main(String[] args) {

        // Supplier Using braces, no input but get output
        Supplier<Integer> randomIntegerSupplier1 = () -> { return new Random().nextInt(1000); };

        // Supplier Braces can be omitted
        Supplier<Integer> randomIntegerSupplier2 = () -> new Random().nextInt(1000);

        System.out.println("Random Number 1: " + randomIntegerSupplier1.get());
        System.out.println("Random Number 2: " + randomIntegerSupplier2.get());

        UnaryOperator<Integer> unaryOperator = (num) -> 3 * num;

        System.out.println("UnaryOperator 1: " + unaryOperator.apply(5));
        System.out.println("UnaryOperator 2: " +  unaryOperator.apply(10));
    }

}
BIPredicate, BIFunction, BiConsumer
public class bipred_bifunc_biconsum {

    // BIPredicate - accepts two arguments, returns a boolean
    // BIFunction  - accepts two arguments, returns output in same data type
    // BiConsumer  - accepts two arguments, does not return anything

    public static void main(String[] args) {

        // You can use braces to handle more complex logic
        BiPredicate<Integer, String> biPredicate = (num, str) -> num == Integer.parseInt(str);

        System.out.println("BiPredicate test 1: " + biPredicate.test(5, "5"));
        System.out.println("BiPredicate test 1: " + biPredicate.test(5, "6"));

        // <input1, input2, output>
        BiFunction<Integer, String, String> biFunction = (num, str) -> num + str;
        System.out.println(biFunction.apply(1, ". Paul"));

        BiConsumer<Integer, String> biConsumer = (num, str) -> System.out.println(num + str);
        biConsumer.accept(1, ". Paul");
    }
}


Note:
    BIPredicate - accepts two arguments, returns a boolean
    BIFunction  - accepts two arguments, returns output in same data type
    BiConsumer  - accepts two arguments, does not return anything
Method References
public class method_references {

    private static void print(String str) {
        System.out.println(str);
    }

    public static void main(String[] args) {

        List<String> courses = List.of("Spring", "Spring Boot", "API" ,
                "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        courses.stream()
                .map(String::toUpperCase)               // using a normal method
                .forEach(method_references::print);     // using a static method

        // Constructor reference
        Function<String, String> function = String::new; // returns a new string
        String name = function.apply("\nPaul");         // returns a new string with Paul
        System.out.println(name);
    }
}
Primitives
public class primitives {

    public static void main(String[] args) {

        // There are a number of primitive Predicates, Functions, Consumer, etc
        // IntBinaryOperator, IntConsumer, IntFunction, IntPredicate
        // IntSupplier, IntToDoubleFunction, IntoToLongFunction, IntUnaryOperator

        // Using primitives
        IntBinaryOperator intBinaryOperator = (num1, num2) -> num1 + num2;
        System.out.println("IntBinaryOperator: " + intBinaryOperator.applyAsInt(5, 5));

        DoubleBinaryOperator doubleBinaryOperator = (num1, num2) -> num1 + num2;
        System.out.println("DoubleBinaryOperator: " + doubleBinaryOperator.applyAsDouble(1.5, 2.5));
    }
}


Note:
    There are a number of primitive Predicates, Functions, Consumer, etc
      - IntBinaryOperator, IntConsumer, IntFunction, IntPredicate
      - IntSupplier, IntToDoubleFunction, IntoToLongFunction, IntUnaryOperator

Functional Programming Examples

In this section we using functional programming to work with arrays, big numbers, high order, joining strings and performance.

Joing Strings (flatmap)
public class strings_joining_flapmap {

    public static void main(String[] args) {

        List<String> courses = List.of("Spring", "Spring Boot", "API" , "Microservices",
                "AWS", "PCF","Azure", "Docker", "Kubernetes");

        List<String> courses2 = List.of("Spring", "Spring Boot", "API" , "Microservices",
                "AWS", "PCF","Azure", "Docker", "Kubernetes");

        System.out.println(
            courses.stream().collect(Collectors.joining(":"))
        );

        // the below has a problem as it contains a list of string arrays
        System.out.println(
                courses.stream().map(course -> course.split("")).collect(Collectors.toList())
        );

        // to solve the above we can use a flatMap, you flatten the string arrays
        System.out.println(
                courses.stream().map(course -> course.split(""))
                        .flatMap(Arrays::stream).collect(Collectors.toList())
        );

        // Join elements in 1st list with elements in 2nd list
        System.out.println(
                courses.stream().flatMap(course -> courses2.stream().map(course2 -> List.of(course, course2)))
                .collect(Collectors.toList())
        );

        // filter out elements which are the same in both lists
        System.out.println(
                courses.stream()
                       .flatMap(course -> courses2.stream().map(course2 -> List.of(course, course2)))
                       .filter(list -> !list.get(0).equals(list.get(1)))
                       .collect(Collectors.toList())
        );

        // now filter out the courses with the same length as courses2, thus we end up with tuples
        System.out.println(
                courses.stream()
                        .flatMap(course -> courses2.stream()
                                .filter(course2 -> course2.length() == course.length())
                                .map(course2 -> List.of(course, course2))
                        )
                        .filter(list -> !list.get(0).equals(list.get(1)))
                        .collect(Collectors.toList())
        );
    }
}
High Order
public class high_order {

    public static void main(String[] args) {

        List<Course> courses = List.of(
                new Course("Spring", "Framework", 98, 20000),
                new Course("Spring Boot", "Framework", 95, 18000),
                new Course("API", "Microservices", 97, 22000),
                new Course("Microservices", "Microservices", 96, 25000),
                new Course("FullStack", "FullStack", 91, 14000),
                new Course("AWS", "Cloud", 92, 21000),
                new Course("Azure", "Cloud", 99, 21000),
                new Course("Docker", "Cloud", 92, 20000),
                new Course("Kubernetes", "Cloud", 91, 20000));

        Predicate<Course> scoreGreaterThan90Predicate = CreatePredicateWithCutOffReviewScore(90);
        Predicate<Course> scoreGreaterThan95Predicate = CreatePredicateWithCutOffReviewScore(95);
        Predicate<Course> scoreLessThan90Predicate = course -> course.getReviewScore() < 90;
    }

    // This is a high order function, its a function that returns another function
    private static Predicate<Course> CreatePredicateWithCutOffReviewScore(int cutOffReviewScore) {
        return course -> course.getReviewScore() > cutOffReviewScore;
    }
}
Arrays and Primitives
public class arrays_primitives {

    public static void main(String[] args) {

        // You can use Streams directly, the primitives will be auto-boxed to Integers
        System.out.println(
                Stream.of(12,5,6,3,13,8,12,15).reduce(0, Integer::sum)
        );

        // you can use Streams with Arrays
        int[] numberArray = {12,5,6,3,13,8,12,15};
        System.out.println(
                Arrays.stream(numberArray).reduce(0, Integer::sum)
        );

        // create a range of numbers using a Stream
        System.out.println(
            IntStream.range(1,10).sum()     // the 10 is not included
        );

        System.out.println(
                IntStream.rangeClosed(1,10).sum()     // the 10 is included
        );

        // iterate until the specified limit
        System.out.println(
                // displays the calculation
                IntStream.iterate(1, e -> e + 2).limit(10).peek(System.out::println).sum()
        );

        System.out.println(
                IntStream.iterate(1, e -> e * 2).limit(5).peek(System.out::println).sum()
        );

        // to convert primitives to objects use boxed()
        List<Integer> list = IntStream.iterate(1, e -> e * 2)
                .limit(10).boxed().collect(Collectors.toList());
        System.out.println("List: " + list);
    }
}
Big Numbers
public class big_numbers {

    public static void main(String[] args) {

        // you can exceed number limits
        System.out.println(
            IntStream.rangeClosed(1,50).reduce(1, (x,y) -> x * y)  // result is 0, its too big
        );

        // you get around this problem
        System.out.println(
        IntStream.rangeClosed(1,50).mapToObj(BigInteger::valueOf)
                .reduce(BigInteger.ONE, BigInteger::multiply)
        );
    }
}
Performance 1
public class performance_example1 {

    public static void main(String[] args) {

        List<String> courses = List.of("Spring", "Spring Boot", "API" ,
                "Microservices","AWS", "PCF","Azure", "Docker", "Kubernetes");

        System.out.println(
            courses.stream()
                    .filter(course -> course.length() > 11)
                    .map(String::toUpperCase)
                    .findFirst().get()
        );

        System.out.println("---------------------------------------");
        courses.stream()
                .peek(System.out::println)                  // you will see each element
                .filter(course -> course.length() > 5)
                .map(String::toUpperCase)                   // at this point only one element goes through
                .peek(System.out::println)                  // notice that other elements do not appear
                .findFirst().get();                         // this is a terminal operator and thus the peek above does
                                                            // not display other elements
    }
}
Performance 2
public class performance_example2 {

    public static void main(String[] args) {

        long time1 = System.currentTimeMillis();

        // Using old way, which is quicker than the non-parallel processing below
        long sum = 0;
        for (long i = 0; i < 1000000000; i++) {
            sum = sum + i;
        }
        System.out.println((Long) sum);
        System.out.println(System.currentTimeMillis() - time1);

        long time2 = System.currentTimeMillis();

        // Non-parallel processing
        System.out.println(
            LongStream.range(0, 1000000000).sum()
        );
        System.out.println(System.currentTimeMillis() - time2);

        long time3 = System.currentTimeMillis();

        // Parallel processing, simply add parallel()
        System.out.println(
                LongStream.range(0, 1000000000).parallel().sum()    // should get about 10-15% improvement
        );
        System.out.println(System.currentTimeMillis() - time3);
    }
}

Functional Programming using Files and Threads

We can use functional programming on Files and Threads

Files
public class files {

    public static void main(String[] args) throws IOException {

        Files.lines(Paths.get("src\\example.txt"))
                .forEach(System.out::println);

        System.out.println("-----------------------------------------");
        Files.lines(Paths.get("src\\example.txt"))
                .map(str -> str.split(" "))
                .flatMap(Arrays::stream)
                .distinct()
                .sorted()
                .forEach(System.out::println);

        System.out.println("-----------------------------------------");
        Files.list(Paths.get("."))
                .filter(Files::isDirectory)     // lots of options, isHidden, is Readable, etc
                .forEach(System.out::println);
    }
}
Threads
public class threads {

    public static void main(String[] args) {

        // old way
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(
                            Thread.currentThread().getId() + ":" + i
                    );
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("------------------------");

        // using functional way, we use a lambda expression and streams
        Runnable runnable2 = () -> {
            IntStream.range(0, 10)
                    .forEach(i -> System.out.println( Thread.currentThread().getId() + ":" + i));
        };

        Thread thread2 = new Thread(runnable2);
        thread2.start();
    }
}