Title Text

Title Text

Title Text

Title Text

Introduction to programming

FUNCTIONAL JAVA

Functional Programming

The Functional Paradigm

Most of these changes were designed and implemented due to the rise of the Functional Paradigm.

With the impending demise of Moore's law, engineers started to increase computer power using a technique called horizontal scaling (distributing computation across several cores). 

This meant slicing up our programs into parallel parts.

Remember threads?

Functional Programming

vs. PARALLELISM

We haven't looked into multi-thread programming yet, so you haven't felt the burden of having to deal with its woes.

 

Programming according to the Functional Paradigm principles, we can eliminate most of the parallelism problems.

(We'll come back to this, later.)

Immutable data

All variables and data structures must be immutable.

Once it's created, the state of an object must be impossible to change.

 

Even when dealing with the OOP paradigm, proper state management should be high on your priorities list. 

Remember encapsulation?

Functional programming takes state management to the extreme!

public class Square {

    private final String color;
    private final int width;

    public Square(String color, int width) {
        this.color = color;
        this.width = width;
    }
}

Pure Functions

A pure function is a function which operates only on its input parameters and produces an output result.

Pure functions also have no side effects!

RECURSION

Recursion is the process of solving a problem repeating the solution to a smaller version of itself.

 

The Functional Paradigm works based on the idea of declarative code (what to do instead of how to do it).

In the figure above, which approach looks more declarative?

First-class functions

In functional programming, we are able to work with code as data. This means being able to store a function in a variable or data structure, pass it as an argument to another function, or use it as a return value.

First-class functions

This concept of using code as data is dependent on the programming language itself. The language creators define how functions are treated.

 

Unfortunately, in Java, we don't have this option. Right?

LAMBDA-EXPRESSIONS

Anonymous classes

Anonymous classes already allowed us to pass functionality to a method. It's the closest we've been to code as data in Java.

public class Main {

    public static void main(String[] args) {

        List<Person> people = Arrays.asList(new Person(23), new Person(12), new Person(18), new Person(64));
        
        people.sort(new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.getAge() - p2.getAge();
            }
        });
    }
}

But Anonymous classes introduce a lot of boilerplate code, making our program harder to read, and masking it's intent.

Enter... Lambda expressions!

In computer programming, a lambda expression is an anonymous function. Anonymous functions are often arguments being passed to higher-order functions.

public class Main {

    public static void main(String[] args) {

        List<Person> people = Arrays.asList(new Person(23), new Person(12), new Person(18), new Person(64));

        people.sort((p1, p2) -> p1.getAge() - p2.getAge());
    }
}

Lambdas have a cleaner, shorter syntax, while providing the same functionality as anonymous classes.

LAMBDA SYNTAX I

If the body of the lamba requires more than one line, the brackets and return keyword must be included.

List<Person> people = Arrays.asList(new Person(23), new Person(12), new Person(18), new Person(64));

people.sort((p1, p2) -> {
    System.out.println("Person1 age: " + p1.getAge() + ", Person2 age: " + p2.getAge());
    return p1.getAge() - p2.getAge();
});

BE CAREFUL WITH THIS. If a lambda embodies too many lines, then it shouldn't be a lambda.

people.sort((p1, p2) -> p1.getAge() - p2.getAge()); // two parameters -> mandatory parenthesis
people.removeIf(person -> person.getAge() < 18); // one parameter -> optional parenthesis
// if there are no parameters, then the parenthesis are also mandatory; EX: () -> 21;

LAMBDA EXPRESSIONS

VS. ANONYMOUS CLASSES

public class SomeClass {

    private static final int NUMBER = 0;
    private String name;

    public void someMehtod() {

        List<Person> people = Arrays.asList(new Person(23), new Person(12), new Person(18), new Person(64));
        boolean truth = false;
        
        people.sort((p1, p2) -> {
        
            // Both anonymous classes and lambdas can access instance and class members of the enclosing class
            System.out.println(NUMBER);  
            System.out.println(name); 
            
            // Neither anonymous classes, nor lambdas can access local variables unless they're final or effectively final
            truth = false; // COMPILATION ERROR
            
            // Unlike anonymous classes, lambdas do not define a new scope
            boolean truth = true; // COMPILATION ERROR
            
            System.out.println(this); // this REFERS TO THE ENCLOSING CLASS
            
            return p1.getAge() - p2.getAge();
        });
    }
}

