Liskov Substitution principle is one of the principles of SOLID. I have read this principle some number of times now and have tried to understand it.
Here is what I make out of it,
This principle is related to strong behavioral contract among the hierarchy of classes. The subtypes should be able to be replaced with supertype without violating the contract.
I have read some other articles too and I am a bit lost thinking about this question. Do Collections.unmodifiableXXX()
methods not violate LSP?
An excerpt from the article linked above:
In other words, when using an object through its base class interface, the user knows only the preconditions and postconditions of the base class. Thus, derived objects must not expect such users to obey preconditions that are stronger then those required by the base class
Why I think so?
Before
class SomeClass{
public List<Integer> list(){
return new ArrayList<Integer>(); //this is dumb but works
}
}
After
class SomeClass{
public List<Integer> list(){
return Collections.unmodifiableList(new ArrayList<Integer>()); //change in implementation
}
}
I cannot change the implentation of SomeClass
to return unmodifiable list in future. The compilation will work but if the client somehow tried to alter the List
returned then it would fail at runtime.
Is this why Guava has created separate ImmutableXXX interfaces for collections?
Isn't this a direct violation of LSP or I have totally got it wrong?
I think you are not mixing things here.
From LSP:
LSP refers to subclasses.
List is an interface not a superclass. It specifies a list of methods that a class provides. But the relationship is not coupled as with a parent class. The fact that class A and class B implement the same interface, does not guarantee anything about the behavior of these classes. One implementation could always return true and the other throw an exception or always return false or whatever but both adhere to the interface as they implement the methods of the interface so the caller can call the method on the object.
LSP says that every subclass must obey the same contracts as the superclass. Wether or not this is the case for
Collections.unmodifiableXXX()
thus depends on how this contract reads.The objects returned by
Collections.unmodifiableXXX()
throw an exception if one tries to call any modifying method upon them. For instance, ifadd()
is called, anUnsupportedOperationException
will be thrown.What is the general contract of
add()
? According to the API documentation it is:If this was the full contract, then indeed the unmodifiable variant could not be used in all places where a collection can be used. However, the specification continues and also says that:
This explicitly allows an implementation to have code which does not add the argument of
add
to the collection but results in an exception. Of course this includes the obligation for the client of the collection that they take that (legal) possibility into account.Thus behavioural subtyping (or the LSP) is still fulfilled. But this shows that if one plans to have different behaviours in subclasses that must also be foreseen in the specification of the toplevel class.
Good question, by the way.
I don't believe it's a violation because the contract (i.e. the
List
interface) says that the mutation operations are optional.Yes, I believe you have it correct. Essentially, to fulfill the LSP you have to be able to do anything with a subtype that you could do with the supertype. This is also why the Ellipse/Circle problem comes up with the LSP. If an Ellipse has a
setEccentricity
method, and a Circle is a subclass of Ellipse, and the objects are supposed to be mutable, there is no way that Circle can implement thesetEccentricity
method. Thus, there is something you can do with an Ellipse that you can't do with a Circle, so LSP is violated.† Similarly, there is something you can do with a regularList
that you can't do with one wrapped byCollections.unmodifiableList
, so that's an LSP violation.The problem is that there is something here that we want (an immutable, unmodifiable, read-only list) that is not captured by the type system. In C# you could use
IEnumerable
which captures the idea of a sequence you can iterate over and read from, but not write to. But in Java there is onlyList
, which is often used for a mutable list, but which we would sometimes like to use for an immutable list.Now, some might say that Circle can implement
setEccentricity
and simply throw an exception, and similarly an unmodifiable list (or an immutable one from Guava) throws an exception when you try to modify it. But that doesn't really mean that it is-a List from the point of view of the LSP. First of all, it at least violates the principle of least surprise. If the caller gets an unexpected exception when trying to add an item to a list, that is quite surprising. And if the calling code needs to take steps to distinguish between a list it can modify and one it can't (or a shape whose eccentricity it can set, and one it can't), then one is not really substitutable for the other.It would be better if the Java type system had a type for a sequence or collection that only allowed iterating over, and another one that allowed modification. Perhaps Iterable can be used for this, but I suspect it lacks some features (like
size()
) that one would really want. Unfortunately, I think this is a limitation of the current Java collections API.Several people have noted that the documentation for
Collection
allows an implementation to throw an exception from theadd
method. I suppose that this does mean that a List that cannot be modified is obeying the letter of the law when it comes to the contract foradd
but I think that one should examine one's code and see how many places there are that protect calls to mutating methods of List (add
,addAll
,remove
,clear
) with try/catch blocks before arguing that the LSP is not violated. Perhaps it isn't, but that means that all code that callsList.add
on a List it received as a parameter is broken.That would certainly be saying a lot.
(Similar arguments can show that the idea that
null
is a member of every type is also a violation of the Liskov Substitution Principle.)† I know that there are other ways of addressing the Ellipse/Circle problem, such as making them immutable, or removing the setEccentricity method. I'm talking here only about the most common case, as an analogy.