Как стать автором
Обновить

Комментарии 27

Спасибо за пост!
с null pointerами конечно найс, многих избавит от боли
Вот бы структуры завезли как в C#, которые обладали бы поведением, но хранились на стеке, и копировались по значению. И чтобы их, вместе с примитивами можно было упаковывать в списки и передавать как параметр дженерикам:
val list = new ArrayList<int>(); // вместе с Lombok будет красота

Исторический костыль с int, Integer, double, Double очень раздражает.
Пожалуй самый бросающийся в глаза, если учесть что 95% работы рядового программиста, это работа со списками.
И хотя говорят что эффективность не фича Java, но вы прикиньте как бы мы разгрузили GC, если бы переложили часть операций на стек.
Согласен, может кому то и не понадобится, но кучу высоконагруженных проектов это спасет от жестких оптимизаций, когда все хранится в виде кучи полей-массивов из примитивов (привет процедурное программирование). Можно будет писать требующие быстродействия и экономии вещи в привычном ООП стиле.
Для этого стоит дождаться value types от Valhalla. Value types выглядят как очень хороший шаг вперед в области эффективности при должном уровне абстракции. Но пока там есть много открытых вопросов. Это реально один из самых интересных проектов Java. Наверно Loom и Valhalla для меня самые интересные на текущий момент.
Насколько я понял концепцию value types, там все же используется куча, а не настоящий стек, просто добавляется семантика копирования как у примитивных типов. Но хотя бы так! Жду их еще со времен Java 7 :)
На последнем Joker хороший доклад Олега Куксенко на эту тему был. Если кратко, решает JVM. Она может положить как в стек, так и в кучу.
Вот вырезка из PDF доклада
JVM decides if:
● allocate on heap
OR
● put on stack (locals, parameters, result)
● inline into container class
● inline into array (flattened array)

Посмотрим, конечно, что в финале выйдет. Но тем не менее это уже шаг вперед.
В общем у меня надежда есть, но к сожалению я не перформанс инженер Java\JVM и сказать авторитетно ничего не смогу, пока не выйдет в public финальная версия. А потом можно будет попробовать сравнить, сделав какие-нибудь объективные тесты на одинаковом окружении.
Мое мнение тут субъективно. Пока все последние оптимизации JVM и стандартной библиотеки очень даже радуют.
НЛО прилетело и опубликовало эту надпись здесь

Ну в целом мы в наших проектах активно используем javax.annotation.nonnull и nullable. Вместе с современной IDE в нашем коде nptr ex не случается, максимум на стыке со сторонними библиотеками. Но есть побочные явления:
1) код естественно захламляется
2) можно забыть поставить аннотацию и ide ничего не скажет (код ревью нам в помощь)
3) в постороннем коде аннотаций может и не быть (и скорее всего и не будет)
Добавить в jdk такую аннотацию можно было бы и в compile time проверять. Это безусловно усложнит анализ кода. Надо будет отслеживать проверки на null. Кроме того весь багаж кода как-то надо будет поддержать по обратной совместимости. Не будут же все переписывать библиотеки?


Честно говоря мне очень нравится в этом плане Kotlin. nullable типы как отдельный класс типов очень удобен. Пока явно не указал, все notnull. Кроме того Kotlin компилятор хорошо понимает все виды java notnull аннтотации. Прям хорошая работа

Заранее извиняюсь, но любопытство не порок. Я крайне плохо разбираюсь в Java и как следствие некоторый код мне не понятен. В частности, что вот тут происходит:


if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}

Зачем делать преобразование типов, если переменна obj и так является String? Или это не преобразование?

Java является строго типизированным языком. Все классы в иерархии идут от класс Object. Зачастую методы могут принимать обобщенный тип, аж до Object и в зависимости от типа менять свое поведение. Кроме того, компилятор не запоминает контекст, в отличии, например, от Kotlin. Для него то, что мы проверили на String уже на следующей строке не имеет значения.
public class Sample {
   public void method(Object obj) {
      System.out.println(obj.toUpperCase()); // compile-time error
   }
   public void methodB(Object obj) {
      if (obj instanceof String) {
         System.out.println(obj.toUpperCase()); // compile-time error
      }
   }
}


