В чём разница между class, trait, abstract class?

В Scala class, trait и abstract class представляют разные механизмы описания типов и поведения объектов, и используются в зависимости от целей проектирования. Каждый из них имеет свою область применения, ограничения и особенности, особенно в контексте множественного наследования, композиции и абстракции.

class — обычный класс

Класс в Scala — это основная единица инкапсуляции логики и данных. Он может содержать поля (состояние), методы (поведение), конструкторы, вложенные классы, а также наследовать другие классы или реализовывать трейты.

Пример:

class Person(val name: String, val age: Int) {
def greet(): String = s"Hello, my name is $name"
}

Особенности:

  • Может иметь конструктор с параметрами.

  • Может быть наследован (extends).

  • Поддерживает переопределение методов (override).

  • Может быть мутабельным или иммутабельным.

  • Может реализовывать любое количество trait.

trait — интерфейс с реализацией

trait — это гибкий механизм повторного использования кода. Это нечто среднее между интерфейсом и абстрактным классом в Java, с возможностью содержать как абстрактные, так и конкретные методы.

Пример:

trait Greeter {
def greet(): String // абстрактный
def defaultGreet(): String = "Hello!" // с реализацией
}

Особенности:

  • Может содержать как абстрактные, так и реализованные методы.

  • Не может иметь конструктор с параметрами.

  • Не может содержать поля в конструкторе, только val/var, определённые внутри.

  • Используется в качестве механизма множественного наследования.

  • Можно миксовать несколько трейтов в один класс через with.

Пример использования:

class User(val name: String) extends Greeter {
def greet(): String = s"Hi, I'm $name"
}

abstract class — абстрактный класс

abstract class — это класс, который не может быть инстанцирован и может содержать как абстрактные, так и конкретные методы и поля. Используется, когда требуется определить общую базовую функциональность, но с возможностью указания конструктора и хранения состояния.

Пример:

abstract class Animal(val name: String) {
def makeSound(): String
def description: String = s"This is $name"
}

Особенности:

  • Может иметь конструктор с параметрами.

  • Может содержать и реализованные, и абстрактные члены.

  • Используется, когда нужен базовый тип с состоянием и частичной реализацией.

  • Поддерживает extends только для одного родителя.

  • Не может быть instantiated напрямую.

Сравнительная таблица

Характеристика class abstract class trait
Может быть инстанцирован ❌ (не напрямую)
--- --- --- ---
Может иметь конструктор ❌ (нет параметров конструктора)
--- --- --- ---
Может содержать реализацию
--- --- --- ---
Может содержать абстрактные члены ❌ (только через abstract или def)
--- --- --- ---
Множественное наследование ✅ (через with)
--- --- --- ---
Инициализация состояния ⚠️ только через val/var внутри
--- --- --- ---
Является типом
--- --- --- ---

Когда использовать class

  • Когда вы создаёте конкретный объект с поведением и состоянием.

  • Когда не требуется абстрактных членов.

  • Когда вам нужен простой инстанциируемый тип.

Пример:

class Rectangle(val width: Int, val height: Int) {
def area: Int = width \* height
}

Когда использовать abstract class

  • Когда вы хотите определить общую логику, но оставить часть реализации подклассам.

  • Когда нужен конструктор с параметрами.

  • Когда вы хотите совместимость с Java-кодом.

  • Когда нужен частичный шаблон с дефолтным поведением и состоянием.

Пример:

abstract class Shape(val color: String) {
def area: Double
}

Когда использовать trait

  • Когда вы хотите описать поведение, которое может быть подмешано в другие классы.

  • Когда вы хотите создать интерфейс с дефолтной реализацией.

  • Когда нужно множественное наследование логики.

Пример:

trait Logger {
def log(message: String): Unit = println(s"\[LOG\] $message")
}

И затем:

class Service extends Logger {
def run(): Unit = log("Service started")
}

Комбинирование trait и abstract class

Очень часто используется совместно:

trait Swimmable {
def swim(): String = "Swimming"
}
abstract class Fish(val species: String) {
def sound(): String
}
class Shark extends Fish("Shark") with Swimmable {
def sound(): String = "Silent"
}

Особенности JVM и трейтов

Хотя Scala позволяет использовать трейты как интерфейсы с реализацией, на уровне JVM trait реализуется через дополнительные классы (интерфейсы и helper-классы), что может в редких случаях вызывать сложности при интеграции с Java-кодом или сериализацией.

Наследование трейтов

Можно наследовать несколько трейтов:

trait A { def a(): String = "A" }
trait B { def b(): String = "B" }
class AB extends A with B

Порядок with влияет на линейную инициализацию и разрешение конфликтов.

Разрешение конфликта трейтов

Если несколько трейтов содержат одинаковые методы, требуется явное указание override:

trait T1 { def hello(): String = "T1" }
trait T2 { def hello(): String = "T2" }
class C extends T1 with T2 {
override def hello(): String = super\[T1\].hello() + " & " + super\[T2\].hello()
}

Это позволяет контролировать поведение в сложных случаях множественного наследования.