Java Function Programming
Cyrus Hardison
Posted on March 8, 2023
Contents:
- Re-think
-
Outline
- Revisit – Defining a method
- Lambda Expressions
- Syntax
- Implicit Parameter Types
- Implicit Return
- One Parameter
- Method Reference
- Lambdas Summary
- Quiz
- More Practise
- Function Interface
- An example - Consumer
- Instance Instantiation
- Functional Interfaces and Lambdas
- Using Functional Interfaces
- Streams
- Revisit - working with Collections
- What are Streams?
- Stream Pipelines
- Lazy vs eager operations
- Stream Advantages
- Creating Streams from Arrays
- Creating Streams from Collections
- Creating ordered sequence of integer Streams
- Common Intermediate Operations
- Filtering
- One way to understand a stream’s method
- Sorting
- Transforming
- Terminal Operations
- “Iterating"
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;
}
class Person {
private String firstName;
private String lastName;
private int age;
private int kids;
// Constructors, getters
// omitted for brevity
}
How can we print the full names of the 3 youngest people?
Opening Problem-Algorithm
- Sort the list of people by age, in ascending order
- Get the first 3 people from the stored list
- 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);
}
}
}
}
-
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; }
Get the first 3 people in the sorted list, and.
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
});
Outline
Revisit – Defining a method
A method definition consist of
- Method name
- Parameters
- Return type
- Method body
- Return value
int min(
int num1, int num2){
if(num1<num2){
return num1;
}
return num2
}
How to make method definition shorter /more concise ?
Lambda Expressions
A lambda expression represents an anonymous methods, in short-hand notation
it consists of:
Method name- Parameters
Return type- Method boday
- Return value
()->{}
Lambda Expression
(e1,e2)->e1+e2
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;
}
Lambda:
(int num1,int num2)->{
if(num1<num2){
return num1;
}
return num2;
}
Implicit Parameter Types
The parameter types are usually omitted
Method:
int min(int num1, int num2) {
if (num1 < num2) {
return num1;
}
return num2;
}
Lambda:
(num1, num2) -> {
if (num1 < num2) {
return num1;
}
return num2;
}
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;
}
Lambda:
(num1, num2) -> {
return num1 + num2;
}
//......
(num1, num2) -> num1 + num2
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);
}
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)
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
//Method:
int compare(Person a, Person b) {
return a.compareByAge(b);
}
//Lambda
(a, b) -> a.compareByAge(b)
//Method Reference:
Person::compareByAge
Lambdas Summary
- A lambda is a method with no method name, no return type
- A nd remove types of parameters
- One expression in method body? Remove return keyword and curly braces {}
- 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;
}
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;
}
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()
void printFullName(Person p) {
System.out.println(p.getFirstName() + " " + p.getLastName());
}
//Answer:
p -> System.out.println(p.getFirstName() + " "+ p.getLastName())
String getFirstName(Person p) {
return p.getFirstName();
}
class Person {
private String firstName;
private String lastName;
// Other code omitted
// for brevity
}
//Answer:
Person::getFirstName
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);
}
An example - Consumer
Interface Consumer, method accept(T) Perform a*task* with the given T, e.g.,
interface Consumer<T> {
void accept(T t);
}
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);
}
}
- To instantiate an instance, start with new and the interface name Consumer
- Because interface Consumer supports generic type, specify the generic type. In here, it is Integer
- 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
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));
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);
}
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
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);
}
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;
};
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
- Iterate over its elements
- 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;
}
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();
}
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
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
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
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
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
One way to understand a stream’s method
.filter(x -> x.getKids() == 2)
- I want to filter elements in a stream
- Ok, you need to call filter() method, and give me a Predicate in form of a lambda
- I will loop through every element in the stream, and with the current element…
- Let’s name that element as x, the left side of the lambda
- 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
- 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
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
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
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
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
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
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
- Call forEach()
- 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
Posted on March 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.