Difference between Inheritance and Subtyping

We say that a subclass is inheritance and subtyping. We have studied subtyping in Typing and Subtyping and it entails mainly two things:

  • Liskov Substitution
  • Polymorphism of the types

While inheritance is just the code reuse and specialization. Another example of code reuse can be the “has-a” relation composition, which couples two objects together. With aggregations, we can also see delegation calls, when you have a class, and call a method of that class. Inheritance-20251009104450885

Type relations between Mutable and Immutable

The cleanest solution between these two methods is just to separate the interfaces since:

  • If Mutable < Immutable, we violate behavioural subtyping.
  • If Immutable < Mutable, we violate the syntactic subtyping, since mutable is wider interface usually (not always!).

Java has a bad design because Immutable < Mutable.

Separation can be done in two ways usually:

  1. Aggregation with delegation (this is not a bad idea actually).
    1. This solution might lead to lots of boilerplate code.
    2. Access methods for local methods.
  2. Common superclass.
  3. We can also inherit without subtyping (like C++ does support that with private or protected inheritance, but not safely, also Eiffel).
    1. Inheritance might be sometimes unnecessary.

Suggestions for good code

  • We should not change to dynamically-bound methods, this can lead to many kinds of unexpected behaviours.
  • We should not inherit from classes that change often.
  • Use subclassing only if there is an “is-a” relation:
    • Focus on Syntactic and behavioral subtypes.
  • Do not rely on implementation details:
    • Use precise documentation (contracts where possible).
  • When evolving superclasses, do not mess around with dynamically-bound methods:
    • Do not add or remove calls, or change the order of calls.
  • Do not specialize superclasses that are expected to change often.

Method Resolution

Languages differ in method resolution in classes, you need to be able to resolve them correctly yourself, the main difference between the methods we explored are the following:

Java:

  • Chooses most type specific method among the hierarchy

  • This means method declaration in all possible overloads.

  • First looks at the static declaration of the function, if it’s not present, then you can already return the error

  • Check if the function is accessible at that level.

  • Dynamic binding, that is the standard in Java, but you could also have static methods. This is enough to generate the bytecode.

At run time we:

  • Compute receiver reference, then we can look at the class of the receiver (going up in the class hierarchy, and finding the most specific type).

C#

  • This is bound statically.
  • Chooses the most specific method in the class of the receiver.

Impotant to control the override if we want methods to behave effectively.

Binary Methods

For standard binary methods like equal to, we would like the subtype to have the same parameter of itself, but we know that this is not typesafe. We now want some methods to resolve these.

Type Tests

  • We could make explicit type tests, and get the more general value, for example:
boolean equals(Object o) {
    if (o instanceof Circle) {
        Circle c = (Circle) o;
        // check equality
    } else return false;
}

But this is very very tedious to write, and check everytime. Luckily nowadays we have copilots.

Problems- Tedious to write- Code is not extensible- Requires type cast

Visitor Patterns

class Shape {
    Shape intersect( Shape s ){
        return s.intersectShape( this );
    }
    Shape intersectShape( Shape s ){
        // general code for all shapes
    }
    Shape intersectRectangle( Rectangle r ){
        return intersectShape( r );
    }
}

We call more specialized methods.

Overloading

  • Overloading and Dynamic resolution of the method, but we are not solving the typesafety problem.
class Shape {
    Shape intersect( Shape s ) {
        // general code for all shapes
    }
} 

class Rectangle : Shape {
    Rectangle intersect( Rectangle r ) {
        // efficient code for two rectangles
    }
} 

static Shape intersect( Shape s1, Shape s2 ) {
    return( s1 as dynamic ).intersect( s2 as dynamic );
}

In this case, the method is resolved to the Rectangle only if both methods are Rectangle, if any of them, or if I remove the dynamic resolution of any of the methods then, it will call the Shape method.

Multiple Dispatch

This is relatively a new method to handle the dispatch of the methods. It is implemented in languages like, CLU, Cecil, Fortress, MultiJava, not any that I know of.

  • Problems
  • Performance overhead of method look-up at run time
  • Extra requirements are needed to ensure there is a“unique best method” for every call
  • I would also personally add that it would be difficult and expensive to implement everything.

This has a drawback.

Information Hiding

Basic Principles

  • Hide implementation details
  • Reduce dependencies between modules
  • Classes can be understood, reused,and modified in isolation.

One classical principle is having interfaces and their implementations, in this manner you expose only the contract, or interface in front of it.

Client Interface

Check out here for a discussion. But the rough idea is everything that you need to define the public methods.

  • Class name
    • is the unique identifier for a class. Think of it as the title of a blueprint. A class is a template for creating objects, and the class name is how you refer to that template.
  • Type parameters and their bounds
    • This concept relates to generics, which allow you to write classes and methods that can work with any data type.
  • Super-class
    • (also called a parent class or base class) is a class from which another class inherits. This is the core of the “is-a” relationship in inheritance.
  • Super-interfaces
    • is an interface that a class implements or that another interface extends. An interface is like a contract that specifies what a class must do (i.e., what methods it must have) but not how it must do them.
  • Signatures of exported methods and fields
    • This refers to the declarations of the methods and variables (fields) that are accessible from outside the class. “Exported” typically means public, protected, or (in some languages) package-private. anything that isn’t private.
  • Client interface of direct superclass
    • is the set of all public (and protected) methods and fields of the one class it directly inherits from.

Subclass Interface

This topic describes the special implementation-level relationship between a superclass (parent) and a subclass (child).

This “subclass interface” is a special set of tools given only to the subclasses to help them build upon the parent’s functionality. This is most often managed using the protected keyword. It’s the collection of fields and methods that a superclass exposes only to its subclasses, but not to the general public.

Friend Interface

