I spent the evening shaving old code with fresh lambdas and it felt good.
There is a special kind of joy in taking a brittle util class or a forest of anonymous classes and swapping them for clean lambda expressions. The code stops yelling and starts whispering. Today lambdas are everywhere. Java 8 finally landed on most servers, C# has had them for a while with LINQ, and the web crowd keeps writing JavaScript arrow functions like there is no tomorrow. I have been refactoring a batch of legacy jobs and I want to share what worked, what did not, and how to keep it readable for the next person. This is not about chasing clever tricks. It is about taking old code that tries to say a thing and letting it say that thing in fewer lines with better intent.
The first target is the classic Java anonymous class that only exists to implement a single method. These pile up in callbacks and comparators, and they are perfect for Java 8 lambdas. Here is a tiny example from a sorting utility that used to sit in every other project I touched:
// Before
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
});
// After
people.sort((a, b) -> a.getLastName().compareTo(b.getLastName()));That is the easy win. The signature is clear and the intent jumps out. If I want a reverse sort I can flip it with Comparator.reverseOrder on the key or just swap operands. When the body is longer than a single expression I avoid cramming it into one line. A lambda is not a dare. Give multi step logic a proper block with braces and keep naming honest. One more tip. Replace custom callback interfaces with java.util.function types like Predicate, Function, and Consumer where it makes sense. Fewer custom types means fewer concept hoops for new readers, and the entire JDK already speaks those interfaces.
Lambdas also shine with streams, but streams are a tool not a lifestyle. Here is a small refactor I used on a data loader that filtered and mapped items before storing them. The original did two loops and a temp list. The stream version reads like a pipeline and leaves fewer places to mess up:
// Before
List<UserDto> out = new ArrayList<>();
for (User u : users) {
if (u.isActive()) {
out.add(UserDto.from(u));
}
}
Collections.sort(out, new UserDto.ByName());
saveAll(out);
// After
List<UserDto> out =
users.stream()
.filter(User::isActive)
.map(UserDto::from)
.sorted(Comparator.comparing(UserDto::name))
.collect(Collectors.toList());
saveAll(out);This reads top to bottom. Filter a thing, map it, sort it, collect it. I still watch for long pipelines that turn into a riddle. If a step needs explanation, I extract a method with a strong name, then plug it in as a method reference. That keeps the stream tidy while giving room to comment the tricky part. Performance is fine for most use cases. If you are pushing millions of items then profile before and after. And if you are on Android today, native Java 8 is still rolling out, so tools like Retrolambda can help, but keep your eye on the toolchain and the support libraries.
On the C sharp side, lambdas pair with LINQ like espresso with a good biscuit. Some codebases still do loops and manual filters where a simple query reads better. I took this block that walked a list of orders and pushed discounted ones into another list:
// Before
var discounted = new List<Order>();
foreach (var o in orders) {
if (o.IsPaid && o.Total > 50) {
discounted.Add(o);
}
}
discounted.Sort((a, b) => a.Customer.CompareTo(b.Customer));
// After
var discounted =
orders
.Where(o => o.IsPaid && o.Total > 50)
.OrderBy(o => o.Customer)
.ToList();Shorter is not the only win. The words match the intention. Where, then order by, then to list. Event handlers also get cleaner with lambdas. Just remember to keep a reference if you need to unsubscribe later. Inline lambdas without a reference are easy to add and hard to remove.
JavaScript arrow functions are the friend that shows up with pizza and then helps you move a couch. They clean up callbacks and bring a lexical this. That saves me from the old var self trick. Still, I avoid compressing logic until it looks like math homework. A small example from a fetch plus render flow:
// Before
api.get('/posts', function(err, posts) {
if (err) return showError(err);
var featured = posts.filter(function(p) { return p.featured; })
.map(function(p) { return toCard(p); });
render(featured);
});
// After
api.get('/posts', (err, posts) => {
if (err) return showError(err);
const featured = posts
.filter(p => p.featured)
.map(toCard);
render(featured);
});That is easier on the eyes and keeps this stable if the callback touches instance state. When the mapping step grows teeth, I pull it into a named function and keep the arrow call site neat. Names beat comments in most of these cases.
Refactoring with lambdas is a mood, but it is also a discipline. I take tiny steps, run tests, and push small commits with clear messages like refactor comparator to lambda or replace loop with stream. If a change reduces lines but hurts clarity I roll it back. The goal is readability with fewer moving parts. I also watch for swallowed exceptions in old anonymous classes. Lambdas make it simple to forget that a call can blow up, so I either wrap and rethrow with context or keep a small helper that logs and returns a default. Do not hide failure with empty lambdas.
One more practical detail. When you replace custom callbacks, update your tests to express intent with predicates and suppliers. A short example in Java that turned a bespoke filter into a standard predicate made the test itself smaller and clearer:
// Before
ActiveUserFilter f = new ActiveUserFilter(clock);
assertTrue(f.accept(user));
// After
Predicate<User> isActive = u -> u.isActive(clock.now());
assertTrue(isActive.test(user));If your team is curious about Kotlin or other languages that treat functions like first class citizens, lambdas will make the switch smoother. Even if you stay where you are, learning to think in terms of small functions that do one thing well pays off right away. The big win is not clever syntax. The big win is code that reads like the story you meant to tell, trimmed of ceremony, friendly to tests, and easy to change next week when the product surprises you.
Refactor for intent, not for sport.
Posted at 02:42 while the city sleeps.