Define Functional Interface
An interface specifying a single abstract method (SAM).
// Define
public interface IntPredicate {
public boolean test(int value);
}
Essentially, Interface — can have multiple abstract methods, all without implementations Functional interface — same thing, but restricted to just one abstract method
Once you define the functional interface you can make use 3 ways to implement it:
Implement Method 1: Named Class
jshell> class EvenPredicate implements IntPredicate {
...> public boolean test(int x) {
...> return x % 2 == 0;
...> }
...> }
jshell> new EvenPredicate().test(8)
$.. ==> true
Other example
class AndPredicate implements IntPredicate {
private final IntPredicate p1;
private final IntPredicate p2;
AndPredicate(IntPredicate p1, IntPredicate p2) {
this.p1 = p1;
this.p2 = p2;
}
public boolean test(int x) {
return p1.test(x) && p2.test(x);
}
}
IntPredicate and(IntPredicate p1, IntPredicate p2) {
return new AndPredicate(p1, p2);
}
Implement Method 2: Local Class / Anonymous Inner Class
IntPredicate pred = new IntPredicate() {
public boolean test(int x) { return x % 2 == 0; }
};
so but
jshell> pred.test(8)
$.. ==> true
Include this into a method (from Recitation 4)
IntPredicate and(IntPredicate p1, IntPredicate p2) {
return new IntPredicate() {
public boolean test(int x) {
return p1.test(x) && p2.test(x);
}
};
}
Instead of creating a class that implements the interface and creating an instance, i just create a instance of the interface, assign it to a variable “pred” and define how “test” method works.
Implement Method 3: Lambda
jshell> IntPredicate pred = x -> x % 2 == 0 // lambda expression
pred ==> $Lambda..
jshell> pred.test(8)
$.. ==> true
Include this into a method (from Recitation 4)
IntPredicate and(IntPredicate p1, IntPredicate p2) {
return x -> p1.test(x) && p2.test(x);
}
Completely skips the interface. Just logic. pred is the variable.
IntUnaryOperator and Mapping (Example)
public interface IntUnaryOperator {
public int applyAsInt(int operand);
}
Local Class
IntUnaryOperator mapper = new IntUnaryOperator() {
public int applyAsInt(int x) {
return x + 1;
}
};
mapper.applyAsInt(3);
// ==> 4
IntStream.range(1, 10).map(mapper).forEach(x -> System.out.print(x + " "));
// 2 3 4 5 6 7 8 9 10
Lambda
IntUnaryOperator mapper = x -> x + 1;
mapper.applyAsInt(3);
// ==> 4
IntStream.range(1, 10).map(mapper).forEach(x -> System.out.print(x + " "));
// 2 3 4 5 6 7 8 9 10
How do we map other objects, and between different objects?
Generic Functional Interface
How do we map other objects, and between different objects? IntPredicate only works with int. The solution is Predicate<T>, where T is a placeholder for any type. When something needs to work with multiple types, you’ll usually see the <>
Generic type: T in Predicate<T> Parameterized type: Predicate<Integer> Known as parametric polymorphism.
Define Generic Functional Interface
public interface Predicate<T> {
public boolean test(T t);
}
Implement Method 3: Lambda
jshell> Predicate<Integer> pred = x -> x % 2 == 0
pred ==> $Lambda..
jshell> IntStream.range(1, 10). // IntStream
...> boxed(). // Stream<Integer>
...> filter(pred).
...> toList()
$.. ==> [2, 4, 6, 8]
Auto-boxing/unboxing under the hood
For generics, you can’t write Function<int, int>, it has to be Function<Integer, Integer>. Auto-boxing: Java automatically convert int (primitive) to Integer (wrapper type). Auto-unboxing: Java automatically convert Integer back to int
The difference between int and Integer: int is a primitive type, Integer is a class
Generics are Invariant
Let S be substitutable for T, denoted S <: T. pred is a Predicate<T>, so pred.test(new S()) works fine because S is a subtype of T.
However, Predicate<S> and Predicate<T> are unrelated and have no relationship. Given S <: T, neither C<S> <: C<T> nor C<T> <: C<S> holds.
Substitutability in Generics, Consumer
Pre-amble
interface Customer<T> {
void eat(T food);
}
Motivating Example
void serve(Customer<Burger> customer) {
customer.eat(new Burger());
}
FishBurger, i.e. serve(new Customer<FishBurger>())
FastFood, i.e. serve(new Customer<FastFood>())
// Both Doesn't Work due to Invariance
Use super (? super Burger is like Burger and above)
void serve(Customer<? super Burger> customer) { // lower bounded wildcard
customer.eat(new Burger());
}
? super Burger means that “any type that is Burger or a supertype of Burger”. It accepts Customer<Burger> and Customer<FastFood>, but not Customer<FishBurger>.
Substitutability in Generics, Producer
Motivating Example
void employ(Chef<Burger> chef) {
Burger burger = chef.cook();
}
FishBurger, i.e. employ(new Chef<FishBurger>())
FastFood, i.e. employ(new Chef<FastFood>())
Use extend (? extends is Burger and below)
void employ(Chef<? extends Burger> chef) { // upper bounded wildcard
Burger burger = chef.cook();
}
The employ method called chef.cook() and assigns the result to a Burger variable, data is flowing out of the chef.
We need to guarantee that whatever the chef produces is at least a Burger, so we use ? extends Burger. This means any chef who cooks a FishBurger (a subtype) is fine, but a Chef<FastFood> is not, because their output might not be a Burger.
? super Burger — lower bound and going up hierachy ? extends Burger — upper bound and going down hierachy
How does Consumer and Producer play a role?
Consumers - data flows in (go up hierachy) If they can handle a Burger, they can handle a more generic type (supertype) like FastFood. If you want to serve someone a Burger, they need to be at least okay with eating Burger.
Producers - data flows out (go down hierachy) You’re taking something out and assigning it to the Burger variable. Whatever comes out must be at least a Burger. So, any more specific (subtype) can satisfy.
Optional<T> Class
class Optional\<T> {
...
public Optional\<T> filter(Predicate<? super T> predicate) { ... }
public T orElseGet(Supplier<? extends T> supplier) { ... }
...
Test cases with super
jshell> Optional.of("cs2030") // type-inferred to String
$.. ==> Optional[cs2030] // Optional<String>
jshell> Optional.<Object>of("cs2030") // type-inferred to Object
$.. ==> Optional[cs2030] // Optional<Object>
// Predicate<String>: consumes String, tests if length is even
jshell> Predicate<String> predS = x -> x.length() % 2 == 0
predS ==> $Lambda/0x000077a3e400a208@69d9c55
// predS is Predicate<String>, satisfies Predicate<? super String>
jshell> Optional.<String>of("cs2030").filter(predS)
$.. ==> Optional[cs2030] // Optional<String>
// Predicate<Object>: consumes Object, tests if hashCode is even
jshell> Predicate<Object> predO = x -> x.hashCode() % 2 == 0
predO ==> $Lambda/0x000077a3e400a868@61a485d2
// predO is Predicate<Object>, satisfies Predicate<? super String> since Object >: String
jshell> Optional.<String>of("cs2030").filter(predO)
$.. ==> Optional.empty // Optional<String>
Test cases with extend 🔴
jshell> Supplier<String> supplier = () -> "cs" + "1010"
supplier ==> $Lambda/0x00007c81a800b208@66d2e7d9
jshell> Optional.<String>of("cs2030").filter(predO). // Optional<String>
...> orElseGet(supplier)
$.. ==> "cs1010"
jshell> Optional.<Object>of("cs2030").filter(predO). // Optional<Object>
...> orElseGet(supplier)
$.. ==> "cs1010"
In Summary,
In an ideal world, if
Object >: String, you might expectPredicate<Object> >: Predicate<String>to follow automatically. But generics are invariant by default — the compiler treatsPredicate<Object>andPredicate<String>as completely unrelated types, even though there’s a clear relationship betweenObjectandString.So
? superand? extendsare essentially you manually telling the compiler “trust me, this relationship exists and it’s safe”:
? super T— you’re saying “I know any supertype of T works here, allow it”? extends T— you’re saying “I know any subtype of T works here, allow it”
PECS: Producer Extends, Consumer Super
Function<T, R> as a Consumer and Producer
Circle <: Shape (Circle is a Shape) Double <: Number (Double is a Number) Function<Shape, Double>
interface Optional<T> { // <T> declared with class scope
...
// <R> declared with method scope
<R> Optional<R> map(Function<? super T,? extends R> mapper)
...
jshell> Function<Shape, Double> f = shape -> shape.getArea()
f ==> $Lambda..
// Static method (.of) with object creation (new)
jshell> Optional<Circle> oc = Optional.<Circle>of(new Circle(1.0))
oc ==> Optional[Circle with radius 1.0]
//
jshell> Function<Shape, Double> f = shape -> shape.getArea()
f ==> $Lambda..
jshell> Optional<Number> on = oc.map(f)
$.. ==> Optional[3.141592653589793]
Breaking Encapsulation
Imperatively: Telling the computer how to do something step by step
// imperative - step by step instructions
if (occ.isEmpty()) {
return 0;
} else {
Circle cc = occ.get();
// do stuff with cc...
}
Declaratively: Telling the computer what you want directly
return occ.map(cc -> points
.filter(pt -> cc.contains(pt))
.map(pt -> 1)
.reduce(0, (x, y) -> x + y))
.orElse(0);
Think of it like the burger analogy — you shouldn’t reach into the kitchen yourself, you should let the chef handle it through the proper interface.
This links back to the “tell don’t ask” principle in OOP. Can be error prone and defeats the purpose of Optional.
occ.get() // crashes if empty, YOU have to remember to check
occ.map(...).orElse(0) // impossible to forget, handled automatically
List<E>
Consumer
void writeList(List<? super Integer> list) {
list.add(1); // putting data INTO the list
}
Producer
void readList(List<? extends Number> list) {
Number n = list.get(0); // getting data OUT of the list
}
Natural Ordering (no Comparator needed)
// Integers - naturally ordered numerically
Stream.of(5, 2, 8, 1, 9, 3)
.sorted()
.toList();
// Result: [1, 2, 3, 5, 8, 9]
// Strings - naturally ordered alphabetically
Stream.of("banana", "apple", "cherry", "date")
.sorted()
.toList();
// Result: [apple, banana, cherry, date]
Custom Ordering with Comparator (Class)
// Defined separately
class SortByLength implements Comparator<String> {
@Override
public int compare(String x, String y) {
return x.length() - y.length();
}
}
// Used later
SortByLength byLength = new SortByLength();
Stream.of("banana", "apple", "cherry", "date")
.sorted(byLength)
.toList();
// Result: [date, apple, banana, cherry]
Custom Ordering with Comparator (Variable)
// Defined separately as a Comparator variable
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String x, String y) {
return x.length() - y.length();
}
};
// Used later
Stream.of("banana", "apple", "cherry", "date")
.sorted(byLength)
.toList();
// Result: [date, apple, banana, cherry]
Custom Ordering with Comparator (Lambda)
A Comparator is an object that defines a custom ordering for other objects.
interface Comparator<T> {
int compare(T o1, T o2);
...
}
// Strings sorted by LENGTH instead of alphabetically
Stream.of("banana", "apple", "cherry", "date")
.sorted((x, y) -> x.length() - y.length())
.toList();
// Result: [date, apple, banana, cherry]
// Integers sorted in REVERSE order
Stream.of(5, 2, 8, 1, 9, 3)
.sorted((x, y) -> y - x) // These are the comparators written in Lambda
.toList();
// Result: [9, 8, 5, 3, 2, 1]
Useful Built-in Methods
Java’s Comparator interface also comes with helpful static and default methods:
Comparator.naturalOrder()— uses the object’s natural orderingComparator.reverseOrder()— reverses the natural orderingComparator.comparing(keyExtractor)— builds a comparator from a field, e.g.Comparator.comparing(Person::getName).thenComparing(...)— chains comparators for tie-breaking.reversed()— flips any comparator
Comparable Interface
A natural ordering can be defined.
Natural ordering: alphabetically compareTo(other) compares this object with other; returns – < 0 if this object is less than other; – > 0 if other is less than this object; or – 0 otherwise
Stream.of("one", "two", "three", "four")
.sorted()
.toList();
// Result: [four, one, three, two]
Overriding the natural ordering of compareTo
class Person implements Comparable<Person> {
String name;
int age;
@Override
public int compareTo(Person other) {
return this.age - other.age; // natural ordering = by age
}
}
Stream.of(new Person("Alice", 30), new Person("Bob", 25))
.sorted() // no Comparator needed!
.toList();
// Result: [Bob(25), Alice(30)]