Статьи

Какие функции Java были исключены в Scala?

Несмотря на более сложный и менее интуитивно понятный синтаксис по сравнению с Java, Scala фактически отбрасывает некоторые функции Java, иногда насовсем, а иногда предлагает замены на уровне стандартной библиотеки. Как вы скоро увидите, Scala не является надмножеством Java (например, Groovy) и на самом деле удаляет много шума. Ниже приведен каталог отсутствующих функций .

разбить и продолжить в петлях

Каждый раз, когда я вижу такой код:

1
2
3
4
5
6
while(cond1) {
    work();
    if(cond2)
        continue;
    rest();
}

Я чувствую, как будто это было написано парнем, который действительно скучает по временам, когда goto еще не считался вредным. Поднимите руки, кто находит эту версию более читабельной:

1
2
3
4
5
while(cond1) {
    work();
    if(!cond2)
        rest();
}

Чтобы избавиться от break, нужно немного больше, но обычно извлечение цикла для отдельного метода / функции (или, по крайней мере, помещение его в конец существующего метода) и использование return вместо этого поможет. По той причине, что Scala позволяет вам определять функции внутри других функций, чтобы вы не загрязняли пространство имен глобального класса множеством небольших методов, используемых только один раз — проблема, которая иногда возникает при религиозном извлечении методов в Java.

перерыв и продолжение — мы благодарим вас от имени наших отцов и дедов за ваш вклад в императивное программирование. Но ты нам больше не нужен, и мы не будем скучать по тебе.

Массивы

Удивительно, сколько вредных привычек мы узнали за все эти годы и как мы привыкли к противоречивым и просто болезненным идиомам. У вас есть ковариантные массивы в Java с синтаксисом в квадратных скобках, конечным свойством длины и возможностью хранения примитивных типов. У вас также есть инфраструктура коллекций Java с абстракцией List <t>, которая не является ковариантной, использует методы get () и size () и не может хранить примитивы. Список различий на этом не заканчивается, однако не является ли каждый массив просто частным случаем List? Почему у нас есть специальный синтаксис для массивов в языке, в то время как коллекции реализованы поверх языка? И не раздражает ли их постоянное преобразование из одного в другое?

1
2
3
String[] array = new String[10];
List<string> list = Arrays.asList(array);
String[] array2 = list.toArray(new String[list.size()]);

Преобразование из коллекции в массив — моя любимая идиома Java … Почему бы просто не иметь одинаковый синтаксис, одинаковые методы, одинаковую абстракцию, полиморфное поведение — и только разные имена реализации?

1
2
3
4
5
6
7
val array = Array("ab", "c", "def")
println(array(1))
array filter (_.size > 1)
 
val list = List("ab", "c", "def")
println(list(1))
println(list filter (_.size > 1))

И не волнуйтесь, за кулисами компилятор Scala будет использовать тот же эффективный байт-код массива, как если бы вы использовали простые массивы в Java. Никаких волшебных абстракций и нескольких слоев упаковки.

Примитивы

Еще одно странное несоответствие Java — почему у нас есть выбор между примитивным int и упаковкой Integer? Если переменная имеет тип Integer, означает ли это, что она является необязательной (нулевой), или это просто, что вы не можете использовать примитивы в коллекциях (но можете в массивах, как указано выше)? Безопасна ли эта распаковка (также известная как: как это может вызвать исключение NullPointerException? ) Могу ли я сравнить их с целыми числами, используя оператор ==? И можно ли просто вызвать toString (), чтобы получить строковое представление этого числа?

В Scala у вас больше нет выбора, каждый тип примитива является объектом, в то время как большую часть времени он остается примитивом в памяти и в байт-коде. Как это возможно? Посмотрите на следующий популярный пример:

1
2
3
4
val x = 37     //x and y are objects of type Int
val y = 5
val z = x + y  //x.+(y) - yes, Int class has a "+" method
assert(z.toString == "42")

x, y и z являются экземплярами типа Int. Все они являются объектами, даже добавление двух целых чисел — это метод +, вызываемый для x с аргументом y. Если вы думаете, что это должно работать ужасно — еще раз за кулисами это скомпилировано в обычное примитивное дополнение. Но теперь вы можете легко использовать примитивы в коллекциях, передавать их, когда требуется любой тип (объект в Java, любой в Scala), или просто создавать текстовое представление без неудобной идиомы Integer.toString (7). Ооочень много вредных привычек.

Проверенные исключения

