Radical Java

Public fields and the case against get()

The traditional Java “bean” methodology says that you should allow access to an object’s data only via getter methods, like the name field only being accessible via a getName() method. The arguments for it look something like this:

  1. Restrict access to limit mutability.
  2. Allow the object to use a different underlying storage mechanism.
  3. Allow substitution with another implementation exposing the same accessors and mutators.

In the vast majority of situations that I see get methods used, none of these is applicable. Let’s take them one by one.

1. Restrict access to limit mutability

This argument states that it’s good to let external clients query the state of an object but not be able to modify it. However, you get exactly the same guarantee using a final field that you do with a get() without a set. The field cannot be set. Additionally, setting the field to final means the object can’t change its own state either, meaning there will be no surprises for the calling code. Of course, there are situations where you do want the state of an object to change, but this is not where I see get methods most overused.

Note here that final does not guarantee immutability of value, only immutability of reference (except for primitives). This means that a final field of type ArrayList<T>, for example, can still change by having elements added or removed from the list. This restriction exists both with get() methods and with public final fields, so isn’t an argument in favour of one or the other.

2. Allow the object to use a different storage mechanism

This argument goes that you want the object to be able to do something other than retrieve the value of a field when a get() method is called. Unfortunately for this argument, after many years of conditioning everyone expects get() to resolve to an instantaneous field access with no side effects. Accessing an underlying database or store or performing more complex logic violates this expectation and might cause problems if the client expects otherwise. May I suggest that retrieveX() or even retrieveXFromDataStore() might be a better choice for naming such a method.

3. Allow substitution with another implementation

This is irrelevant when you’re talking about simple data records. They mean exactly what they say. To substitute a different implementation would be misleading to the user.

What should I do instead?

Whenever you’re working with a plain data record which can be immutable (ideally you should design such records so that they can be immutable), you can simply use public final fields instead of get() methods.

public final class UserInfo {
    public final String userName;
    public final int age;
    public final Country country;

    public UserInfo(String userName, int age, Country country) {
        this.userName = userName;
        this.age = age;
        this.country = country;
    }
}

/* ... */

public class UserInfoClient {
    public long numUsersOver25(Stream<UserInfo> userInfo) {
        return userInfo.filter(u -> u.age > 25).count();
    }
}

This drastically cuts down the amount of code required to implement a simple structured type, and doesn’t really lose you anything. It also gets rid of a level of indirection, which could make your application run faster (although JIT optimization is pretty good these days).

If you’re working in a mutable context, where objects need to change your internal state, consider if you’d be better to implement state changes and queries in terms of higher-level operations, rather than simply getting and setting private data. Indeed, this kind of exposure of internal state representation breaks exactly the kind of encapsulation the accessor principle is supposed to be avoiding in the first place.