Путівник по Scala для Java-розробників: Класна робота (исходники), Різне, Програмування, статті

В статті за минулий місяць ми лише доторкнулися до синтаксису Scala, отримавши необхідний мінімум для запуску Scala-програми і відзначивши деякі прості особливості мови. Приклади Hello World і Timer з цієї статті дозволили вам побачити Scala-клас Application – Його синтаксис для визначення методів та анонімних функцій, побіжно познайомили з Array[] і трохи – з виведенням типів. Scala може запропонувати набагато більше, тому в цій статті ми продовжимо розбиратися з хитросплетіннями кодування на Scala.


Можливості функціонального програмування на Scala незаперечні, але вони не є єдиною причиною, по якій Java-розробникам слід звернути увагу на цю мову. По суті Scala об’єднує в собі функціональні концепції та об’єктну орієнтованість. Щоб дати Java + Scala-програмісту більше відчуття комфорту, має сенс розглянути особливості об’єктів в Scala і зрозуміти – яким же їх лінгвістичне відображення на Java. Майте на увазі – для деяких таких можливостей не існує прямого відповідності, а в ряді випадків “відповідність” буде швидше аналогом, ніж точної паралеллю. Але там, де відмінності виявляться важливими, я буду окремо звертати вашу увагу.

У Scala теж є класи


Замість того, щоб вдаватися в тяжку і абстрактну дискусію з приводу підтримуваних в Scala особливостей роботи з класами, давайте поглянемо на визначення класу, який міг би використовуватися для подання раціонального числа на платформі Scala:


Лістинг 1. rational.scala





class Rational(n:Int, d:Int)
{
private def gcd(x:Int, y:Int): Int =
{
if (x==0) y
else if (x<0) gcd(-x, y)
else if (y<0) -gcd(x, -y)
else gcd(y%x, x)
}
private val g = gcd(n,d)

val numer:Int = n/g
val denom:Int = d/g

def +(that:Rational) =
new Rational(numer*that.denom + that.numer*denom, denom * that.denom)
def -(that:Rational) =
new Rational(numer * that.denom – that.numer * denom, denom * that.denom)
def *(that:Rational) =
new Rational(numer * that.numer, denom * that.denom)
def /(that:Rational) =
new Rational(numer * that.denom, denom * that.numer)
override def toString() =
“Rational: [” + numer + ” / ” + denom + “]”
}


Хоча загальна структура в лістингу 1 лексично еквівалентна тому, що вам доводилося бачити в Java-коді протягом останнього десятиліття, не можна не помітити і наявності нових елементів. Перш ніж скрупульозно розібрати це визначення, подивимося на код, в якому задіяний новий клас Rational:


Лістинг 2. RunRational





class Rational(n:Int, d:Int)
{ / / … див. вище
}
object RunRational extends Application
{
val r1 = new Rational(1, 3)
val r2 = new Rational(2, 5)
val r3 = r1 – r2
val r4 = r1 + r2
Console.println(“r1 = ” + r1)
Console.println(“r2 = ” + r2)
Console.println(“r3 = r1 – r2 = ” + r3)
Console.println(“r4 = r1 + r2 = ” + r4)
}

Наведене в лістингу 2 не виглядає занадто страхітливо: я створив пару раціональних чисел, додав ще два Rational-А як різниця і суму двох перших, потім вивів все в консоль. (Зауважте: Console.println() прийшло з бібліотеки ядра Scala, розташованої в scala.* і неявно імпортованої в кожну Scala-програму, в точності як java.lang в Java-програмуванні).


І як же мені тебе створити?


Тепер знову подивимося на перший рядок у визначенні класу Rational:


Лістинг 3. Конструктор за умовчанням в Scala





