Что такое companion object?


В Scala companion object — это объект, который определяется в том же исходном файле и с тем же именем, что и класс или trait, и обладает особым статусом: он имеет доступ к приватным членам класса, а класс, в свою очередь, — к приватным членам объекта. Это концепция, уникальная для Scala (в Java нет прямого аналога), и она используется для реализации фабричных методов, хранения констант, описания apply/unapply-методов, имплиситов и многого другого.

Синтаксис и структура

class User(private val name: String)
object User {
def apply(name: String): User = new User(name)
}

Здесь object User — это companion object класса User. Поскольку он определён в том же файле и имеет то же имя, он становится "попутчиком" (companion), и у него есть доступ к private val name.

Основные особенности companion object

1. Объект с тем же именем, что и класс

Scala требует, чтобы companion object и соответствующий класс находились в одном исходном файле. Иначе это будут два независимых элемента.

2. Доступ к private-членам класса

Companion object может обращаться к private и protected членам своего класса:

class Account(private var balance: Double)
object Account {
def showBalance(acc: Account): Double = acc.balance // работает
}

Также и класс имеет доступ к private членам объекта.

Отличие от Java

В Java нет companion object. Обычно аналог реализуется через статические методы:

public class User {
private String name;
public static User create(String name) {
return new User(name);
}
}

В Scala вместо static используется object, который представляет синглтон.

Использование companion object

1. Фабричный метод (apply)

Часто companion object содержит метод apply, позволяющий создавать объекты без явного вызова new.

class Person(val name: String)
object Person {
def apply(name: String): Person = new Person(name)
}
// можно создать без new
val p = Person("Alice") // вызывает Person.apply("Alice")

2. Разбор данных (unapply) и pattern matching

Companion object может содержать unapply метод для использования в match:

class Email(val user: String, val domain: String)
object Email {
def unapply(str: String): Option\[(String, String)\] =
str.split("@") match {
case Array(user, domain) => Some((user, domain))
case _ => None
}
}
val email = "john@example.com"
email match {
case Email(user, domain) => println(s"User: $user, Domain: $domain")
}

3. Имплиситы в companion object

Если вы определяете implicit-значения в companion object, они будут автоматически доступны при импорте класса:

trait Show\[A\] {
def show(a: A): String
}
case class Cat(name: String)
object Cat {
implicit val showCat: Show\[Cat\] = new Show\[Cat\] {
def show(a: Cat): String = a.name
}
}
def printShow\[A\](a: A)(implicit s: Show\[A\]): Unit =
println(s.show(a))
printShow(Cat("Tom")) // найдет имплисит из Cat

4. Хранение констант и утилит

Часто в companion object помещают:

  • Константы (val)

  • Утилитарные методы

  • Общие конфигурации, default-значения

class Config
object Config {
val DefaultPort = 8080
def load(): Config = new Config()
}

5. Работа с sealed trait и ADT (алгебраические типы данных)

Companion object часто используют для перечисления возможных значений:

sealed trait Color
object Color {
case object Red extends Color
case object Green extends Color
case object Blue extends Color
}

Связь с классом: shared namespace

Класс и объект делят одно пространство имён, но содержат разные сущности. Это значит, что нельзя создать val, var или метод в классе и объекте с одинаковым именем:

class Demo {
def foo = 42
}
object Demo {
def foo = "hello" // ошибка: имя уже используется
}

Можно ли определить только объект?

Да, если вы не нуждаетесь в экземплярах класса. Тогда object становится просто синглтоном:

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

Компаньонские объекты и Scala 3

С выходом Scala 3 (Dotty) добавились возможности given/using (новая система имплиситов), которые также часто располагаются в companion object, но синтаксис меняется:

given Show\[Int\] with {
def show(a: Int): String = a.toString
}

Summary примеров с применением companion object

class Car private (val brand: String) // приватный конструктор
object Car {
def apply(brand: String): Car = new Car(brand)
def isElectric(car: Car): Boolean =
car.brand == "Tesla"
}
  • Инкапсуляция конструктора.

  • Альтернативная инициализация.

  • Логика, связанная с объектом — вне класса.

Отладка и тестирование companion object

  • Можно обращаться напрямую: Car.apply("Tesla"), Car.isElectric(...).

  • Можно использовать mock-библиотеки типа Scalamock для изоляции логики.

  • Часто companion object тестируется отдельно от класса.