В этом плане Kotlin является более продвинутым в компиляции. Вот тот же пример на Kotlin
fun sampleFun(obj: Any) {
   if (obj is String) {
      print(obj.toUpperCase())  // it works
  }
}

А если obj is String вынести в функцию и вставить ее вызов в if — сработает?

Нет, так не сработает. Даже если вспомогательная функция будет inline.
Само по себе не сработает, но для этого в Kotlin есть специальная штука — контракты (тут важно заметить что компилятор не проверяет гарантии и можно всё сломать). И я не уверен, но у них вроде бы еще экспериментальный статус (как раз потому что можно всё сломать ими).
fun isNotEmptyString(obj: Any):Boolean {
    contract {
        returns(true) implies (obj is String)
    }
    return (obj is String) && obj.isNotEmpty()
//return (obj is Int) && obj > 0 //still valid with contract 
}

Тут подробнее

Спасибо! Интересная вещь, хотя и не совсем понятно, в чем профит программиста от такого описания контракта? Мы добавляем какую-то метаинфу для компилятора, чтобы он лучше понимал основной код, и, видимо, не выдавал ворнинги. Не проще ли сразу задизайнить язык так, чтобы компилятор понимал код без подсказок?

Как мне кажется, вопрос же сложности компилятора. До какой вложенности стоит запоминать контекст? А если мы присваиваем новой переменной ссылку внутри новой функции, а потом проверяем? Стоит ли нам в этом случае запоминать контекст? Это трейдофф между мощностью компилятора, временем компиляции и конечно же ресурсами, которые используются для компиляции

Я имел ввиду, что можно было бы сделать полноценный паттерн-матчинг как синтаксическую конструкцию. Да, для компилятора это было бы чуть более напряжно. Но пусть уж лучше компилятор напрягается, чем программист, которому теперь надо держать в голове все неявные преобразования, которые делают смарткасты и контракты. Целых два разных механизма, решающих довольно узкую задачу, которая по идее должна достаточно редко возникать в языке с type-safety и null-safety.

С одной стороны когда допилят компилятор такие штуки станут не особо нужны. Но с другой стороны мы можем проверять тип неявно, но при этом быть уверены что тип будет именно тот который нам нужен. Например мы добавляем всем потомкам поле type, по которому можем узнать класс. (Я не уверен насколько оправдано такое решение, но вроде бы оно может быть быстрее чем «x is A»)
код
enum class FigureType {
    CIRCLE, RECTANGLE
}

sealed class Figure {
    internal abstract val type: FigureType
}

class Circle(val x: Int, val y: Int, val radius: Int) : Figure() {
    override val type = FigureType.CIRCLE

}

class Rectangle(val x0: Int, val y0: Int, val x1: Int, val y1: Int) : Figure() {
    override val type = FigureType.RECTANGLE
}

@ExperimentalContracts
fun Figure.isCircle(): Boolean {
    contract {
        returns(true) implies (this@isCircle is Circle)
    }
    return type == FigureType.CIRCLE
}

@ExperimentalContracts
fun Figure.isRectangle(): Boolean {
    contract {
        returns(true) implies (this@isRectangle is Rectangle)
    }
    return type == FigureType.RECTANGLE
}

@ExperimentalContracts
fun test(figure: Figure) {
    when {
        figure.isCircle() -> {
            println(figure.x)
            println(figure.y)
            println(figure.radius)
        }
        figure.isRectangle()-> {
            println(figure.x0)
            println(figure.y0)
            println(figure.x1)
            println(figure.y1)
        }
    }
}

Да, достаточно интересное использование, но думаю перегруженное для такого. По производительности надо замерять конечно.

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


Чисто для сравнения аналогичный вашему код на Scala:


sealed trait Figure
case class Circle(x: Int, y: Int, radius: Int) extends Figure
case class Rectangle(x0: Int, y0: Int, x1: Int, y1: Int) extends Figure