Еще одна особенность, которую я едва могу пропустить. Не так много, чтобы сказать здесь. Ни у какого основного языка, кроме Java, их нет, ни у какого-либо основного языка JVM (кроме Java). Эта тема еще относительно спорная, однако, если вы когда-либо пробовали бороться с повсеместной SQLException или IOException, вы знаете, сколько шаблонным он вводит без уважительной причины. Во всяком случае, посмотрите на следующие примеры …

Интерфейсы

Это хорошо! У Scala нет интерфейсов. Вместо этого он вводит черты — что-то среднее между абстрактными классами (некоторые методы черт могут иметь реализацию) и интерфейсами (можно смешивать более чем одну черту). Таким образом, по существу, черты позволяют вам реализовать множественное наследование, избегая при этом ужасной проблемы с бриллиантами . Как это сделать, требует отдельной статьи (короче: последняя черта побеждает), но я бы лучше показал вам пример того, как полезны черты для уменьшения дублирования.

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

1
2
3
public interface Marshaller {
    long send(byte[] content);
}

Это здорово с точки зрения реализации — просто внедрите один метод, и абстракция готова. Однако пользователи интерфейса жалуются, что он громоздок и не очень удобен. Они хотели бы отправлять строки, двоичные и текстовые потоки, сериализованные объекты и так далее. Они могут либо создать фасад вокруг этого интерфейса (и каждый пользователь создаст свой собственный с определенным набором ошибок), либо заставить автора API расширить его:

01
02
03
04
05
06
07
08
09
10
11
12
13
public interface Marshaller {
 
    long send(byte[] content);
 
    long send(InputStream stream);
 
    long send(Reader reader);
 
    long send(String s);
 
    long send(Serializable obj);
 
}

Теперь API-интерфейс очень прост, однако каждая реализация должна реализовывать пять методов вместо одного. Также отметим, что, поскольку большинство абстрактных протоколов основаны на байтовых массивах, все методы могут быть реализованы в терминах первого. И только первый содержит действительный код сортировки. Это, в свою очередь, приводит к тому, что каждая реализация имеет одинаковые четыре метода — дублирование не исчезло — оно было просто перемещено. На самом деле эта проблема известна как тонкий и богатый интерфейс, и она была описана в большой книге по программированию в Scala . Обычно я предоставлял поставщикам услуг абстрактный класс с типичными реализациями всех методов, кроме корневого, который использовался всеми другими методами:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import org.apache.commons.io.IOUtils;
 
public abstract class MarshallerSupport implements Marshaller {
 
    @Override
    public abstract long send(byte[] content);
 
    @Override
    public long send(InputStream stream) {
        try {
            return send(IOUtils.toByteArray(stream));
        } catch (IOException e) {
            throw new RuntimeException(e);  //choose something more specific in real life
        }
    }
 
