Skip to main content
Red carpet timeline: other languages with FP features while Java arrives 40 years late

CS 3100: Program Design and Implementation II

Lecture 5: Functional Programming and Readability

©2025 Jonathan Bell & Ellen Spertus, CC-BY-SA

Learning Objectives

After this lecture, you will be able to:

  1. Explain why readability is the primary goal of good code design
  2. Describe how treating functions as objects enables more readable code
  3. Read and write Java lambdas, method references, and records
  4. Choose between lambdas, method references, and named methods based on readability
  5. Apply evidence-based naming practices to maximize code clarity

Poll: Which language do you like better?

A. Python

B. Java

C. I like them equally

D. I don't know Java well enough yet to have an opinion

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Poll: How come?

Remember to state the language.

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Where Does Readability Fit in the Design Process?

Software Design Process: Requirements → Design → Implementation → Validation → Operations

Today: Making code readable during Implementation to reduce costs in Operations and when developing new features

Most Software Cost Is Maintenance, Not Development

Small development foothill dwarfed by towering maintenance mountain range

Development takes months. Maintenance takes decades (if you are lucky).

Code Must Be Readable to People Who Aren't You

Your future readers may have:

  • Different backgrounds and experience levels
  • Different native languages
  • Different assumptions about "obvious" patterns
  • Never met you to ask questions

The lottery factor: If code is only readable to its author, the project is at risk.

Meme

Readable Code Reveals Intent, Hides Ceremony

When you read code, you want to see what it does, not how Java makes you say it.

Ceremony (noise):

  • Type declarations the compiler could infer
  • Boilerplate method signatures
  • Structural requirements of the language

Intent (signal):

  • What the code actually does
  • The business logic
  • The meaningful choices

Goal: Maximize signal, minimize noise.

Today: Three Tools for More Readable Java

  1. Lambdas: Express behavior without boilerplate classes
  2. Records: Define data classes without boilerplate methods
  3. Pattern matching: Eliminate boilerplate with variables
  4. Good naming: Make intent clear even when syntax is perfect

The Two Hard Problems in Computer Science

T-shirt reading: 'There are two hard things in Computer Science: cache invalidation, naming things, and off-by-one errors

Tools For More Readable Java

  • Lambdas
  • Records and Pattern Matching
  • Good Naming

What If You Could Store a Function in a Variable?

You can store data in variables:

int brightness = 100;
String name = "Living Room Light";
DimmableLight light = new DimmableLight(name, brightness);

What if you could also store behavior?

??? comparison = <<compare two lights by brightness>>;
lights.sort(comparison); // Use the behavior later

Functions as Objects: Store, Pass, Return

Three panels showing functions being stored, passed, and returned like objects

Functional programming: Functions can be stored, passed, and returned like any other value.

Anonymous Classes vs Lambda Expressions

The traditional way to store "behavior" required creating an object:

// Anonymous class: Create an object that holds the behavior
Comparator<Light> comp = new Comparator<Light>() {
@Override
public int compare(Light l1, Light l2) {
return Integer.compare(l1.getBrightness(), l2.getBrightness());
}
};

Lambda expressions let you write just the behavior:

// Lambda: Just write the behavior
Comparator<Light> comp = (l1, l2) ->
Integer.compare(l1.getBrightness(), l2.getBrightness());

The Same Behavior, Four Ways to Write It

Sorting lights by brightness—verbose to concise:

// 0. Named class (separate file or nested class)
class BrightnessComparator implements Comparator<DimmableLight> {
@Override
public int compare(DimmableLight l1, DimmableLight l2) {
return Integer.compare(l1.getBrightness(), l2.getBrightness());
}
}
lights.sort(new BrightnessComparator());

// 1. Anonymous class
lights.sort(new Comparator<DimmableLight>() {
@Override
public int compare(DimmableLight l1, DimmableLight l2) {
return Integer.compare(l1.getBrightness(), l2.getBrightness());
}
});

// 2. Lambda expression (1 line)
lights.sort((l1, l2) -> Integer.compare(l1.getBrightness(), l2.getBrightness()));

// 3. Method reference (most concise)
lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));

Aside: This Is the Strategy Pattern

The anonymous Comparator is an example of the Strategy Pattern:

Strategy Pattern: Define a family of algorithms, encapsulate each one, and make them interchangeable.

Aside: Comparator as Strategy Pattern

The client chooses the strategy at runtime:

Comparator<Light> comparator = userWantsBrightness
? new BrightnessComparator()
: new LocationComparator();
lights.sort(comparator);

Aside: Lambdas Make Strategy Pattern Trivial