class Rational(n:Int, d:Int)
{
// …

Хоча можна було б подумати, що в лістингу 3 перед вами – щось на кшталт синтаксису, характерного для універсальних типів (generics), насправді – це відокремлений конструктор за умовчанням для класу Rational: N і d – просто параметри цього конструктора.


Таке перевагу одиночним конструкторам в Scala не позбавлене здорового глузду – більшість класів в кінцевому рахунку розташовують одним конструктором або їх набором, де всі вони “зав’язані” на один конструктор з міркувань зручності. При бажанні ви можете визначити для Rational додаткові конструктори, припустимо, так:


Лістинг 4. Набір конструкторів





class Rational(n:Int, d:Int)
{
def this(d:Int) = { this(0, d) }

Зверніть увагу, що ланцюжок Scala-конструкторів реалізує звичне в Java “зчеплення” конструкторів, передаючи виклик у виділений (більш кращий) конструктор (тут – у версію з Int,Int).


Деталі, (реалізація) деталі …


При роботі з раціональними числами буває корисно виконати кілька числових трюків, скажімо, знайти спільний знаменник, щоб полегшити виконувані операції. Якщо ви хочете скласти одну другу (1/2) з двома четвертими (2/4), клас Rational повинен бути достатньо кмітливим, щоб зрозуміти, що 2/4 це те ж саме, що і 1/2 і провести відповідне перетворення, перш ніж виконати складання.


Для цього передбачена вкладена приватна функція gcd() і значення g всередині класу Rational. При виклику конструктора в Scala обчислюється все тіло класу, а це означає, що g буде проініціалізувати найбільшим загальним дільником для n і d, а потім використана для відповідної установки n і d .


Поверніться до лістингу 1 – Тут також виразно видно, що я створив перекритий метод toString для повернення значень Rational, Що буде дуже корисним, коли я почну випробування коду під управлінням RunRational.


Придивіться уважніше до синтаксису для toString: Службове слово override на початку визначення необхідно, тому що дозволяє Scala переконатися в тому, що відповідну ухвалу присутній в базовому класі. Це допомагає запобігти підступні помилки, вироблені випадковими натисканнями на клавіатурі. (Та ж мотивація спонукала до створення анотації @Override в Java 5). Крім того, відзначте – тип значення, що повертається не вказаний, адже це очевидно з визначення тіла методу, більш того – повертається значення явно не позначено службовим словом return, Як того вимагає Java. Натомість останнє значення у функції неявно розглядається як повертається. (При цьому якщо ви віддаєте перевагу синтаксис Java, ви завжди можете використовувати ключове слово return).


Деякі основоположні цінності


Далі переходимо до визначень numer і denom. Наведений синтаксис в першому наближенні може підштовхнути Java-програміста до думки, що numer і denom – Відкриті поля типу Int, Ініціалізіруемих, відповідно, значеннями n-на-g і d-на-g, але це припущення помилково.


Насправді Scala викликає не мають параметрів методи numer і denom, Що швидкий і зручний синтаксис для визначення методів доступу (аксессор). Клас Rational по колишньому містить три приватних поля n, d і g, але вони приховані від зовнішнього світу: призначеним за замовчуванням приватним типом доступу для n і d, і явно зазначеним модифікатором private для g.


Що живе в вас Java-програміст, можливо, запитає в цьому місці: “А де ж відповідні” сетери “(установники) для n і d?”. А їх і немає. Одна із сильних сторін Scala полягає в тому, що мова спочатку орієнтує розробників на створення незмінних (immutable) об’єктів. І хоча сам синтаксис створення методів для зміни внутрішніх елементів Rational доступний, роблячи так, ви порушуєте внутрішню потокобезпечна сутність цього класу. А тому, принаймні для цього прикладу, я збираюся залишити в Rational все як є.


Природно, це породжує питання про те, як же тепер маніпулювати Rational. За аналогією з java.lang.String-Ами, ви не можете взяти наявний екземпляр Rational і змінити його значення. Раз так – єдиною альтернативою залишається створення нових Rational-Ів на основі існуючого екземпляра або ж створення з нуля. І це фокусує нашу увагу на наступному наборі з чотирьох методів, за дивним збігом обставин названих +, , * і /.


І на що б це не було схоже – це зовсім не перевантаження операторів.


Подзвони-но мені за номером, оператор


Згадайте – в Scala все є об’єктом. У попередній статті ви побачили, як цей принцип застосуємо до ідеї про те, що функції самі по собі є об’єкти, що дозволяє Scala-програмістам призначати функції змінним, передавати їх як об’єктні параметри і т.д. Рівнозначним за важливістю є і такий принцип: все є функцією; зокрема, у нашому конкретному випадку немає різниці між функцією, названої add, І функцією, названої +. У Scala всі оператори є функціями класу. І, звичайно, трапляється, що вони мають забавні імена.


Ось і в класі Rational для раціональних чисел визначено чотири операції. Це канонічні математичні операції додавання, віднімання, множення і ділення. Кожна з них пойменована відповідним математичним символом: +, , * і /.


При цьому зауважте, що кожен з цих операторів в результаті конструює новий об’єкт Rational. Знову-таки – це дуже схоже на те, як працює java.lang.String, І це є подразумеваемой реалізацією, породжує потокобезпечна код. (Раз немає внутрішнього стану із загальним доступом – а внутрішній статус об’єкта за замовчуванням вважається станом із загальним доступом – То немає і причин для занепокоєння щодо одночасного доступу до цього статусу.)


Так що ж нового в тобі?


Правило все є функцією володіє двома потужними ефектами:


По-перше, як ви вже бачили, це те, що функції допускають маніпулювання і супровід як для об’єктів. Це приводить нас до розвинених сценаріями повторного використання, подібно розглянутому в першій статтіцієї серії.


Другий ефект полягає в тому, що не існує особливих відмінностей між операторами, які творці Scala визнають за необхідне надати і операторами, які Scala-програмісти порахують такими, які слід було б надати. Наприклад, уявімо на секунду, що виникла потреба в операторі “перевороту”, який міняв би місцями чисельник і знаменник і повертав новий Rational (Тобто для Rational(2,5) повернув би Rational(5,2)). І якщо ви вирішите, що символ ~ найкращим чином відображає цю концепцію, то ви можете визначити новий метод використовуючи таке ім’я і воно буде вести себе як будь-який інший Java-оператор, як показано в лістингу 5:


Лістинг 5. Перевертиш





val r6 = ~r1 Console.println (r6) / / роздрукує [3/1], тому що r1 = [1/3]

Визначення такого Унарне “оператора” в Scala виглядає дещо дивно, але це чисто синтаксична фішка:


Лістинг 6. Перевертаємо





class Rational(n:Int, d:Int)
{ / / … як і раніше …
def unary_~ : Rational =
new Rational(denom, numer)
}

Мудроване, безумовно, в тому, що ми задаємо префікс “unary_“Для ~, Щоб повідомити Scala-компілятору: це слід розглядати як унарний оператор, тому синтаксис був “перевернуть” у порівнянні з традиційним синтаксисом посилання-потом-метод, типовим для багатьох об’єктних мов.


Зверніть увагу, як це поєднується з правилом “все є об’єктом” при створенні деяких просунутих, але легко сприймаються, варіацій коду:


Лістинг 7. Підсумовуємо




