Привет, Хабр!

Меня зовут Дмитрий Мулло, я сотрудник Группы «Иннотех».

В этой статье на несложных примерах рассматриваются понятия объектно‑ориентированного программирования, такие как «класс» и «объект», помогающие структурировать код приложения.

Введение

Название языка Scala переводится как «масштабируемый язык», который по замыслу его разработчиков должен справляться с нарастающей нагрузкой от пользовательских потребностей в крупных системах. Иными словами на Scala можно создавать крупные и сложные системы при помощи суперсилы языка — сочетания функциональной и объектно‑ориентированной составляющей без перегрузки кода приложений «лишними» деталями, ухудшающими его читаемость. На этом языке разработан популярный фреймворк Apache Spark, один из инструментов дата‑инженера.

Система типов в Scala представлена классами, например Int для выполнения операций над целыми числами. А сами операции, например сложение и вычитание, реализованы внутри класса Int методами, которые так и называются + и -.

Везде в примерах используется синтаксис Scala 2.12.

Определение класса

В Scala класс формально определяется как шаблон, который может иметь название, начальное состояние и поведение.

Определение класса начинается ключевым словом class.

class Empty { // определение класса Empty
  // элементы класса
}

Начальное состояние и поведение определяются элементами класса, которыми могут быть:

  1. Поле

  2. Свойство

  3. Главный конструктор

  4. Дополнительный конструктор

  5. Метод

class Counter {              // класс Counter с главным конструктором без аргументов
  val x: Int = 0             // val-поле
  
  def this(y: Int) = {       // дополнительный конструктор
    this()                   // обязательный вызов главного конструктора
    x + y
  }
  
  def increment(): Unit = {  // метод increment
    x + 1
  }
}

Чтобы использовать класс, необходимо создать объект этого класса при помощи ключевого слова new:

val c = new Counter // создать объекта класса Counter
c.increment()       // вызвать метод increment

Если запустить этот код, то получим вывод, похожий на такой:

defined class Counter
c: Counter = Counter@4d85a26a

Какая польза от класса, который складывает 0 и 1, и не использует результаты дополнительного конструктора?

Попробуем улучшить класс Counter — уберем лишние элементы и будем сохранять увеличенное значение поля x:

class Counter {
  val x: Int = 0
  
  def increment(): Unit = x = x + 1
}

Но этот вариант не скомпилируется, так как мы пытаемся изменить значение val‑переменной:

val x: Int = 0
...
x = y // приведет к ошибке компиляции: reassignment to val

Коротко о val- и var-переменных
Для создания переменных в Scala используются ключевые слова val и var, после которых следует название переменной, на которое можно ссылаться, например:

// var-переменная x
var x = 0

// val-переменная msg
val msg = "Hi"

// val-переменная y
val y = x + 1

// доступ к значениям
// напечатать значение msg
println(msg)

// последующие изменения val-переменных
// ОШИБКА, не скомпилируется
msg = "Bye"
// ОШИБКА, не скомпилируется
y = -1

После названия переменной можно указать ее тип: val roundPi: Double = 3.14. Стоит упомянуть, что у Scala мощный компилятор, который чаще всего выводит тип переменной автоматически.

Исправим класс Counter -- определим изменяемое поле x:

class Counter {
  var x: Int = 0
  
  def increment(): Unit = x = x + 1
}

val counter = new Counter
counter.increment()
counter.increment()
counter.increment()
println(counter.x) // напечатает 3

В этом примере для вызова метода и для доступа к значению x используется оператор «точка»: c.increment()c.x.

Что если изменить значение поля xcounter.x = 100? Это сработает, но приведет к повреждению состояния counter:

counter.increment() // 1
counter.increment() // 2
// ... здесь еще код
// counter.x = -10
// ... здесь еще код
if (counter.x > 0) {
  // проверка counter.x или другие вычисления, которые ожидают положительного
  // значения counter.x
  ...
}

Нужен механизм ограничения доступа к внутреннему состоянию counter. Для этого используется модификатор доступа: private, который указывается в начале определения элемента класса. Если никакой модификатор не указан, то подразумевается public — доступ извне.

class Counter {
  private var x: Int = 0
  
  def increment(): Unit = x = x + 1
}

val counter = new Counter
counter.increment()
counter.increment()
println(counter.x)  // ОШИБКА, не скомпилируется:
                    // variable x in class Counter cannot be accessed in Counter

Теперь поле x не доступно извне класса, но и использовать его также нельзя.

Чтобы решить проблему доступа к закрытому полю, нужен элемент внутри класса, к которому есть доступ извне, но вызов которого не приводил бы к изменению состояния экземпляра класса.

Для этого определим в классе Counter свойство только для чтения x.

class Counter {
  private var _x: Int = _ // или явно: var _x: Int = 0
  
  def x: Int = _x         // свойство только для чтения
  
  def increment(): Unit = _x = _x + 1
}

val counter = new Counter
counter.increment()
counter.increment()
println(counter.x)  // запрашивается значение свойства
counter.x = 0       // ОШИБКА, не скомпилируется: value x_= is not a member of Counter
counter._x = 0      // ОШИБКА, не скомпилируется:
                    // variable _x in class Counter cannot be accessed in Counter

