В чём разница между 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()
}
Это позволяет контролировать поведение в сложных случаях множественного наследования.