From the point of view of encapsulation, I normally consider that the best approach is the following:
public class SuaClasse {
// Embora AlgumaCoisa seja imutável, a lista em si é mutável.
private List<AlgumaCoisa> algumaLista;
public SuaClasse() {
// ...
}
public List<AlgumaCoisa> getAlgumaLista() {
return List.copyOf(algumaLista);
}
public void adicionarItem(AlgumaCoisa adicionando) {
this.algumaLista.add(adicionando);
}
}
The method List.copyOf(Collection<E>)
added in Java 10 and creates an immutable copy of a given list. If the given list is modified in the future, the copy will not be changed.
In older versions of Java, I used this (in this case, Java 7):
public List<AlgumaCoisa> getAlgumaLista() {
return Collections.unmodifiableList(new ArrayList<>(algumaLista));
}
Anyway, the internal list never is directly exposed to any other class that wants to access it. This ensures encapsulation. If any external class is interested in modifying the contents of that instance list x
, use any of the methods described by x
, which justifies the fact that the returned list is immutable.
It is also justified to be a copy because subsequent changes through the method adicionarItem
should not mysteriously be reflected in lists that although have been obtained previously from the instance x
, are in codes no longer interested in x
proper.
And even if a copy is returned, any attempt to modify that copy directly is at least one code Smell or a decoy for bugs. So, even though it’s a copy, it still has to be immutable.
Use Iterator
directly is difficult. The Iterator
has limitations of not being able to be iterated more than once (unless you copy all the items in a list, but then it would be easier to already give this list previously built) and also has limitation of not being able to determine the size of the list. In addition, the Iterator
is something quite outdated, because now exists the Stream
.
There are some scenarios where it makes sense to return a Stream
instead of a list. An example is when one is going through the records of a database or a file line by line. In that case, you do it:
public Stream<AlgumaCoisa> {
// Faz alguma coisa.
}
But this is only justified when it is not desired that all items are pre-prepared in memory because of performance or memory savings. If this is not the case, the list is returned.
Finally, one can still be concerned that the getter may get heavy to run, because whenever it is called, a new copy of the list will be made even if it is identical to the copy produced in the previous call to the method. So you can do that:
public class SuaClasse {
// Embora AlgumaCoisa seja imutável, a lista em si é mutável.
private List<AlgumaCoisa> algumaLista;
private List<AlgumaCoisa> algumaListaImutavel;
public SuaClasse() {
// ...
}
public List<AlgumaCoisa> getAlgumaLista() {
if (algumaListaImutavel == null) algumaListaImutavel = List.copyOf(algumaLista);
return algumaListaImutavel;
}
public void adicionarItem(AlgumaCoisa adicionando) {
this.algumaListaImutavel = null;
this.algumaLista.add(adicionando);
}
}
Related: https://www.martinfowler.com/bliki/EncapsulatedCollection.html
– Piovezan