DEV Community

Cyrus Hardison
Cyrus Hardison

Posted on

Java Function Programming

Contents:


Re-think

Openning Problem

Given a list or array of Person objects

public static List<Person> generatePersonList() {
    List<Person> l = new ArrayList<>();
    l.add(new Person("John", "Tan", 32, 2));
    l.add(new Person("Jessica", "Lim", 28, 3));
    l.add(new Person("Mary", "Lee", 42, 2));
    l.add(new Person("Jason", "Ng", 33, 1));
    l.add(new Person("Mike", "Ong", 22, 0));
    return l;
}
Enter fullscreen mode Exit fullscreen mode
class Person {
    private String firstName;
    private String lastName;
    private int age;
    private int kids;
    // Constructors, getters 
    // omitted for brevity
}

Enter fullscreen mode Exit fullscreen mode

How can we print the full names of the 3 youngest people?

Opening Problem-Algorithm

  1. Sort the list of people by age, in ascending order
  2. Get the first 3 people from the stored list
  3. Get the respective names

A Java Solution

static void sortList(List<Person> persons) {
    for (int i = 0; i < persons.size(); i++) {
        for (int j = i + 1; j < persons.size(); j++) {
            if (persons.get(j).getAge() < persons.get(i).getAge()) {
                // Switch position of two persons
                Person temp = persons.get(i);
                persons.set(i, persons.get(j));
                persons.set(j, temp);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Sort the list by age using Bubble SortNote: there’re many other ways to implement it.

    static List<String> getTopThreeNames(
        List<Person> persons) {
        List<String> retNames = new ArrayList<String>();
        for (int i = 0; i < 3; i++) {
            Person curPerson = persons.get(i);
            retNames.add(curPerson.getFirstName() + " " + curPerson.getLastName());
        }
        return retNames;
    }
    
  2. Get the first 3 people in the sorted list, and.

  3. Get the respective names.

A LINQ Solution

If working with C#, we can use LINQ

Persons.OrderBy(person => person.Age)
    .Take(3)
    .Select(person => new
    {
        FullName = person.FirstName + " " + person.LastName
});
Enter fullscreen mode Exit fullscreen mode

Outline

Revisit – Defining a method

A method definition consist of

  1. Method name
  2. Parameters
  3. Return type
  4. Method body
  5. Return value
int min(
    int num1, int num2){
        if(num1<num2){
            return num1;
        }
    return num2
}

Enter fullscreen mode Exit fullscreen mode

How to make method definition shorter /more concise ?

Lambda Expressions

A lambda expression represents an anonymous methods, in short-hand notation

it consists of:

  1. Method name
  2. Parameters
  3. Return type
  4. Method boday
  5. Return value

()->{}
Lambda Expression
(e1,e2)->e1+e2

Enter fullscreen mode Exit fullscreen mode

Syntax

a lambda consists of a parameter list followed by theh arrow token (->) and a body
(parameter list) -> {statements}

Method:

int min(int num1,int num2){
    if(num1<num2){
        return num1;
    }
    return num2;
}
Enter fullscreen mode Exit fullscreen mode

Lambda:

(int num1,int num2)->{
    if(num1<num2){
        return num1;
    }
    return num2;
}
Enter fullscreen mode Exit fullscreen mode

Implicit Parameter Types

The parameter types are usually omitted

Method:

int min(int num1, int num2) {
    if (num1 < num2) {
        return num1;
    }
    return num2;
}

Enter fullscreen mode Exit fullscreen mode

Lambda:

(num1, num2) -> {
    if (num1 < num2) {
        return num1;
    }
    return num2;
}
Enter fullscreen mode Exit fullscreen mode

The compiler can determine the parameter types by the lambda’s context

Implicit Return

When the body contains only one expression, the return keyword and curly braces {} may also be omitted

Method:

int sum(int num1, int num2)
{
    return num1 + num2;
}
Enter fullscreen mode Exit fullscreen mode

Lambda:

(num1, num2) -> {
return num1 + num2;
}

//......

(num1, num2) -> num1 + num2

Enter fullscreen mode Exit fullscreen mode

The compiler can also determine the return type by the lambda’s context

Question
What is/are the data type(s) of variables n1, n2, and n1 + n2?

public static void sum() {
    double[] arr = { 1.1, 2.2, 3.3 };
    double sum =
        DoubleStream.of(arr)
            .reduce((n1, n2) -> n1 + n2))
            .getAsDouble();
    System.out.println("Sum: " + sum);
}

Enter fullscreen mode Exit fullscreen mode

One Parameter

When the parameter list contains only one parameter, parameter parentheses may also be omitted

//Method:
void printValue(
        double myValue) {
    System.out.println(myValue);
}

//Lambda:
(myValue) -> 
    System.out.println(myValue)

//Lambda
myValue -> 
    System.out.println(myValue)

Enter fullscreen mode Exit fullscreen mode

In the previous slide, how can the compiler know the method’s return type is integer while it is void in this slide?

Method Reference

When a lambda expression does nothing but calls one existing method, we can just refer to ClassName::methodName

//Method:
void printValue(double myValue) {
    System.out.println(myValue);
}

//Lambda:
myValue -> 
    System.out.println(myValue)

//Method Reference:
System.out::println

Enter fullscreen mode Exit fullscreen mode
//Method:
int compare(Person a, Person b) {
    return a.compareByAge(b);
}

//Lambda
(a, b) -> a.compareByAge(b)

//Method Reference:
Person::compareByAge

Enter fullscreen mode Exit fullscreen mode

Lambdas Summary

  1. A lambda is a method with no method name, no return type
  2. A nd remove types of parameters
  3. One expression in method body? Remove return keyword and curly braces {}
  4. Only one parameter? Remove parameter parentheses ()

Quiz

Convert each of the following methods to lambdas or method references, in its shortest form

int pow(int base, int exp) {
    int res = 1;
    for (int i = 0; i < exp; i++) {
        res *= base;
    }
return res;
}


//Answer:
(base, exp) -> {
    int res = 1;
    for (int i = 0; i < exp; i++) {
        res *= base;
    }
return res;
}


Enter fullscreen mode Exit fullscreen mode

Convert each of the following methods to lambdas or method references, in its shortest form

boolean isPrime(int num) {
    for (int divisor = 2; 
        divisor < num - 1; divisor++) {
        if (num % divisor == 0)
        return false;
    }
return true;
}

//Answer:

num -> {
    for (int divisor = 2; divisor<num-1; divisor++) {
        if (num % divisor == 0)
        return false;
    }
return true;
}
Enter fullscreen mode Exit fullscreen mode

More Practise

Convert the following methods to lambda expressions or method references, in its shortest form

String getFullName(Person p) {
    return p.getFirstName() + " " + p.getLastName();
}

//Answer:
p -> p.getFirstName() + " " + p.getLastName()

Enter fullscreen mode Exit fullscreen mode
void printFullName(Person p) {
    System.out.println(p.getFirstName() + " " + p.getLastName());
}

//Answer:
p -> System.out.println(p.getFirstName() + " "+ p.getLastName())

Enter fullscreen mode Exit fullscreen mode
String getFirstName(Person p) {
    return p.getFirstName();
}

class Person {
    private String firstName;
    private String lastName;
    // Other code omitted 
    // for brevity
}

//Answer:
Person::getFirstName

Enter fullscreen mode Exit fullscreen mode

Function Interface

  • A functional interface contains exactly one abstract method, called functional method
  • Compiler can map a lambda to some respective functional interfaces
interface Consumer<T> {
    void accept(T t);
}

interface Predicate<T> {
    boolean test(T t);
}

interface BinaryOperator<T> {
    T apply(T t1, T t2);
}

Enter fullscreen mode Exit fullscreen mode

An example - Consumer

Interface Consumer, method accept(T) Perform a*task* with the given T, e.g.,

interface Consumer<T> {
    void accept(T t);
}

Enter fullscreen mode Exit fullscreen mode

Like other interfaces, we need to implement the
abstract method when implementing a functional
interface

How to implement and instantiate instances of functional interfaces?

Instance Instantiation

We can implement and instantiate an instance of a functional interface by new keyword and implement its abstract methods

new Consumer< Integer>() {
    @Override
    public void accept(Integer num) {
        System.out.println(num);
    }
}

Enter fullscreen mode Exit fullscreen mode
  1. To instantiate an instance, start with new and the interface name Consumer
  2. Because interface Consumer supports generic type, specify the generic type. In here, it is Integer
  3. Implement the interface’s abstract method. In here, accept(T) becomes accept(Integer)

Functional Interfaces and Lambdas

We can also implement and instantiate an instance of a functional interface with the respective lambdas

new Consumer<Integer>() {
    @Override
    public void accept(Integer num) {
        System.out.println(num);
    }
}

//lambda
num -> System.out.println(num)

//Lambda
System.out::println
Enter fullscreen mode Exit fullscreen mode

Both lambdas can be used as instances of Consumer

Conversely, given the above lambdas, can the compiler know how to map to Consumer?
Hint: a functional interface only has 1 functional method

Using Functional Interfaces

Functional interfaces are usually used as method parameters For example,later we’ll study streams, which can be iterated using
forEach(Consumer) method

Integer[] arr = {1, 2, 3, 4};
    Arrays.stream(arr)
        .forEach( new Consumer<Integer>() {
            @Override
            public void accept(Integer num) {
                System.out.println(num);
            }
        });


Integer[] arr = {1, 2, 3, 4};
Arrays.stream(arr)
    .forEach( num -> 
        System.out.println(num)); 
Enter fullscreen mode Exit fullscreen mode

In both cases, an instance of Consumer is used as the method parameters

A Example -Pridicate

Interface Predicate, method test(T) Test whether the T argument satisfy a condition

interface Predicate<T> {
    boolean test(T t);
}

Enter fullscreen mode Exit fullscreen mode

Like Consumer, we can implement and instantiate an instance of Predicate as follows

new Predicate<Person>() {
    @Override
    public boolean test(Person p) {
        return p.getKids() == 2; 
    }
}

//Lambda


Enter fullscreen mode Exit fullscreen mode

An example – BinaryOperator

Interface BinaryOperator, method apply(T, T)Performs an operation on the 2 arguments (such as a calculation) and returns a value of the same type

interface BinaryOperator<T> {
    T apply(T t1, T t2);
}

Enter fullscreen mode Exit fullscreen mode

Some lambdas that may be used as instances of
BinaryOperator

(x, y) -> x + y

(str1, str2) -> 
str1 + " " + str2

(x, y) -> {
    if (x > y)
        return x - y;
    return y - x;
};

Enter fullscreen mode Exit fullscreen mode

Common Functional Interfaces

interface Method Arguments What does it do Return
Consumer accept T Perform a task with T, e.g., printing void
Function apply T Call a method on the T argument and return that method’s result R
Predicate test T Test whether the T argument satisfies a condition bool
Supplier get Empty Produce a value of type T, e.g., creating a collection object T
UnaryOperator apply T Perform an operation on the T argument and return a value of T T
BinaryOperator apply T, T Perform an operation on the two T

Streams

Revisit - working with Collections

When processing a collection, we usually

  1. Iterate over its elements
  2. Do some work with each element
public static int
    sumOfEven(int[] arr) {
        int sum = 0;
        for (int num: arr) {
            if (num % 2 == 0) {
                sum += num;
        }
    }
    return sum;
}
Enter fullscreen mode Exit fullscreen mode

What are Streams?

  • A Stream is a sequence of elements on which we perform tasks
  • Specify only what we want to do
  • Then simply let the Stream deal with how to do it
public static int
    sumOfEven2(int[] arr) {
        return IntStream
    .of(arr)
    .filter(x -> x%2==0)
    .sum();
}
Enter fullscreen mode Exit fullscreen mode

Stream Pipelines

Source=>Create Stream=>Operation 1=>Operation2=>.....Operation N=>Terminal Operation=>Result

Source: Usually , an array or a collection
Operation: Filtering, sorting,type conversions, mapping...
Terminal operation: Aggregate results, eg., count, sum or collecting a collection.

IntStream.of(arr).filter(x -> x % 2 ==0).sum()

//arr: Source
//of: Create stream
//filter: Intermediate operation
//sum: Terminal operation

Enter fullscreen mode Exit fullscreen mode

Lazy vs eager operations

Intermediate operations are lazy

=> Not perform until a
terminal operation is
called

Terminal operations are eager
=> Perform right away when being called

Stream Advantages

  • Allows us to write more declarative and more concise programs
  • Allows us to focus on the problem rather than the code
  • Facilitates parallelism

Creating Streams from Arrays

Streams can be created from arrays with different
approaches

public static void streamFromArray1() {
    int[] arr = {3, 10, 6, 1, 4}; 
        IntStream.of(arr)
            .forEach(e -> System.out.print(e + " "));
}

//3 10 6 1 4

public static void streamFromArray2() {
    Integer[] arr = {2, 9, 5, 0, 3};
        Arrays.stream(arr)
            .forEach(e -> System.out.print(e + " "));
}
//2 9 5 0 3

Enter fullscreen mode Exit fullscreen mode

Creating Streams from Collections

Streams can also be created from any implementation of Collection interface, e.g., List, Set…

public static void streamFromList() {
    List<String> myList = new ArrayList<>();
    myList.add("Hi");
    myList.add("SA");
    myList.add("students");
    myList.stream()
        .forEach(System.out::println);
}

//Hi
//SA
//students
Enter fullscreen mode Exit fullscreen mode

Creating ordered sequence of integer Streams

Ordered sequence of integers can be created using
IntStream.range() and IntStream.rangeClosed()

public static void orderedSequenceStream1() {
    IntStream
        .range(1, 10)
        .forEach(e -> System.out.print(e + " "));
}
//1 2 3 4 5 6 7 8 9

public static void orderedSequenceStream2() {
    IntStream
        .rangeClosed(1, 10)
        .forEach(e -> System.out.print(e + " "));
}

//1 2 3 4 5 6 7 8 9 10

Enter fullscreen mode Exit fullscreen mode

Common Intermediate Operations

Method Parameter Description
filter Predicate Returns a stream consisting of the elements of this stream that match the given predicate.
sorted Comparator Returns a stream consisting of the elements of this stream, sorted according to the provided Comparator.
map Function Returns a stream consisting of the results of applying the given function to the elements of this stream.
distinct No Returns a stream consisting of the distinct elements (according to Object.equals(Object)) of this stream.
limit long Returns a stream consisting of the elements of this stream, truncated to be no longer than the given number in length.
skip long Returns a stream consisting of the remaining elements of this stream after discarding the first n elements of the stream. If this stream contains fewer than n elements then an empty stream will be returned

Filtering

Elements in a stream can be filtered using filter(Predicate)

public static List<Person> generatePersonList() 
{
    List<Person> l = new ArrayList<>();
    l.add(new Person("John", "Tan", 32, 2));
    l.add(new Person("Jessica", "Lim", 28, 3));
    l.add(new Person("Mary", "Lee", 42, 2));
    l.add(new Person("Jason", "Ng", 33, 1));
    l.add(new Person("Mike", "Ong", 22, 0));
    return l;
}

public static void filtering() {
    List<Person> persons = 
    generatePersonList();
    persons
    .stream()
    .filter( x -> x.getKids() == 2)
    .forEach(System.out::println);
}


class Person {
    private String 
    firstName;
    private String 
    lastName;
    private int age;
    private int kids;
    // Other code omitted 
    // for brevity
}


//John, Tan, 32, 2
//Mary, Lee, 42, 2
Enter fullscreen mode Exit fullscreen mode

One way to understand a stream’s method

.filter(x -> x.getKids() == 2)
Enter fullscreen mode Exit fullscreen mode
  1. I want to filter elements in a stream
  2. Ok, you need to call filter() method, and give me a Predicate in form of a lambda
  3. I will loop through every element in the stream, and with the current element…
  4. Let’s name that element as x, the left side of the lambda
  5. You need to let me know what to do with x, using any method of the data type in the stream holding x. For example, Person in the last slide
  6. I want to filter only x having 2 kids, so I return a Boolean Expression with such condition. It is put in the body, the right side of the lambda

=>Most of stream methods happen in this manner. Step 2, 4 and 6 change depending on the scenario

Sorting

Streams can be sorted using sorted(Comparator) As usual, a Comparator object can be created using a lambda

public static List<Person> generatePersonList() {
    List<Person> l = new ArrayList<>();
    l.add(new Person("John", "Tan", 32, 2));
    l.add(new Person("Jessica", "Lim", 28, 3));
    l.add(new Person("Mary", "Lee", 42, 2));
    l.add(new Person("Jason", "Ng", 33, 1));
    l.add(new Person("Mike", "Ong", 22, 0));
    return l;
}

public static void sortBySingleField() {
    List<Person> persons = generatePersonList();
    persons
    .stream()
    .sorted( (p1, p2) -> 
    p1.getFirstName().compareTo( 
    p2.getFirstName()))
    .forEach(x -> System.out.println(x));
}


//Jason, Ng, 33, 1
//Jessica, Lim, 28, 3
//John, Tan, 32, 2
//Mary, Lee, 42, 2
//Mike, Ong, 22, 0

Enter fullscreen mode Exit fullscreen mode

Alternatively, a Comparator object can be created using Comparator.comparing(Function)

public static List<Person> generatePersonList() {
    List<Person> l = new ArrayList<>();
    l.add(new Person("John", "Tan", 32, 2));
    l.add(new Person("Jessica", "Lim", 28, 3));
    l.add(new Person("Mary", "Lee", 42, 2));
    l.add(new Person("Jason", "Ng", 33, 1));
    l.add(new Person("Mike", "Ong", 22, 0));
    return l;
}

public static void sortBySingleField () {
    List<Person> persons = generatePersonList();
    persons
    .stream()
    .sorted( Comparator.comparing(
    Person::getFirstName))
    .forEach(x -> System.out.println(x));
}

//Jason, Ng, 33, 1
//Jessica, Lim, 28, 3
//John, Tan, 32, 2
//Mary, Lee, 42, 2
//Mike, Ong, 22, 0

Enter fullscreen mode Exit fullscreen mode

Streams can also be sorted with multiple fields and in reversed order

public static List<Person> generatePersonList() {
    List<Person> l = new ArrayList<>();
    l.add(new Person("John", "Tan", 32, 2));
    l.add(new Person("Jessica", "Lim", 28, 3));
    l.add(new Person("Mary", "Lee", 42, 2));
    l.add(new Person("Jason", "Ng", 33, 1));
    l.add(new Person("Mike", "Ong", 22, 0));
    return l;
}

public static void sortByMultiFields () {
    List<Person> persons = generatePersonList();
    persons.stream()
    .sorted(Comparator
    .comparing(Person::getKids)
    .thenComparing(Person::getAge)
    .reversed())
    .forEach(x -> System.out.println(x));
}

//Jason, Ng, 33, 1
//Jessica, Lim, 28, 3
//John, Tan, 32, 2
//Mary, Lee, 42, 2
//Mike, Ong, 22, 0

Enter fullscreen mode Exit fullscreen mode

Transforming

Each of elements in a Stream can be transformed to another value (even another type) using map(Function)

public static List<Person> generatePersonList() {
    List<Person> l = new ArrayList<>();
    l.add(new Person("John", "Tan", 32, 2));
    l.add(new Person("Jessica", "Lim", 28, 3));
    l.add(new Person("Mary", "Lee", 42, 2));
    l.add(new Person("Jason", "Ng", 33, 1));
    l.add(new Person("Mike", "Ong", 22, 0));
    return l;
}

public static void transforming1() {
    List<Person> persons = generatePersonList();
    persons
    .stream()
    .sorted(Comparator.comparing(
    Person::getFirstName))
    .map( x -> x.getFirstName() + 
    " " + x.getLastName())
    .forEach(System.out::println);
}


//Jason Ng
//Jessica Lim
//John Tan
//Mary Lee
//Mike Ong

Enter fullscreen mode Exit fullscreen mode

Of course, map(Function) can also be applied to
other types of streams

public static void transforming2() {
    int[] arr = {0, 1, 2, 3, 4, 5};
    IntStream.of(arr)
    .map(e -> e * 2)
    .forEach(e -> System.out.print(e + " "));
}

//0 2 4 6 8 10

public static void transforming3() {
    String[] arr = {"aa", "bb", "cc", "dd"};
    Arrays.stream(arr)
    .map(String::toUpperCase)
    .forEach(e -> System.out.print(e + " "));
}
//AA BB CC DC

Enter fullscreen mode Exit fullscreen mode

A stream can be mapped to a numeric stream

public static void transforming4() {
String[] names = 
{"John", "Jessica", "Mary", "Jason", "Mike"};
int maxLength =
Stream.of(names)
.mapToInt(x -> x.length())
.max()
.getAsInt();
System.out.println("Name with maximum length is " + 
maxLength);

//Name with maximum length is 7

Enter fullscreen mode Exit fullscreen mode

Terminal Operations

Method Parameters Description
forEach Consumer Performs an action for each element of this stream.
reduce T, BinaryOperator Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value.
reduce BinaryOperator Performs a reduction on the elements of this stream, using an associative accumulation function, and returns an Optional describing the reduced value, if any.
min Comparator Returns the minimum element of this stream according to the provided Comparator. This is a special case of a reduction.
max Comparator Returns the maximum element of this stream according to the provided Comparator. This is a special case of a reduction.
average No Return the average of all elements in a numeric stream.

“Iterating"

Performs an action for each element of the stream using forEach(Consumer)

public static void forEachStream() 
{
    List<String> list = new ArrayList<>();
    list.add("aa");
    list.add("bb");
    list.add("cc");
    list
    .stream()
    .forEach( e
    -> System.out.print(e + " "));
}

//aa bb cc
Enter fullscreen mode Exit fullscreen mode
  1. Call forEach()
  2. Given a Consumer object in the form of lambda as the argument We can think like this: given each element e, what Java should do with it (and return nothing as defined in Consumer interface)? In here,we ask Java to print the value of element e

=>List also has forEach() method, operating in the
same manner

Top comments (0)