Краткое руководство по некоторым синтаксическим различиям между Scala и Java.

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

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

Давайте погрузимся.

Объявления типов идут после имен переменных

В Java мы объявляем типы, а затем имена переменных, например:
Animal a = getAnimal();

В Scala после идет тип:
val a: Animal = getAnimal();

Переменные объявлены как неизменяемые (val) или изменяемые (var).

Вместо того, чтобы начинать объявление переменной с имени класса, как мы это делаем в Java, мы начинаем их с val или var в Scala.

val (сокращение от значение) указывает, что переменная неизменяема (аналогично final в Java):
val a: Animal = getAnimal(); // `a` cannot be subsequently reassigned

var (сокращение от variable) указывает, что переменная изменяема:
var a: Animal = getAnimal(); // `a` can be subsequently reassigned

Scala поддерживает неизменность, поэтому чаще всего используется val.

Объявления типа обычно можно не указывать.

В Scala очень строгий вывод типов. Поэтому нам редко нужно явно объявлять тип. Это одна из причин, почему при объявлении переменной класс идет последним; его легко просто пропустить:
var a = getAnimal(); // `a` is still an instance of Animal

Точка с запятой не обязательна для обозначения конца строки.

Точка с запятой действительно нужна только тогда, когда несколько операторов встречаются в одной строке:
var a = getAnimal(); println(a)

Мы можем и почти всегда можем опускать точки с запятой в конце строк:
var a = getAnimal()

Методы и функции объявляются с помощью ключевого слова def.

Методы и функции (функция похожа на метод, но не принадлежит ни к какому классу) объявляются с помощью ключевого слова def. Их возвращаемые типы объявляются после имени функции и двоеточия. И их тела обычно идут после знака равенства:

def getAnimal(): Animal = {
  // body goes here ...
}

При вызове метода или функции без аргументов скобки можно опустить.

В Java мы всегда используем круглые скобки при вызове метода:

Animal a = getAnimal();

При вызове функции или метода Scala, которые не принимают аргументов, скобки можно опустить:

val a = getAnimal()
// or
val a = getAnimal

Методы и функции не должны ничего явно возвращать

Последнее значение или выражение в методе или функции становится неявным возвращаемым значением:

def add(x: Int, y: Int): Int = {
  x + y  // the sum of x and y will be implicitly be returned
}

Таким образом, хотя ключевое слово return существует в Scala, оно почти никогда не используется.

Единица означает недействительность

Если метод Java не имеет возвращаемого значения, он объявляется как возвращающий void:

public void output(String s) {
  System.out.println(s);
}

В Scala тип возвращаемого значения будет объявлен как Unit:

def output(s: String): Unit = {
  println(s)
}

Методы и функции редко нуждаются в объявлении своего возвращаемого типа.

Строгий вывод типов в Scala означает, что нам редко нужно объявлять возвращаемый тип наших функций / методов:

def add(x: Int, y: Int) = {
  x + y  // The compiler infers that an Int will be returned
}

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

  • метод абстрактный
  • мы хотим вернуть суперкласс предполагаемого возвращаемого типа
  • мы просто хотим быть более ясными
def getAnimal(): Animal {
  // if we omitted the return type, Scala would infer Dog
  new Dog() 
}

Для методов и функций фигурные скобки не требуются.

Если тело метода или функции не занимает более одной строки (или выражения), фигурные скобки можно опустить:

def add(x: Int, y: Int) = 
  x + y
// or
def add(x: Int, y: Int) = x + y

Методы и функции могут быть вызваны с именованными аргументами

В Java при вызове метода, который принимает несколько параметров, мы не можем включать имена параметров. Вместо этого мы просто передаем значения в том порядке, в котором они объявлены в методе:

int i = add(6, 8);

В Scala мы можем выбрать либо вызов метода / функции «по-Java», либо включение всех или некоторых имен параметров:

val i = add(6, 8)
// or
val i = add(x = 6, y = 8)
// or
val i = add(x = 6, 8)
// note: below will not compile:
val i = add(6, y = 8)

Черты вместо интерфейсов

В то время как Java предлагает интерфейсы, Scala предлагает особенности. Подобно интерфейсам Java, трейты могут включать абстрактные методы. Как и стандартные методы интерфейсов Java, трейты могут предлагать реализованные методы. В отличие от интерфейсов, трейты также могут включать переменные.

trait Shape {
  val point: Point
  def print() // no implementation, so implicitly abstract
}

`extends` и` with`

Как и в Java, классы Scala не могут расширять несколько родительских классов. Также как и в Java, классы Scala могут реализовывать (или, говоря языком Scala, расширять) несколько свойств.

