7

The Java List<E> interface includes methods, such as add(E) and remove(Object), that work as the names suggest on instances of mutable concrete implementations, such as ArrayList<E> and LinkedList<E> but not on immutable lists, such as those returned by List.of().

The javadoc for these methods state that they are optional operations and that implementations may throw UnsupportedOperationException. The Java Collections API Design FAQ calls this the "most controversial design decision in the whole API."

I am teaching the SOLID principles and want to make sure I understand which principles, if any, this design decision violates. Candidates are:

  • The Liskov Substitution Principle, since immutable lists cannot be substituted for [mutable] lists.
  • The Interface Separation Principle, since the interface contains methods not supported by immutable lists.

Of course, technically no implementation violates the interface, since the specification includes the option of throwing UnsuppportedOperationException, but that behavior still is surprising to users.

I am not asking whether the design was a good or bad decision, just how it applies to SOLID principles.

21
  • 4
    It is difficult to be very precise in these kinds of observations as covering for one violation may lead to another. For example, if you look at the LSP bad examples and "what it would lead to" (checking for subtypes and then handling things differently based on subtype), that can equally be called an OCP violation (as it's an ever-growing list of special subcases). Additionally, SRP and ISP can be argued to be two sides of the same coin, just focusing on another type (classes vs interfaces). I would suggest not trying to treat SOLID as a checklist of individually exclusive issues. Commented Oct 19 at 22:01
  • 1
    Why doesn't Java 8 include immutable collections? Commented Oct 19 at 22:57
  • 1
    UnsupportedOperationException in java collections framework interfaces Commented Oct 19 at 22:57
  • 1
    @Flater your comment is a good answer, are you going to post? Commented Oct 19 at 23:51
  • 2
    The Java language might be the first attempt at an OOP standard library. They may have overdone it. Many lessons have been learned since then. Collections are not a good example of OOP, I would avoid using them as an example for teaching except maybe to point out how we now know that things could have been done better. Commented Oct 20 at 6:06

3 Answers 3

1

It does not violate the Liskov Substitution Principle. The LSP states that a subtype should not violate guarantees given by a supertype. But the List interface explicitly states that the methods add(), remove() etc. are optional and may throw this exception. So LSP is not broken by subtypes throwing this exception, even though the "principle of least astonishment" is obviously broken.

It is not obvious to what extent the Interface Segregation Principle can be applied to general-purpose library classes, since we don't know what subsets of an interface clients might want to use. The principle is best applied in application code when we see that different clients need different subsets of the interface of the same class. In the case of List it would require sub-typing it in your own application code in order to segregate the interface.

That said, the existence of the optional methods does indicate that the interface is not fine-grained enough. Having a specific UnmodifiableList interface would make it much clearer in client code what contract is expected and avoid the possibility of runtime exceptions for unsupported methods.

4
  • @JacquesB can't ISP be violated simply by writing a program that uses a combination of methods that no interface in the library supports exactly? As long as you refuse to write your own interface, that only provides methods you use, then you're forced to violate ISP when you have a program that doesn't use everything in the interface. Commented Oct 20 at 9:09
  • @candied_orange: I have edited the answer to make it more clear. But it is not about the specific subsets used by each individual method, since this would lead to a combinatorial explosion. Commented Oct 20 at 9:22
  • I think ISP is much easier to understand, once you remember that interfaces are designed for consumers. Ideally each argument of any method, has it own type and clients adapt their objects to that requested type. This way, the principle is simplified to "make your API as narrow as possible". Interface is a loaded word - one meaning is something you receive, another - something you expose. These are conceptually different. Commented Oct 20 at 9:32
  • 2
    @JacquesB the combinatorial explosion is exactly why library authors can't write every role interface for you. However, you can since you know what your programs client actually uses. Commented Oct 20 at 14:22
-3

Interface Segregation Principle (ISP) is challenged because List was designed by people who had no idea what exactly your program needs (it's from a library) and so they threw together everything that might be needed from a List. It's rare for a program to need every method in it. Including more than you need in an interface violates ISP. Alternatives would be a hand rolled interface that communicates actual needs or simply narrower interfaces from the library (Iterable, Collection).


The Liskov Substitution Principle is challenged because ArrayList performs differently than LinkedList. Performance can impact correctness.

This claim has stirred some controversy. Please allow me to clarify:

A correct program satisfies requirements. Performance can be a requirement. Performance is a behavior. LSP requires subtypes to have the same behavior in the context of a program p. Changing behavior violates LSP. It's up to you to decide if it's enough to care.

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T

wikipedia - LSP

LinkedList o1 = new LinkedList(); // Consider LinkedList type S
List o2 = new ArrayList(); // Consider List type T
MyProgram p = new MyProgram();

// Test if S is a subtype of T for P
compareBehavior(p, o1, o2); 

If you're thinking it's obvious that LinkedList is a List in the type system then you are missing the point. That isn't what LSP tests.

The LSP makes clear that in OOD the ISA relationship pertains to behavior. Not intrinsic private behavior, but extrinsic public behavior; behavior that clients depend upon.

objectmentor.com - LSP

Your languages type system simply can't test that for you. You have to check.

Consider program p that requires a List. But p was also written expecting ArrayList performance. That expectation is now required of every listy thing you pass to p. Not because the listy thing is a List in the type system. Because p demands it.

According to LSP whether o1 is a subtype of o2 isn't simply about if the type system is ok with the substitution that compareBehavior will make when it calls program p.run(o). It's about how p will behave. Whether that behavior is different with different objects.

That doesn't mean o2 must run identical code to o1. It means the externally visible behavior of that code must be identical.

And that includes performance.

And that maters if your requirements say it does.

And so correctness is impacted if it does.

And since this is my hypothetical example, it does.


It should be noted that the SOLID principles are simply warning signs that point to hidden costs that may be worth managing. Do not treat them as gospel or you'll find that every working program ever written is heretical. Forgive us lowly sinners.

1
-6
  • SRP is a bogus principle, as responsibility and count and scope thereof are free subjective parameters of design, chosen and assigned at will. Any program can be argued to have a single responsibilty, even if it does both Moon landings and fishing. Sure, mutability and state management are an additional responsibility, lets consider SRP violated, why not?
  • OC is a bogus principle, because it does not define when a component should be extensible and what modification is. Sure lets consider OC violated, because collections are hard to meaningfully extend. No one modifes a standard library, so they are closed for modification for sure. Unless you are Oracle's Doctor Deprecator and eat SecurityManagers for breakfast.
  • LSP is not a principle, it is a definition, nothing to violate here, unless you consider Java interface and its implementations to have a subtype relationship (which is not necessary).
  • Interface segregation principle is a bogus principle, because it does not define how segregation is to be done. Sure, lets consider it violated, because mutability and indexed access are not segregated.
  • Dependency inversion is not applicable, because there not a lot of dependencies in collections. Also, it does not define what is a dependency and what is implementation detail and where do they split.

In other words, each and every of these "principles" are uselessly vague enough to be both violated and not for any component under consideration. Uncle Bob should have just do quantum mechanics instead of education.

I hear a lot of noise - "this is just a recommendation", "a guide, not a rule", "use with care", "apply common sense", etc. I reply - if I need to be a qualified enough to distinguish when each principle applies and how to interpret omitted definitions, by that point I am surely qualified enough to make design decisions without refering to these principles at all.

As for lecture notes - please omit any principles that are not practically applicable or do not have a SOLID rationale.

8
  • 2
    -1 because this answer asserts that the lack of a scientific proof or rigid definition for these principles invalidates their usefulness... but +1 for "... qualified enough to distinguish when each principle applies and how to interpret omitted definitions, by that point I am surely qualified enough to make design decisions without refering to these principles at all." Commented Oct 20 at 10:12
  • "LSP is not a principle," LSP stands for 'Liskov Substitution Principle'. Literally, your statement reads as "[the] Liskov Substitution Principle is not a principle". Is that really what you mean here? Commented Oct 21 at 16:49
  • @JimmyJames yes, read the principle - it DEFINES what a SUBTYPE is. Therefore it is a definition. At best is a principal definition, or a principle of definition. Commented Oct 22 at 16:41
  • @Basilevs So you are saying the LSP is not a principle, but a definition of a principle. I'm not sure how that distinction is useful. It's kind of like saying that 'bird' is not a bird but a word that describes a bird. Commented Oct 22 at 19:06
  • 3
    When people say "are you following the LSP" about a Java program, they are meaning something along the lines of "when you are using Java's nominal subtyping, you'd better be careful that it matches up with behavioural subtyping", which is a principle, not a definition. Commented Oct 23 at 16:08

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.