val test: Figure => Unit = _ match {
  case Circle(x, y, radius) => println(s"$x, $y, $radius")
  case Rectangle(x0, y0, x1, y1) => println(s"$x0, $y0, $x1, $y1")
}
Насколько я понимаю конструкция
_ match {
  case Circle(x, y, radius) => println(s"$x, $y, $radius")
  case Rectangle(x0, y0, x1, y1) => println(s"$x0, $y0, $x1, $y1")
}
внутри содержит instanceof (посмотрел декомпилированный код). А я как раз хотел показать пример где instanceof не используется и компилятор нуждается в подсказке.
Код на котлине эквивалентеый вашему коду на Scala
when(figure){
   is Circle -> with(figure) { println("$x $y $radius") }
   is Rectangle -> with(figure) { println("$x0 $y0 $x1 $x1") }
}
А я как раз хотел показать пример где instanceof не используется и компилятор нуждается в подсказке.

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


Код на котлине эквивалентеый вашему коду на Scala

Он не совсем эквивалентный.


Во-первых, в котлиновской версии одна и та же переменная figure имеет сначала один тип, а потом уже другой. Это облегчает написание кода (не надо самому писать каст), но усложняет чтение. Даже в джаве, которая гораздо сильнее ограничена в плане синтаксиса, это сделали по-нормальному.


Во-вторых, насколько такой подход масштабируется на чуть более сложные случаи типа:


case Vacancy(id, _, Some(Salary(from, to, Some(currency))), Nil) => ...
Как раз нашёл статью где сравниваются два подхода — instanceOf и enum, с instanceOf медленнее, хоть и красивее. habr.com/ru/post/430014
По моему идея прятать не очень красивые, но более эффективные решения в глубины кода в целом неплохая. А то если совсем не заглядывать под капот, то можно получить проблемы с производительностью (но тут речь скорее про библиотеки, а не прикладные программы)

Ну из за особенностей синтаксиса я не мог написать на котлине точно так же как на Scala. Со сложными случаями выглядит интересно, спасибо почитаю.
Не проще ли сразу задизайнить язык так, чтобы компилятор понимал код без подсказок?

Подозреваю это противоречит необходимости не уходить синтаксически далеко от Java. Все же для такой точной компиляции надо менять систему типов, делая её похожей на Haskell.

Ну имхо, котлиновский синтаксис when весьма далек от джавы, точно не ближе скаловского match/case.

(Хм, я таки забыл refresh перед комментированием. Пусть уже остаётся.)

К моменту выполнения кода, obj имеет тип Object, то есть, ссылки на что угодно, которое является объектом (не примитивным типом, как int). Мы тут не уверены, что это String; чтобы использовать, надо это проверить. Если мы попытаемся obj, который не String, «преобразовать» к String, получим исключение.

На уровне рантайма это не преобразование — это проверка типа и присвоение переменной, сама ссылка остаётся точно такой же. Но на уровне языка это метод проверить и, если тип подходит, сделать присвоение. Слово «преобразование» тут немного условно, но устоялось.

Ситуаций, в которых такое нужно — проверить и выполнить действие соответственно типу — очень много, хотя в основном это действия достаточно нижнего уровня (системные библиотеки, всякие универсальные фреймворки).

В языках, которые писались, когда грабли этой концепции были уже хоть частично пройдены, обычно есть совмещённый оператор проверки-и-конверсии, результатом которого является или ссылка уже точно нужного типа, или null. Например, C# использует для этого «as»: можно было бы написать

if ((String s = obj as String) != null) {
    System.out.println(s.toUpperCase());
}


но авторы Java пропустили эту возможность (и не стоит их сильно винить за это, они были почти первопроходцы), а затем решили расширить синтаксис уже существующей проверки.
Упомянутый pattern matching — ещё более продвинутый вариант такой проверки (обычно со множественными вариантами).
Зарегистрируйтесь на Хабре, чтобы оставить комментарий