Before Java 8 (Strategy classes):

class BrightnessComparator
implements Comparator<Light> {
@Override
public int compare(Light l1, Light l2) {
return Integer.compare(
l1.getBrightness(),
l2.getBrightness());
}
}

lights.sort(new BrightnessComparator());

After Java 8 (Lambdas):

lights.sort((l1, l2) ->
Integer.compare(
l1.getBrightness(),
l2.getBrightness()));

// Or even simpler:
lights.sort(Comparator
.comparingInt(Light::getBrightness));

Recognize the Strategy Pattern. Implement it with lambdas.

Intent vs Ceremony: What You Mean vs What You Write

Two icebergs: pre-Java 8 intent buried under ceremony vs post-Java 8 intent visible

The anonymous class has 6 lines. Only one line expresses intent.

Anatomy of a Lambda Expression

(l1, l2) -> Integer.compare(l1.getBrightness(), l2.getBrightness())

Three parts:

  • (l1, l2)Parameters: The inputs to the function
  • ->Arrow: "maps to" or "produces"
  • Integer.compare(...)Body: The result or action

Types are inferred. You can add them if it helps readability:

(DimmableLight l1, DimmableLight l2) -> 
Integer.compare(l1.getBrightness(), l2.getBrightness())

Lambda Syntax Quick Reference

ParametersSyntaxExample
Zero() -> body() -> System.out.println("Hi")
Onex -> bodys -> s.toUpperCase()
Multiple(x, y) -> body
(a, b) -> {
  int c = a + b;
  return c;
}

Rules:

  • Types are inferred (usually omit them)
  • One parameter: parentheses optional
  • Multiple parameters: parentheses required
  • The body can be a single expression or multiple expressions:
    • Single expression: return is implicit
    • Multiple expressions: block body with braces and explicit return

Method References Point to Existing Behavior

If a lambda just calls one method, use a method reference:

Lambda:

light -> light.getBrightness()

Method reference:

DimmableLight::getBrightness

The :: means "reference to this method":

  • ClassName::staticMethod — static method
  • ClassName::instanceMethod — instance method (called on parameter)

The Full Progression: From Ceremony to Clarity

// Level 1: Anonymous class (ceremony dominates)
lights.sort(new Comparator<DimmableLight>() {
@Override
public int compare(DimmableLight l1, DimmableLight l2) {
return Integer.compare(l1.getBrightness(), l2.getBrightness());
}
});

// Level 2: Lambda (intent visible)
lights.sort((l1, l2) -> Integer.compare(l1.getBrightness(), l2.getBrightness()));

// Level 3: Method reference (pure intent)
lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));

Level 3 reads like English: "Sort by comparing int: brightness"

Lambdas Implement Functional Interfaces

A functional interface has exactly one abstract method:

@FunctionalInterface  // Optional but recommended
public interface Comparator<T> {
int compare(T o1, T o2); // The ONE abstract method
// Other methods are OK:
default Comparator<T> reversed() { ... }
static <T> Comparator<T> naturalOrder() { ... }
}

The lambda's parameters and return type match that method:

// Lambda implements compare(T o1, T o2) -> int
(l1, l2) -> Integer.compare(l1.getBrightness(), l2.getBrightness())

Java Provides Standard Functional Interfaces

Before creating your own, check java.util.function:

InterfaceMethodDescription
Predicate<T>boolean test(T)Test a condition
Function<T, R>R apply(T)Transform T to R
Consumer<T>void accept(T)Do something with T
Supplier<T>T get()Produce a T
Predicate<Light> isBright = light -> light.getBrightness() > 50;
Function<Light, String> getName = Light::getName;
Consumer<Light> turnOn = Light::turnOn;
Supplier<Light> factory = () -> new DimmableLight("default", 100);

Primitive Interfaces Avoid Boxing Overhead

Generic interfaces like Function<Integer, Integer> require boxing. Use primitive specializations:

GenericPrimitive VersionSignature
Predicate<Integer>IntPredicateint → boolean
Function<T, Integer>ToIntFunction<T>T → int
Function<Integer, R>IntFunction<R>int → R
Consumer<Integer>IntConsumerint → void

That's why it's Comparator.comparingInt(), not comparing():

// comparingInt takes ToIntFunction (T → int), avoids boxing
lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));

Lambdas Can Access Surrounding Variables (If Final)

Lambdas share scope with their enclosing method:

int threshold = 50;  // Effectively final
Predicate<Light> isBright = light -> light.getBrightness() > threshold;

But captured variables must be effectively final:

