47

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?

Narendra Pathai
  • 41,187
  • 18
  • 82
  • 120
  • Highly related: http://stackoverflow.com/q/20290834/319403 – cHao Feb 27 '14 at 03:21
  • 2
    It might be worth pointing to the first entry of the Collections API design FAQ here: [**Why don't you support immutability directly in the core collection interfaces so that you can do away with optional operations (and UnsupportedOperationException)?**](https://docs.oracle.com/javase/8/docs/technotes/guides/collections/designfaq.html#a1). – Marco13 Aug 13 '16 at 22:51
  • Great link. Thanks @Marco13 – Narendra Pathai Aug 15 '16 at 19:12

4 Answers4

43

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, if add() is called, an UnsupportedOperationException will be thrown.

What is the general contract of add()? According to the API documentation it is:

Ensures that this collection contains the specified element (optional operation). Returns true if this collection changed as a result of the call. (Returns false if this collection does not permit duplicates and already contains the specified element.)

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:

If a collection refuses to add a particular element for any reason other than that it already contains the element, it must throw an exception (rather than returning false). This preserves the invariant that a collection always contains the specified element after this call returns.

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.

Matt
  • 868
  • 8
  • 12
  • additionally, the documentation defines exceptions which should be thrown in specific cases when adding is refused, including the UnsupportedOperationException – Silly Freak Feb 26 '14 at 19:35
  • This is also the reason why you should never modify collections you get from APIs you don't know. If it doesn't state, the collection is mutable, you should always create a new one. – Kirill Rakhman Feb 28 '14 at 11:03
  • @cypressious: It's only a very small part of the reason. A much bigger reason is that even if one knew the collection would allow the mutation, one wouldn't know whether one had been given a copy of a collection or else a view of collection which one wasn't supposed to change. – supercat Nov 17 '14 at 23:57
15

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 the setEccentricity 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 regular List that you can't do with one wrapped by Collections.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 only List, 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 the add 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 for add 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 calls List.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.

David Conrad
  • 15,432
  • 2
  • 42
  • 54
  • however, the manipulation methods of List specify that there are multiple ways how they may behave. The contract only states that one of these must be taken, and this is the case for both modifiable and unmodifiable Lists. Strictly speaking, everyone who assumes a List's add method won't throw UnsupportedOperationException is violating substitution, not the classes implementing List. – Silly Freak Feb 26 '14 at 19:47
  • I think that technically you're correct, but there is the theory, and then there is the contract in practice. If there were two types, ImmutableList and List, and the add method only existed on the second, we never would have ended up in this situation in the first place. – David Conrad Feb 26 '14 at 19:51
  • 3
    To put it another way, putting a method into your API and then declaring that sometimes it will do something, and sometimes it will throw, is just inviting trouble. It's a bug waiting to happen, and it undermines the very thing the LSP is supposed to protect you from. – David Conrad Feb 26 '14 at 19:52
  • Of course there are other design possibilities where there is a type distinction between mutable and immutable lists, but I'd argue that this is not necessary. What is important is that assertions are communicated between callers and callee - types do that. But even without a type distinction, a method can specify in its documentation that the return value is an "immutable List", and even though this does not carry meaning to the compiler, the programmer can tell that they can't modify it or substitute it where a modifiable List is expected. – Silly Freak Feb 26 '14 at 20:17
  • strictly speaking, the same goes for mutable lists, so people should specify that lists they return etc. are indeed modifiable, but I'd say that this is expected as per principle of least surprise, so it's really much more important to document immutable lists. – Silly Freak Feb 26 '14 at 20:19
5

I don't believe it's a violation because the contract (i.e. the List interface) says that the mutation operations are optional.

Ian Roberts
  • 120,891
  • 16
  • 170
  • 183
  • 3
    It's not really clear to me what "optional" means with respect to LSP. Client code must never use it since it might not have it. – djechlin Feb 26 '14 at 19:18
  • @djechlin Yes that's what I think. Unsupported operations should not really be part of Immubtable interface it they are not supported. – Narendra Pathai Feb 26 '14 at 19:29
  • @NarendraPathai: If many implementations of interface `Wozzler` also implement method `Wuzzle`, and many consumers of `Wozzler` would expect to use method `Wuzzle` if available and otherwise do something else, then it would IMHO generally be much better to have `Wizzler` include `Wuzzle`, as well as a means of determining whether a given instance supported it, than to have `Wuzzle` only appear in a `Wuzzler` interface but not in `Wizzler`. If fixed-sized collections which support write-by-index and variable-sized collections which support `add` but not write-by-index share a common interface – supercat Dec 26 '14 at 18:57
  • ...then one "WrappedObservableCollection" type will be able to operate with both (allowing clients to use whatever features are supported by the underlying collection). Segregating interface abilities in the type system, rather than by ability-reporting methods, means that every combination of abilities a collection might have would require a different wrapper class. – supercat Dec 26 '14 at 19:00
-1

I think you are not mixing things here.
From LSP:

Liskov's notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).

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.

Jim
  • 18,826
  • 34
  • 135
  • 254
  • 6
    Interfaces can basically be considered the same as superclasses for this purpose. (In fact, they're largely just a half-assed workaround for the lack of multiple inheritance -- and in some languages that have MI, like C++, "interfaces" *are* just classes with only pure virtual members.) An interface rarely just supplies a list of function names; it typically also defines what the functions do, and anything conforming to the interface had better do those things. – cHao Feb 27 '14 at 03:38
  • @cHao:I don't agree.Interfaces and superclasses are unrelated concepts.LSP is only about subtyping.The mechanism is similar, having an argument in a method of `IntefaceA` and passing `InterfaceImpl` or `InterfaceImpl2` but we are still not subtyping the way LSP says. Qoting from oracle: `If your class claims to implement an interface, all methods defined by that interface must appear in its source code before the class will successfully compile.` http://docs.oracle.com/javase/tutorial/java/concepts/interface.html – Jim Feb 27 '14 at 18:35
  • Oracle is focusing on the language itself. Java doesn't care about SOLID, and is unable to enforce it to any useful degree. But **LSP still applies**. What's more, **you actually *depend* on it applying.** Proof: Say i make a `List` where every function throws `RuntimeException`. This is allowed by the compiler, and is a violation of LSP if, and *only* if, the interface itself has known properties that are expected but absent in the implementation. So i pass my "list", you try to use it, and *kaboom*! Now, who is to blame for the resulting crash? If you blame me, then you relied on LSP. – cHao Mar 05 '14 at 15:06
  • I agree with @cHao, [LSP applies to the contract](https://stackoverflow.com/questions/12252363/does-liskov-substitution-principle-also-apply-to-classes-implementing-interfaces), regardless of how that is specified. – Marco Lackovic Aug 31 '20 at 10:36
  • @cHao saying "interface is a half-assed workaround for the lack of multiple inheritance" is like saying "Maybe monad is a half-assed workaround for the lack of null" in something like haskell. It's deliberate. It's an attempt to safeguard users of the language while still retaining useful usecases. – Coderino Javarino Jun 15 '22 at 14:53
  • @Coderino: Deliberate or not, it's still half-assed. MI is not something to be feared, and every attempt to remove it but keep the good parts, is ugly and/or broken in some way. Hell, this answer itself is evidence -- mentally distinguishing interfaces from classes has led to a misinterpretation of LSP. But the half-assedness or not is entirely tangential to the point. – cHao Jun 15 '22 at 15:30