Когда класс Scala расширяет класс или черту, используется ключевое слово extends. Если класс затем расширяет какие-либо дополнительные черты, то используется ключевое слово with:

class Shape { ... }
trait Drawable { ... }
trait Movable { ... }
class Circle extends Shape with Drawable with Movable
class Raster extends Drawable with Movable

Классы объявляются, а их члены сразу же указываются в скобках.

В то время как мы могли бы определить простой класс в Java следующим образом:

public class Foo {
  private String a;
  private int b;
  // getters, setters, constructors, etc
}

Обычно мы определяем класс Scala в одной строке, например:

class Foo(a: String, b: Int) { }

Скобки являются необязательными, если не нужно объявлять дополнительные члены, методы и т. Д.:

class Foo(a: String, b: Int)

Классы случаев представляют собой специализированные классы данных

В Java 14 появилась концепция записей. Если вы знакомы с записями, то быстро познакомитесь с кейс-классами. В противном случае классы case являются специальными классами данных. Их синтаксис похож на обычные классы:

case class Foo(a: String, b: Int)

Однако компилятор автоматически сгенерировал стандартные методы, необходимые для класса данных, такие как equals(), hashCode() и toString() методы, геттеры и т. Д.

Он также генерирует «сопутствующий объект» с помощью методов apply() и unapply() (мы обсудим их чуть позже).

Универсальные типы обозначаются квадратными скобками ([]), а не угловыми скобками (<>)

В Java мы обозначаем общий тип угловыми скобками:
List<String> list;

В Scala квадратные скобки используются для обозначения универсальных типов:
List[String] list

Строковая интерполяция выполняется строковыми литералами с префиксом `s`

В Java для быстрого объединения строк мы используем символ плюса (+):
System.out.println("Hello, "+ firstName + " "+ lastName + "!");

В Scala мы выполняем строковую интерполяцию, добавляя к строковому литералу префикс s и ссылаясь на переменные со знаком доллара ($):
println(s"Hello, $firstName $lastName!")

Для доступа к вложенному значению или выражению используются фигурные скобки:

println(s"Hello, ${name.first} ${name.middle.getOrElse("")} ${name.last}!")

Анонимные функции похожи на лямбды Java, но с символом = ›

Java предоставляет синтаксический сахар при создании лямбда-функций:
list.foreach(item -> System.out.println("* " + item));

В Scala мы используем символ => с анонимными функциями:
list.foreach(item => println(s"* $item"))

Подчеркивание служит заполнителем

Подчеркивание используется Scala в качестве заполнителей. Вы увидите подчеркивание в нескольких различных случаях использования, в том числе:

Анонимные функции:
Вместо объявления переменной в анонимной функции:
list.foreach(item => println(item))

Мы можем опустить переменную item и заменить ее знаком подчеркивания:
list.foreach(println(_))

Обратите внимание: как и в случае с Java, мы можем упростить строку выше, указав ссылку на функцию println (которая неявно принимает текущую переменную в качестве аргумента):
list.foreach(println)

Сопоставление с образцом
. Когда мы выполняем сопоставление с образцом, мы пытаемся сопоставить экземпляр с его типом. Скажем, у нас есть такой класс case:

case class Dog(name: String) extends Animal

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

a match {
  case Dog(name) => println(s"Found a dog named $name");
  ...
}

Или нас может не волновать имя Собаки. Конструктор Dog по-прежнему принимает аргумент, поэтому мы можем просто использовать подчеркивание в качестве заполнителя:

a match {
  case Dog(_) => println(s"Found some dog");
  ...
}

Точно так же мы можем захотеть узнать, нашли ли мы Dog или что-то еще, кроме Dog. Снова мы использовали бы подчеркивание:

a match {
  case Dog(_) => println(s"Found a dog");
  case _ => println("Found something other than a dog")
}

Объекты-компаньоны вместо статики

В Java есть ключевое слово static, которое описывает переменные и методы, которые привязаны к определенному классу, а не экземплярам объекта.

public class UserManager {
  public static void save(User u) {
    //...
  }
}
// we can invoke that static method via:
UserManager.save(new User("Pat"));

Scala не предлагает ключевое слово static. Однако с помощью Scala мы можем создавать объекты, которые фактически предлагают то же самое:

object User {
  def save(u: User) = {
    // ...
  }
}
// we can invoke that object's method via:
User.save(new User("Pat"))

Более того, Scala позволяет нам создавать «сопутствующие объекты»; то есть объекты, которые соответствуют классам с одинаковыми именами:

class User(name: String) {
}
// This is the companion object to the User class
object User {
  def apply() {
    new User("Noname")
  }
  def apply(name: String) = {
    new User(name)
  }
  def unapply(u: User) = {
    Some(u.name)
  }
  def save(u: User) = {
    // ...
  }
}

