Julia's Type Hierarchy
Every value in Julia has exactly one type, and every type in Julia participates in a single-rooted hierarchy where Any sits at the top as the supertype of all types. Types are split into abstract types, which exist purely to organize the hierarchy and cannot be instantiated (you can never create a value whose type is literally Real or Number), and concrete types, which are the leaves of the tree and can actually be instantiated — Float64, Int64, and String are all concrete. The subtype operator <: expresses these relationships, so Float64 <: AbstractFloat <: Real <: Number <: Any all hold true, and functions like supertype(Float64) and subtypes(Real) let you inspect the hierarchy interactively.
Cricket analogy: Julia's type hierarchy is like the ICC's format classification — 'Cricket' (Any) is the broad root, 'Limited Overs' is an abstract grouping you can't literally play, and 'T20' or 'ODI' are the concrete formats actually played on the field, just like Float64 or Int64 are the concrete types actually instantiated.
Defining Abstract Types and Subtyping
You declare an abstract type with abstract type Animal end, and then attach concrete or further-abstract types to it using the <: subtyping syntax, as in struct Dog <: Animal name::String end and struct Cat <: Animal name::String end. This hierarchy exists purely for organizing dispatch and reasoning about code — Animal itself carries no fields and can't be constructed — but it lets you write functions like function speak(a::Animal) that accept any current or future subtype of Animal, and use isa(fido, Animal) or fido isa Dog to check membership in the hierarchy at runtime.
Cricket analogy: abstract type Bowler end with struct Pacer <: Bowler and struct Spinner <: Bowler is like the team classifying all its bowling options under one selection category so the captain's function to pick an over works for any current or future bowler type without rewriting the team sheet logic.
abstract type Animal end
struct Dog <: Animal
name::String
end
struct Cat <: Animal
name::String
end
speak(a::Dog) = "$(a.name) says Woof!"
speak(a::Cat) = "$(a.name) says Meow!"
function greet(a::Animal)
println(speak(a))
end
greet(Dog("Fido")) # dispatches to the Dog method
greet(Cat("Whiskers")) # dispatches to the Cat method
fido_is_animal = Dog("Fido") isa Animal # trueMultiple Dispatch
Multiple dispatch means a function call's implementation is chosen based on the types of all its arguments, not just one privileged 'receiver' as in classical single-dispatch object orientation — calling collide(a, b) can select a different method depending on whether (a, b) is (Circle, Circle), (Circle, Rectangle), or (Rectangle, Rectangle). Julia resolves this at each call by finding the most specific applicable method among all methods defined for that function name, and calling methods(f) lists every method currently defined for a generic function f; when two methods are equally specific for a given call and neither is more specific than the other, Julia raises a MethodError for ambiguity rather than guessing.
Cricket analogy: Multiple dispatch on collide(a, b) is like a DRS review's outcome depending on the combination of both the mode of dismissal claimed AND the type of delivery bowled simultaneously — a caught-behind review is judged differently from an LBW review, just as dispatch differs per argument-type combination.
You can inspect what method Julia will actually call for given argument types using @which f(args...), and see the full list of applicable methods with methods(f) — both are invaluable for understanding dispatch behavior when debugging unexpected method selection.
Type Stability and Performance
A function is type-stable when the type of its return value can be inferred from the types of its inputs alone, without depending on runtime values — this lets Julia's compiler generate specialized machine code ahead of time instead of falling back to slow, boxed, dynamically-dispatched operations. The macro @code_warntype prints the compiler's inferred types for a function, highlighting any Any or Union types in red as a warning sign of instability; a common cause is a container like Vector{Any} (or an abstract-typed struct field) forcing every element access to be dynamically typed, and a common fix is ensuring containers and fields use concrete, consistent element types.
Cricket analogy: A type-stable function is like a bowler whose delivery is always predictably a yorker at the death overs given the match situation, letting the batsman's brain 'compile' a fixed response in advance; an unpredictable mixed-delivery bowler forces the batsman into slower, reactive decision-making each ball, like dynamic dispatch.
A Vector{Any} — an array whose element type is the abstract Any — is one of the most common accidental performance bugs in Julia. Every read from such an array requires the compiler to insert a dynamic type check and dispatch, often making loops 10-100x slower than the equivalent operation on a concretely-typed Vector{Float64} or Vector{Int}; prefer building typed containers or using Union{T,Nothing} for the narrower case of 'a value or nothing' instead of falling back to Any.
- Every Julia value has exactly one type, all rooted under the single supertype Any.
- Abstract types organize the hierarchy and cannot be instantiated; concrete types are the leaves you actually construct.
- The <: operator expresses subtyping; supertype() and subtypes() let you inspect the hierarchy.
- abstract type declarations plus <: let you write functions that generically accept any current or future subtype.
- Multiple dispatch selects a method based on ALL argument types, not a single privileged receiver.
- Ambiguous method calls (no single most-specific method) raise a MethodError rather than guessing.
- @code_warntype reveals type instability (Any/Union return types), a major cause of slow Julia code.
Practice what you learned
1. What is the single root of Julia's type hierarchy?
2. Can you construct a value whose type is literally an abstract type like Real?
3. How does Julia's multiple dispatch differ from classical single-dispatch object orientation?
4. What does @code_warntype help diagnose?
5. What happens when two methods of a generic function are equally specific for a given call, with neither more specific than the other?
Was this page helpful?
You May Also Like
Custom Types and Structs
Learn how to define composite types with struct, control mutability and construction, use parametric fields for performance, and attach behavior via multiple dispatch.
Arrays in Julia
Learn how Julia's built-in Array type stores, indexes, and mutates collections of data, and why column-major memory layout matters for performance.
Broadcasting in Julia
Learn Julia's dot-syntax broadcasting mechanism, how chained operations fuse into allocation-free loops, and how to make custom types broadcast correctly.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics