Pull to refresh

Comments 46

Не type interference («интерференция типов», «помехи типов»), а type inference.
UPD. Можно, конечно, оставить и имеющееся, ради прикола, но тогда переделать 1-й абзац.
За статью спасибо, но хочется отметить, что стиль изложения ближе к научному труду, а не к познавательной статье. Мне, местами, приходилось несколько раз перечитывать один и тот же абзац, чтобы понять, что же имелось в виду. Про людей совсем не знакомых с темой читать будет очень тяжко.

После стольких лет существования дженериков в Java есть ещё не знакомые с темой?

После стольких лет существования рифм вы все еще не пишите, как Пушкин?

За столько лет существования рифм уже написано много статей, книг и прочей литературы с полным разбором всего и вся.
И для Java Generics ситуация ровно такая же: есть и вводные курсы и подробное описание всё есть.
Зачем оно тут? Я вот даже не уверен что это 100% авторский контент — есть ощущение что эти примеры я уже видел.


Java Generics появились в JDK 1.5, которая вышла 30 сентября 2004 года — через пару месяцев им будет 14 лет. Знаете когда эта статья была полезна? 14 лет назад — вообще must have, ну лет 10 назад (хотя и так уже было много литературы).
А сейчас?


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

Если статья рассчитана на тех, кто и так все это знает, то какой в ней смысл? А если на тех, кто только учится, то она излишне сложна для восприятия.
Чтобы не делать лишний вызов на каждой итерации цикла. Экономия на спичках, в общем-то, но ничего страшного от такой оптимизации не случается.
А насколько сильная это экономия? С одной стороны, вы написали что это экономия на спичках, а с другой стороны, почему ради такой маленькой экономии код становится менее красивым?
Код не становится менее красивым. Непривычным — да, но после привыкания к такому стилю он читается столь же просто как стандартный.
Экономия на спичках, в общем-то

А у вас есть замеры в JMH, что экономия дает хоть какие-то плюсы? Ведь Java прекрано умеет оптимизировать "method inlining".

Инлайнинг через интерфейс? Нет, для JIT, конечно же, нет никаких теоретических препятствий так и сделать — но, насколько я знаю, такие оптимизации возможны только для долго работающей программы.

А использование переменной работает и на «холодном» коде тоже.

mayorovp, еще раз: есть ли доказательства того, что оптимизация имеет смысл? Или их нет? Да или нет?

А что значит «оптимизация имеет смысл»? Имеет смысл вызывать метод List.size() один раз вместо N раз? Я думаю тут есть смысл. Особенно, если учесть, что в данном примере мы не знаем какая реализация скрывается за этим листом (я могу передать в метод свою реализацию интерфейса List, которая будет вычислять size не за константное время).
Сейчас так конечно никто не пишет. Зачем? ведь есть же foreach. И это верно! Всегда используйте foreach вместо for, если коллекция реализует интерфейс Iterable. Но в далекие времена Java 2.0 не было Iterable.
А что значит «оптимизация имеет смысл»?

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


Я думаю тут есть смысл.

Нет, если нет ускорения работы программы.


Вы же и без меня знаете, что оптимизаторы в Java действительно хорошо работают. Они умеют инлайнить методы, создавать объекты на стеке, сворачивать известные конструкции. В частности, при итерировании по массиву JIT умеет находить знакомые циклы и убирать проверку выхода за границу массива. JIT умеет "девиртуализовывать" методы и делать еще тьму важных моментов.


Я не понимаю, ну почему Вы спорите… Ну если будет код работать быстрее — так приведите доказательство, что тут сложного? Или скажите, что доказательств нет. Вы же в Сбертехе работаете, должны ведь уметь делать тесты на производительность. Вы можете всего лишь форкнуть репозиторий, а в нем сделать две реализации — с вашей идей и без. Разве это сложно?

Скажите, а AoT-компилятор тоже умеет делать девиртуализацию?

Вы не правы. Вообще-то правильно выносить вычисления, чтобы сделать их один раз, чем делать их много раз.


Ну и про инлайн тут вообще при чем? Если тут и идёт о чем-то речь, то о подстановке результата вызова функции как значения, но есть разные вещи, когда такие оптимизации могут быть недопустимы и при большом значении N, накладные расходы могут быть значимы.


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

