The Motivation
Until now, your time in Java has dealt with named classes. A class has a name; you use it to declare variables, and to create instances.
But it turns out Java supports more than just named classes. Sometimes, a class can have so many variants, that it becomes infeasible to try and name them all; furthermore, all these named classes would clog up your codebase, making it difficult to find where you put the right one.
When it makes more sense to simply define a full class right where it's used in the code, and it doesn't make sense to share that class with the world, Java grants you the ability to create an anonymous class.
Framing the Example
To keep topical with the times in which this article is written, I'm going to use the example of a lab struggling to keep up with processing COVID-19 tests. Up until now, the lab has been dealing with samples one-by-one, but no longer! A new batch tester has been shipped in, capable of processing dozens of tests in one pass! DOZENS!
Let's take a look at the internals of the code driving our batch testing machine.
Patient Samples
This simple class is just a data payload with a couple of simple wrappers. It tells us who our patient is, and what was measured from the samples they gave us.
Immunity Test (Interface)
This interface describes classes that can test for immunity for some disease. Given a sample, it will return true
if the patient is immune, and false
if not.
See that @FunctionalInterface
annotation? That's a formality, but a helpful one. I won't get into its meaning in this article, but I'd recommend starting your own research into it if it the mystery is compelling to you. :)
Note for the curious: These types of interfaces - taking arguments and returning booleans - are known as predicates, and you can find them under that name in the Java core libraries.
COVID-19 Immunity Test
This is a concrete class, implementing our ImmunityTest
interface. Breaking it down simply, a patient is considered immune if:
- They have T-Cell memory for other coronaviruses, or
- They have the required antibodies at a certain density in their bloodstream.
Author's caveat: do not take this as a realistic test. This is entirely fictitious for example purposes only. :)
Batch Testing Machine
Finally, the machine itself! It walks through a batch - here encoded as a List
of samples - applying the provided test for each one, and appending a corresponding result to an output list.
Path A - Using our Named Class
We start with this initial version of our program. In it, we load up our batch of patients, set up the machine, create the test we want to use for our samples, process the batch, and print out the results to be delivered to the relieved (or mortified!) patients.
Look at this code, and now consider: What would we need to change if we wanted to test for a new disease? Malaria 2.0 is scheduled to arrive next year, and it'll be a doozy. Do we make a new class, Malaria2ImmunityTest
? How many disease test classes will we need to create, and where do we put them all?
Let's see if we can't give ourselves a different way to accomplish the same thing.
Path B - Using an Anonymous Class
This version of our program is very similar, except for one thing: we've changed how we create our test. We've eschewed the use of our named class, and instead appear to be creating something new right in the middle of our code.
Curiously, we're claiming to create a new instance of our interface, ImmunityTest
. "But that can't be right," I can hear you object; "You can't create instances of interfaces! That's the whole point!" And to that, I'd say you are absolutely correct. So what exactly are we doing here?
When we say new ImmunityTest { ... }
, we are not making a new instance of ImmunityTest
itself: we are defining an anonymous class, a class with no name that implements ImmunityTest
, just as our original named class did. We then proceed to implement the internals of the class right here and now, inside the braces:
@Override
public boolean isImmune(PatientSample sample){
return sample.hasTCellMemory() || sample.hasAntibodyDensityOver(0.2);
}
To confirm for yourself that this is doing the same thing, compare this code to the code in Covid19ImmunityTest
. You'll see they are identical in every way!
The last thing this form does is to create a single instance of this new, anonymous class. All that work was done just to get us an actual object that we could use.
And that's it! An anonymous class is just a class you define and use once, in-line; no more, no less.
Note for the curious: You can extend classes in the same way that we implemented an interface here. By using braces instead of parentheses (e.g.
new MyClass { ... }
), you are creating an anonymous class, not creating an object.
Path C - Using a Lambda
Our ImmunityTest
interface has a special property: there is exactly one method that its implementer needs to define, isImmune
. This means we can take our anonymous class creation one step further, using the delicious syntax sugar known as lambdas.
Don't fear; lambdas aren't a dirty word used by functional programmers only. In Java's case, they are simply a way of removing all the extra cruft that the anonymous class syntax creates.
Again, very little has changed, except for one key difference: our test has been compacted down to a simple form. This is a lambda definition, and it describes the exact same anonymous class we just created. Let's go through it piece by piece:
-
(PatientSample sample)
- This is the parameter to our lambda. Note that it looks just like the parameter list of our anonymous class'isImmune
definition! -
->
- This lets Java know you're creating a lambda. What follows is what is returned from the method you're describing (isImmune
, here). -
sample.has [...] ;
- This is the body of our function, the same as it was in the anonymous class, except for one thing: becauseisImmune
is known to return a boolean, we don't need areturn
statement: the results of this expression are already assumed to be the return value!
Take a few minutes to look at this lambda and its equivalent anonymous class side-by-side. One is simply a much more compact syntax sugar for the other.
Questions for Consideration
There's a lot that's not covered here, but let me leave you with these questions to think about and research:
- Are there cases where I can use anonymous classes, but not lambdas?
- Hint: What if I need to implement more than one method? What if I'm extending a class, not implementing an interface?)
- Why does the simpler syntax commented in
PathC
work? Can you assure yourself it's the same code? - Can anonymous classes and lambdas see and use other variables in the code around them? What are the restrictions?
- When would you make the decision to use an anonymous class instead of a named one? When would you decide to use a named class over an anonymous one?
Conclusion
Java gives a lot of useful tools in its toolkit that can reduce the weight of your codebase. As with all tools, learn to use them wisely: too much of a good thing can make code obtuse, and make its purpose unclear.
Names are an important part of a communicative codebase. But the burden of naming many highly similar things can become a problem of its own, and can interfere with comprehension. Choose which tool best communicates intent; and if you can't decide, make a choice, and learn from what follows.
Top comments (0)