The main idea is that with friends I can access all the private and protected methods of another class.

This set of concepts describes a pattern for managing tightly coupled classes—classes that must work together very closely to function.

This pattern is about breaking the normal rules of encapsulation (private, public) in a controlled, explicit way, usually for performance or to hide complex internal details.

Java and Eiffel Access Modifiers

Mostly three types of access modifiers that we list here. Also remember that overrides can only be about more visible methods, not less visible ones. The default access level in Java is package-private.

  • public: Client interface

  • protected: Subclass + friend interface (within the same package)

  • (Default/Package-Private): Friend interface (within the same package)

  • private: Implementation

    • This means we can access any variable within the same class only.
  • Eiffel: clients clause in feature declarations

    • feature { ANY }: Client interface (accessible to all classes)
    • feature { T }: Friend interface for class T (accessible only to class T and its descendants)
    • feature { NONE }: Implementation (accessible only by the object itself, using this, and only using the this keyword, and not other keywords, most languages cannot express this)
    • Note: All exports inherently include subclasses (descendants).

Overriding or Hiding?

class Super {...private void m( ){ System.out.println(Super); }public void test( Super v ){ v.m( ); }}
class Sub extends Super {public void m( ){ System.out.println(Sub); }}
Super v = new Sub( );v.test( v );
  • Private methods cannot be overidden, just hidden. We say override if the method was accessible from the subclass.

Fragile Base Class Problem

When we change the base class, we might break the subclasses. This makes modification of badly done abstractions very costly, for this reason it would be also suggested to have as few abstaction chains as possible. Here is an example of such a problem:

class C {int x;public void inc1( ){ this.inc2( ); }private void inc2( ){ x++; }}
class CS extends C {public void inc2( ) { inc1( ); }}
CS cs = new CS( 5 );cs.inc2( );System.out.println( cs.x );

If you change from private to protected or public, then you would see the problems!

Multiple Inheritance

The main idea here is when a single type can have several supertypes, which forms a DAG. This allows also for code re-use.

Simulating Multiple Inheritance

Languages like Java or C# do not allow for multiple inheritance. If that is needed, then it is simulated with aggregation.

Inheritance-20251016124123668

Image from the course slides

Problems with Multiple Inheritance

  • Diamond Problem: The classic problem where a class inherits from two classes that both inherit from a common superclass. This can lead to ambiguity about which inherited properties or methods to use. The main problem here is how to solve the ambiguity.

Resolving Ambiguities

  1. In C++ the ambiguities are resolved by the client (but its bad since the client needs to know the implementation details, which is not always granted).
  2. We can also override both methods, but this is ok only if we are specializing something similar, if their usage is different, then it’s not sure if this is the correct behaviour.
  3. Or we can Rename as Eiffel does.

But when we have a diamond it’s really difficult. How many fields should we instantiate? Should they be different for the two we have?

Virtual and Non-Virtual Inheritance

Non-Virtual means we will have two copies of the superclass, while virtual means we will have only one copy of the superclass. C++ supports both virtual and non-virtual inheritance. By default it uses non-virtual inheritance, but you can specify virtual inheritance using the virtual keyword, but this adds some runtime overhead (we need to add the virtual keyword in the inheritance part). When virtual is used, the baseclass is called only once, by the smallest subclass.

graph TD
    A --> B
    A --> C
    B --> D
    B --> A2["Stack: A/B"]
    C --> A3["Stack: A/C"]
    D --> A4["Stack: A/B/C/D"]
graph TD
    A --> B
    A --> C
    B --> D
    B --> A2["Stack: A (p)/B"]
    C --> A3["Stack: A(p)/C"]
    D --> A4["Stack: A, pointed by both C/D /B"]

Pros and Cons for Multiple-Inheritance

Pros Cons
Increases expressiveness Ambiguity resolution
- Explicit selection
- Merging
- Renaming
Avoids overhead of delegation pattern Repeated inheritance
- Complex semantics
- Initialization
Complicated!

Linearization

Mixins and traits provide a form of reuse- Methods and state that can be mixed into various classes- Example: Functionality to persist an object. Python, Ruby, Scala, Squeak Smalltalk. So it’s a different way to add functionality to classes.

Traits

Traints can extend exactly one superclass. They solve the diamond problem with linearization, we will analyze this phenomenon later. ▪ Traits are very dynamic,which complicates staticreasoning▪ Traits do not know howtheir superclasses getinitialized▪ Traits do not know whichmethods they override▪ Traits do not know wheresuper-calls are bound to


trait Backup extends Cell {
  var backup: Int = 0

  override def put( v: Int ) = {
    // Save the old value before calling the next put (super.put)
    backup = value
    super.put( v )
  }

  def undo = {
    super.put( backup )
  }
}

class Cell {
  var value: Int = 0

  def put( v: Int ) = {
    value = v
  }

  def get: Int = value
}

object Main1 {
  def main( args: Array[ String ] ) = {
    val a = new Cell with Backup
    a.put( 5 )  // value=5, backup=0
    a.put( 3 )  // value=3, backup=5 (value before put(3) was 5)
    a.undo      // value is set back to backup (5)
    println( a.get ) // Output: 5
  }
}

Mathematical Model for Linearization

The key concept to understanding the semantics of Scala traits: bring types in a linear order

  • Define overriding and super-calls according to this order (so easy resolution).

For a class or trait C extends C’ with C₁ … with Cₙ the linearization L(C) is C, L(Cₙ) • … • L(C₁) • L(C')

$$ \epsilon • B = B $$$$ (a, A) • B = \begin{cases} a, (A • B) & \text{if } a \neq B \\ A • B & \text{otherwise} \end{cases} $$

Method Selection with Linearization

Inheritance-20251023114640691

Slide from Course Slides

Problems with Traits