Статьи

10 функций, которые хотелось бы, чтобы Java украла у котлинского языка

Эта статья просрочена. После того, как шумиха вокруг релиза Kotlin 1.0 закончилась, давайте серьезно рассмотрим некоторые особенности языка Kotlin, которые мы должны иметь и в Java.

В этой статье я не собираюсь желать единорогов. Но есть некоторые низко висящие плоды (насколько я наивно вижу), которые можно было бы ввести в язык Java без особого риска. Пока вы читаете эту статью, не забудьте скопировать примеры вставки на http://try.kotlinlang.org , онлайн-ответ для Kotlin.

1. Класс данных

Языковые дизайнеры вряд ли когда-либо согласятся с необходимостью и сферой действия класса. Любопытно, что в Java у каждого класса всегда есть идентичность, которая на самом деле не нужна в 80% — 90% всех реальных классов Java. Аналогично, у класса Java всегда есть монитор, на котором вы можете синхронизировать .

В большинстве случаев, когда вы пишете класс, вы действительно просто хотите сгруппировать значения, такие как Strings, Ints, Doubles. Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class Person {
    final String firstName;
    final String lastName;
    public JavaPerson(...) {
        ...
    }
    // Getters
    ...
 
    // Hashcode / equals
    ...
 
    // Tostring
    ...
 
    // Egh...
}

К тому времени, как вы закончите вводить все вышеперечисленное, ваши пальцы больше не будут. Разработчики Java реализовали ужасные обходные пути для вышеперечисленного, такие как генерация кода IDE или lombok , который является самым большим из всех хаков. В лучшей Java ничего в Lombok действительно не понадобилось бы.

Как, например, если бы в Java были классы данных Kotlin:

1
2
3
4
data class Person(
  val firstName: String,
  val lastName: String
)

Вышесказанное — это все, что нам нужно для объявления эквивалента предыдущего кода Java. Поскольку класс данных используется для хранения данных (duh), то есть значений, реализация таких вещей, как hashCode() , equals() , toString() очевидна и может быть предоставлена ​​по умолчанию. Кроме того, классы данных являются кортежами первого класса, поэтому их можно использовать как таковые, например, для их деструктурирования в отдельных ссылках:

1
2
val jon = Person("Jon", "Doe")
val (firstName, lastName) = jon

В этом случае мы можем надеяться. Valhalla / Java 10 разрабатывается, а вместе с ним и типы значений . Посмотрим, сколько функций будет предоставлено непосредственно в JVM и на языке Java. Это, безусловно, будет захватывающим дополнением.

Обратите внимание, что val возможен в Kotlin: вывод типа локальной переменной. Это обсуждается для будущей версии Java прямо сейчас .

2. Параметры по умолчанию

Сколько раз вы перегружаете API следующим образом:

1
2
3
4
interface Stream<T> {
    Stream<T> sorted();
    Stream<T> sorted(Comparator<? super T> comparator);
}

Выше приведены точно такие же операции JDK Stream . Первый просто применяет Comparator.naturalOrder() ко второму. Таким образом, мы могли бы написать следующее в Kotlin :

1
2
fun sorted(comparator : Comparator<T>
         = Comparator.naturalOrder()) : Stream<T>

Преимущество этого не сразу видно, когда есть только один параметр по умолчанию. Но представьте себе функцию с множеством дополнительных параметров:

1
2
3
4
5
6
7
fun reformat(str: String,
             normalizeCase: Boolean = true,
             upperCaseFirstLetter: Boolean = true,
             divideByCamelHumps: Boolean = false,
             wordSeparator: Char = ' ') {
...
}

Который может быть вызван любым из следующих способов:

1
2
3
4
5
6
7
8
reformat(str)
reformat(str, true, true, false, '_')
reformat(str,
  normalizeCase = true,
  upperCaseFirstLetter = true,
  divideByCamelHumps = false,
  wordSeparator = '_'
)

Сила параметров по умолчанию заключается в том, что они особенно полезны при передаче аргументов по имени, а не по индексу. В настоящее время это не поддерживается в JVM, которая до Java 8 вообще не сохраняет имя параметра ( в Java 8 вы можете включить флаг JVM для этого , но со всем наследием Java вы не должны полагаться на этом пока нет).

Черт возьми, эту функцию я использую в PL / SQL каждый день. Конечно, в Java вы можете обойти это ограничение, передав объект параметра .

3. Упрощенный экземпляр проверки

Если хотите, это действительно экземпляр переключателя. Некоторые люди могут утверждать, что этот материал злой, плохой ОО дизайн. Ня ня. Я говорю, это происходит время от времени. И, по-видимому, в Java 7 строковые переключатели считались достаточно распространенными, чтобы изменить язык, чтобы разрешить их. Почему бы не instanceof переключатели?