Хотя мы можем определять любые функции в сопутствующем объекте класса, методы apply() и unapply() имеют особое значение.

Если бы мы вызывали имя объекта, за которым следовала бы скобка с любым количеством аргументов, был бы вызван соответствующий apply(…) метод в объекте.

В приведенном выше примере вызов User() будет вызывать вызов User.apply(); вызов User("Chris") фактически вызовет User.apply("Chris"). Обычно создается и возвращается экземпляр класса-компаньона. Таким образом, apply() методы сопутствующего объекта могут - и обычно используются - как фабричные методы.

По этой причине мы обычно видим объекты, создаваемые следующим образом:

val u = User("Suzie")

вместо этого:

val u = new User("Suzie")

С другой стороны, unapply() используется как деконструктор. Всякий раз, когда требуются отдельные компоненты класса (например, во время сопоставления с образцом), его метод unapply() вызывается за кулисами.

Исчерпывающий пример

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

case class Point(x: Int, y: Int)  // 1

trait Drawable {  // 2
  def draw(): Unit  // 3
}

abstract class Shape(val center: Point) {  // 4
  def area(): Double 
}

case class Circle(override val center: Point, radius: Int) extends Shape(center) with Drawable {  // 5
  override def draw(): Unit = { // 6
    println(s"⊙ ")
  }
  override def area() = Math.PI * (radius * radius) 
  def growBy(add: Int) = Circle(center, radius + add)  // 7
}

case class Square(override val center: Point, side: Int) extends Shape(center) with Drawable {
  override def draw(): Unit = {
    println("◼︎")
  }
  override def area() = Math.PI * (side * side)
}

object Canvas {  // 8

  def main(args: Array[String]): Unit = {  // 9
    val circle1 = Circle(Point(50, 25), radius = 10)  // 10
    val circle2 = circle1.growBy(5)
    val square = Square(Point(100, 150), side = 30)

    val l: List[Drawable] = List(circle1, circle2, square)  // 11
    l.foreach(_.draw())  // 12

    l.map(d => d match {  // 13
      case Circle(_, r) => s"Circle of radius $r"  // 14
      case Square(_, s) => s"Square of side length $s"
      case _ => "Unknown shape"  // 15
    }).foreach(println)  // 16
  }

}
  1. Мы определяем класс case Point. Компилятор за кулисами создает для нас набор шаблонов, в том числе Point сопутствующий класс.
  2. Мы определяем черту Drawable с помощью…
  3. … Неявно абстрактный метод draw(). Поскольку метод является абстрактным, мы указываем тип возвращаемого значения как Unit (что означает, что ничего не будет возвращено)
  4. Мы определяем абстрактный класс Shape, который объявляет val, center, и неявно абстрактный метод area()
  5. Мы определяем класс case Circle, который расширяет класс Shape и признак Drawable.
  6. Мы используем ключевое слово override для реализации абстрактного draw() метода родительского класса Shape. Здесь мы решили заключить тело метода в фигурные скобки.
  7. Затем мы определяем еще два метода: метод area(), переопределенный из родительского класса Shape, и новый метод growBy(), который возвращает новый экземпляр Circle. В обеих базах мы опускаем фигурные скобки в теле метода.
  8. Создаем объект Canvas. Все его функции можно рассматривать как аналогичные статические методы Java.
  9. Мы можем определить метод main, как в Java. Находясь в объекте, этот метод является статическим и может вызываться как Canvas.main(Array()).
  10. Мы создаем Circle экземпляр, используя синтаксический сахар, который вызывает Circle метод apply() сопутствующего объекта. Напомним, что сопутствующий объект Circle и его метод apply() генерируются компилятором, поскольку Circle - это класс case.
  11. Мы создаем List из Drawable, используя квадратные скобки для обозначения общего типа List.
  12. Мы используем foreach метод List для перебора его Drawable, вызывая каждый метод draw(). Для краткости мы используем символ подчеркивания для представления текущего Drawable.
  13. Затем мы используем метод List map для преобразования каждого Drawable в значение String.
  14. Мы используем сопоставление с образцом, чтобы определить конкретный тип Drawable, который мы повторяем. В этой строке мы ищем Circle с любым значением center (представленным символом подчеркивания) и захватываем значение radius с помощью r переменная. Затем мы используем строковую интерполяцию для создания String, содержащего это значение r. Мы делаем нечто подобное, сопоставляя Square в строке ниже.
  15. Мы фиксируем случаи, когда мы сопоставляем Drawable, который не является ни Circle, ни Square, с помощью символа подчеркивания.
  16. Затем мы вызываем foreach для результирующего List из Strings и ссылаемся на функцию println(), которая неявно вызывается с String, который в настоящее время повторяется.