1.1 Types, Subtypes, and Polymorphism: What Programs Can Do
Languages express type rules in very different ways, but they begin with the same question: how can a program constrain which combinations of values and operations are valid before and during execution?
A type describes values and the operations allowed on them. Static checking rejects some invalid combinations before execution; its guarantees depend on the language and how you use it. A wrongly calculated price can still have the right type. The compiler has no idea which discount you meant to offer.
Timing of Static and Dynamic Type Checking
Static type systems establish type relationships for expressions before runtime, while dynamic languages defer more checks until execution. Both approaches may still involve runtime type information, reflection, and explicit checks.
The distinction between "strong" and "weak" typing lacks a universally consistent scale across languages and is often misused to describe implicit conversions, memory safety, or runtime validation. When comparing languages, it's better to state the specific rules: whether numeric and string values can be implicitly added, whether boundaries can be bypassed, and at which stage errors occur.
Nominal vs. Structural Determination of Compatibility
Nominal typing uses declared type identities and inheritance relationships. Save this as TypesDemo.java and run java TypesDemo.java with JDK 17 or later:
class TypesDemo {
interface Reader { byte[] read(); }
static final class FileReader implements Reader {
public byte[] read() { return new byte[] {1, 2, 3}; }
}
static final class Lookalike {
public byte[] read() { return new byte[] {1, 2, 3}; }
}
static int readLength(Reader reader) { return reader.read().length; }
public static void main(String[] args) {
System.out.println(readLength(new FileReader()));
// System.out.println(readLength(new Lookalike()));
}
}The output is 3. Uncommenting the second call causes a compilation error: Lookalike has the same read method but does not implement Reader.
Structural typing primarily checks for the required members without requiring the same interface declaration. Both approaches still need rules for mutability, visibility, and function parameter compatibility. Matching method names alone does not establish compatibility.
Structural equivalence does not imply semantic equivalence. Two types Meters and Seconds might each contain just a single floating-point number, yet they should not be interchangeable. Using distinct or newtype wrappers can preserve unit information within the type system.
Subtyping Is Not Code Reuse
The type rule S <: T allows an S in the corresponding positions expecting T. Behavioral substitution also requires the caller’s contract to hold: preconditions cannot become stronger, postconditions cannot become weaker, and invariants must remain intact. Ordinary compilers generally check types and method signatures without proving these business properties.
Inheritance can reuse implementation without ensuring behavioral substitutability. A square extending a mutable rectangle is the familiar example: its setWidth and setHeight behavior may violate the rectangle’s contract even though the compiler accepts the classes. Composition, immutable values, and narrow interfaces often make the contract easier to preserve.
Polymorphism has multiple sources
Ad-hoc polymorphism: overloading, type class/trait implementation
Parametric polymorphism: the same structure used for any T
Subtype polymorphism: dynamic dispatch via a common upper typeParametric polymorphism lets one implementation serve many types. An identity<T>(x: T): T that returns its input can be reasoned about without choosing T. In a language with reflection, type tests, exceptions, or null, however, the signature alone does not prove that behavior. A generic function that needs comparison, serialization, or addition should state the required capabilities.
Type Safety with Boundaries
External JSON, databases, FFI, reflection, and unsafe code can introduce inputs outside the static guarantees. A type declaration does not automatically validate them. Parse and validate at the boundary before constructing internal values:
untrusted bytes → parser → validated domain value → typed coreRaw types, any, and unchecked casts weaken static guarantees. Ordinary Java reference casts generally retain runtime checks, but those checks do not validate business rules. Encapsulate these operations and test their assumptions with boundary checks and failure cases.
The next lesson addresses the most confusing relationship in generics: Cat <: Animal does not automatically imply Box<Cat> <: Box<Animal>.
References
- Oracle, Java Language Specification: Types, Values, and Variables
- TypeScript, Type Compatibility