1
2
3
4
val hasPrefix = when(x) {
  is String -> x.startsWith("prefix")
  else -> false
}

Это не только делает instanceof switch, но и делает это в форме присваиваемого выражения. Котлин версия этого, when выражение мощное . Вы можете смешивать любые выражения предикатов, аналогично выражению SQL CASE . Например, это возможно также:

1
2
3
4
5
6
when (x) {
  in 1..10 -> print("x is in the range")
  in validNumbers -> print("x is valid")
  !in 10..20 -> print("x is outside the range")
  else -> print("none of the above")
}

Сравните с SQL (не реализовано во всех диалектах):

1
2
3
4
5
6
CASE x
  WHEN BETWEEN 1 AND 10 THEN 'x is in the range'
  WHEN IN (SELECT * FROM validNumbers) THEN 'x is valid'
  WHEN NOT BETWEEN 10 AND 20 'x is outside the range'
  ELSE 'none of the above'
END

Как видите, только SQL более мощный, чем Kotlin.

4. Карта ключ / обход значения

Теперь это действительно можно сделать очень легко только с помощью синтаксиса Sugar. Конечно, наличие локальных переменных уже будет плюсом, но проверьте это

1
val map: Map<String, Int> = ...

И теперь вы можете сделать:

1
2
3
for ((k, v) in map) {
    ...
}

В конце концов, большую часть времени при обходе карты это происходит с помощью Map.entrySet() . Карта могла бы быть расширена для расширения Iterable<Entry<K, V>> в Java 5, но не имеет. Это действительно жаль. В конце концов, это было улучшено в Java 8, чтобы позволить внутреннюю итерацию по входу, установленному в Java 8 через Map.forEach() :

1
2
3
map.forEach((k, v) -> {
    ...
});

Еще не поздно, боги JDK. Вы все еще можете позволить Map<K, V> extend Iterable<Entry<K, V>>

5. Карта доступа к литералам

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

1
2
int[] array = { 1, 2, 3 };
int value = array[0];

Обратите внимание также на тот факт, что у нас есть литералы инициализатора массива в Java, и это здорово. Так почему бы не разрешить доступ к элементам карты с одинаковым синтаксисом?

1
2
3
val map = hashMapOf<String, Int>()
map.put("a", 1)
println(map["a"])

Фактически, x[y] — это просто синтаксический сахар для вызова метода, поддерживаемого x.get(y) . Это так здорово, мы сразу же переименовали наши Record.getValue() в jOOQ в Record.get() (конечно, оставив старые как синонимы), так что теперь вы можете разыменовывать значения записей вашей базы данных как таковые. в Котлине

1
2
3
4
5
6
7
8
ctx.select(a.FIRST_NAME, a.LAST_NAME, b.TITLE)
   .from(a)
   .join(b).on(a.ID.eq(b.AUTHOR_ID))
   .orderBy(1, 2, 3)
   .forEach {
       println("""${it[b.TITLE]}
               by ${it[a.FIRST_NAME]} ${it[a.LAST_NAME]}""")
   }

Поскольку jOOQ содержит всю информацию о типах столбцов в отдельных столбцах записи, вы можете заранее знать, что it[b.TITLE] является выражением String. Отлично, а? Таким образом, этот синтаксис можно использовать не только с картами JDK, но и с любой библиотекой, которая предоставляет основные методы get() и set() .

6. Функции расширения

Это одна спорная тема, и я могу прекрасно понимаю, когда разработчики языка держаться подальше от него. Но время от времени функции расширения очень полезны. Синтаксис Kotlin здесь на самом деле просто для функции, которая притворяется частью типа получателя:

1
2
3
4
5
fun MutableList<Int>.swap(index1: Int, index2: Int) {
  val tmp = this[index1] // 'this' corresponds to the list
  this[index1] = this[index2]
  this[index2] = tmp
}

Теперь это позволит менять элементы в списке:

1
2
val l = mutableListOf(1, 2, 3)
l.swap(0, 2)

Это было бы очень полезно для библиотек, таких как jOOλ , которые расширяют Java 8 Stream API, оборачивая его в тип jOOλ ( другая такая библиотека — StreamEx , с немного другим фокусом). Тип оболочки jOOλ Seq самом деле не важен, поскольку он претендует на роль стрима на стриоидах. Было бы здорово, если бы методы jOOλ могли быть искусственно помещены в Stream , просто импортировав их:

1
2
3
list.stream()
    .zipWithIndex()
    .forEach(System.out::println);

