
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:
- Explain why readability is the primary goal of good code design
- Describe how treating functions as objects enables more readable code
- Read and write Java lambdas, method references, and records
- Choose between lambdas, method references, and named methods based on readability
- 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

Text espertus to 22333 if the
URL isn't working for you.
Poll: How come?
Remember to state the language.

Text espertus to 22333 if the
URL isn't working for you.
Where Does Readability Fit in the Design Process?

Today: Making code readable during Implementation to reduce costs in Operations and when developing new features
Most Software Cost Is Maintenance, Not Development

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.

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
- Lambdas: Express behavior without boilerplate classes
- Records: Define data classes without boilerplate methods
- Pattern matching: Eliminate boilerplate with variables
- Good naming: Make intent clear even when syntax is perfect
The Two Hard Problems in Computer Science

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

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

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
| Parameters | Syntax | Example |
|---|---|---|
| Zero | () -> body | () -> System.out.println("Hi") |
| One | x -> body | s -> 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:
returnis implicit - Multiple expressions: block body with braces and explicit
return
- Single expression:
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 methodClassName::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:
| Interface | Method | Description |
|---|---|---|
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:
| Generic | Primitive Version | Signature |
|---|---|---|
Predicate<Integer> | IntPredicate | int → boolean |
Function<T, Integer> | ToIntFunction<T> | T → int |
Function<Integer, R> | IntFunction<R> | int → R |
Consumer<Integer> | IntConsumer | int → 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:
| Situation | Best Choice | Why |
|---|---|---|
| Simple, obvious behavior | Method reference | Maximum clarity |
| Need parameter names for clarity | Lambda | Names document intent |
| Complex logic (>3 lines) | Named method | Can be documented |
| Reused in multiple places | Named method | Single 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 isfinal
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

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

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:
- Select concepts to include in the name
- Choose words to represent each concept (consistently!)
- Construct the name following codebase conventions
The Name Mold Method: Building Good Names

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)

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)

The Reader's Journey: Maze or Path?

Both paths lead to understanding. One respects your reader's time.
Summary: Modern Java Tools for Readable Code
- Readability is the goal — code is read far more than written
- Functions as objects — store, pass, and return behavior
- Lambdas reveal intent — express behavior without boilerplate
- Records eliminate boilerplate — define data classes concisely
- Pattern matching reduces ceremony — combine type checks and casts
- Choose based on readability — not just conciseness
- 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):
- Prefer lambdas to anonymous classes (EJ Item 42)
- Prefer method references to lambdas (EJ Item 43)
- Favor standard functional interfaces (EJ Item 44)
- On Naming (The Programmer's Brain, Ch. 8)
Bonus Cartoon