    @Override
    public long send(Reader reader) {
        try {
            return send(IOUtils.toByteArray(reader));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
 
    @Override
    public long send(String s) {
        try {
            return send(s.getBytes("UTF8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
 
    @Override
    public long send(Serializable obj) {
        try {
            final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            new ObjectOutputStream(bytes).writeObject(obj);
            return send(bytes.toByteArray());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
 
}

Теперь все довольны — вместо того, чтобы копировать все перегруженные методы снова и снова, просто создайте подкласс MarshallerSupport и реализуйте то, что вам нужно. Но что, если ваша реализация интерфейса также должна наследовать какой-то другой класс? Тогда тебе не повезло. Однако в Scala вы изменяете интерфейс на черты, открывая возможность смешивать (думать что-то между расширением и реализацией) несколько других черт. Кстати, ты помнишь, что я сказал о проверенных исключениях?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
trait MarshallerSupport extends Marshaller {
 
    def send(content: Array[Byte]): Long
 
    def send(stream: InputStream): Long = send(IOUtils.toByteArray(stream))
 
    def send(reader: Reader): Long = send(IOUtils.toByteArray(reader))
 
    def send(s: String): Long = send(s.getBytes("UTF8"))
 
    def send(obj: Serializable): Long = {
        val bytes = new ByteArrayOutputStream
        new ObjectOutputStream(bytes).writeObject(obj)
        send(bytes.toByteArray)
    }
}

Смена оператора

В Scala нет оператора switch. Называть шаблон, соответствующий лучшему переключателю, было бы кощунством. Не только потому, что сопоставление с образцом в Scala является выражением, возвращающим значение, а также не потому, что вы можете переключать буквально любое значение, если хотите. Даже потому, что здесь нет провалов, разрывов и дефолтов. Это потому, что сопоставление с образцом в Scala позволяет сопоставлять целые структуры объектов и списки, даже с подстановочными знаками. Рассмотрим метод упрощения выражений, первоначально взятый из уже упомянутого программирования в Scala :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
 
//...
 
def simplify(expr: Expr): Expr = expr match {
    case UnOp("-", UnOp("-", e)) => e  //double negation
    case BinOp("+", e, Number(0)) => e //adding zero
    case BinOp("*", e, Number(1)) => e //multiplying by one
    case _ => expr
}

Посмотрите внимательно, насколько умный этот код! Если наше выражение является унарной операцией «-», а аргумент является второй унарной операцией «-» с любым выражением e в качестве аргумента (думаю: — (- e)), то просто верните e. Если вы считаете этот пример сопоставления с образцом трудным для чтения, ознакомьтесь с примерно эквивалентным кодом Java. Однако, пожалуйста, помните: размер не имеет значения (вероятно, можно сделать то же самое с однострочным Perl) — речь идет о читабельности и ремонтопригодности:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public Expr simplify(Expr expr) {
    if (expr instanceof UnOp) {
        UnOp unOp = (UnOp) expr;
        if (unOp.getOperator().equals("-")) {
            if (unOp.getArg() instanceof UnOp) {
                UnOp arg = (UnOp) unOp.getArg();
                if (arg.getOperator().equals("-"))
                    return arg.getArg();
            }
        }
    }
    if (expr instanceof BinOp) {
        BinOp binOp = (BinOp) expr;
        if (binOp.getRight() instanceof Number) {
            Number arg = (Number) binOp.getRight();
            if (binOp.getOperator().equals("+") && arg.getNum() == 0 ||
                    binOp.getOperator().equals("*") && arg.getNum() == 1)
                return binOp.getLeft();
        }
    }
    return expr;
}

InstanceOf / литье

Как и во многих других функциях, Scala не имеет встроенного синтаксиса для instanceof и downcasting. Вместо этого язык предоставляет вам методы для реальных объектов:

1
2
val b: Boolean = expr.isInstanceOf[UnOp]
val unOp: UnOp = expr.asInstanceOf[UnOp]

В Scala многие функции, обычно рассматриваемые как часть языка, фактически реализованы в самом языке или, по крайней мере, не требуют специального синтаксиса. Мне нравится эта идея, фактически я нахожу способ создания объектов в Ruby (Foo.new — метод вместо оператора new) очень привлекательным и даже необычным отсутствием условных выражений в Smalltalk, требует некоторого внимания .

Перечисления

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

01
02
03
04
05
06
07
08
09
10
object Status extends Enumeration {
   type Status = Value
 
   val Pending = Value("Pending...")
   val Accepted = Value("Accepted :-)")
   val Rejected = Value("Rejected :-(")
}
 
assume(Status.Pending.toString == "Pending...")
assume(Status.withName("Rejected :-(") == Status.Rejected)

Или, если вас не волнует текстовое представление enum:

1
2
3
4
5
object Status extends Enumeration {
   type Status = Value
 
   val Pending, Accepted, Rejected = Value
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sealed abstract class Status(val code: Int) {
 def name: String
}
 
case object Pending extends Status(0) {
 override def name = "?"
}
 
case object Accepted extends Status(1) {
 override def name = "+"
}
 
case object Rejected extends Status(-1) {
 override def name = "-"
}
 
//...
 
val s: Status = Accepted
 
assume(s.name == "+")
assume(s.code == 1)
 
s match {
    case Pending =>
    case Accepted =>
    case Rejected =>  //comment this line, you'll see compiler warning
}

Этот подход, хотя и не имеет ничего общего с перечислениями как таковыми, имеет много преимуществ. Самое важное, что компилятор предупредит вас при выполнении неисчерпывающего сопоставления с образцом — подумайте: переключите перечисление в Java без явной ссылки на каждое значение или блок по умолчанию.

Статические методы / поля

Scala не имеет понятия о статических полях и методах. Вместо этого он имеет функцию именованных объектов в отличие от классов. Когда вы определяете класс с помощью ключевого слова объекта , среда выполнения Scala с радостью создаст один экземпляр этого класса и сделает его доступным под именем класса. По сути, это единый шаблон, встроенный в язык, но наиболее важным является сдвиг мышления, представленный этим подходом. Вместо набора статических функций, искусственно собранных внутри класса (который в данном случае является пространством имен де-факто ), у вас есть синглтон с истинными методами:

01
02
03
04
05
06
07
08
09
10
11
12
sealed abstract class Status
case object Pending extends Status
case object Accepted extends Status
case object Rejected extends Status
 
case class Application(status: Status, name: String)
 
object Util {
 
    def groupByStatus(applications: Seq[Application]) = applications groupBy {_.status}
 
}

Вот как работает синтаксис (и хороший пример ScalaTest DSL):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RunWith(classOf[JUnitRunner])
class UtilTest extends FunSuite with ShouldMatchers {
 
   type ? = this.type
 
   test("should group applications by status") {
      val applications = List(
         Application(Pending, "Lorem"),
         Application(Accepted, "ipsum"),
         Application(Accepted, "dolor")
      )
 
      val appsPerStatus = Util.groupByStatus(applications)
 
      appsPerStatus should have size (2)
      appsPerStatus(Pending) should (
            have size (1) and
            contain (Application(Pending, "Lorem"))
      )
      appsPerStatus(Accepted) should (
            have size (2) and
            contain (Application(Accepted, "ipsum")) and
            contain (Application(Accepted, "dolor"))
      )
   }
}

volatile / transient / native и serialVersionUID ушли

Разработчики языка решили преобразовать первые три ключевых слова в аннотации. Оба подхода имеют свои плюсы и минусы, трудно найти явного победителя. Однако превращение serialVersionUID в аннотацию на уровне класса является довольно хорошим выбором. Я знаю, что это поле существовало задолго до того, как аннотации были введены в язык Java, поэтому мы не должны винить его. Но я всегда ненавидел, когда в статически типизированных языках некоторые имена / поля имеют особое значение, не отраженное нигде, кроме самой спецификации языка ( магические числа? ). К сожалению, в Scala есть примеры такого неприятного поведения, а именно специальная обработка метода apply () и методы, заканчивающиеся двоеточием. Печалька.

Pre / пост-инкремент

Вы не можете делать i ++ и ++ i в Scala. Период. Вам нужно больше подробностей i + = 1 — и, что еще хуже, это выражение возвращает единицу измерения (думаю: void ). Как мы можем справиться с отсутствием этой заметной функции? Оказывается, что очень часто этот тип конструкций является обязательным стилевым наследием, и их можно легко избежать, используя более функциональные и чистые конструкции. Возьмем следующую проблему в качестве примера:

У вас есть два массива одинакового размера: один с именами, а второй с возрастами. Теперь вы хотите отобразить каждое имя с соответствующим возрастом — каким-то образом итерируя по обоим массивам параллельно. В Java это удивительно сложно реализовать чисто:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
String[] names = new String[]{"Alice", "Bobby", "Eve", "Jane"};
Integer[] ages = new Integer[]{27, 31, 29, 25};
 
int curAgeIdx = 0;
for (String name : names) {
    System.out.println(name + ": " + ages[curAgeIdx]);
    ++curAgeIdx;
}
 
//or:
 
for(int idx = 0; idx < names.length; ++idx)
    System.out.println(names[idx] + ": " + ages[idx]);
}

В Scala, возможно, она короче, но поначалу очень загадочная:

1
2
3
4
var names = Array("Alice", "Bobby", "Eve", "Jane")
var ages = Array(27, 31, 29, 25)
 
names zip ages foreach {p => println(p._1 + ": " + p._2)}

почтовый индекс ? Я призываю вас немного поиграть с этим примером. Если вы не хотите запускать всю IDE, попробуйте Scala REPL:

1
2
3
$ scala
scala> Array("one", "two", "three") zip Array(1, 2, 31)  
res1: Array[(java.lang.String, Int)] = Array((one,1), (two,2), (three,31))

Посмотрите внимательно, видите ли вы массив результатов, содержащий пары соответствующих элементов из первого и второго массивов, « сжатых » вместе? Один простой эксперимент, и теперь вдруг он должен быть понятным и гораздо более читабельным, чем обычное императивное решение.

Изобретатели Scala очень тщательно изучили язык Java и не просто добавили синтаксический сахар (например, функциональные литералы или неявные преобразования). Они обнаружили множество несоответствий и неприятностей в Java, избавились от них и предложили более краткие и продуманные замены. Несмотря на конструкции более высокого уровня, такие как примитивные и массивные объекты, под капотом генерируется такой же быстрый и простой байт-код.

Ссылки: Какие функции Java были исключены в Scala? от нашего партнера JCG Томека Нуркевича в NoBlogDefFound

Удачного кодирования! Не забудь поделиться!

Статьи по Теме: