06-polymorphism
06-polymorphism
Polymorphism
1
The type system and how it may lead to the
hiding of type information,
3
Terminology for a class hierarchy
• A class that is derived from another class is
called a subclass, extended class or child
class.
4
A simple example
public class Organism {
private String species;
private double age;
private double health;
5
A simple example
• A class is extended by a subclass typically because, while maintaining
various properties of a type, the subtype has some properties specific only to
itself.
E.g., all animals have some properties in common, but the subtype “insect” will have
additional subtype-specific properties.
• Moreover, some properties may need to be modified only for the subtype.
E.g., all insects have wings, except for, say, ants.
6
}
Polymorphism
7
• The phenomenon where a single name may be
Polymorphism
used to represent multiple types is known as
polymorphism.
• This provision is there in most modern
programming languages you are likely to use
(e.g., Java, OCaml, Python, C#, JavaScript).
There are two main types of polymorphism you
will see:
1. subtype polymorphism, and
2. parametric polymorphism.
8
• A polymorphic variable can hold values of different types.
Polymorphism
• A polymorphic function can be applied to arguments of a
variety of types.
In a way, we can think of it as one function with multiple
interpretations.
• Overriding is not exactly the same as polymorphism, but it is
a specific form of polymorphism.
When there is a method call from an instance of the subtype
referenced by the supertype, the method from the subtype is
dynamically bound.
• Method overloading is the act of declaring multiple methods
with the same name but different signatures, within the same
9
Parametric • We often deal with objects and behaviors that require
a generic way of handling values in a uniform
polymorphism manner, without worrying about their type.
10
Types and polymorphism
• Parametric polymorphism is
ubiquitous in functional programming
languages. So much so that when
speaking in the context of functional
programming, people often just say
“polymorphism” to mean “parametric
polymorphism”.
• In object-oriented programming, we
11
Types and polymorphism
• Polymorphism allows a single name to
represent multiple types.
12
Polymorphism • In Java, casting up along the type hierarchy is
automatic and implicit. This is called upcasting or
and type broadening conversion.
conversion • This is based on the common-sense knowledge of the
“is-a” relation. For example, a car is-a vehicle. So
why not let the conversion be automatic? Hence, in
Java we can write code like:
Vehicle v = new Car();
But OCaml does not allow this automatically, and the
conversion must be explicit:
let v : vehicle = (new car:> vehicle);;
13
Polymorphism & inheritance
• We looked at the notion of inheritance, i.e., properties of a superclass
automatically become the properties of a subclass.
E.g., if the type car has four wheels, then every subtype of car will also have four
wheels (unless this property is overridden). This would typically be implemented
with an attribute, e.g., numWheels, and a corresponding access method, e.g.,
getNumWheels().
• We also saw that the type hierarchy can, in general, be a lattice structure
instead of a tree. That is, a type may be able to inherit properties from more
than one parent type.
This can lead to the diamond problem.
14
The “diamond
problem”
• I’m breathing as a snake.
16
The “diamond
problem”
• What if the Reptile class
overrides the breathe()
method and Snake doesn’t?
17
The “diamond
problem”
• C++ allows multiple
inheritance of classes, but
Java does not.
Interfaces
public static class ArrayStack implements Stack {
private char[] stack;
• To get around the restriction of single private int top;
inheritance of classes, Java introduces
another reference type called the interface. public ArrayStack(int n) {
stack = new char[n];
• A declarative approach to polymorphism. top = -1;
}
Two elements are polymorphic (w.r.t a set
of behaviors) if they implement the same public void push(char item) { stack[++top] = item; }
interface(s). public char pop() { return stack[top--]; }
public char peek() { return stack[top]; }
We can think of an interface as an API,
19
Interfaces
• An interface cannot be directly A few things to note about Java interfaces:
Like classes, an interface can be extended using the extends keyword.
instantiated. But in Java, we can write •
• When one interface extends another
Stack s = new ArrayStack(10);
• it inherits all the methods and constants of its parent type, and
• it can define new methods and constants.
• What is the type of this s? • Since Java allows multiple inheritance of interfaces, an interface may have
This is how Java indirectly allows more than one parent interface.
polymorphism in spite of enforcing single
inheritance of classes.
20
© 2022 Ritwik Banerjee
Polymorphism Suppose we have a Shape class, and we want to
a) introduce the notion that every shape has a center,
21
public interface Shape {
double getPositionX();
Information
double getPositionY();
}
information. Let’s see what could happen: public class Square implements Shape {
double left_top_x, left_top_y, side;
• We want a list to hold a collection of Shape instances.
public Square(double left_top_x, double left_top_y, double side) {
• When retrieving the shapes, List#get(int i) this.left_top_x = left_top_x;
returns a java.lang.Object. This is correct, because this.left_top_y = left_top_y;
this.side = side;
every type “is-a” java.lang.Object. }
• Advantage of polymorphism: we can put different public double getPositionX() { return left_top_x; }
22
public interface Shape {
double getPositionX();
Information
double getPositionY();
}
going on here:
}
2) even if the programmer remembers (mostly public Square(double left_top_x, double left_top_y, double side) {
because of the variable name, shapes) that all this.left_top_x = left_top_x;
this.left_top_y = left_top_y;
the items are Shape instances, casting to specific this.side = side;
}
subtypes of Shape can again be problematic.
public double getPositionX() { return left_top_x; }
We would certainly prefer a system that
dynamic typing!
shapes.add(new Square(1.5, 1.5, 1));
shapes.add(new String("tada!"));
23
Type viewed as a ‘container’
The problem posed by polymorphism can (at least partly) be resolved, again,
by polymorphism.
• Think of the data type as a container, and now we need a way to specify the
type of the payload in that container! Going one step further, think of the
type also as a container that holds instances of another reference type.
This idea gives rise to the diamond syntax used in Java (for the ‘payload’ type): <T>
• Such a Box interface is the generic construct for
interface Box<T> {
our notion of a container, and the ‘payload’ type in
void box(T t);
it is denoted by the parameter T.
24
Type as a parameter
• What we saw vis-à-vis type conversion and subtypes is a glimpse into the world of
subtype polymorphism.
• In many cases, it is possible to write OCaml programs with subtype polymorphism. But your
code will be full of explicit type conversions. That is, lines that look like this:
let v : vehicle = (new car:> vehicle)
25
Type as a parameter
Java OCaml
• Broadening conversions are implicit, but • Broadening conversion requires extra
parametric polymorphism requires extra code: code, but parameterization is implicit,
List<String> strings = new LinkedList<String>(); and no additional code is needed.
item.add(“this is a string”); let items = Queue.create();;
Queue.add "s" items;;
• The generic parameter is coded using a pair of
angular brackets: <T>. • Since the use of parameters is automatic
and implicit, the code is less verbose.
26
Parametric polymorphism
• Perhaps the single most important advantage of the ability to use a type as a
parameter is that we need to write functions that are not type-specific only
once, and reuse them throughout, with different parameters.
• Let’s think of a function that swaps the two items in an ordered pair:
let swap (x, y) = (y, x);;
val swap : 'a * 'b -> 'b * 'a = <fun>
Here, 'a and 'b are type variables, and the function’s type is derived from that.
27
Parametric polymorphism
• Perhaps the single most important advantage of the ability to use a type as a
parameter is that we need to write functions that are not type-specific only
once, and reuse them throughout, with different parameters.
• Let’s think of a function that swaps the two items in an ordered pair:
let swap (x, y) = (y, x);;
val swap : 'a * 'b -> 'b * 'a = <fun>
Here, 'a and 'b are type variables, and the function’s type is derived from that.
• Instead, we can use the polymorphic swap function by replacing the type
variables by concrete types as and when needed:
28
Parametric polymorphism
• Writing the polymorphic swap function was a no-brainer in OCaml, because
OCaml performs automatic type inference.
29
Parametric polymorphism
Now let us see how to write the exact same function in another statically
typed language, Java.
30
Parametric public static class PolymorphicPair<A, B> {
A first;
polymorphism
B second;
public PolymorphicPair(A a, B b) {
this.first = a;
this.second = b;
}
• We need to first define the proper
data type. /* Define swap as an instance method */
Without this, we cannot even figure out public PolymorphicPair<B, A> swap() {
the return type of the swap function. return new PolymorphicPair<>(second, first);
}
• Then, we have two ways of defining }
swap: 1
1) an instance method within the body of
the type we just defined, or
Parametrized
E peek();
boolean isEmpty();
String toString();
types
}
32
}
}
(* polymorphic lists *)
type 'a tree = Leaf | Node of ('a tree) * 'a * ('a tree) ;;
35
Type erasure
Generics are a compile-time construct, i.e., the JVM does not know anything
about the type parameters since they do not exist at runtime. This removal of
the parameter type’s information is called type erasure.
So how can a compiler translate a code with parameter types into parameter-
free byte code that the virtual machine can correctly interpret?
There are two choices:
2) Generate code for only one representation for every instantiation of a generic type or
function, and map all the instantiations of that type or method to this unique
representation. To retrieve the generic type, then, perform type conversions
36
interface Comparable<A> { int compareTo(A that); }
Type erasure
NumericValue y = (NumericValue) numberList.get(0); /* typecast added */
}
37
}
Parameter types and subtyping
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
• The first line is fine, of course. But the second line is tricky. The key
question here is “since String is a subtype of Object, is a list of Strings a
subtype of a list of Objects?”
38
Parameter types and subtyping
• In general, if A is a subtype of B, and R is some parameterized type (e.g.,
List, Set), it is NOT the case that R<A> is a subtype of R<B>.
• Suppose we have the following type hierarchy:
You should be aware that the following are incorrect (except for the first instantiation):
39
Parameters and
algebraic types
void printCollection(Collection c) {
• In general, if A is a subtype of B, and R is some Iterator i = c.iterator();
parameterized type (e.g., List, Set), it is NOT the for (int k = 0; k < c.size(); k++)
case that R<A> is a subtype of R<B>. System.out.println(i.next());
}
• This solves one type of problems:
Imagine a situation where a list of drivers were
being maintained by the DMV, and they shared that
data with law enforcement, who treated it as a
List<Person> and corrupted it by adding non-
drivers to that list.
40
Parameters and
algebraic types
void printCollection(Collection c) {
• In general, if A is a subtype of B, and R is some Iterator i = c.iterator();
parameterized type (e.g., List, Set), it is NOT the for (int k = 0; k < c.size(); k++)
case that R<A> is a subtype of R<B>. System.out.println(i.next());
}
• This solves one type of problems:
Imagine a situation where a list of drivers were
being maintained by the DMV, and they shared that But the code below is not very useful, because
data with law enforcement, who treated it as a it only takes Collection<Object>, which,
List<Person> and corrupted it by adding non- as we just saw, is not a supertype of all kinds
drivers to that list.
of collections!
41
What is the
• More generally, if we have a sum type (e.g., List, Set,
Collection) with a parameter, what is the supertype of all
such data types?
collections?
with parameterized algebraic data types. We can call the code
shown here with any type parameter.
• Note that inside the code, each individual item is still treated
as an Object. This is type-safe – whatever the collection’s
type was, the individual items are all subtypes of
java.lang.Object.
42
Working • Adding items:
The add() method’s parameter is a type variable E, which
denotes the element type of the collection.
with the Since we don’t know what that is, we can’t use the add()
method on a collection of the unknown type.
43
public class Canvas {
to type variables
}
}
44
Adding flexibility
public void drawAll(List<Shape> shapes) {
to type variables
for (Shape s : shapes) s.draw(this);
}
• No matter what the final drawing looks
public void drawAll(List<? extends Shape> shapes) { like, we probably need to maintain a
for (Shape s : shapes) s.draw(this); collection of all the shapes needed to get
} to the final result.
• The problem is that drawAll() can only
take a list of Shapes, and not a list of
Circles (because, remember, a
List<Circle> is not a subtype of
List<Shape>).
• There is a subtle but important
distinction we want here: we want the
method to accept any kind of Shape,
46
Adding flexibility
public void drawAll(List<Shape> shapes) {
to type variables
for (Shape s : shapes) s.draw(this);
}
• Just like we saw with the unbound wildcard, we
public void drawAll(List<? extends Shape> shapes) {
still can’t add() items to a List<? extends
for (Shape s : shapes) s.draw(this);
}
Shape>.
void addRectangle(List<? extends Shape> s)
{ s.add(0, new Circle()); }
47
converted to capture#1 of ? extends Shape
Polymorphic methods
• Since we should not work with algebraic types parameterized by Object, and using
<?> doesn’t solve our problems either, we should be using a specific type variable for
generic methods:
/* T is inferred to be a String */
addToCollection("s", new HashSet<>());
49
Polymorphic methods
• Let’s look at some actual methods in boolean containsAll(Collection<?> c);
Java’s collection library. boolean addAll(Collection<? extends E> c);
• We just saw the issues with using the boolean removeAll(Collection<?> c);
wildcard type or simply using Object boolean removeIf(Predicate<? super E> filter);
as our type variable.
boolean retainAll(Collection<?> c);
• We also saw that using a specific
type variable as the parameter seems
to resolve these issues.
So under what circumstances, and why, can/should we use the wildcard (bounded or otherwise)?
50
Polymorphic methods with unknown type
parameters
• Consider the method to copy items from a source list src, to a destination list dest:
public static <T, S extends T> void copy(List<S> src, List<T> dest) {
/* implementation */
}
• Note the T is used in the type of dest and in the definition of S, but S itself is only
used once in the definition. Nothing else depends on S.
51
• We saw upper bounds on type parameters, e.g., <? extends T> or
Bounded type
parameters
<S extends T>.
• But lower bounds are possible too! That is, we can specify bounds
like <? super T>.
• The upper bounds allow for flexible use of the type, but make the
data structure holding this type (usually a collection) effectively
“read only”.
• The lower bounds have the opposite effect – they create collections
that are effectively “write only”.
• Together, this is often called the PECS principle, a mnemonic for
“producer extends consumer super”:
If you want to traverse through a collection and do something with each
52
Bounded wildcards in “write only” structures Try running these examples to see for yourself what happens
public class BoundedWildcards {
interface Output<T> { void print(T t); }
}
}
53
Bounded wildcards in “write only” structures Try running these examples to see for yourself what happens
public class BoundedWildcards {
interface Output<T> { void print(T t); }
}
}
54
Bounded wildcards in “write only” structures Try running these examples to see for yourself what happens
public class BoundedWildcards {
interface Output<T> { void print(T t); }
// this is ok
String s = writeAll(strings, output);
}
}
55
• Overloading and
overriding with type
parameters
• Reified types
56
Overloading with
type parameters class OverloadingDemo {
public <T> void method(T t) {
• Methods with type parameters can /* implementation */
be overloaded as usual. }
57
Overloading with
type parameters public class OverloadingDemo {
public void aMethod(List<String> strings) {
/* implementation */
• But is this code legal? }
58
Overloading with
type parameters
• But is this code legal? class OverloadingDemo {
public <T extends Number> void method(T arg) {
/* implementation */
}
60
Overriding with type parameters
• The converse is illegal – that is, if the
superclass signature is the erasure of class Super {
the subclass signature. public void printAll(Collection c) {
for (Object o : c)
• This is a name clash error. It is System.out.println(o.toString());
neither overloading nor overriding. }
}
61
• To reify means to make an abstract item more concrete or real. In
Reification
programming, this term is used to denote the opposite of type erasure.
• Java removes the parameter type, so the virtual machine (JVM) has
no information about, say, whether something is a List<Integer> or
a List<Integer>. It only sees the raw type.
• In Java,
arrays reify their component types, generic data structures do not.
primitive types, non-parameterized reference types, and the raw
62
Unlike other collection data types, arrays in Java reify their component.
Reification
•
63
Compile-time and runtime types
Type erasure leads to a difference in the type of a parameterized type as seen
by the compiler and as seen by the JVM at runtime. So, it makes sense to ask
the type of aList in this code:
List<String> aList = new ArrayList<>();
Depends on whether we are considering the list at compile-time, or runtime.
The compiler will see it as a “List of Strings”. The compiler will use this type
information (i.e., “of Strings”) to detect compile-time code errors such as attempting
to add() an element of a wrong type to the list.
The JVM at runtime will see it as an ArrayList with of the raw type.
The runtime type – called the actual type or real type – is more specific than the
64
Java arrays and • This leads to some “interesting” behavior in Java when it
comes to using arrays with type parameters.
their components • We already saw that even though String is a subtype of
Object, List<String> is not a subtype of
List<Object>.
65
Java arrays and There are two reasons why this error can’t be caught at
compile-time:
their components 1) Arrays are covariant in Java. That is, a T[] can
contain items of any subtype of T (including, of course,
T itself).
2) If A is a subtype of B, then A[] is a considered a
subtype of B[].
66
Java arrays and There are two reasons why this error can’t be caught at
compile-time:
their components 1) Arrays are covariant in Java. That is, a T[] can
contain items of any subtype of T (including, of course,
T itself).
2) If A is a subtype of B, then A[] is a considered a
subtype of B[].
67
Variance • The term “covariant” comes from the more general notion of
variance – which refers to how the type-relations between
sum data types (e.g., lists) relates to the type-relations
between their components.
• For example:
OCaml: a list of mammals would be a subtype of a list of
animals (covariance).
Java: a list of mammals has no subtype relation to a list
of animals (invariance).
68
© 2022 Ritwik Banerjee
We say the type parameter T is covariant in the generic
Variance of type R<T> if, whenever A is a subtype of B, R<A> is a
subtype of R<B>.
parametric types in In Java, there is no subtype-supertype relation between
Java List<Mammal> and List<Animal>, i.e., generics are
invariant.
69
<? extends Animal>
Java
As such, generics are covariant in their upper bounds. That is,
List<? extends Mammal> is a subtype of List<?
extends Animal>.
70
mal>
Mam
uper
s
<?
t>
Ca
r
pe
su
<?
72
Variance of return
types and argument
types
public interface Positionable {
• Variance plays a role in the flexibility void setPosition(Collection<? extends Point> points);
of overriding methods. For example, }
it is reasonable to ask the questions:
// This is not a valid override (even though List<? extends Point> is
if I’m overriding a method in a subclass, // a subtype of Collection<? extends Point>) since Java is invariant
can the method return a subtype? // in the argument types.
if I’m overriding a method in a subclass, public class Quadrilateral implements Positionable {
can the method accept arguments from a @Override
subtype? public void setPosition(List<? extends Point> points) {
/* implementation */
• The answers are language dependent. }
73