
Начало работы со Scala
Это первая часть серии руководств, которые я буду писать на Scala.
Если вы из Java, есть много вещей, которые могут быть трудными для понимания, и самый простой способ - отучиться и заново изучить их.
- Введение в Scala и настройку среды
- Первый привет, мир !!
- Переменные
- Ссылка vs неизменность значения
- Неизменность под капотом
- Сравнение и сопоставление val и final
- Типы данных в Scala
- Вывод типа
- Инициализация переменной
- Аннотации типов
- Описание типа
- Ленивый вал
Введение в Scala и настройку среды
В Интернете доступно множество руководств, в которых рассказывается о настройке Scala, поэтому я не буду повторять их снова.
Рекомендуемая настройка разработчика.
- Операционная система: Windows, Linux, Mac
- Версия сообщества Intellij IDEA с установленным плагином Scala
Если вы закончили их настройку, тогда приступим.
Первый Hello World !!
Давайте сразу перейдем к некоторым.
object Test {
def main(args : Array[String]){
println("Hello world")
}
}
Вы можете скопировать этот код в свою Intellij IDE, а затем запустить его. Если все пойдет хорошо, вы должны увидеть Hello World в консоли.
Давайте разберемся с этим. Первое, что следует заметить, это то, что весь код находится внутри объектного блока. В отличие от Java, имена классов не обязательно должны совпадать с именами файлов, это не имеет большого значения, но здесь у нас есть такая свобода.
Далее следует странный синтаксис def main (). Для начала def - ключевое слово для объявления методов. Для метода могут быть аргументы, и в нашем случае это Array [String]. Это похоже на основной метод Java, где массив строк является аргументом для основного метода. Может использоваться для начальной конфигурации или любых переменных времени выполнения.
За этим следует вызов метода println (), который выводит операторы на консоль. Если вы используете IDE, вы можете отследить весь вызов, удерживая ctrl + щелчок по методу (ярлыки различаются в зависимости от операционной системы). Он просто выводит заданную строку на консоль. В Scala точка с запятой не обязательна, в отличие от Java, и компилятор полагается на разрывы строк для идентификации следующего литерала / блока кода.
Трассировка стека метода println () выглядит следующим образом.

Если у вас есть прикрепленный исходный код, он показывает исходный код напрямую, в противном случае декомпилятор из Intellij показывает декомпилированный код. Печать как из Scala, так и из Java завершается одним и тем же вызовом метода. Как упоминалось ранее, Scala построен поверх JVM и может беспрепятственно взаимодействовать с Java-кодом, если нет конкретной причины для другой реализации, тогда это будет просто заново изобретать колесо, и, следовательно, все они полагаются на существующий Функциональность библиотеки JDK.
Predef.scala похож на пакет java.lang. По умолчанию они доступны для всех файлов Scala без какого-либо импорта.
Простой привет мир открыл множество тем для изучения, в частности, три.
- Методы
- Объекты и классы
- Вывод типа - причина, по которой Scala является статически типизированным динамическим языком.
Переменные
Мне следовало объяснить типы данных, прежде чем мы перейдем к переменным, но есть некоторые фундаментальные различия, которые мы должны понимать.
var и val - два ключевых слова, которые используются для объявления переменных.
var используется для объявления изменяемых переменных, а val - для объявления неизменяемых.
Ссылка vs неизменность значения
Если val неизменяемо, то его нельзя изменить? Похоже ли это на ключевое слово final в Java или что-то связано с неизменяемостью String?
Чтобы лучше понять, давайте рассмотрим пример кода ниже.
var myVar = 10 //Works fine myVar = myVar + 10 val myNum = 6 //Will Result in compilation error //Reassignment to val myNum = myNum + 10
Если вы запустите приведенный выше код, вы заметите ошибку при самой компиляции, такую как переназначение val. Если вы используете IDE, это будет отображаться при вводе из-за предварительной компиляции, которую предоставляет IDE.
Во-первых, это определенно не похоже на неизменяемость String, где она не видна программисту и контролируется на уровне компиляции. Следующий вопрос, похоже ли это на final в Java?
С высоты птичьего полета это похоже на то, что после того, как ему присвоено значение, его нельзя изменить, но внутри JVM final не имеет ничего общего с неизменяемостью и используется для того, чтобы классы не могли быть расширены и в случае методов это нельзя переопределить.
Давайте рассмотрим приведенный ниже код Java, чтобы продемонстрировать, что это другое.
final ArrayList<Integer> arrList = new ArrayList<Integer>(); /* This does not result in an error, we are mutating the object itself. If it were mutable it would result in error to something like it cannot be changed */ arrList.add(20); /* This results in error as we are modifying the reference and not the object itself */ arrList = null;
Если попробовать такой же финал с примитивными типами, то его значение изменить нельзя. Означает ли это, что примитивы неизменны? Причина возникновения ошибки заключается в том, что Java использует передачу по значению для примитивных типов, поэтому нет смысла передавать по ссылке, поскольку они вообще не являются объектами. Таким образом, если переменная изменяется, ее ссылка (можно сказать, как место в памяти) изменяется из-за механизма передачи по значению, а не потому, что эти примитивы неизменяемы.
Важно понять, что неизменяемость ссылок и значений не имеет большого значения в Scala, поскольку в Scala вообще нет примитивных типов, а есть только объекты.
Неизменность под капотом
Давайте углубимся в наше понимание неизменяемости, исследуя байтовый код, генерируемый декомпиляцией сгенерированных файлов классов Scala.
Давайте объявим класс с именем Parent и значение внутри него.
class Parent {
val x = 10
}
Увидев сгенерированный байт-код, мы видим, что он переводится в примитивный тип java, в том, что касается времени выполнения, нет ничего особенного.
javap -c filename.class
дает декомпилированный ниже код.
public class Parent {
public int x();
Code:
0: aload_0
1: getfield #13 // Field x:I
4: ireturn
public Parent();
Code:
0: aload_0
1: invokespecial #19 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #13 // Field x:I
10: return
}
Понятно, что val - это всего лишь ограничение времени компиляции и не имеет ничего общего с испускаемым байтовым кодом.
Мы можем использовать этот подход чтения декомпилированного байтового кода, чтобы понять вещи более глубоко, но в большинстве случаев это не требуется.
Поэтому всякий раз, когда мы говорим о неизменности в Scala, мы говорим о неизменяемости ссылок. Неизменяемые переменные имеют определенные преимущества в производительности и ближе к идее написания кода без побочных эффектов.
Даже если значение переменной можно изменить, ее тип изменить нельзя.
var x = 20 //Allowed x= 40 var a = 10 //Not allowed a = "Test"
Это скин поведения в Java, когда вы не можете присвоить строку целому числу. Это доказывает, что вывод типа действительно происходит во время компиляции.