Bye-Bye Anonymous?

No.

Some situations will always require the use of a good, old-fashioned, anonymous class.

@Override
public Iterator iterator() {
    return new Iterator() {

        @Override
        public boolean hasNext() {...}

        @Override
        public Integer next() {...}
    };
}

In this case, we couldn't have used a lambda expression.

The target of a lambda expression must ALWAYS be a functional interface.

Functional Interfaces

A Functional Interface is any interface that has one, and ONLY one, abstract method.

 

Since Java SE 8, the Java platform offers the package java.util.function that includes a variety of functional interfaces. Later, we'll look into a few of them in detail.

Type inference

How can the compiler recognise the argument and return types of a lambda expression, if they are not specified in the lambda expression itself?

Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type of the argument (or arguments) that make the invocation applicable.

This concept has been present since we first started working with generics.

Method references

A method reference is a more succinct way of generating function objects.

It's basically a shorter version of a lambda expression.

public class SomeClass {
    
    public void someMehtod() {
        (...)
        people.forEach(System.out::println);
    }
}

Functional Interfaces

Supplier

Supplier is a functional interface, whose functional method is get().

 

It represents a supplier of results.

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

Consumer

A Consumer is a functional interface whose method is accept(Object).

 

It represents an operation that accepts a single input and returns no result.

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}

PREDICATE

Predicate is a functional interface whose functional method is test(Object).

 

It represents a predicate (boolean-valued function) of one argument.

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

FUNCTION

A Function is a functional interface whose method is apply(Object).

 

It represents a function that accepts an argument and produces a result. These don't have to be of the same type.

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

STREAM API

Collections API

Java 8's most importante changes occurred at the Collections framework level: the way we iterate over a collection and performed any operation over its data changed forever.

The old way of iterating

But we know that the code above is just syntactic sugar for the following construct:

public class Main {

    public static void main(String[] args) {
        List<String> names = new LinkedList<>();
        (...)
        for(String name : names) {
            System.out.println(name);
        }
    }
}
public class Main {

    public static void main(String[] args) {
        List<String> names = new LinkedList<>();
        (...)
        String name;
        for(Iterator<String> it = names.iterator(); it.hasNext(); name = it.next()) {
            System.out.println(name);
        }
    }
}

The NEW way of iterating

As we've seen before, since Java 8 it is possible to ask the Collection to iterate over its elements. We just need to specify what operation we want to perform during that iteration.

public class Main {

    public static void main(String[] args) {
        List<String> names = new LinkedList<>();
        (...)
        names.forEach(System.out::println);
    }
}

This is called internal iteration. It focuses on the what's instead of the how's. However, for more complex operations, the foreach method is not enough.

Enter... Stream API

A Stream is a sequence of elements that supports sequential and parallel aggregate operations. 

 

To perform a computation, stream operations are composed into a stream pipeline.

 

A stream pipeline consists of a source, zero or more intermediate operations, and a terminal operation.

StreaMS FOR DUMMIES

Streams - a few important bits

Streams are lazy; computation won't start until a terminal operation is initiated.

 

Streams function based on the immutability principle: the underlying data source is never modified by the stream.

 

Intermediate operations always return a new Stream. Here lies the power of streams: the composition of operations following one another.

 

Terminal operations always return a value. A terminal operation exhausts the stream.

StreaMS - CREATION

There are many ways to create a Stream instance.

(...)

List<Human> humans = Arrays.asList(
    new Human("Alexandra", 30),
    new Human( "Jonh", 28)
);

// A STREAM CREATED FROM A COLLECTION
Stream<Human> humanStream = humans.stream();

String[] moreHumans = {
    humans.get(0).getName(),
    humans.get(1).getName()
};

// A STREAM CREATED FROM AN ARRAY
Stream<String> namesStream = Arrays.stream(moreHumans);

// A STREAM CREATED FROM A SET OF VALUES
Stream<Integer> numbersStream = Stream.of(23, 34, 58, 21);

StreaMS - TERMINAL OPERATIONS

(...)

String tongueTwister = "Peter Piper picked a peck of pickled peppers.";