                 1 + 2 + 3 / / то ж що і 1. + (2. + (3)) r1 + r2 + r3 / / то ж що і r1. + (r2. + (r3))

Як і слід було очікувати, компілятор Scala правильно робить свою справу в прикладах з простим складанням цілих, але синтаксично це все одне й те саме. Це означає, що ви можете розробляти типи, нічим не відрізняються від вбудованих, що є частиною мови Scala.


Компілятор Scala навіть спробує вивести якийсь сенс для “операторів”, що мають деякий зумовлене значення, таких як оператор +=. Наприклад, наступний код спрацює як належить, незважаючи на той факт, що клас Rational не містить явного визначення для +=:


Лістинг 8. Scala робить логічний висновок





var r5 = new Rational(3,4)
r5 += r1
Console.println(r5)

При виведенні в консоль r5 має значення [13 / 12], А це саме те, що і повинно бути.


Scala під ковпаком


Згадайте – Scala компілюється в байт-код Java, тобто код запускається під JVM. Якщо вам необхідно доказ, досить того факту, що компілятор породжує. Class-файли, що починаються з 0xCAFEBABE, Як і у випадку javac. Також відзначте, що відбувається, якщо запустити для цього байт-коду Java дизассемблер, що поставляється з JDK (javap), І вказати йому на згенерований клас Rational, Як показано в лістингу 9:


Лістинг 9. Класи, скомпільовані з rational.scala





C:Projectsscala-classescode>javap -private -classpath classes Rational
Compiled from “rational.scala”
public class Rational extends java.lang.Object implements scala.ScalaObject{
private int denom;
private int numer;
private int g;
public Rational(int, int);
public Rational unary_$tilde();
public java.lang.String toString();
public Rational $div(Rational);
public Rational $times(Rational);
public Rational $minus(Rational);
public Rational $plus(Rational);
public int denom();
public int numer();
private int g();
private int gcd(int, int);
public Rational(int);
public int $tag();
}
C:Projectsscala-classescode>

“Оператори”, визначені в Scala-класі, перетворюються на виклики методів в кращих традиціях Java-програмування, хоча імена ці методів кілька дивно. У класі визначено два конструктора: один приймає int, А інший – пару int-Ів. І якщо вас, між справою, зацікавить те, що запис Int у верхньому регістрі є певним способом маскування для java.lang.Integer, Врахуйте, що компілятор Scala достатньо кмітливий, щоб перетворити це в звичайні Java-примітиви int у визначенні класу.


Тести, тести, раз-два-три …


Загальновідомо, що хороші програмісти пишуть код, а великі програмісти пишуть тести; до сих пір я погано слідував цьому правилу для свого Scala-коду, тому давайте подивимося – що станеться, коли ви Розмістити клас Rational в традиційному тестовому модулі JUnit, як показано в лістингу 10:


Лістинг 10. RationalTest.java





import org.junit.*;
import static org.junit.Assert.*;
public class RationalTest
{
@Test public void test2ArgRationalConstructor()
{
Rational r = new Rational(2, 5);
assertTrue(r.numer() == 2);
assertTrue(r.denom() == 5);
}

@Test public void test1ArgRationalConstructor()
{
Rational r = new Rational(5);
assertTrue(r.numer() == 0);
assertTrue(r.denom() == 1); / / 1 – т.к. на етапі конструювання викликається gcd (); / / 0-на-5 це те ж що і 0-на-1
}

@Test public void testAddRationals()
{
Rational r1 = new Rational(2, 5);
Rational r2 = new Rational(1, 3);
Rational r3 = (Rational) reflectInvoke(r1, “$plus”, r2); //r1.$plus(r2);
assertTrue(r3.numer() == 11);
assertTrue(r3.denom() == 15);
}
/ / … деякі деталі опущені
}






 



SUnit

Вже існує орієнтований на Scala комплект модульного тестування, поширюваний під ім’ям SUnit. Якщо ви задіюєте SUnit для тіста, показаного в лістингу 10, вам не доведеться мати справу з вивертами на базі технології відображення (reflection). Scala-специфічний код модульного тестування скомпілюйте прямо в Scala-клас, тому компілятор зможе вибудувати символьну інформацію. Деякі розробники дійсно знайдуть більш привабливим процес написання модульних тестів в Scala, при якому замість обхідних маневрів використовуються прості об’єкти POJO.


SUnit є частиною стандартної поставки Scala і розташований в пакеті scala.testing.


Крім перевірки того, що клас Rational веде себе, скажімо так, раціонально, наведений вище тестовий комплект переконує нас в можливості виклику Scala-коду з Java-коду (хоча і з деякими складнощами, коли справа доходить до операторів). Що дійсно вражає в цій ситуації, так це можливість освоювати Scala поступово, виконуючи міграцію від Java-класів до Scala-класам без якої-небудь необхідності міняти стоять за цим тести.


Єдина дивина, яку можна було б відзначити в тестовому коді, пов’язана з викликом операторів, в даному випадку методу + класу Rational. Поверніться назад до висновку javap – Scala безпосередньо транслює функцію + в метод $plus для JVM, але специфікація мови Java не допускає наявності символу $ в ідентифікаторах (з причини його використання в іменах вкладених і анонімних вкладених класів).


Для організації виклику таких методів вам або потрібно писати тести на Groovy або JRuby (або на якомусь іншому мовою, в якому не заявлено обмежень на символ $), Або ж ви можете обійтися для виклику невеликою кількістю Reflection-Коду. Я прийняв другий підхід, який зовсім не цікавий з точки зору Scala, але результат вас потішить (див. підбірку вихідних кодів до цієї статті)


Потрібно розуміти, що прийоми на зразок цього необхідні лише для імен функцій, які не є легітимними ідентифікаторами Java.


“Покращений” Java


У минулому, коли я тільки починав вивчати C + +, Бьерн Страуструп висловив рада, що один із шляхів при вивченні C + + – це сприйняття його як “поліпшеного C”. Таким же чином сьогоднішні Java-розробники можуть розглядати Scala як “покращений Java” тому що він забезпечує більш короткий і лаконічний спосіб написання традиційних POJO-об’єктів Java. Розглянемо звичайний POJO PersonНаведений в лістингу 11:


Лістинг 11. JavaPerson.java (вихідний POJO)





public class JavaPerson
{
public JavaPerson(String firstName, String lastName, int age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

public String getFirstName()
{
return this.firstName;
}
public void setFirstName(String value)
{
this.firstName = value;
}

public String getLastName()
{
return this.lastName;
}
public void setLastName(String value)
{
this.lastName = value;
}

public int getAge()
{
return this.age;
}
public void setAge(int value)
{
this.age = value;
}

public String toString()
{ return “[Ім’я персони:” + firstName + “Прізвище:” + lastName + “Вік:” + age + “]”;
}

private String firstName;
private String lastName;
private int age;
}


Тепер погляньте на його еквівалент, написаний на Scala:


Лістинг 12. person.scala (потокобезпечна POJO)





class Person(firstName:String, lastName:String, age:Int)
{
def getFirstName = firstName
def getLastName = lastName
def getAge = age
override def toString = “[Ім’я персони:” + firstName + “Прізвище:” + lastName + “Вік:” + age + “]”
}

Це не повний еквівалент беручи до уваги, що вихідний об’єкт Person містить деякі змінюють стан сетери (setters). Але враховуючи, що вихідний Person до того ж не має коду синхронізації навколо цих сеттерів, версія Scala буде безпечніше у використанні. Більш того, якщо дійсно задатися метою зменшити кількість рядків коду в Person, Ви б могли повністю видалити get-Методи доступу до властивостей, тому що Scala згенерує методи-аксессор (accessors) для кожного з параметрів конструктора – firstName(), Який повертає String, lastName(), Який повертає String і age(), Який повертає int.


Навіть якщо без цих змінюють стан сеттерів не можна обійтися, версія Scala і раніше простіше, як ви можете побачити з лістингу 13:


Лістинг 13. person.scala (повне визначення POJO)





class Person(var firstName:String, var lastName:String, var age:Int)
{
def getFirstName = firstName
def getLastName = lastName
def getAge = age

def setFirstName(value:String):Unit = firstName = value
def setLastName(value:String) = lastName = value
def setAge(value:Int) = age = value
override def toString = “[Ім’я персони:” + firstName + “Прізвище:” + lastName + “Вік:” + age + “]”
}


До речі, зверніть увагу на що з’явилося поряд з параметрами конструктора службове слово var. Якщо не вдаватися в деталі, var повідомляє компілятору, що це значення – змінюване. В результаті Scala згенерує і метод-аксессор (String firstName(void)) І метод-мутатор (void firstName_$eq(String)). Ну а далі не представляє особливих труднощів створити інші setХХХ-Мутатори, що використовують всередині себе згенеровані методи.


Висновок


Scala є спробою реалізувати концепції і лаконічність функціонального програмування без шкоди для багатств об’єктної парадигми. Як ви вже, можливо, стали розуміти з цієї серії, Scala також усуває деякі очевидні (з урахуванням наших поточних знань) синтаксичні проблеми, виявлені в мові Java.


Ця друга стаття з серії Путівник по Scala для Java-розробників сфокусована на об’єктних можливості Scala, які дозволяють вам почати використання Scala без занадто глибокого занурення в функціональний вир. Озброївшись усім тим, що вам відомо на даний момент, ви вже в змозі використати Scala для скорочення трудовитрат на програмування. Серед іншого, ви можете використовувати Scala для породження таких же POJO, які необхідні для сторонніх середовищ програмування, типу Spring або Hibernate.


Так що натягуйте свої гідрокостюми та інше дайверське спорядження, тому що стаття наступного місяця позначить початок нашого занурення в глибини функціонального програмування.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*