Сравнение и сопоставление val и final
Scala, с другой стороны, также имеет ключевое слово final, которое работает очень похоже.
Лучший способ визуализировать разницу между val и final - на примере.
Возьмем простой родительский класс.
class Parent {
val age = 10
}
В отличие от java, Scala также может переопределять переменные.
class Child extends Parent{
override val age = 30
def printVal ={
println(age)
}
}
Теперь, если мы объявим переменную age как final в родительском классе, тогда дочерний класс не сможет ее переопределить, и это вызовет ошибку, как показано ниже.

Это реальный пример использования final.
Обратите внимание, что мы не нарушаем неизменяемость, переопределяя val в дочернем классе. Дочерний класс создает собственный экземпляр вместо изменения переменной родительского класса.
Типы данных в Scala
Scala имеет те же типы данных, что и Java, с той же памятью и точностью.
Все типы данных являются объектами, и в них можно вызывать методы так же, как и для объекта.
val t = 69 //Prints 'E' the ASCII value of E is 69 println(t.toChar) val s = "Hello World" //Just like String char at, prints l //Trace leads to the same String class charAt method println(s.charAt(2))
К тому времени возник бы другой вопрос? Где типы в нашем коде?
В отличие от java, где мы объявляем переменные с типами данных, а затем даем имя переменной, в Scala есть нечто, называемое выводом типа.
Вывод типа
Вывод типа - это не что иное, как вывод типов во время компиляции. Погодите, разве не это означает динамическая типизация? Ну нет, обратите внимание, что я сказал о дедукции типов, это кардинально отличается от того, что делают языки с динамической типизацией. Основное отличие состоит в том, что типы выводятся во время компиляции, а не во время выполнения.
Это встроено во многие языки, но реализация варьируется от одного языка к другому. Давайте перейдем к Scala REPL и поэкспериментируем.

Из изображения выше видно, что здесь нет никакой магии. Переменные автоматически выбираются из наиболее подходящих типов во время компиляции.
Вот еще код для дальнейшего понимания.
val x = 20 //print to the console //legit, gets inferred as an integer println(x+10) //Something stupid as below will throw compile time error val z = 40 println(z / "justastring")
Поиграйте с этими переменными, вы защищены от безопасности типов во время компиляции, так что не бойтесь бездельничать.
Если вам интересно, какие классы расширяют переменные, вы можете копнуть глубже и найти класс под названием AnyVal. Это часть совершенно другой темы унифицированной системы типов Scala, которая представляет собой не что иное, как иерархию классов.
Инициализация переменной
В Scala нельзя просто создать переменную и оставить ее неинициализированной.