int threshold = 50;
threshold = 60; // Now it's not final!
Predicate<Light> isBright = light -> light.getBrightness() > threshold; // ERROR

"Effectively final" = could have final keyword without changing behavior.

The Right Tool Depends on Readability

More concise isn't always more readable. Choose based on clarity:

SituationBest ChoiceWhy
Simple, obvious behaviorMethod referenceMaximum clarity
Need parameter names for clarityLambdaNames document intent
Complex logic (>3 lines)Named methodCan be documented
Reused in multiple placesNamed methodSingle source of truth

Method Reference When a Method Already Exists

// Good: Method name documents intent
lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));
lights.forEach(Light::turnOn);
strings.stream().map(String::toUpperCase);

Use lambda when parameter names add clarity:

// Lambda is clearer: names explain the roles
map.merge(key, newValue, (oldVal, newVal) -> oldVal + newVal);

// Method reference would hide which is which:
map.merge(key, newValue, Integer::sum); // Which is old? Which is new?

Named Method When Logic Needs Documentation

❌ Lambda is too complex:

data.stream()
.filter(x -> x.getValue() > threshold && x.isActive() &&
!x.getCategory().equals("excluded") &&
x.getDate().isAfter(cutoffDate))
.collect(Collectors.toList());

✓ Named method documents intent:

data.stream()
.filter(this::isEligibleForProcessing) // Clear intent
.collect(Collectors.toList());

/** Returns true if item should be processed based on business rules. */
private boolean isEligibleForProcessing(DataItem x) {
return x.getValue() > threshold
&& x.isActive()
&& !x.getCategory().equals("excluded")
&& x.getDate().isAfter(cutoffDate);
}

Tools For More Readable Java

  • Lambdas
  • Records and Pattern Matching
  • Good Naming

Can we reduce Java boilerplate code?

Before Records: 30 Lines for "I Want x and y"

public final class Point {           // final = can't extend
private final int x; // final = immutable
private final int y;

public Point(int x, int y) { this.x = x; this.y = y; }

public int x() { return x; } // accessors
public int y() { return y; }

@Override public boolean equals(@Nullable Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point other)) return false;
return x == other.x && y == other.y;
}

@Override public int hashCode() { return Objects.hash(x, y); }

@Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}

All of this just to say: "I have an immutable point with x and y."

Records (Java 16, 2021): Data Classes Without Boilerplate

A record is all 30 lines in one:

public record Point(int x, int y) {}

You get automatically:

  • Constructor: new Point(1, 2)
  • Accessors: point.x(), point.y()
  • Correct equals, hashCode, toString
  • Immutability: all fields are final, class is final

Records Can Have Validation and Custom Methods

public record ColorTemperature(int kelvin) {
// Compact constructor for validation
public ColorTemperature {
if (kelvin < 1000 || kelvin > 10000) {
throw new IllegalArgumentException(
"Kelvin must be between 1000 and 10000");
}
}

// Custom methods
public int toMired() {
return 1000000 / kelvin;
}
public static ColorTemperature warm() {
return new ColorTemperature(2700);
}
}

Pattern Matching for instanceof

Traditional approach:

if (obj instanceof DimmableLight) {
DimmableLight light =
(DimmableLight) obj;
light.setBrightness(50);
}

With pattern matching:

if (obj instanceof DimmableLight light) {
// light is already cast!
light.setBrightness(50);
}

Pattern variable is only in scope where it's valid. Use it in equals()!

if (!(obj instanceof DimmableLight light)) return false;
return this.brightness == light.brightness; // light in scope here

Tools For More Readable Java

  • Lambdas
  • Records and Pattern Matching
  • Good Naming

Style Guides Provide Consistency, Not Readability

Style guides (like Google's Java Style Guide) enforce consistent formatting:

  • Indentation (tabs vs spaces)
  • Brace placement
  • Line length limits
  • Import ordering

But consistency ≠ readability. A consistently formatted mess is still a mess.

We use automated formatters (Spotless + google-java-format). Configure once, forget forever.

Style Guides Prevent Fights

Hackles cartoon in which anthropomorphic animals argue about style

All the Concise Syntax Can't Save Bad Names

Lambdas, records, and pattern matching reduce ceremony.
But they can't make unclear code clear.

// Concise but cryptic
lights.sort(Comparator.comparingInt(DimmableLight::getX));
data.stream().filter(d -> d.getA() > d.getB()).map(Thing::doC);
// Concise AND clear
lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));
orders.stream().filter(o -> o.getTotal() > o.getDiscount()).map(Order::ship);

Naming is the foundation. Everything else builds on it.

Future Developers Will Excavate Your Code