Метод zipWithIndex() самом деле не существует. Вышеприведенное просто переведет на следующий, менее читаемый код:

1
2
3
seq(list.stream())
    .zipWithIndex()
    .forEach(System.out::println);

Фактически, методы расширения позволят даже обойти явное завершение в stream() . Например, вы можете сделать:

1
2
list.zipWithIndex()
    .forEach(System.out::println);

Поскольку все методы jOOλ могут быть разработаны для применения к Iterable .

Опять же, это спорная тема. Например, потому что

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

7. Оператор безопасного звонка (а также: оператор Элвис)

Необязательно, это мех. Понятно, что Optional тип необходимо было ввести для абстрагирования от отсутствия значений примитивного типа, которые не могут быть нулевыми. Теперь у нас есть такие вещи, как OptionalInt , например, для моделирования таких вещей, как:

1
2
3
4
5
6
7
OptionalInt result =
IntStream.of(1, 2, 3)
         .filter(i -> i > 3)
         .findFirst();
 
// Agressive programming ahead
result.orElse(OR_ELSE);

Необязательно это монада

Да. Это позволяет вам flatMap() отсутствующее значение.

Конечно, если вы хотите заниматься сложным функциональным программированием, вы начнете печатать map() и flatMap() везде. Как сегодня, когда мы печатаем геттеры и сеттеры. Вскоре появится lombok, генерирующий вызовы плоских карт, а Spring добавит аннотацию в стиле @AliasFor для плоских карт. И только просвещенный сможет расшифровать ваш код.

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

1
String name = bob?.department?.head?.name

Мне очень нравится этот тип прагматизма в Котлине. Или вы предпочитаете (плоский) картографирование?

1
2
3
4
Optional<String> name = bob
    .flatMap(Person::getDepartment)
    .map(Department::getHead)
    .flatMap(Person::getName);

Можешь прочитать это? Я не могу. Я тоже не могу написать это. Если вы ошибетесь, вы получите бокс.

Конечно, Цейлон является единственным языком, который получил правильные нули . Но у Цейлона есть множество функций, которые Java не получит до версии 42, и я не желаю единорогов. Я желаю оператора безопасного вызова (а также оператора elvis, который немного отличается), который также может быть реализован в Java. Вышеприведенное выражение является просто синтаксическим сахаром для:

1
2
3
4
5
6
7
8
9
String name = null;
if (bob != null) {
    Department d = bob.department
    if (d != null) {
        Person h = d.head;
        if (h != null)
            name = h.name;
    }
}

Что может быть не так с этим упрощением?

8. Все это выражение

Теперь это может быть просто единорог. Я не знаю, есть ли ограничение JLS / парсера, которое навсегда удержит нас в страдании доисторического различия между утверждением и выражением.

В какой-то момент люди начали использовать операторы для вещей, которые дают побочные эффекты, и выражения для более функциональных вещей. Поэтому неудивительно, что все методы String действительно являются выражениями, работающими с неизменяемой строкой и постоянно возвращающими новую строку.

Это, кажется, не подходит, например, для if-else в Java, который, как ожидается, будет содержать блоки и операторы, каждый из которых может привести к побочным эффектам.

Но действительно ли это требование? Разве мы не можем написать что-то подобное на Java?

1
val max = if (a > b) a else b

ОК, у нас есть странное условное выражение, использующее ?: . Но как насчет того, when Kotlin (то есть switch Java)?

1
2
3
4
val hasPrefix = when(x) {
  is String -> x.startsWith("prefix")
  else -> false
}

Разве это не намного полезнее, чем следующий эквивалент?

1
2
3
4
5
6
boolean hasPrefix;
 
if (x instanceof String)
    hasPrefix = x.startsWith("prefix");
else
    hasPrefix = false;

