Краткое руководство по некоторым синтаксическим различиям между 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 } }
- Мы определяем класс case
Point
. Компилятор за кулисами создает для нас набор шаблонов, в том числеPoint
сопутствующий класс. - Мы определяем черту
Drawable
с помощью… - … Неявно абстрактный метод
draw()
. Поскольку метод является абстрактным, мы указываем тип возвращаемого значения какUnit
(что означает, что ничего не будет возвращено) - Мы определяем абстрактный класс
Shape
, который объявляетval
,center
, и неявно абстрактный методarea()
- Мы определяем класс case
Circle
, который расширяет классShape
и признакDrawable
. - Мы используем ключевое слово
override
для реализации абстрактногоdraw()
метода родительского классаShape
. Здесь мы решили заключить тело метода в фигурные скобки. - Затем мы определяем еще два метода: метод
area()
, переопределенный из родительского классаShape
, и новый методgrowBy()
, который возвращает новый экземплярCircle
. В обеих базах мы опускаем фигурные скобки в теле метода. - Создаем объект
Canvas
. Все его функции можно рассматривать как аналогичные статические методы Java. - Мы можем определить метод
main
, как в Java. Находясь в объекте, этот метод является статическим и может вызываться какCanvas.main(Array())
. - Мы создаем
Circle
экземпляр, используя синтаксический сахар, который вызываетCircle
методapply()
сопутствующего объекта. Напомним, что сопутствующий объектCircle
и его методapply()
генерируются компилятором, посколькуCircle
- это класс case. - Мы создаем
List
изDrawable
, используя квадратные скобки для обозначения общего типаList
. - Мы используем
foreach
методList
для перебора егоDrawable
, вызывая каждый методdraw()
. Для краткости мы используем символ подчеркивания для представления текущегоDrawable
. - Затем мы используем метод
List
map
для преобразования каждогоDrawable
в значениеString
. - Мы используем сопоставление с образцом, чтобы определить конкретный тип
Drawable
, который мы повторяем. В этой строке мы ищемCircle
с любым значением center (представленным символом подчеркивания) и захватываем значение radius с помощью r переменная. Затем мы используем строковую интерполяцию для созданияString
, содержащего это значение r. Мы делаем нечто подобное, сопоставляяSquare
в строке ниже. - Мы фиксируем случаи, когда мы сопоставляем
Drawable
, который не является ниCircle
, ниSquare
, с помощью символа подчеркивания. - Затем мы вызываем
foreach
для результирующегоList
изStrings
и ссылаемся на функциюprintln()
, которая неявно вызывается сString
, который в настоящее время повторяется.