// PRINT ALL THE INDIVIDUAL WORDS IN A STRING
Arrays.stream(tongueTwister.split("\\W+"))
        .forEach(System.out::println);

int[] numbers = {120, 15, 24, 30, 90, 16};

// SUM ALL THE NUMBERS IN AN ARRAY
int reduce = Arrays.stream(numbers)
        .reduce(0, (acc, element) -> acc + element);

Almost all terminal operations produce some kind of result, i.e., they reduce the elements to a single value.

StreaMS - OTHER REDUCE OPERATIONS

(...)

String tongueTwister = "Peter Piper picked a peck of pickled peppers.";

// COUNT THE NUMBER OF WORDS IN A STRING
long count = Arrays.stream(tongueTwister.split("\\W+"))
        .count();

// PRINT THE MINIMAL VALUE IN AN ARRAY
System.out.println(Arrays.stream(new int[]{12, 34, 11, 19, 2})
        .min());

// CALCULATE THE AVERAGE FROM A SET OF VALUES AND PRINT THE RESULT
System.out.println(Arrays.stream(new int[]{12, 34, 11, 19, 2})
        .average());

Without the aggregation of intermediate operations this doesn't look very exciting, does it?

StreaMS - Intermediate operations

(...)

Human[] humans = {
    new Human("Alexandra", 30),
    new Human("John", 12),
    new Human("Charlotte", 22),
    new Human("Edith", 42),
    new Human("Anna", 3)
};

// COUNT THE AMOUNT OF ADULTS IN OUR SAMPLE
long count = Arrays.stream(humans)
    .filter(human -> human.getAge() >= 18)
    .count();

// PRINT HUMAN NAMES IN ALPHABETIC ORDER
Arrays.stream(humans)
    .map(Human::getName)
    .sorted()
    .forEach(System.out::println);

StreaMS - MORE operations

// CREATING AN INFINITE STREAM
Stream.generate(() -> new Random().nextInt(12))
    .limit(5) // SHORT CIRCUIT METHOD
    .forEach(System.out::println);

// CREATING A PRIMITIVE STREAM AND ADDING ALL THE EVEN NUMBERS IN A SET
int sum = IntStream.of(23, 34, 56, 12, 2, 94)
    .filter(num -> num % 2 == 0)
    .sum();

// POPULATING A LIST WITH NUMBERS
List<Integer> list = new LinkedList<>();

IntStream.iterate(0, num -> num + 1)
    .limit(3000000)
    .forEach(list::add);

// RUNNING AN OPERATION IN PARALLEL
long count = list.parallelStream()
    .map(num -> num * 10)
    .filter(num -> num % 3 == 0)
    .count();

EXERCISE

Map-Filter-Reduce Trinity

String prayer = "Oh Lord, won't you buy me a trash Mercedes Benz\n" +
                "My friends all drive trash Porsches, I must make trash amends\n" +
                "Worked hard all my trash lifetime, no help from my trash friends\n" +
                "So Lord, won't you buy me a trash Mercedes Benz";

The prayer above was infected by trash.

Remove the trash, and print it in uppercase.

OPTIONALS

Human[] humans = {
    new Human("Alexandra", 30),
    new Human("John", 12),
    new Human("Charlotte", 22),
    new Human("Edith", 42),
    new Human("Anna", 3)
};

// FINDING AN ADULT HUMAN WHOSE NAME STARTS WITH A
Optional<Human> result = Arrays.stream(humans)
    .filter(human -> human.getName().startsWith("A"))
    .filter(human -> human.getAge() > 18)
    .findFirst();

// DEALING WITH AN OPTIONAL OBJECT
String human = result.isPresent() ? result.get().getName() : "no results";
System.out.println(human);

An Optional is a container object which may or may not contain a non-null value.

EXERCISE

The Employee Analyzer


  1. Count the number of employees that having been working at that department for more than n years
  2. Find the name of the employees that have a salary over n
  3. Get the oldest n employees
  4. Find the first employee older than n
  5. Find the average salary in a department
  6. Finding common first names between the employees of two departments

Create a class capable of analysing a list of department employees.

This class should be able to:

Functional Java

By Soraia Veríssimo

Functional Java

  • 1,734