Developers as archaeologists excavating code: good names as Rosetta Stone, bad names as hieroglyphics

Your code will outlive your memory of writing it. Name accordingly.

Research Shows: Names Are Navigation Beacons

Studies on programmer cognition reveal that names help developers:

  • Navigate: Find relevant code quickly
  • Understand: Grasp behavior without reading implementation
  • Remember: Build mental models of the codebase

Dror Feitelson describes the Name Mold Method—a systematic approach:

  1. Select concepts to include in the name
  2. Choose words to represent each concept (consistently!)
  3. Construct the name following codebase conventions

The Name Mold Method: Building Good Names

Three-stage assembly line: select concepts, choose words, construct name

Unit Confusion Has Caused Expensive Disasters

When names don't clarify units, mistakes happen:

  • Mars Climate Orbiter (1999): Lost ($327M) — newton-seconds vs pound-force-seconds
  • Gimli Glider (1983): Plane ran out of fuel mid-flight — pounds vs kilograms (no serious injuries)
  • Tokyo Disneyland Space Mountain (2003): Axle broke — metric vs inches (no serious injuries)

When units matter, include them in names:

int colorTemperatureKelvin;  // vs colorTemperatureMired
int delayMillis; // vs delaySeconds
double distanceMeters; // vs distanceFeet

Step 1: Select the Concepts to Include

What ideas does this variable, method, or class represent?

Consider a field storing the color temperature of a light:

  • What we're tracking (color temperature)
  • When it applies (startup? current? target?)
  • Unit of measurement (Kelvins? Is that obvious to everyone?)
// Concepts: "startup" + "color temperature"
int startupColorTemperature;

// Should we include the unit?
int startupColorTemperatureKelvin;

Decision depends on your codebase. If ALL temperatures are in Kelvins, including the unit adds noise.

Step 2: Choose Words Consistently

Same concept = same word EVERYWHERE.

✓ Consistent:

int getColorTemperature();
void setColorTemperature(int ct);
Predicate<Light> filterByColorTemperature;
ColorTemperature defaultColorTemperature;

✗ Inconsistent:

int getColorTemperature();
void setCT(int ct);
Predicate<Light> filterByWhitePoint;
ColorTemp defaultColorTemp;

Mixing synonyms forces readers to verify: "Do these mean the same thing?"

Step 3: Construct the Name Consistently

Assemble words in a consistent order:

Modifier-first:

startupColorTemperature
currentColorTemperature
targetColorTemperature

Modifier-last:

colorTemperatureStartup
colorTemperatureCurrent
colorTemperatureTarget

Name length should match scope:

// Short scope = short name is fine
for (TunableWhiteLight light : lights) {
light.setColorTemperature(2700);
}

// Long-lived field = descriptive name
private final int startupColorTemperatureKelvin;

Loop Variable Names (meme)

Tweet by Carla Notarboot: 'My team had a debate over what the best looping variable name is...i won

Naming in Practice: Before and After

// BEFORE: Cryptic, inconsistent, ambiguous
class LightMgr {
private int ct; // What is ct?
private int lvl; // Level of what?
private boolean stat; // Status? Static?

void proc(int x) { // Process what? What is x?
if (stat) lvl = x;
}

int getTemp() { return ct; } // Temperature? Temporary?
}
// AFTER: Clear, consistent, unambiguous
class DimmableLightController {
private int colorTemperatureKelvin;
private int brightnessPercent;
private boolean isOn;

void setBrightness(int brightnessPercent) {
if (isOn) this.brightnessPercent = brightnessPercent;
}

int getColorTemperatureKelvin() { return colorTemperatureKelvin; }
}

Long Java Names (meme)

Meme showing extremely wide monitor for Java variable names

The Reader's Journey: Maze or Path?

Split panel: tangled maze (bad code, 30 min) vs clear path (good code, 30 sec)

Both paths lead to understanding. One respects your reader's time.

Summary: Modern Java Tools for Readable Code

  1. Readability is the goal — code is read far more than written
  2. Functions as objects — store, pass, and return behavior
  3. Lambdas reveal intent — express behavior without boilerplate
  4. Records eliminate boilerplate — define data classes concisely
  5. Pattern matching reduces ceremony — combine type checks and casts
  6. Choose based on readability — not just conciseness
  7. Naming is the foundation — syntax can't save bad names

Modern Java has powerful tools for readable code. Use them.

Next Steps

⚠️ Assignment 1 due tonight (Thursday, Jan 15)!

Optional readings (from Effective Java and The Programmer's Brain):

Bonus Cartoon

cartoon