This question already has answers here:
Do Collections.unmodifiableXXX methods violate LSP? [closed]
(4 answers)
Closed 6 years ago.
Arrays.asList(..) returns a List wrapper around an array. This wrapper has a fixed size and is directly backed by the array, and as such calls to add() or other functions that attempt to modify the list will throw an UnsupportedOperationException.
Developers are often surprised by this, as is evident from questions in stackoverflow.
However the List interface has an add() method which should work unsurprisingly for all derivers of List, according to the Liskov Substitution Principle (LSP)
Is the type returned by Arrays.asList() an example of a violation of the Liskov Substitution Principle?
Strictly speaking, it is, because LSP has no notion of optional interface members: a method is either part of an interface, or it is not part of an interface.
However, Java Class Library explicitly allows for violations of LSP when it designates certain interface methods as optional. List<T>.add() is one such method. Other mutating methods (addAll, remove, etc.) are also marked optional.
Essentially, designers of Java library took a shortcut: rather than making a separate interface for mutable list (extending a read-only list) they went for designating an operation "optional". Moreover, they have not provided a way for you to test the instance of list for being read-only, so your only option is to catch a runtime exception, which is a very bad idea. This amounts to you having to keep track of where your lists came from, and performing optional operations only when you are 100% certain of your list origin.
I think it is not a violation of LSP.
LSP says that all instances of classes implementing a given interface can be used interchangably.
The documentation of List.add (and other mutating methods) clearly states that an UnsupportedOperationException might be thrown by implementations of that method.
Throws
UnsupportedOperationException - if the add operation is not supported by this list
As such, if you're going to invoke this method on an instance of List from an unknown source, you need to handle the case that add throws an UnsupportedOperationException.
If you don't, you're not using the API correctly.
This is not to say that I like the design. I think the fact that trying to invoke the method is the only way to detect that any given method is not supported on an instance is rubbish. I mean, heck, give us an isAddSupported() method (or similar).
I just don't see that following documented behaviour can be violating LSP.
If you think about it in very technical terms, then of course it is a violation. LSP states that a bad design is one in which the inherited class cannot use superclass methods. Java though, doesn't always care for violations. This is often, as you suggested, a way for confusion among developers. The add() method is one example, so is remove(). Both of these are available for an immutable list, and that cannot be changed. The good thing is that at least an exception is thrown.
Further reading: http://c2.com/cgi/wiki?UnmodifiableListIsStupidAndItBreaksLsp
Related
This question already has answers here:
Do interfaces inherit from Object class in java
(9 answers)
Closed 2 years ago.
I came across fact that Collection interface has methods equals and hashCode which are also contained in Object. Same is the case with List interface. I was having following doubts:
Why interfaces should have these methods? Is it only because they have different meaning than those in Object ?
According to this question, including these methods does not enforce the implementing class to provide their implementation as these implementations are already provided and inherited from Object. So technically including them in the interfaces does not have any effect. This again underlines importance of first doubt, why interfaces need to have these methods?
This page says:
These methods perform computations over the object’s state, but the interface, in general, has no access to state; only the implementing class has access to this state.
Which I feel further increases importance of first question.
If we at all need them in interfaces, then why Java framework dont have super-interface containing them and have such interfaces implement this interface, just like as all classes are subclasses of Object?
The List interface declares equals and hashCode so that it can document extra constraints that the implementations must follow. For example, the List interface documentation requires that the equals method must consider two lists equal if the have the same items in the same order, no matter how the lists are implemented.
There is no way the compiler or runtime can enforce these requirements, so breaking them leads to runtime bugs that may be hard to find.
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 4 years ago.
Improve this question
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?
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.
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.
I don't believe it's a violation because the contract (i.e. the List interface) says that the mutation operations are optional.
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.
When in the java documentation for Set it says in the specification of a method Optional Operation e.g. (emphasis by me)
add(E e)
Adds the specified element to this set if it is not already present (optional operation).
What does the optional mean here?
That if I use a JVM other than SUN/Oracle, this operation may not be provided by that implementation of Java?
Set is an interface. Classes implementing that interface do not necessarily need to provide an implementation for an optional operation.
I think those optional operations go back to the general Collection interface where operations are made optional which do not make sense for some kinds of collections. E.g. add is an operation that isn't really useful on some kind of read-only collection. It's spelt out explicitly in the Javadoc so it becomes part of what all collection classes offer but someone using it knows that, given some collection they do not know exactly, it could be that the method just throws an UnsupportedOperationException.
From the java.util.Collections documentation:
The "destructive" methods contained in this interface, that is, the
methods that modify the collection on which they operate, are
specified to throw UnsupportedOperationException if this collection
does not support the operation. If this is the case, these methods
may, but are not required to, throw an UnsupportedOperationException
if the invocation would have no effect on the collection. For example,
invoking the addAll(Collection) method on an unmodifiable collection
may, but is not required to, throw the exception if the collection to
be added is empty.
Note that many of the methods described there are not optional.
The Java Collections Framework is, arguably, not perfect; this may be one of the imperfections rearing its (tiny) head.
That an interface method is specified as optional in the JavaDoc means that classes implementing this interface does not necessarily have to implement that method. Instead, they could for example, throw an exception.
More specifically, that an interface method is optional in the JavaDoc does not mean that it is implementation-specific behavior. Each concrete implementation of the class will specify whether it implements it or not. Looking at the HashMap class it includes the add operation and it does not specify it as optional. Thus, every implementation of the Java library will have to include an implementation of this method for their HashMap class. The same goes for TreeMap etc.
The reason why it might make sense for this operation to be declared as optional is because some sets may be conceptually immutable, such as those returned by Collections.unmodifiableSet
Item 16 of Effective Java 2nd edition, favor composition over inheritance says the following
"If the superclass acquires a new method in a subsequent release and
you have the bad luck to have given the subclass a method with the same signature
and a different return type, your subclass will no longer compile.
If you’ve given the subclass a method with the same signature and return type as the
new superclass method, then you’re now overriding it"
How likely is that these cases arise in a real world situation? Could anyone here give me an example from a real business app (stripping out proprietary info if needed)?
This isn't about how likely that scenario is. (Besides, I'm sure you've heard of Murphy's Law)
It's about the fact that composition is much less of a binding contract than inheritance.
Inheritance is a very strong way to bind behavior between classes, and basically Item 16 states that you should use that strong connection only when it is clear that it is necessary. For all other uses, composition should be preferred.
If I have a class that needs to implement an interface but one or more of the methods on that interface don't make sense in the context of this particular class, what should I do?
For example, lets say I'm implementing an adapter pattern where I want to create a wrapper class that implements java.util.Map by wrapping some immutable object and exposing it's data as key/value pairs. In this case the methods put and putAll don't make sense as I have no way to modify the underlying object. So the question is what should those methods do?
Any method that cannot be implemented according to the semantics of the interface should throw an UnsupportedOperationException.
That depends on your business case. 2 options:
Do nothing.
Throw an UnsupportedOperationException.
Use whichever makes more sense. If you do nothing, you are not obeying the contract of the interface. However, throwing a runtime exception can wreak havoc on the calling code. Thus, the decision will have to be made based on how you will be using the class. Another option would be to use a simpler or different interface, if possible.
Do note that the Java library goes the exception route in the specific case of read-only collections.
It was noted below that UnsupportedOperationException is a part of the java collections framework. If your situation is outside of collections, and the semantics bother you, you can roll your own NotImplementedException, or if you are already using commons-lang, you could use theirs.
That read-only collection already provided by Java throw the UnsupportedOperationException during write operations is already an unfortunate design hack. The collection classes should have been written with separate read-only and write-only interfaces that are both inherited by the full read-write interface. Then you know what you're getting.
Your two choices are really only:
Do nothing.
Throw an exception.
Both have disadvantages. In the first case, by having an empty method, you could mislead the programmer into thinking something happened to your data. The second case breaks the whole idea of polymorphism inherent in interfaces.
Note that UnsupportedOperationException is only OK because of the particular property of the Java Collections Framework, that implementations are permitted to "goof off" implementing part of the interface because they're immutable.
So it's fine for put() (assuming all the mutator methods do the same thing), but a Map which throws UnsupportedOperationException from the size() method would just be broken. If you're trying to implement a kind of map which doesn't know how big it is, you may be in trouble (although sometimes you can return Integer.MAX_VALUE).
Also note that the class documentation for UnsupportedOperationException says that it is part of the Java Collections Framework. Outside the collections framework, throwing UnsupportedOperationException is not be expected and could lead to client code that plain doesn't work. Sure, it's a RuntimeException, but just because you can throw it doesn't mean that your method will work if it always does.
Instead you could either refactor the interface (perhaps splitting it in two), or else rethink why this class is claiming to be a Foo when it plainly isn't, since it can't do the things that Foos are defined to be able to do.