Вообще-то правильно выносить вычисления, чтобы сделать их один раз, чем делать их много раз.

Пруф? Или это Ваше мнение?


при большом значении N

Вы говорите про алгоритмическую сложность? Мы же обсуждаем, что константа (в терминах O(...)) одинакова как для функции с оптимизацией, так и для функции без неё.


Ну и про инлайн тут вообще при чем?

Если мы говорим про оптимизацию производительности, то хотелось бы видеть обоснование — почему добавление строк кода (а зачастую это ведет к созданию менее читаемого кода) дает прирост производительности? Какой прирост тогда будет?


Всё дело в том, что замерить производительность не просто, а очень просто.


Смотрите:


  • Шаг 1: скачиваете код с проектом, который меряет производительность. Например, этот.
  • Шаг 2: пишете два варианта функции: с оптимизацией и без
  • Шаг 3: запускаете замеры из командной строки ./gradle jmh

На всё про всё — максимум 30 минут, учитывая, что еще надо поставить JRE и IntelliJ Idea Community.


А теперь, вопрос: если при такой простоте замеров нет доказательств производительности, то есть ли вообще ускорение? Может, всем и так известно, что прироста нет никакого, потому никто и не публикует замер?

Замер будет зависеть от самой виртуальной машины и реализации интерфейса List, экземпляр которого метод получает в качестве аргумента.
Если виртуальная машина поддерживает и сможет доказать возможность девиртуализации, то разницы между n = list.size() и i < list.size() нету.
Если же мы, например, передадим в качестве параметра экземпляр SynchronizedList, то разница будет.

Да и зацепились вы за пример из проекта написанного в эпоху динозавров. В то время HotSpot не умел выполнять такого рода оптимизации.
зацепились вы за пример

Я зацепился на преждевременную оптимизацию. Если в статье есть примеры кода в стиле "как можно", то хорошо бы вычищать их от подобных "советов".


Если же мы, например, передадим в качестве параметра экземпляр SynchronizedList, то разница будет.

А какая? Код будет работать в 10 раз быстрее? Или в 10 раз медленнее? Зачем усложнять код на ровном месте-то?

Я понимаю что бремя доказательства лежит на Marvinorez, а не на вас и вам доказывать ничего не нужно.
Хотя именно у вас есть опыт в создании JMH-тестов — могли бы и помочь человеку, но не захотели — бывает :)


Я решил помочь. Вот тесты производительности, вот их сырые результаты — если я что-то не то или не так тестировал — милости просим в PR.


И вот табличка с результатами (Habr не смог переварить такую большую таблицу :( ).
И я, из результатов тестов, вижу что кеширование размера коллекции может давать профит, но в зависимости от типа коллекции:


  • java.util.HashSetвыгодно
  • java.util.TreeSet — не выгодно
  • java.util.ArrayListвыгодно
  • java.util.LinkedListвыгодно
Спасибо. Но большинство проверок — лишние. Нет никакого смысла проверять HashSet, TreeSet и LinkedList — потому что их никто и никогда не обходит по индексу.

Что действительно нужно сравнивать — так это ArrayList и обычные массивы.
большинство проверок — лишние.

Я это понимаю — я тут заодно прокачивал свой навык написания бенчмарков на JMH :)


Что действительно нужно сравнивать — так это ArrayList и обычные массивы.

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


Сильно подозреваю что доступ к элементу массива будет быстрее доступа к элементу ArrayList-а, но опять же копеечно и только из-за того что каждый вызов ArrayList#get(int) это:


  • собственно вызов метода
  • проверка выхода за правую границу
  • вызов внутреннего метода для каста
  • обращение к массиву по индексу

Просто обращение к массиву по индексу будет явно быстрее из-за отсутствия всех этих вещей, как бы их JVM не инлайнила.

Нет, вы не поняли. Я предложил сравнить работу метода size в кешированном и некешированном варианте цикла у какого-нибудь Arrays.asList

Да я и сейчас не понял.


Вы предлагаете узнать профит от кеширования размера коллекции при обходе new int[]{1, 2, 3} и Arrays.asList(1, 2, 3)?

Бенчмарк, сырые данные, табличка, вывод: с кешем быстрее.
Возможно сказывается то что кешированный размер лежит в локальной переменной, а не достаётся через обращения к полю/методу другого объекта.

