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 expect Predicate<Object> >: Predicate<String> to follow automatically. But generics are invariant by default — the compiler treats Predicate<Object> and Predicate<String> as completely unrelated types, even though there’s a clear relationship between Object and String.

So ? super and ? extends are 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 ordering
  • Comparator.reverseOrder() — reverses the natural ordering
  • Comparator.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)]