Свойства внутри класса определяются как и методы ключевым словом def от «definition». Поля определяются ключевыми словами val и var от «value» и «variable» соответственно.

Также можно определить свойство для чтения и записи, или просто -- свойство. Метод для записи определяется при помощи символа «_», за которым следует символ «=» в названии:

class Counter {
  private var _x: Int = _
  
  def x: Int = _x
  
  private var _step: Int = 1        // закрытое поле
                                    // свойство step это пара методов:
  def step: Int = _step             // метод для чтения
  
  def step_=(value: Int): Unit = {  // метод для записи
    _step = value
  }
  
  def increment(): Unit = _x = _x + step
}

Определить свойство только для записи в Scala невозможно, например, этот код не скомпилируется:

class Counter {
  private var _x: Int = _
  
  def x: Int = _x
  
  private var _step: Int = 1       // закрытое поле

                                   // метод для чтения step отсутствует
  
  def step_=(value: Int): Unit = { // метод для записи
    _step = value
  }
  
  def increment(): Unit = _x = _x + _step
}

val counter = new Counter
counter.step = 10 // ОШИБКА, не скомпилируется: value step is not a member of Counter

Вопрос: можно ли определить свойство как var x: Int = _x в классе Counter?

Ответ: формально -- нет, так как свойства определяются ключевым словом def, но даже если и можно было бы, то смысла в этом нет, так как главный конструктор выполнит все инструкции в определении класса до того, как можно будет обращаться к его свойствам.

class Counter {
  println("start main ctor")
  
  private var _x: Int = _
  
  val x: Int = _x // присвоит x значение _x и больше его не изменит, сколько бы
                  // к нему ни обращались
  
  private var _step: Int = 1
  
  def increment(): Unit = _x = _x + _step
  
  println("end main ctor")
}

val counter = new Counter
counter.increment()
counter.increment()
println(counter.x)

Напечатает результат, похожий на этот:

defined class Counter
start main ctor
end main ctor
counter: Counter = Counter@464b627c
0

Предположим, что необходимо уметь задавать начальное значение для Counter при создании его экземпляров. Здесь возможны варианты, но например, определим главный конструктор с параметрами:

class Counter(private var _x: Int = 0) {  // главный конструктор с параметром:
                                          // Counter(Int)
  def x: Int = _x
  
  private var _step: Int = 1
  
  def step: Int = _step
  
  def step_=(value: Int): Unit = _step = value
  
  def increment(): Unit = _x = _x + step
}

Или можно определить главный конструктор без параметров и необходимые дополнительные («auxilliary») конструкторы:

class Counter {  // неявный главный конструктор без параметров:
                 // Counter()
  println("start main ctor")
  
  private var _x: Int = 0
  
  def x: Int = _x
  
  private var _step: Int = 1
  
  def step: Int = _step
  
  def step_=(value: Int): Unit = _step = value
  
  def increment(): Unit = _x = _x + step
  
  def this(initX: Int) = {  // первый дополнительный конструктор: Counter(Int)
    this()                  // обязательный вызов главного конструктора первым
    println("start aux ctor 1")
    _x = initX
    println("end aux ctor 1")
  }
  
  def this(initX: Int, initStep: Int) = {  // второй дополнительный конструктор: Counter(Int, Int)
    this(initX)                            // обязательный вызов дополнительного конструктора,
                                           // но можно вызвать и главный конструктор
    println("start aux ctor 2")
    _step = initStep
    println("end aux ctor 2")
  }
  
  println("end main ctor")
}

val counter = new Counter(10, 2)
counter.increment()
println(counter.x)

Напечатает результат, похожий на этот:

defined class Counter
start main ctor
end main ctor
start aux ctor 1
end aux ctor 1
start aux ctor 2
end aux ctor 2
counter: Counter = Counter@64d9850
12

А если требуется закрыть доступ к главному конструктору извне? Воспользуемся модификатором доступа private:

class Counter private(private var x: Int = 0) {
  // определение класса Counter
}

Закрытыми могут быть и методы:

class Counter {
  private def watchDog(): Unit = // ...
  
  def increment(): Unit = {
    watchDog()
    // ...
  }
}

Определение объекта

Объект в Scala определяет единственный экземпляр указанного класса, который может иметь начальное состояние и поведение:

object Empty { // определение объекта Empty
  // элементы класса
}

Поскольку это определение класса специального вида, то оно мало чем отличается от обычного класса. Объекты не поддерживают конструкторы с параметрами, но обладают главным конструктором и часто используются как объекты-компаньоны своих классов.

Конструктор объекта вызывается при первом обращении к нему:

class Counter private(_init: Int) {
  println("Counter class ctor")
  
  private def show(): Unit = println("inside Counter class ctor")
  
  show()
  
  private var _x: Int = _init
  
  def current: Int = _x
  
  def increment(step: Int = 1): Unit = {
    _x = Counter.add(_x, step)
  }
  
  println("Counter class ctor END")
}

object Counter {
  println("Counter object ctor")
  
  private def show(): Unit = println("inside Counter object ctor")
  