Наткнулся в видео на аналогичный пример в .Net, правда с массивом.


Но разница значительная по скорости, но в обратную сторону. .Net увидев паттерн сравнения с длиной массива в цикле, убирает из IL проверки на выход за пределы массива.


В общем, вывод, что нужно знать платформу на которой пишешь.

Извиняюсь за то что не могу привести доказательства, но на Java я не писал уже 10 лет. Не вижу смысла разбираться с методами установки jmh только ради одного комментария.

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

1. использование AOT-компилятора вместо JIT;
2. частый холодный запуск утилиты;
3. запуск на Android с его Davlik;
4. запуск под IKVM.NET на Unity с его устаревшим форком Mono в браузере через WebAssembly;
5. запуск в браузере через GWT;
6. запуск на Java Card?

Кстати, поддерживает ли JMH все перечисленные мною сценарии?
Статья достаточно полезная, хотелось бы больше услышать о реализации Type Inference в jdk 8.
Я вспомнил, где видел данные примеры: Доклад А.Маторина «Неочевидные дженерики» на JPoint и JBreak 2016.
UFO just landed and posted this here
Вот с NPE как раз ничего не упадет — instanceof для null всегда ложный. А с остальным согласен.
UFO just landed and posted this here
Но разве это не то, что ожидается? Т.е. если вместо списка объектов типа Account передали список, например, Employee, то программа должна упасть и как можно громче.

В сигнатуре метода нет никакого упоминания о том, что список должен содержать только объекты класса Account. Возможна ситуация когда нам передали список из множества объектов типа Account и Employee или список содержащий элемент равный null. По хорошему, конечно, об этом нужно писать в комментарии и это был единственный способ рассказать о том, что же ожидает метод до появления Generics.

То что она должна упасть как можно громче… Эммм… не всегда это верно. Программа может упасть с грохотом на этапе валидации — это хорошо, это fail-fast. Но что, если программа у вас падает где-то глубоко в бизнес логике, где падение может привести к неконсистентным данным, незавершенным транзакциям и т.д. — это уже не fail-fast.

Да еще и с NPE упадет если null передать. Получается одно проверили, а другое забыли.
да, это хорошее замечание. Проверка на null там не будет лишней.
UFO just landed and posted this here
Все же после интуитивно понятных дженериков C#, здешние со своими уайлдкардами по-началу взрывают мозг. Может кто-нибудь привести пример задачи, где джавовые дженерики себя бы лучше проявили?

Да запросто. В Java можно написать Collection<? extends Foo>, и это будет автоматически работать для любой коллекции — а в C# для этой цели пришлось придумывать отдельный интерфейс IReadOnlyCollection<Foo>, который, конечно же, никакой класс автоматически реализовывать не начал.


К примеру, в .net 4.5 класс HashSet почему-то не реализовывал этот интерфейс...

Сомнительное "будет работать". Метод add есть, но вызвать его нельзя. По мне так это довольно абсурдно. C IReadOnlyCollection же всё понятно.

К примеру, в .net 4.5 класс HashSet почему-то не реализовывал этот интерфейс...

Что за гнусное вранье, зачем вы обманываете IL_Agent?


Вот ссылка HashSet на MSDN, он реализует IReadOnlyCollection.


И работает эта конструкция ровно так, как и ожидается: если A наследует B, то вместо IReadOnlyCollection<B> можно передавать IReadOnlyCollection<A>. В .Net еcть ковариантность и контрвариантность на уровне компилятора.

Вот ссылка HashSet на MSDN, он реализует IReadOnlyCollection.

… начиная с 4.6


И работает эта конструкция ровно так, как и ожидается: если A наследует B, то вместо IReadOnlyCollection<B> можно передавать ReadOnlyCollection<A>.

А вот для ICollection<> такого нету. В отличии от Java, где для любого интерфейса можно автоматически получить его ковариантную и контравариантную части.

UFO just landed and posted this here
И выбрали такой упоротый метод как раз чтобы коллекции не пришлось распиливать на 2 интерфейса из-за обратной совместимости.

Ну а в C# решили распилить, из-за чего местами появилась та самая несовместимость.

UFO just landed and posted this here
Sign up to leave a comment.