Encapsulation is one of the tenets of object-oriented programming. It states that objects should hide their inner workings from external view. Good encapsulation increases code modularity by preventing objects from interacting with each other in unexpected ways, which in turn makes future development efforts and refactoring easier.
Getters and setters
The first step toward building objects with good encapsulation is to have the getter/setter pair access private data fields. Requiring other objects that want to read and write fields in your object to do so through a mechanism you control allows you to enforce legal values and general internal data consistency. If you have a field named duration that is expected to hold a positive integer, you can throw an IllegalArgumentException when another object tries to setDuration(-4). If you’ve left your duration member public, you can’t prevent someone from calling yourObject.duration=-4 and messing up the internal consistency of your data.
Immutability of return values
The data that your object makes available through method return values should not be modifiable in a way that alters the internal state of your object. Returning primitives provides no worry in this regard. When you return a Char, others can alter the Char they received without modifying the Char, as stored in your application, because Java uses pass by value to return primitives.
However, when returning objects, you must be careful to monitor their mutability. When an object’s value cannot be altered after instantiation, it is said to be immutable. Many of the objects in the java.lang package hierarchy are immutable, including String, Char, and Short.
Making an object immutable requires three steps:
- · Data members must be private to prevent their direct manipulation by outside code.
- · There must be no setter methods provided—disallowing the alteration of the encapsulated data is the whole point.
- · The class itself must be final. Declaring the class as final prevents another class from creating a mutable subclass of your immutable class.
Returning mutable objects allows code outside your object to modify data internal to your object without warning or possible intervention. For example, say you have a Letter object representing an office letter, and it contains a Recipient object that contains an address. If that Recipient object is mutable, any object that has called the getRecipient method in the past can modify the recipient of the letter—without requiring a setRecipient method—simply by altering the data stored inside the returned Recipient object.
Returning collections of objects can make maintaining good encapsulation even more problematic. Whether you use the legacy Hashtable and Vector objects or their replacements in the Collections API, you need to keep in mind the level of mutability of the collections themselves and their contents.
The first step in achieving good encapsulation with returned collections is to make sure that the collection itself is either immutable or harmlessly mutable—that is, it doesn’t alter the contents of the object. The easiest, though not necessarily the most efficient, way to do this is to return the .clone() of any collections. If your class holds a HashSet of String object, returning theSet.clone() instead of theSet itself will prevent the recipient of that collection from altering the contents of the collection named theSet within your object.
The cloning solution is sufficient when your collection contains immutable objects like Strings, but in the case where the collection to be returned contains mutable objects, you’ll need to do a deep clone. Deep cloning is a process in which all of the contained objects are cloned along with the collection itself. Using a deep clone prevents the recipient from altering any data stored inside your object, but it can mean a significant memory and CPU resource drain.
Return immutable iterators
Returning an Iterator that produces the contents of a collection to be returned is another way to provide immutability. Iterators have a small set of provided methods and only one method that can alter the underlying source data. Furthermore, it’s easy to get an Iterator for any collection found in the collections API.
Making an Iterator immutable is a simple matter of disabling the remove method. This can be easily done at the time of Iterator instantiation using an anonymous inner subclass of the Iterator returned by the source collection. You can see an example of this in Listing A.
It should be noted, however, that this method is suitable only when the objects inside the source collection are immutable. When they’re mutable, you're stuck with the deep clone or a loss of encapsulation. Another concern is the dreaded ConcurrentModificationException thrown by Iterators if the source collection is modified before they have been fully consumed. As there’s no way to control the rate at which the caller will consume the returned Iterator, you have to ensure that source data changes are made only when you're certain that there are no outstanding Iterators to be served.
Is it worth it?
Maintaining good encapsulation has its costs in both the level of care it requires and the memory it consumes. Some Java developers pay no heed to encapsulation at all, and as long as they’re working in a small project and in close contact with other developers, that’s certainly a viable choice. But if you're writing code that is to be used by many developers, such as a library, good encapsulation is a must. Few bugs are as difficult to diagnose as those resulting from inconsistent data within a class—especially when the source of that inconsistency is the unintentional mutability of a class.