The main advantage of using empty collections or "empty" actions instead of zero is that most of the time such objects will continue to work in code without further modification. The null value, in essence, is simply much more error prone due to its nature.
Take the following code, for example:
String[] names = data.getNames(); if (names != null) { for (String name : names) {
null check required or you will get NPE. Using a standard for a loop does not solve the problem. On the other hand, if you know that you will always have some kind of array, your code will work fine, without additional checks. The loop will not start at all if the array is empty. The problem is resolved.
The same is true for code that implements some form of action. Another example:
Action myAction = data.getActionToRun(); if (myAction != null) { myAction.run(); }
Once again you need a null check. If the action is guaranteed, then you can always call action.run() without side effects, but empty actions just won't do anything. It's simple.
In many cases, null checks can simply be discarded if you change the methods return methods, which will lead to a significantly simplified and understandable code. In some cases, returning null is the right choice (for example, getting an object from a collection of keys and values), since there is no default "inaction" value. But null indicates no value at all, and this requires additional processing. Using empty, inactive, non-zero objects allows you to handle the error with a data object. This is a good encapsulation. This is good programming. It just works. β’
Finally, returning null is certainly not a good way to handle errors. If something goes wrong in code that will never be wrong, if you, as a programmer, did not make a programming error, use assert or exceptions. These are failures. Do not use null as a case of failure, use it as a simple lack of value.