Comments 48
Обобщая написанное, у меня все еще остается несколько вопросов.
Неужели этот код накидан на скорую руку и никто не хочет им заниматься?
Тоже возникало достаточно много вопросов при изучении исходников как JDK, так и стандартной библиотеки .NET.
И если в случае .NET есть .NET Core, где старый код переписывается с нуля,
то в случае Java ситуация чуть другая — в какой-то момент она стала быстро развиваться, в разы быстрее обычного, и действительно есть ощущение, что часть фич не успевает продуматься как со стороны контракта, так и со стороны реализации.
С другое стороны, реализацию, в отличие от контракта, всегда можно доработать в новых выпусках.
Если только уже не написано много кода, зависящего от побочных эффектов старых реализаций.
Например, в .NET, когда был введен оператор "is null", вначале он был реализован "в лоб" — вызовом object.Equals(o, null), что было неоптимально как для ссылочных типов, так и структур (упаковка).
Потом исправили.
Наверное, и в Java будут дорабатывать реализацию JDK.
habrahabr.ru/post/129494
(Там пишут про enum, но и к первому варианту это тоже применимо)
Lazy init в случае List0 совершенно не нужен. Вы, видимо, плохо понимаете вообще, когда он нужен. Если не бывает обращений к классу без обращений к инстансу синглтона, то смысла в lazy init никакого. Тут как раз такой случай.
Неужели этот код накидан на скорую руку и никто не хочет им заниматься?
Нет, этот код писали и переписывали очень долго и обсуждали с кучей людей, и ругались. И в Java 10 ещё допереписывали. Это выверенная и продуманная реализация.
Вот это очень неожиданно. Почему нельзя было просто всегда возвращать false? Зачем тут проверка на null. Это мне остается непонятным.
Открываем контракт класса Collection и читаем:
Throws:
NullPointerException
— if the specified element is null and this collection does not permit null elements (optional)
Коллекция поддерживает null-элементы? Нет. Значит, по контракту имеет право кидать NPE. Зачем это сделано? Если вы случайно вызовете на ней contains(null)
(что не очень имеет смысл) вы вместо тихого false словите громкое исключение и сразу же поймёте, что в коде ошибка.
Вы измеряли реальный GC pressure от использования time API в реальных сценариях? Или так сорцы почитали и сразу поняли, что всё будет тормозить?
Возражение №1: А что если вам только кажется, что объект выделяется? Если в коде написано new
, это ещё не означает, что объект действительно выделится.
Возражение №2: А что если накладные расходы на выделение объекта настолько малы, что составляют 0.0001% от производительности программы, но при этом уберегают вас от возможных ошибок, которые бы вы совершили, если бы API выделяло меньше объектов? Если же вы не готовы пожертвовать даже 0.0001% производительности, тогда вам не следует писать на Java, потому что вы уже жертвуете гораздо большим процентом.
Я не знаю таких оптимизаций в JVM, которые бы удаляли new в момент компиляции.
Ну если вы не знаете, это же не значит, что их нет.
то я бы конечно выбрал реализацию, где объектов выделяется меньше.
Такое чувство, что вам просто не нравятся объекты сами по себе. Не там вы ищете узкое место производительности, совсем не там.
Напоминает мне людей, которые говорили, что IntStream.range(0, 100).forEach(System.out::println)
ужасно тормозит по сравнению с for(int i=0; i<100; i++) System.out.println(i);
и тоже рассуждали про кучу объектов, про ужасно развесистую реализацию стримов, про то что method-reference разворачивается в класс в рантайме. При этом никого не волнует, что каждый вызов System.out.println
на несколько порядков медленнее всего остального (а если он пишет в системную виндовую консоль, то это вообще капец). Просто потому что println
— это старый добрый знакомый метод, а стримы — новое, а значит неизвестное и страшное.
— Вы уверены?
— Конечно! Когда я писал «B» с использованием «C», а потом перешёл на «D», то стало медленнее. Значит, «A» тормозит.
Изменения ради изменений?
Если вы работаете в IDEA и она начинает "желтить" с предложением перевести куски кода циклов в Stream API, то вы не будете на это обращать внимание? Я потихоньку за полгода, год почти все циклы конвертнул.
Если инспекция потенциально указывает на баг, то для начала разберусь, баг ли это в самой инспекции или баг в проекте. Затем — либо багрепорт в JB, либо исправление бага в проекте.
IDEA так не делает, не надо инсинуаций. Эта инспекция по умолчанию в режиме information-only (то есть код не жёлтый, но по Alt+Enter фикс доступен). Если вы её сами включили в своём проекте, IDEA не виновата. Если вы не включали и она вам подсвечивает на только что созданном проекте, то, пожалуйста, сообщите нам.
Кушайте на здоровье, если сами гуглить не умеете.
Оптимизация происходит в runtime.
А что, Java какие-то оптимизации делает не в рантайме?
Ну StringBuilder — это не оптимизация, а необходимость, потому что специального байткода для конкатенации строк нет. Раз ее знаете, то я вам говорю: оптимизации делает оптимизирующий JIT-компилятор, который работает в рантайме. Javac ничего не оптимизирует, если не считать минимального фолдинга констант и удаления недостижимых веток в соответствии с JLS.
И к чему нужна была такая вот реализация? Этот вопрос все еще для меня открыт.
Лучше сразу в публичном API сделать оверлоады, а потом дооптимизировать реализацию, если необходимо. Это можно сделать даже в минорных апдейтах, и ваша программа автоматически станет быстрее (даже без перекомпиляции). Ну и байткод короче будет у вас, что иногда полезно.
Мне кажется, что я понятно ответил на вопрос: «почему List<T> не может содержать null?».
К сожалению при наборе потерял <T>, возможно это послужило причиной недопонимания.
По поводу ListN и varargs: если вызывать перегрузки List.of()
без varargs, ListN использоваться не будет. Он как раз для случая с заранее неизвестным количеством элементов.
машина (речь больше о компиляторе) глупее человека
Не компилятор дурак, а Java не позволяет выразить, что метод не возвращает управление нормальным образом. В Kotlin, например, это пофиксили типом Nothing
.
Почему у меня в списке не может храниться null?
Потому, что null
где попало лучше избегать, это известный подход. И конечно же, такое поведение не сюрприз, а написано в документации
Зачем было создавать столько методов?
Чтобы не плодить массивы из varargs на ровном месте. Я не нашёл подтверждения в статье, что это не работает.
static <E> List<E> of(E e1, E e2, E e3) {
return new ImmutableCollections.ListN<>(e1, e2, e3);
}
Если это ещё не сделали — значит, это не так уж приоритетно и влияет на производительность.
что метод не возвращает управление нормальным образом.
В данном случае это не применимо. Метод checkIndex может вернуть управление нормальным образом в зависимости от параметров. Если ему передавать вторым параметром 0, тогда не может.
А что я тут ещё заметил: для списков разной длины возвращается свой класс. Это плохо, потом будет какой-то код, написанный для List<T>
получать всё это многообразие разных реализаций и нарвёмся мы на мегаморфные вызовы, что сразу убьёт все полезные оптимизации.
List.of() и все, все, все…