Это выбор дизайна, который сделали разработчики языка scala. Очевидная причина - не оставлять переменные неинициализированными, а также избегать исключений с нулевым указателем.
Единственное место, где мы не присваиваем значения переменным, - это внутри абстрактных классов. Мы увидим больше, когда узнаем о классах в scala.
Аннотации типов
В Scala есть возможность явно указать тип.
val y : Integer = 20
Подобные аннотации типов важны для общедоступных API / методов.
def getInfoFromBackend() = {
val dataList = List(1,"Literature",2,"Science")
dataList
}
Без явной аннотации информации о типе разработчики, использующие ее, не смогут понять. Помните, что не все языки в JVM имеют вывод типа, такой как java, и, следовательно, изменение типа может нарушить клиентский (потребляющий) код.
Лучшая версия будет, как показано ниже.
def getInfoFromBackend() = List {
val dataList = List(1,"Literature",2,"Science")
dataList
}
Эта концепция применяется не только к параметрам метода, но и к переменным, объявленным с помощью ключевого слова val. Пример просто демонстрирует идею в более понятной форме.
Тип описания
Приписывание типов - это нечто более сложное. Это процесс сообщения компилятору, какого типа вы ожидаете от операции, которую собираетесь выполнить.
Типичный вариант использования - это приведение типов в java.
int x = 20; //Valid conversion System.out.println((byte) x); //Run time error Object s = new Object(); System.out.println((byte) s);
Приведенный выше код приведет к исключению / ошибке времени выполнения, как показано ниже.
20 Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Byte at JavaExample.main(JavaExample.java:14) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
В Scala, когда мы используем приписывание типа, код даже не компилируется.

Между аннотацией типа и приписыванием типа нет синтаксической разницы, что часто приводит к путанице между этими двумя темами.
Мы могли бы выбрать тот же путь Java, используя приведение типов во время выполнения.
val x = new Object().asInstanceOf[Byte]
В приведенном выше случае вы можете заметить, что он не вызывает ошибку во время компиляции и приведет к той же трассировке стека исключений, то есть java.lang.ClassCastException во время выполнения.
Приписывание типов может быть невероятно полезным при выполнении приведения / преобразования типа. Вы можете проверить безопасность типов во время компиляции, а не во время выполнения, чтобы убедиться, что это не приведет к ошибкам в коде.
Ленивый вал
Как следует из названия, lazy val в scala похожа на val, но его значение оценивается только при использовании переменной.
import scala.io.Source._
object ReadFileExample extends App{
println(System.getProperty("user.dir"))
lazy val lines = fromFile(System.getProperty("user.dir") + "/file1.txt").getLines
println(lines)
}
Я рекомендую вам попробовать это самостоятельно. Сначала, закомментировав println (строки), вы могли увидеть, что это не привело к ошибке, даже если файла там не было.
Это рабочий пример, в котором вы можете поместить реальный файл на верхнем уровне проекта, который вы вводите / помещаете в определенный путь самостоятельно, а затем программа просто распечатает содержимое файла.
Это контролируется во время компиляции, поскольку доступ к переменной может быть известен во время компиляции.
Очень полезно в таких ситуациях, как окно загрузки файла в браузере. Пользователь может или не может загружать файл, поэтому лучше отложить / лениво оценивать, пока не произойдет событие.

Ключевое слово Lazy применяется только к val, но не к var. Это связано с тем, что var не рассматривается как определение значения, поскольку он может изменять свои значения во время выполнения. Трудно сказать, когда это точно изменится, и, следовательно, вся концепция ленивого вычисления не имеет смысла, когда дело доходит до изменения переменных.
Примечание:
Из того, что мы видели в этом посте, мы можем продолжить и изучить следующие темы.
Массивы точек руководства дает хороший обзор синтаксиса.
Обязательно прочтите Официальную документацию по массивам на языке Scala.
Массивы - это самый упрощенный тип коллекций на языке Scala, и понимание их дает нам хорошее начало.
Scala REPL - очень удобный инструмент, который мы можем использовать для экспериментов с короткими фрагментами кода. Обязательно ознакомьтесь с Ammonite REPL, который не является частью стандартного набора инструментов Scala, но гораздо более продвинутый и удобный для пользователя.