CSE331�Lecture 16.1
Subtypes: Java details, equals redux, and alternatives
Ardi Madadi �Summer 2021�(Based on slides by Kevin Zatloukal, Mike Ernst, Dan Grossman, and many others)
1
SUBTYPES VS SUBCLASSES
2
Substitution principle for classes
If B is a subtype of A, then a B can always be substituted for an A
Any property guaranteed by A must be guaranteed by B
B is permitted to strengthen properties and add properties
B is not permitted to weaken the spec
CSE 331 Summer 2021
Substitution principle for methods
Constraints on methods
Each overridden method must strengthen (or match) the spec:
CSE 331 Summer 2021
Spec strengthening: argument/result types
For method inputs:
For method outputs:
CSE 331 Summer 2021
LibraryHolding
Book
CD
A
B
Shape
Circle
Rhombus
Recall: Subtyping Example
class Product {
private int price; // in cents
public int getPrice() {
return price;
}
public int getTax() {
return (int)(getPrice() * 0.086);
}
}
class SaleProduct extends Product {
private float factor;
public int getPrice() {
return (int)(super.getPrice()*factor);
}
}
CSE 331 Summer 2021
Substitution exercise
Suppose we have a method which, when given one product, recommends another:
class Product {� Product recommend(Product ref);
}
Which of these are possible forms of this method in SaleProduct (a true subtype of Product)?
Product recommend(SaleProduct ref);
SaleProduct recommend(Product ref);
Product recommend(Object ref);
Product recommend(Product ref)
throws NoSaleException;
CSE 331 Summer 2021
// good
// good, but in Java is overloading
// bad
// bad
Java subtyping
CSE 331 Summer 2021
Java subtyping guarantees
A variable’s run-time type (i.e., the class of its run-time value) is a Java subtype of its declared type
Object o = new Date(); // OK
Date d = new Object(); // compile-time error
If a variable of declared (compile-time) type T1 holds a reference to an object of actual (runtime) type T2,�then T2 must be a Java subtype of T1
Corollaries:
Rules out a huge class of bugs
CSE 331 Summer 2021
Java subtyping non-guarantees
Java subtyping does not guarantee that overridden methods
CSE 331 Summer 2021
EQUALS WITH SUBCLASSES
11
equals specification
public boolean equals(Object obj) should be:�
CSE 331 Summer 2021
Really fixed now
public class Duration {
@Override
public boolean equals(Object o) {
if (!(o instanceof Duration))
return false;
Duration d = (Duration) o;� return this.min==d.min && this.sec==d.sec;� }
}
CSE 331 Summer 2021
Two subclasses
class CountedDuration extends Duration {
public static numCountedDurations = 0;
public CountedDuration(int min, int sec) {
super(min,sec);
++numCountedDurations;
}
}
class NanoDuration extends Duration {
private final int nano;
public NanoDuration(int min, int sec, int nano){
super(min,sec);
this.nano = nano;
}
public boolean equals(Object o) { … }
…
}
CSE 331 Summer 2021
CountedDuration is (probably) fine
CSE 331 Summer 2021
NanoDuration is (probably) not fine
@Override
public boolean equals(Object o) {
if (!(o instanceof NanoDuration))
return false;
NanoDuration nd = (NanoDuration) o;
return super.equals(nd) && nano == nd.nano;
}
CSE 331 Summer 2021
The symmetry bug
public boolean equals(Object o) {
if (!(o instanceof NanoDuration))
return false;
NanoDuration nd = (NanoDuration) o;
return super.equals(nd) && nano == nd.nano;
}
This is not symmetric!
Duration d1 = new NanoDuration(5, 10, 15);
Duration d2 = new Duration(5, 10);
d1.equals(d2);
d2.equals(d1);
CSE 331 Summer 2021
// false
// true
Fixing symmetry
This version restores symmetry by using Duration’s equals if the argument is a Duration (and not a NanoDuration)
public boolean equals(Object o) {
if (!(o instanceof Duration))
return false;
// if o is a normal Duration, compare without nano if (!(o instanceof NanoDuration))
return super.equals(o);
NanoDuration nd = (NanoDuration) o;
return super.equals(nd) && nano == nd.nano;
}
Alas, this still violates the equals contract
CSE 331 Summer 2021
The transitivity bug
CSE 331 Summer 2021
Duration d1 = new NanoDuration(1, 2, 3);
Duration d2 = new Duration(1, 2);
Duration d3 = new NanoDuration(1, 2, 4);
d1.equals(d2);
d2.equals(d3);
d1.equals(d3);
NanoDuration
min
sec
nano
1
2
3
Duration
min
sec
1
2
NanoDuration
min
sec
nano
1
2
4
The transitivity bug
CSE 331 Summer 2021
Duration d1 = new NanoDuration(1, 2, 3);
Duration d2 = new Duration(1, 2);
Duration d3 = new NanoDuration(1, 2, 4);
d1.equals(d2);
d2.equals(d3);
d1.equals(d3);
NanoDuration
min
sec
nano
1
2
3
Duration
min
sec
1
2
NanoDuration
min
sec
nano
1
2
4
// true
The transitivity bug
CSE 331 Summer 2021
Duration d1 = new NanoDuration(1, 2, 3);
Duration d2 = new Duration(1, 2);
Duration d3 = new NanoDuration(1, 2, 4);
d1.equals(d2);
d2.equals(d3);
d1.equals(d3);
NanoDuration
min
sec
nano
1
2
3
Duration
min
sec
1
2
NanoDuration
min
sec
nano
1
2
4
// true
// true
The transitivity bug
CSE 331 Summer 2021
Duration d1 = new NanoDuration(1, 2, 3);
Duration d2 = new Duration(1, 2);
Duration d3 = new NanoDuration(1, 2, 4);
d1.equals(d2);
d2.equals(d3);
d1.equals(d3);
NanoDuration
min
sec
nano
1
2
3
Duration
min
sec
1
2
NanoDuration
min
sec
nano
1
2
4
// true
// true
// false!
No perfect solution
CSE 331 Summer 2021
Option 1: avoid subclassing
Choose composition over subclassing (Effective Java)
public class NanoDuration {
private final Duration duration;
private final int nano;
…
}
Solves some problems:
Introduces others:
expected (since it is not a subtype)
CSE 331 Summer 2021
Option 2: the getClass trick
Check if o is a Duration and not a subtype:
@Overrides�public boolean equals(Object o) { // in Duration
if (o == null)
return false;
if (!o.getClass().equals(getClass()))
return false;
Duration d = (Duration) o;
return d.min == min && d.sec == sec;
}
But this breaks CountedDuration!
CSE 331 Summer 2021
Subclassing summary
CSE 331 Summer 2021
DESIGNING FOR INHERITANCE
27
Inheritance can break encapsulation
public class InstrumentedHashSet<E>
extends HashSet<E> {
private int addCount = 0; // count # insertions
public InstrumentedHashSet(Collection<? extends E> c){
super(c);
}
public boolean add(E o) {
addCount++;
return super.add(o);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() { return addCount; }
}
CSE 331 Summer 2021
Solutions
CSE 331 Summer 2021
Dependence on implementation
What does this code print?
InstrumentedHashSet<String> s =
new InstrumentedHashSet<String>();
System.out.println(s.getAddCount());
s.addAll(Arrays.asList("CSE", "331"));
System.out.println(s.getAddCount());
Dependence on implementation
What does this code print?
InstrumentedHashSet<String> s =
new InstrumentedHashSet<String>();
System.out.println(s.getAddCount());
s.addAll(Arrays.asList("CSE", "331"));
System.out.println(s.getAddCount());
// 0
Dependence on implementation
What does this code print?
InstrumentedHashSet<String> s =
new InstrumentedHashSet<String>();
System.out.println(s.getAddCount());
s.addAll(Arrays.asList("CSE", "331"));
System.out.println(s.getAddCount());
// 0
// 4?!
Solution: composition
public class InstrumentedHashSet<E> {
private final HashSet<E> s = new HashSet<E>();
private int addCount = 0;
public InstrumentedHashSet(Collection<? extends E> c){
this.addAll(c);
}
public boolean add(E o) {
addCount++; return s.add(o);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() { return addCount; }
// ... and every other method specified by HashSet<E>
}
CSE 331 Summer 2021
Solution: composition
public class InstrumentedHashSet<E> {
private final HashSet<E> s = new HashSet<E>();
private int addCount = 0;
public InstrumentedHashSet(Collection<? extends E> c){
this.addAll(c);
}
public boolean add(E o) {
addCount++; return s.add(o);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() { return addCount; }
// ... and every other method specified by HashSet<E>
}
CSE 331 Summer 2021
Delegate
Solution: composition
public class InstrumentedHashSet<E> {
private final HashSet<E> s = new HashSet<E>();
private int addCount = 0;
public InstrumentedHashSet(Collection<? extends E> c){
this.addAll(c);
}
public boolean add(E o) {
addCount++; return s.add(o);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() { return addCount; }
// ... and every other method specified by HashSet<E>
}
CSE 331 Summer 2021
The implementation no longer matters
Delegate
Composition (wrappers, delegation)
Implementation reuse without inheritance
CSE 331 Summer 2021
Composition does not preserve subtyping
CSE 331 Summer 2021
Interfaces reintroduce Java subtyping
public class InstrumentedHashSet<E> implements Set<E> {
private final Set<E> s = new HashSet<E>();
private int addCount = 0;
public InstrumentedHashSet(Collection<? extends E> c){
this.addAll(c);
}
public boolean add(E o) {
addCount++;
return s.add(o);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() { return addCount; }
// ... and every other method specified by Set<E>
}
CSE 331 Summer 2021
Interfaces reintroduce Java subtyping
public class InstrumentedHashSet<E> implements Set<E> {
private final Set<E> s = new HashSet<E>();
private int addCount = 0;
public InstrumentedHashSet(Collection<? extends E> c){
this.addAll(c);
}
public boolean add(E o) {
addCount++;
return s.add(o);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() { return addCount; }
// ... and every other method specified by Set<E>
}
CSE 331 Summer 2021
normal Java style
Interfaces and abstract classes
Provide interfaces for your functionality
Consider also providing helper/template abstract classes
CSE 331 Summer 2021
Java library interface/class example
// root interface of collection hierarchy
interface Collection<E>
// skeletal implementation of Collection<E>
abstract class AbstractCollection<E>
implements Collection<E>
// type of all ordered collections
interface List<E> extends Collection<E>
// skeletal implementation of List<E>
abstract class AbstractList<E>
extends AbstractCollection<E>
implements List<E>
// an old friend...
class ArrayList<E> extends AbstractList<E>
CSE 331 Summer 2021
Why interfaces instead of classes?
Java design decisions:
Observation:
CSE 331 Summer 2021
Pluses and minuses of inheritance
CSE 331 Summer 2021