  show()
  
  def apply(init: Int): Counter = {
    new Counter(init)
  }

  private def add(a: Int, b: Int): Int = a + b
  
  private def check(): Unit = {
    require(add(1, 2) == 3,
    """
      |ERROR!
      |self check: 1 + 2 must be 3, but not today!
      |""".stripMargin)
    println("self check is OK")
  }
  
  check()
  
  println("Counter object ctor END")
}

val counter = Counter(100) // "синтаксический сахар"
// эквивалентно вызову Counter.apply(100)
// при печати видно вызов конструктора объекта
counter.increment()
counter.increment()
counter.increment()
println(counter.current)
val another = Counter(200) // вызова конструктора объекта нет
another.increment(2)
another.increment(2)
another.increment(2)
println(another.current)

Напечатает приблизительно следующее:

Counter object ctor
inside Counter object ctor
self check is OK
Counter object ctor END
Counter class ctor
inside Counter class ctor
Counter class ctor END
counter: Counter = Counter@496c3c91
103
Counter class ctor
inside Counter class ctor
Counter class ctor END
another: Counter = Counter@340823cf
206

Классы и объекты в Java

Java-разработчик может определить класс Counter и аналог объекта-компаньона при помощи статического вложенного класса:

class Counter {
    private void show() {
        System.out.println("inside Counter class ctor");
    }
  
    private int x;                  // закрытое поле

    private int getX() {            // метод для чтения
        return x;
    }

    private void setX(int value) {  // метод для записи
        x = value;
    }

    public int current() {
        return getX();
    }

    public void increment(int step) {
        setX(CounterObject.add(getX(), step));
    }

    public void increment() {
        setX(CounterObject.add(getX(), 1));
    }

    private Counter(int init) {  // запрещаем прямое создание объектов
        System.out.println("Counter class ctor");
        show();
        setX(init);
        System.out.println("Counter class ctor END");
    }

    /**
     * одна из реализаций шаблона "Одиночка" (Singleton)
     */
    static class CounterObject {
        private static CounterObject instance;

        private CounterObject() {
        }

        public static synchronized CounterObject getInstance() {
            if (instance == null) {
                check();
                System.out.println("call ctor inside CounterObject.getInstance");
                instance = new CounterObject();
            }
            return instance;
        }

        public Counter Counter(int init) {  // аналог Counter.apply(init: Int)
            return new Counter(init);
        }

        private static int add(int a, int b) {
            return a + b;
        }

        private static void check() {
            assert add(1, 2) == 3 : "ERROR! self check: 1 + 2 must be 3, but not today!";
        }
    }
}

class Main {
    public static void main(String[] args) {
        // только в Java
        Counter.CounterObject cObj = Counter.CounterObject.getInstance();
        Counter counter = cObj.Counter(100);  // на Scala: Counter(100)
        counter.increment();
        counter.increment();
        counter.increment();
        System.out.println(counter.current());
        Counter another = cObj.Counter(200);  // на Scala: Counter(200)
        another.increment(2);
        another.increment(2);
        another.increment(2);
        System.out.println(another.current());
    }
}

Напечатает следующее:

call ctor inside CounterObject.getInstance
Counter class ctor
inside Counter class ctor
Counter class ctor END
103
Counter class ctor
inside Counter class ctor
Counter class ctor END
206

Скомпилируем класс Counter на Scala, а затем выведем идентификаторы в скомпилированном классе. Получим вывод, похожий на этот:

> scalac Counter.scala
> javap -private Counter
Compiled from "Counter.scala"
public class Counter {
  private int _x;            // в коде: private var _x: Int
  public static Counter apply(int);
  private void show();
  private int _x();          // компилятор: def _x(): Int
  private void _x_$eq(int);  // компилятор: def _x_=(Int)
  public int current();
  public void increment(int);
  public int increment$default$1();
  public Counter(int);
}

В распечатке видно, что компилятор создает методы чтения и записи для закрытого поля _x в байт-коде класса Counter. Помимо класса Counter скомпилирован его объект-компаньон, к названию которого добавлен символ «$»:

> javap -private Counter$
Compiled from "Counter.scala"
public final class Counter$ {         // теперь это класс Counter$
  // ссылка на единственный экземпляр класса Counter$
  // доступ к нестатическим методам: Counter$.MODULE$
  public static final Counter$ MODULE$;
  public static {};                   // блок статической инициализации
  private void show();
  public Counter apply(int);
  public int Counter$$add(int, int);
  private void check();
  private Counter$();                 // закрытый конструктор класса Counter$
}

Объект Scala в байт-коде объявлен классом, потому что в Java отсутствует точная реализация объекта Scala. В распечатке видно дополнительное статическое поле MODULE$, которое ссылается на единственный экземпляр класса Counter$, блок статической инициализации и закрытый конструктор Counter$.

Заключение

Разработчик, знакомый с Java, может обратить внимание на лаконичность языка при разработке классов. Scala обладает настолько мощным компилятором, что он автоматически добавит необходимые конструкции в классы приложения, для работы которого понадобится стандартная Java-машина. Кроме того, на Scala приятно программировать.

Комментарии (0)