(да, я знаю о ?: . Я просто нахожу, if-else легче читать, и я не понимаю, почему это должно быть утверждение, а не выражение. Черт, в Kotlin, даже try является выражением, а не утверждением :

1
2
3
4
5
val result = try {
    count()
} catch (e: ArithmeticException) {
    throw IllegalStateException(e)
}

Красивая!

9. Функции одного выражения

Теперь это. Это сэкономит так много времени на чтение и написание простого кода. И фактически, у нас уже есть синтаксис в аннотациях. Посмотрите, например, волшебную аннотацию SpringAlasFor для Spring. Это дает:

1
2
3
4
5
6
public @interface AliasFor {
    @AliasFor("attribute")
    String value() default "";
    @AliasFor("value")
    String attribute() default "";
}

Теперь, если вы действительно сильно щуритесь, это всего лишь методы, дающие постоянные значения, потому что аннотации — это просто интерфейсы с сгенерированным байтовым кодом для их реализации. Мы можем обсудить синтаксис. Конечно, это нерегулярное использование default является странным, учитывая, что оно не использовалось повторно в Java 8 для методов по умолчанию, но я предполагаю, что Java всегда нуждается в дополнительном синтаксисе, чтобы разработчики чувствовали себя живыми, поскольку они могли лучше чувствовать свои пальцы ввода. Ничего страшного. Мы можем жить с этим. Но опять же, почему мы должны? Почему бы просто не сходиться к следующему?

1
2
3
4
public @interface AliasFor {
    String value() = "";
    String attribute() = "";
}

И то же самое для методов класса / интерфейса по умолчанию?

1
2
3
4
5
// Stop pretending this isn't an interface
public interface AliasFor {
    String value() = "";
    String attribute() = "";
}

Теперь это будет выглядеть красиво. Но учитывая существующий синтаксис Java, это может быть просто единорог, поэтому давайте перейдем к…

10. Чувствительная к потоку печать

Теперь это . ЭТО!

Мы уже писали о типах сумм. У Java есть типы сумм с исключениями начиная с Java 7:

1
2
3
4
5
6
7
try {
    ...
}
catch (IOException | SQLException e) {
    // e can be of type IOException and/or SQLException
    // within this scope
}

Но у Java, к сожалению, нет чувствительной к потоку типизации. Чувствительная к потоку типизация имеет существенное значение в языке, который поддерживает типы сумм, но в других случаях она также полезна. Например, в Котлине:

1
2
3
when (x) {
    is String -> println(x.length)
}

Нам не нужно x is String , очевидно, потому что мы уже проверили, что x is String . И наоборот, в Java:

1
2
if (x instanceof String)
    System.out.println(((String) x).length());

Аааа, все это печатать. Автозаполнение IDE достаточно умен, чтобы уже предлагать методы контекстного типа, а затем генерировать ненужные для вас приведения. Но было бы замечательно, если бы это никогда не понадобилось, каждый раз, когда мы явно сужали тип, используя структуры потока управления.

Для получения дополнительной информации см. Эту запись в Википедии о типизации, чувствительной к потоку . Функция, которая может быть абсолютно добавлена ​​в язык Java. В конце концов, мы уже получили чувствительные к потоку конечные локальные переменные начиная с Java 8.

11. (Бонус) декларация сайта отклонений

И последнее, но не менее важное: улучшенные дженерики с помощью декларации на сайте . Это знают и многие другие языки, например, IEnumerable ‘s IEnumerable :

открытый интерфейс IEnumerable <out T>: IEnumerable

Ключевое слово здесь означает, что универсальный тип T создается из типа IEnumerable (в отличие от in , который обозначает потребление). В C #, Scala, Ceylon, Kotlin и многих других языках мы можем объявить это в объявлении типа, а не в его использовании (хотя многие языки допускают оба варианта). В этом случае мы говорим, что IEnumerable ковариантен с его типом T , что снова означает, что IEnumerable<Integer> является подтипом IEnumerable<Object>

В Java это невозможно, поэтому у нас возник вопрос о новичках в Java из-за переполнения стека . Почему я не могу …

1
2
Iterable<String> strings = Arrays.asList("abc");
Iterable<Object> objects = strings; // boom

На таких языках, как Kotlin, вышесказанное было бы возможно. В конце концов, почему бы и нет? Вещи, которые могут создавать строки, могут также создавать объекты, и мы даже можем использовать это таким образом в Java:

1
2
3
4
Iterable<String> strings = Arrays.asList("abc");
for (Object o : strings) {
    // Works!
}

Отсутствие декларации сайта отклонения сделали многие API очень понятными. Рассмотрим Stream :

1
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

Это просто шум. Функция противоречива с ее типом аргумента и ковариантна с ее типом результата по природе, лучшее определение Function или Stream было бы:

1
2
interface Function<in T, out R> {}
interface Stream<out T> {}

Если бы это было возможно, все это ? super ? super а ? extends ? extends мусор может быть удален без потери какой-либо функциональности.

Если вам интересно, о чем я вообще говорю?

  • Хорошая новость заключается в том, что это обсуждается для (ближайшей) будущей версии Java: http://openjdk.java.net/jeps/8043488

Вывод

Котлин является многообещающим языком, даже если уже слишком поздно для игры, которая, кажется, уже решена, не в пользу альтернативных языков в JVM. Тем не менее, это очень интересный язык для изучения и с очень многими очень хорошими решениями, принятыми в отношении некоторых простых вещей.

Мы надеемся, что некоторые из этих решений будут приняты богами языка Java и интегрированы в Java. Этот список показывает некоторые функции, которые «легко» добавить.