What is the difference between a trait and an abstract class?

← Classes and OOP · Ref: Q110

In EK9, traits and abstract classes both define contracts, but differ in state, multiplicity, and purpose. Traits provide pure behaviour. Abstract classes provide shared state and partial implementation.

KEY DIFFERENCES

State: Abstract classes can hold properties. Traits cannot hold mutable state.
Multiple: A class can implement many traits but extend only one abstract class.
Purpose: Traits define what a type CAN DO. Abstract classes define what a type IS.

WHEN TO USE EACH

Use a trait when: you want behaviour shared across unrelated classes, you need multiple implementations, or you want delegation support.
Use an abstract class when: you need shared state (properties), you need constructors, or you have a genuine type hierarchy.

CAN YOU COMBINE THEM

Yes. A class can extend an abstract class AND implement traits:

  Car extends Vehicle with trait of Insurable, Trackable

This combines the state from Vehicle with behaviours from Insurable and Trackable.

DECISION GUIDE

Ask: does the contract need state? If yes, abstract class. Does the contract need to be mixed into multiple unrelated types? If yes, trait.

See Q103 for abstract classes. See Q106 for traits. See Q119 for constructs overview.

Example

defines module qa.oop.traitvsabstract

  defines trait

    Insurable
      premium() as pure
        <- rtn as Float: 100.0

      override operator ? as pure
        <- rtn as Boolean: true

    Trackable
      location() as abstract
        <- rtn as String?

      override operator ? as pure
        <- rtn as Boolean: true

  defines class

    Vehicle as abstract
      make <- String()
      year <- Integer()

      Vehicle()
        ->
          make as String
          year as Integer
        this.make: make
        this.year: year

      describe()
        <- rtn as String: `${make} (${year})`

      speed() as abstract
        <- rtn as Float?

      default operator ?

    Car extends Vehicle with trait of Insurable, Trackable
      topSpeed <- 0.0

      Car()
        ->
          make as String
          year as Integer
          topSpeed as Float
        super(make, year)
        this.topSpeed: topSpeed

      override speed()
        <- rtn as Float: topSpeed

      override premium() as pure
        <- rtn as Float: 200.0

      override location()
        <- rtn as String: "garage"

      default operator ?

    Truck extends Vehicle
      payload <- 0.0

      Truck()
        ->
          make as String
          year as Integer
          payload as Float
        super(make, year)
        this.payload: payload

      override speed()
        <- rtn as Float: 80.0

      default operator ?

  defines program

    TraitVsAbstractDemo()
      stdout <- Stdout()

      // === ABSTRACT CLASS: shared state ===

      car <- Car("Toyota", 2024, 130.0)
      stdout.println(`Car: ${car.describe()}, speed: ${car.speed()}`)

      // === TRAIT: behaviour contract ===

      stdout.println(`Premium: ${car.premium()}`)
      stdout.println(`Location: ${car.location()}`)

      // === Abstract without trait ===

      truck <- Truck("Ford", 2023, 5000.0)
      stdout.println(`Truck: ${truck.describe()}, speed: ${truck.speed()}`)

Common mistakes

E05120 — When implementing an abstract method from Vehicle, both Car and Truck must use 'override speed()'. Omitting 'override' triggers E05120. See ek9 -h E05120 for details.

Incorrect:

speed()

Correct:

override speed()

E05120 — When overriding a default trait method from Insurable, the 'override' keyword is mandatory. Car must use 'override premium()' to provide its custom implementation. See ek9 -h E05120 for details.

Incorrect:

premium() as pure

Correct:

override premium() as pure

E50060 — EK9 has no toString() method. Use the $ prefix operator or string interpolation for string conversion. See ek9 -h E50060 for details.

Incorrect:

stdout.println(car.toString())

Correct:

stdout.println(`Car: ${car.describe()}, speed: ${car.speed()}`)

E50060 — The method is speed(), not getSpeed(). EK9 uses descriptive method names without Java-style 'get' prefix. See ek9 -h E50060 for details.

Incorrect:

stdout.println(truck.getSpeed())

Correct:

stdout.println(`Truck: ${truck.describe()}, speed: ${truck.speed()}`)
Other ways to ask this
  • When should I use a trait vs an abstract class in EK9?
  • How do traits compare to abstract classes in EK9?
  • Should I use a trait or an abstract base class?

Coming from another language?

Java: interfaces (no state) vs abstract classes (state + partial impl), single inheritance for classes. Python: ABCs for abstract, mixins for trait-like behaviour, multiple inheritance allowed. Rust: only traits exist, no abstract classes, traits can have default methods. Go: only interfaces, no abstract classes, no state in interfaces. Kotlin: interfaces with properties (no backing fields) vs abstract classes, similar trade-offs. EK9: traits for behaviour contracts (no mutable state), abstract classes for shared state and hierarchy, combinable on same class.

Keywords: combine, behaviour, decision, class, multiple, object-oriented, abstract, state, oop, difference, trait, hierarchy