I think you've exposed a more important question behind the original one: should you work to prevent exceptional cases in getters and setters?
The answer is yes, you should work to avoid trivial exceptions.
What you have here is effectively lazy instantiation that is not at all motivated in this example:
In computer programming, lazy initialization is the tactic of delaying
the creation of an object, the calculation of a value, or some other
expensive process until the first time it is needed.
You have two problems in this example:
Your example is not thread safe. That check for null can succeed (i.e., find that the object is null) on two threads at the same time. Your instantiation then creates two different lists of strings. Non-deterministic behavior will ensue.
There is no good reason in this example to defer the instantiation. It's not an expensive or complicated operation. This is what I mean by "work to avoid trivial exceptions": it is worth investing the cycles to create a useful (but empty) list to ensure that you aren't throwing delayed detonation null pointer exceptions around.
Remember, when you inflict an exception on outside code, you're basically hoping that developers know how to do something sensible with it. You're also hoping that there isn't a third developer in the equation whose wrapped everything in an exception eater, just to catch and ignore exceptions like yours:
try {
// I don't understand why this throws an exception. Ignore.
t.getStrings();
} catch (Exception e) {
// Ignore and hope things are fine.
}
In the example below, I am using the Null Object pattern to indicate to future code that your example hasn't been set. However, the Null Object runs as non-exceptional code and, therefore, doesn't have the overhead and impact on the workflow of future developers.
public class App {
static class Test {
// Empty list
public static final List<String> NULL_OBJECT = new ArrayList<String>();
private List<String> strings = NULL_OBJECT;
public synchronized List<String> getStrings() {
return strings;
}
public synchronized void setStrings(List<String> strings) {
this.strings = strings;
}
}
public static void main(String[] args) {
Test t = new Test();
List<String> s = t.getStrings();
if (s == Test.NULL_OBJECT) {
// Do whatever is appropriate without worrying about exception
// handling.
// A possible example below:
s = new ArrayList<String>();
t.setStrings(s);
}
// At this point, s is a useful reference and
// t is in a consistent state.
}
}