Collections and Encapsulation in Java
A core tenant of object-oriented programming is encapsulation.
Join the DZone community and get the full member experience.
Join For FreeA core tenant of object-oriented programming is encapsulation: callers should not be allowed to directly access fields of a class. This is something that newer languages, including Kotlin, Swift, and Ceylon, have solved well with first-class properties.
Java does not have the concept of first-class properties. Instead, the JavaBeans spec was introduced as Java’s method of enforcing encapsulation. Writing JavaBeans means that you need to make your class’s fields private, exposing them only via getter and setter methods.
If you’re like me, you’ve often felt when writing JavaBeans that you were writing a bunch of theoretical boilerplates that rarely served any practical purpose. Most of my JavaBeans have consisted of private fields and their corresponding getters and setters that do nothing more than, well, get and set those private fields. More than once, I’ve been tempted to simply make the fields public and dispense with the getter/setter fanfare, at least until a stern warning from the IDE sent me back, tail between my legs, to the JavaBeans standard.
Recently, though, I’ve realized that encapsulation and the JavaBean/getter/setter pattern are quite useful in a common scenario: collection-type fields. How so? Let’s fabricate a simple class:
public class MyClass {
private List<String> myStrings;
}
We have a field — a List of Strings — call myStrings
, which is encapsulated in MyClass
. Now, we need to provide accessor methods:
public class MyClass {
private List<String> myStrings;
public void setMyStrings(List<String> s) {
this.myStrings = s;
}
public List<String> getMyStrings() {
return this.myStrings;
}
}
Here, we have a properly-encapsulated — if not verbose — class. So we’ve done good, right? Hold that thought.
Optional lessons
Consider the Optional class, introduced in Java 8. If you’ve done much work with Optionals, you’ve probably heard the mantra that you should never return null
from a method that returns an Optional. Why? Consider the following contrived example:
public class Foo {
private String bar;
public Optional<String> getBar() {
return (bar == null) ? null : Optional.of(bar);
}
}
Now, clients can use the method thusly:
foo.getBar().ifPresent(log::info);
and risk throwing a NullPointerException
. Alternatively, they could perform a null
check:
if (foo.getBar() != null) {
foo.getBar().ifPresent(log::info);
}
Of course, doing that defeats the very purpose of Optionals. In fact, it so defeats the purpose of Optionals that it’s become standard practice that any API that returns Optional will never return a null
value.
Back to Collections. Much like an Optional contains either none or one, a Collection contains either none or some. And much like Optionals, there should be no reason to return null
Collections (except maybe in rare, specialized cases, of which I can’t currently think of any). Simply return an empty (zero-sized) Collection to indicate the lack of any elements.
It’s for this reason that it’s becoming more common to ensure that methods that return Collection types (including arrays) never return null
values, the same as methods that return Optional types. Perhaps you or your organization have already adopted this rule in writing new code. If not, you should. After all, would you (or your clients) rather do this?:
boolean isUnique = personDao.getPersonsByName(name).size() == 1;
Or, have your code littered with the likes of this? :
List<Person> persons = personDao.getPersonsByName(name);
boolean isUnique = (persons == null) ? false :persons.size() == 1;
So how does this relate to encapsulation?
Keeping Control of Our Collections
Back to our MyClass
class. As it is, an instance of MyClass
could easily return null
from the getMyStrings()
method; in fact, a fresh instance would do just that. So, to adhere to our new never-return-a-null-Collection guideline, we need to fix that:
public class MyClass {
private List<String> myStrings = new ArrayList<>();
public void setMyStrings(List<String> s) {
this.myStrings = s;
}
public List<String> getMyStrings() {
return this.myStrings;
}
}
Problem solved? Not exactly. Any client could call aMyClass.setMyStrings(null)
, in which case we’re back to square one.
At this point, encapsulation sounds like a practical — rather than solely theoretical — concept. Let’s expand the setMyStrings()
method:
public void setMyStrings(List<String> s) {
if (s == null) {
this.myStrings.clear();
} else {
this.myStrings = s;
}
}
Now, even when null
is passed to the setter, myStrings
will retain a valid reference (in the example here, we take null
to mean that the elements should be cleared out, a reasonable assumption). And, of course, calling aMyClass.getMyStrings() = null
will have no effect on MyClass
’ underlying myStrings
variable. So are we all done?
Er, well, sort of. We could stop here. But really, there’s more we should do.
Consider that we are replacing our private ArrayList
with the List
passed to us by the caller. This has two problems: first, we no longer know the exact List
implementation used by myStrings
. In theory, this shouldn't be a problem, right? Well, consider this:
myClass.setMyStrings(Collections.unmodifiableList("Heh, gotcha!"));
So, if we ever update MyClass
such that it attempts to modify the contents of myStrings
, bad things can start happening at runtime.
The second problem is that the caller retains a reference to our underlying List
. So now, that caller can now directly manipulate our List
.
What we should be doing is store the elements passed to us in the ArrayList
to which myStrings
was initialized. While we're at it, let’s really embrace encapsulation. We should be hiding the internals of our class from outside callers. The reality is that callers of our classes shouldn’t care whether there’s an underlying List, Set, array, or some runtime dynamic code-generation voodoo, that’s storing the Strings that we pass to it. All they should know is that Strings are being stored somehow. So let’s update the setMyStrings()
method thusly:
public void setMyStrings(Collection<String> s) {
this.myStrings.clear();
if (s != null) {
this.myStrings.addAll(s);
}
}
This has the effect of ensuring that myStrings
ends up with the same elements contained within the input parameter (or is empty if null is passed), while ensuring that the caller doesn't have a reference to myStrings
.
Now that myStrings
' reference can't be changed, let’s just make it a constant:
public class MyClass {
private final List<String> myStrings = new ArrayList<>();
...
}
While we’re at it, we shouldn’t be returning our underlying List via our getter. That too would leave the caller with a direct reference to myStrings. To remedy this, recall the "defensive copy" mantra that Effective Java beat into our heads (or, at least, should have):
public List<String> getMyStrings() {
// depending on what, exactly, we want to return
return new ArrayList<>(this.myStrings);
}
At this point, we have a well-encapsulated class that eliminates the need for null
-checking whenever its getter is called. We have, however, taken some control away from our clients. Since they no longer have direct access to our underlying List, they can no longer, say, add or remove individual Strings.
No problem. If we can simply add methods like
public void addString(String s) {
this.myStrings.add(s);
}
and
public void removeString(String s) {
this.myStrings.remove(s);
}
Might our callers need to add multiple Strings at once to a MyClass
instance? That’s fine as well:
public void addStrings(Collection<String> c) {
if (c != null) {
this.myStrings.addAll(c);
}
}
And so on...
public void clearStrings() {
this.myStrings.clear();
}
public void replaceStrings(Collection<String> c) {
clearStrings();
addStrings(c);
}
Collecting Our Thoughts
Here is what our class might ultimately look like:
public class MyClass {
private final List<String> myStrings = new ArrayList<>();
public void setMyStrings(Collection<String> s) {
this.myStrings.clear();
if (s != null) {
this.myStrings.addAll(s);
}
}
public List<String> getMyStrings() {
return new ArrayList<>(this.myStrings);
}
public void addString(String s) {
this.myStrings.add(s);
}
public void removeString(String s) {
this.myStrings.remove(s);
}
// And maybe a few more helpful methods...
}
With this, we've achieved a class that:
- is still basically a POJO that conforms to the JavaBean spec
- fully encapsulates its private member(s)
And, most importantly, it ensures that its method that returns a Collection always does just that — returns a Collection and never returns null
.
Opinions expressed by DZone contributors are their own.
Comments