Функциональный стиль в java: когда он нужен?
Большинство задач в энтерпрайзе делятся на две группы:
1️⃣ Одиночный запрос.
Покупка в интернет-магазине, вывод курса доллара — такие запросы затрагивают мало данных и много компонентов. Эти бизнес-процессы хорошо описываются объектами и отношениями между ними.
2️⃣ Обработка данных.
Статистика покупок, поиск рекомендаций, прогноз погоды - в центре внимания обработка данных, а не взаимодействие сущностей. Задача разработчика — чтобы обработка была быстрой. Нужно по максимуму использовать ресурсы процессора и доступную память, снизить время простоя и количество блокировок.
Вот здесь пригодятся некоторые подходы функционального программирования(ФП) :
1️⃣ Понятие чистой функции.
В многопоточном программировании главная проблема — работа с общими переменными. Чтобы безопасно их обновлять и всегда читать актуальные знвчения используются средства синхронизации. Большинство из них снижают общую пропускную способность.
Чистые функции - методы, которые характеризуются только входными и выходными данными. У них нет сайд-эффектов, Они не меняют внешних объектов и не бросают эксепшены. Для одинаковых входных данных результат всегда будет один и тот же.
✅ Нет общих переменных — нет синхронизации.
✅ Можно кэшировать некоторые результаты.
2️⃣ Неизменяемые данные
Исходные данные не должны меняться. Меняться могут только локальные переменные.
✅ Синхронизация не нужна — общие переменные не меняются и всегда актуальны.
Пример: добавление элемента в список.
Подход ООП: добавляем в исходный список - меняем внешний объект.
❌ Для запуска в многопоточной среде нужна синхронизация.
❌ Если метод прервался, то нужно выяснять, что он успел изменить, а что нет.
Подход ФП: создаём новый список на основе исходного и добавляем туда элемент.
✅ Если что-то пошло не так, просто ещё раз вызываем метод.
❌ Большой расход памяти.
3️⃣ Декларативный стиль написания кода.
Обход коллекции в цикле — это императивный подход. Для каждого элемента задаётся чёткая последовательность действий.
Stream API — декларативный. Благодаря большому количеству встроенных функций можно просто описать то, что нужно: отфильтровать, сгруппировать, просуммировать.
Лучшая читаемость - не единственное преимущество такого подхода. Чистые функции и неизменяемые данные сами по себе тоже не сделают программу эффективной. Но их использование в Stream API дают компилятору и JVM большие возможности для оптимизаций. Cамая мощная из них — опция parallel(), она автоматически распределяет данные по параллельным подзадачам.
Начиная с 8 версии java можно применять и другие идеи ФП — функции высших порядков, currying, pattern matching и т.д. Некоторые подходы были доступны и раньше, например, использовать хвостовую рекурсию вместо итерации. Но наибольшую практическую пользу можно извлечь из обработки больших коллекций с помощью Stream API.
Писать программы в функциональном стиле на чистой java можно, но на практике это пока не оптимально. Для обработки потоков данных больше подходят библиотеки для реактивного программирования и стрим-фреймворки.
Большинство задач в энтерпрайзе делятся на две группы:
1️⃣ Одиночный запрос.
Покупка в интернет-магазине, вывод курса доллара — такие запросы затрагивают мало данных и много компонентов. Эти бизнес-процессы хорошо описываются объектами и отношениями между ними.
2️⃣ Обработка данных.
Статистика покупок, поиск рекомендаций, прогноз погоды - в центре внимания обработка данных, а не взаимодействие сущностей. Задача разработчика — чтобы обработка была быстрой. Нужно по максимуму использовать ресурсы процессора и доступную память, снизить время простоя и количество блокировок.
Вот здесь пригодятся некоторые подходы функционального программирования(ФП) :
1️⃣ Понятие чистой функции.
В многопоточном программировании главная проблема — работа с общими переменными. Чтобы безопасно их обновлять и всегда читать актуальные знвчения используются средства синхронизации. Большинство из них снижают общую пропускную способность.
Чистые функции - методы, которые характеризуются только входными и выходными данными. У них нет сайд-эффектов, Они не меняют внешних объектов и не бросают эксепшены. Для одинаковых входных данных результат всегда будет один и тот же.
✅ Нет общих переменных — нет синхронизации.
✅ Можно кэшировать некоторые результаты.
2️⃣ Неизменяемые данные
Исходные данные не должны меняться. Меняться могут только локальные переменные.
✅ Синхронизация не нужна — общие переменные не меняются и всегда актуальны.
Пример: добавление элемента в список.
Подход ООП: добавляем в исходный список - меняем внешний объект.
public void add(List list, Integer value)
{ list.add(value); }
✅ Минимальный расход памяти.❌ Для запуска в многопоточной среде нужна синхронизация.
❌ Если метод прервался, то нужно выяснять, что он успел изменить, а что нет.
Подход ФП: создаём новый список на основе исходного и добавляем туда элемент.
public List add(List list, Integer value {
List newList=new List();
newList.addAll(list);
newList.add(value);
return list;
}
✅ Можно использовать в многопоточной среде.✅ Если что-то пошло не так, просто ещё раз вызываем метод.
❌ Большой расход памяти.
3️⃣ Декларативный стиль написания кода.
Обход коллекции в цикле — это императивный подход. Для каждого элемента задаётся чёткая последовательность действий.
Stream API — декларативный. Благодаря большому количеству встроенных функций можно просто описать то, что нужно: отфильтровать, сгруппировать, просуммировать.
Лучшая читаемость - не единственное преимущество такого подхода. Чистые функции и неизменяемые данные сами по себе тоже не сделают программу эффективной. Но их использование в Stream API дают компилятору и JVM большие возможности для оптимизаций. Cамая мощная из них — опция parallel(), она автоматически распределяет данные по параллельным подзадачам.
Начиная с 8 версии java можно применять и другие идеи ФП — функции высших порядков, currying, pattern matching и т.д. Некоторые подходы были доступны и раньше, например, использовать хвостовую рекурсию вместо итерации. Но наибольшую практическую пользу можно извлечь из обработки больших коллекций с помощью Stream API.
Писать программы в функциональном стиле на чистой java можно, но на практике это пока не оптимально. Для обработки потоков данных больше подходят библиотеки для реактивного программирования и стрим-фреймворки.
👍1
java.util.Set: сравнение реализаций.
Set - интерфейс для коллекций из уникальных элементов. В стандартной библиотеке java 6 реализаций:
HashSet, TreeSet, LinkedHashSet, CopyOnWriteArraySet, ConcurrentSkipListSet и EnumSet.
В этой статье рассмотрим и сравним первые три из них.
Детали реализации:
1️⃣ TreeSet базируется на TreeMap, а он - на основе красно-чёрного дерева. Элементы коллекции должны реализовать интерфейс Comparable, либо экземпляр Comparable нужно передать в конструкторе, потому что compareTo - основной метод для вставки и поиска элементов.
✅ Отсортированные значения.
✅ Быстрая ребалансировка.
✅ Можно искать по ближайшему ключу - доступны функции lower(e), floor(e), ceiling(e), higher(e).
2️⃣ HashSet внутри себя содержит HashMap. Он реализован так:
Есть массив корневых элементов - их ещё называют бакетами. Размер массива по умолчанию 16.
Если нужно добавить элемент в коллекцию, то сначала вычисляется его хэш. Далее ищется индекс корневого элемента - остаток от деления хэша на размер массива. Если по этому индексу ничего нет, корнем становится новый элемент. Если есть - добавляется в структуру под ним.
В java 7 этой структурой был список, с java 8 - бинарное дерево.
Эффективность HashSet зависит от равномерности распределения хэшей. В энтерпрайз приложениях хэш вычисляется по набору полей, и равномерность часто не достигается. Поэтому на практике HashSet не всегда является оптимальной структурой данных. Если известно примерное количество элементов, можно задать loadFactor и capacity в конструкторе.
✅ Возможность оптимизации.
✅ Быстрый поиск.
❌ Значения не отсортированы.
❌ Долгая перестройка при увеличении размера коллекции.
3️⃣ LinkedHashSet расширяет класс HashSet. Помимо основной структуры новые элемент также добавляются в двусвязный список.
✅ Значения отсортированы по порядку вставки.
✅ Возможность оптимизации.
Знание деталей реализации даёт простые правила выбора:
1️⃣ Частый поиск - выбираем TreeSet.
2️⃣ Порядок не важен - HashSet.
3️⃣ Порядок вставки важен - LinkedHashSet.
Мы проверили, насколько велика разница между реализациями:
1️⃣ Добавить в Set миллион чётных чисел.
2️⃣ Вставить миллион нечётных.
3️⃣ Найти миллион случайных.
Запустили несколько раз для HashSet, TreeSet, LinkedHashSet, а также для HashSet и LinkedHashSet с заранее заданной ёмкостью в 2 миллиона. Проверили на java 7 и 11.
В качестве типов данных использовали Integer(идеальное распределение хэшей) и класс с несколькими полями (неравномерное распределение).
Результаты на картинке внизу поста. Тест не претендует на точность, но теперь можно добавить ещё несколько рекомендаций:
✅ В новых версиях java все операции всех реализаций работают на 5-20% быстрее.
✅ Выбор реализации имеет значение.
✅ Знаете будущий размер коллекции - передавайте его в конструкторе.
✅ Для чисел лучше использовать HashSet.
✅ Для экземпляров классов всё неоднозначно, имеет смысл попробовать разные варианты.
Set - интерфейс для коллекций из уникальных элементов. В стандартной библиотеке java 6 реализаций:
HashSet, TreeSet, LinkedHashSet, CopyOnWriteArraySet, ConcurrentSkipListSet и EnumSet.
В этой статье рассмотрим и сравним первые три из них.
Детали реализации:
1️⃣ TreeSet базируется на TreeMap, а он - на основе красно-чёрного дерева. Элементы коллекции должны реализовать интерфейс Comparable, либо экземпляр Comparable нужно передать в конструкторе, потому что compareTo - основной метод для вставки и поиска элементов.
✅ Отсортированные значения.
✅ Быстрая ребалансировка.
✅ Можно искать по ближайшему ключу - доступны функции lower(e), floor(e), ceiling(e), higher(e).
2️⃣ HashSet внутри себя содержит HashMap. Он реализован так:
Есть массив корневых элементов - их ещё называют бакетами. Размер массива по умолчанию 16.
Если нужно добавить элемент в коллекцию, то сначала вычисляется его хэш. Далее ищется индекс корневого элемента - остаток от деления хэша на размер массива. Если по этому индексу ничего нет, корнем становится новый элемент. Если есть - добавляется в структуру под ним.
В java 7 этой структурой был список, с java 8 - бинарное дерево.
Эффективность HashSet зависит от равномерности распределения хэшей. В энтерпрайз приложениях хэш вычисляется по набору полей, и равномерность часто не достигается. Поэтому на практике HashSet не всегда является оптимальной структурой данных. Если известно примерное количество элементов, можно задать loadFactor и capacity в конструкторе.
✅ Возможность оптимизации.
✅ Быстрый поиск.
❌ Значения не отсортированы.
❌ Долгая перестройка при увеличении размера коллекции.
3️⃣ LinkedHashSet расширяет класс HashSet. Помимо основной структуры новые элемент также добавляются в двусвязный список.
✅ Значения отсортированы по порядку вставки.
✅ Возможность оптимизации.
Знание деталей реализации даёт простые правила выбора:
1️⃣ Частый поиск - выбираем TreeSet.
2️⃣ Порядок не важен - HashSet.
3️⃣ Порядок вставки важен - LinkedHashSet.
Мы проверили, насколько велика разница между реализациями:
1️⃣ Добавить в Set миллион чётных чисел.
2️⃣ Вставить миллион нечётных.
3️⃣ Найти миллион случайных.
Запустили несколько раз для HashSet, TreeSet, LinkedHashSet, а также для HashSet и LinkedHashSet с заранее заданной ёмкостью в 2 миллиона. Проверили на java 7 и 11.
В качестве типов данных использовали Integer(идеальное распределение хэшей) и класс с несколькими полями (неравномерное распределение).
Результаты на картинке внизу поста. Тест не претендует на точность, но теперь можно добавить ещё несколько рекомендаций:
✅ В новых версиях java все операции всех реализаций работают на 5-20% быстрее.
✅ Выбор реализации имеет значение.
✅ Знаете будущий размер коллекции - передавайте его в конструкторе.
✅ Для чисел лучше использовать HashSet.
✅ Для экземпляров классов всё неоднозначно, имеет смысл попробовать разные варианты.
Что выведет следующий код?
List list=new ArrayList();
list.add(1);
list.add(2);
Stream stream=list.stream();
list.remove(0);
println(stream.count());Stream API: обзор и основные методы.
Stream API - удобный инструмент обработки данных, который появился в java 8. Два принципа, на которых строится Stream API - composition & single responsibility. Действия собираются из простых независимых друг от друга блоков без сайд-эффектов. Этим достигается удобство чтения и поддержки:
✅ Стандартные названия.
✅ Нет локальных переменных.
✅ Линейная последовательность действий.
Стрим состоит из 3 частей:
1️⃣ Источник данных.
2️⃣ Преобразования.
3️⃣ Конечная операция.
Основные способы создать стрим:
🔸 Из готовой коллекции
🔸 Из набора элементов
🔸 Из массива
🔸 Задать первый элемент и правило вывода последующего, получится бесконечный стрим:
🔸 Генерировать независимые друг от друга элементы:
🔹
🔹
🔹
🔹
🔹
Вычисления выполняются не сразу, а только когда вызывается конечная операция. Конечная операция описывает желаемый результат. Результатом может быть:
◾️ Структура данных:
◾️ Результат поиска:
◾️ Сайд-эффект:
Стрим - не структура хранения данных. Он обходит источник данных (или сам его генерирует) и сохраняет промежуточные результаты. По этой причине нельзя копировать стримы и исполнять их несколько раз - пришлось бы контролировать этапы исполнения, консистентность данных и добавить синхронизацию для работы в многопоточной среде. При попытке переиспользования стрима выбрасывается
✅ Исходные данные не меняются.
✅ Стрим нельзя переиспользовать.
✅ Пока не вызвана конечная операция, стрим можно менять как угодно.
Пример из опроса выше вернёт 1.
Если Вас раздражает, что метод
Stream API - удобный инструмент обработки данных, который появился в java 8. Два принципа, на которых строится Stream API - composition & single responsibility. Действия собираются из простых независимых друг от друга блоков без сайд-эффектов. Этим достигается удобство чтения и поддержки:
✅ Стандартные названия.
✅ Нет локальных переменных.
✅ Линейная последовательность действий.
Стрим состоит из 3 частей:
1️⃣ Источник данных.
2️⃣ Преобразования.
3️⃣ Конечная операция.
Основные способы создать стрим:
🔸 Из готовой коллекции
🔸 Из набора элементов
🔸 Из массива
🔸 Задать первый элемент и правило вывода последующего, получится бесконечный стрим:
Stream.iterate(T, BinaryOperator)
🔸 Задать первый элемент, правило вывода последующего и условие остановки: Stream.iterate(T, Predicate, BinaryOperator) (java 9) 🔸 Генерировать независимые друг от друга элементы:
Stream.generate(Supplier)
🔸 Из диапазона с включением граничных значений: IntStream.range(..)
🔸 Диапазон без граничных значений: IntStream.rangeClosed(..)
🔸 Из строк: BufferedReader.lines()
🔸 Из символов строки: CharSequence.chars()
Названия большинства методов преобразований говорят сами за себя: filter, distinct, limit, sorted. Есть менее понятные:🔹
map: применить функцию к каждому элементу.🔹
flatmap: положить элементы из "списка списков" в единый стрим. Также с его помощью можно уменьшить размерность массива.🔹
takeWhile(Predicate): брать элементы из стрима, пока выполняется условие. Метод доступен c java 9.🔹
dropWhile(Predicate): пропускать элементы, пока условие не нарушится. Метод доступен c java 9.🔹
peek(Consumer): сделать что-то с каждым элементом стрима, не меняя исходный стрим. Часто используется при дебаге, например, вывести в консоль текущие элементы.Вычисления выполняются не сразу, а только когда вызывается конечная операция. Конечная операция описывает желаемый результат. Результатом может быть:
◾️ Структура данных:
toArray, collect.◾️ Результат поиска:
anyMatch, allMatch, nonMatch, findFirst, findAny.
◾️ Результат агрегации: min, max, count, reduce.◾️ Сайд-эффект:
forEach, forEachOrdered.Стрим - не структура хранения данных. Он обходит источник данных (или сам его генерирует) и сохраняет промежуточные результаты. По этой причине нельзя копировать стримы и исполнять их несколько раз - пришлось бы контролировать этапы исполнения, консистентность данных и добавить синхронизацию для работы в многопоточной среде. При попытке переиспользования стрима выбрасывается
IllegalStateException.✅ Исходные данные не меняются.
✅ Стрим нельзя переиспользовать.
✅ Пока не вызвана конечная операция, стрим можно менять как угодно.
Пример из опроса выше вернёт 1.
list.remove(0) удаляет элемент с индексом 0, поэтому на момент старта вычислений stream.count() в источнике данных остаётся всего один элемент.Если Вас раздражает, что метод
list.remove принимает не сам элемент, а индекс, то некоторые методы Stream API Вас тоже разочаруют. Например, такой код компилируется:Stream.of(-1, 0, 1).max(Math::max).get();Но в результате получается -1, потому что входной параметр метода
max - Comparator. Math.max по сигнатуре на него похож, поэтому получается неверный результат😒👍8
Stream API: забудьте про peek()
Один из недостатков Stream API по сравнению с циклами - неудобный дебаг. До сих пор метод
Один из недостатков Stream API по сравнению с циклами - неудобный дебаг. До сих пор метод
peek() был единственным способом посмотреть элементы внутри стрима:list.stream().filter(..)Недавно в Intellij IDEA вышел потрясающий апдейт, который выводит дебаг стримов на новый уровень:
.peek(e->println("filt:"+e))
.map(..)
.peek(e->println("map:"+e))
...
🔥6
10 правил безопасных java приложений.
Скорость, масштабируемость, удобство использования — такие требования обычно ставят заказчики. Устойчивость к атакам и защита данных редко проговаривается и подразумевается по умолчанию.
Для кода на java есть много рекомендаций, например, Secure Coding Guidelines for Java SE. На практике большинство ошибок можно избежать очень просто:
1️⃣ Понятный код и однозначная бизнес-логика - уязвимости легко пропустить в чём-то сложном и запутанном. Сужайте и скрывайте доступную функциональность на всех уровнях:
🔸 Модификаторы полей и классов: всё, что может быть private, делайте private. Если для задачи удобно поменять private на public, подумайте о других вариантах.
🔸 Reflection и проверка имён классов: используйте осторожно, т.к такой код не адаптируется к изменениям. Поменяется имя метода, добавится ещё один класс, а рефлекшн останется тем же.
🔸 Внешний API: чем проще, тем лучше.
2️⃣ Избегайте сериализации: чтобы хранить и передавать объекты внутри системы используйте облегчённые протоколы с поддержкой версионирования: gRPC, Avro, Thrift и т.д.
3️⃣ Шифруйте персональную информацию: пароли, номера карточек, кодовые слова. Не записывайте её в лог! В 2019 году перехват незащищённых данных всё ещё входит в топ-3 опасностей по версии OWASP.
4️⃣ Проверяйте входные данные от пользователей и сторонних систем. Даже если похожие проверки есть на фронтенде. Ограничивайте размер пользовательского ввода и результатов: не позволяйте загружать 5 ГБ картинку и запрашивать триллион записей.
5️⃣ Используйте PreparedStatement для запросов к БД. Для большинства NoSQL они тоже есть.
6️⃣ Скрывайте технические детали в сообщениях об ошибке: не показывайте стек трейс конечному пользователю и в консоли браузера.
7️⃣ Обновляйте библиотеки, не игнорируйте авто-апдейты.
8️⃣ Проверяйте open-source библиотеки, которые используются на проекте, например, с помощью OWASP Dependency-Check. Коммит вредоносного кода в open-source - реальная угроза.
9️⃣ Записывайте и анализируйте активность пользователей - как минимум неудачные попытки авторизации.
🔟 Тестируйте методы на переполнение, оно случается.
Скорость, масштабируемость, удобство использования — такие требования обычно ставят заказчики. Устойчивость к атакам и защита данных редко проговаривается и подразумевается по умолчанию.
Для кода на java есть много рекомендаций, например, Secure Coding Guidelines for Java SE. На практике большинство ошибок можно избежать очень просто:
1️⃣ Понятный код и однозначная бизнес-логика - уязвимости легко пропустить в чём-то сложном и запутанном. Сужайте и скрывайте доступную функциональность на всех уровнях:
🔸 Модификаторы полей и классов: всё, что может быть private, делайте private. Если для задачи удобно поменять private на public, подумайте о других вариантах.
🔸 Reflection и проверка имён классов: используйте осторожно, т.к такой код не адаптируется к изменениям. Поменяется имя метода, добавится ещё один класс, а рефлекшн останется тем же.
🔸 Внешний API: чем проще, тем лучше.
2️⃣ Избегайте сериализации: чтобы хранить и передавать объекты внутри системы используйте облегчённые протоколы с поддержкой версионирования: gRPC, Avro, Thrift и т.д.
3️⃣ Шифруйте персональную информацию: пароли, номера карточек, кодовые слова. Не записывайте её в лог! В 2019 году перехват незащищённых данных всё ещё входит в топ-3 опасностей по версии OWASP.
4️⃣ Проверяйте входные данные от пользователей и сторонних систем. Даже если похожие проверки есть на фронтенде. Ограничивайте размер пользовательского ввода и результатов: не позволяйте загружать 5 ГБ картинку и запрашивать триллион записей.
5️⃣ Используйте PreparedStatement для запросов к БД. Для большинства NoSQL они тоже есть.
6️⃣ Скрывайте технические детали в сообщениях об ошибке: не показывайте стек трейс конечному пользователю и в консоли браузера.
7️⃣ Обновляйте библиотеки, не игнорируйте авто-апдейты.
8️⃣ Проверяйте open-source библиотеки, которые используются на проекте, например, с помощью OWASP Dependency-Check. Коммит вредоносного кода в open-source - реальная угроза.
9️⃣ Записывайте и анализируйте активность пользователей - как минимум неудачные попытки авторизации.
🔟 Тестируйте методы на переполнение, оно случается.
❤5
Обход коллекций в Java.
Чтобы ответить на вопросы выше, посмотрим на обход более сложных сущностей, например, графа. Граф - структура данных из вершин и рёбер. В графах часто встречаются циклические пути. Найти гамильтонов путь, то есть обойти вершины графа по одному разу - непростая задача. Алгоритм ведёт учёт посещённых вершин и перебирает разные варианты прежде, чем утвердить один из них. В computer science обход структуры данных называется traverse. Основная идея — выводим текущий элемент, следующий пока неизвестен и считается отдельно.
Вернёмся в мир java. Здесь нет зацикленных графов, все просто — списки, множества, очереди, деревья. При обходе следующий элемент всегда однозначен, а его отсутствие означает конец работы. Поэтому обход выглядит так:
❌ Так как следующий элемент известен заранее, итератор может показать удалённый элемент. Или не вывести только что добавленный.
ArrayList, HashMap, HashSet и тд. не синхронизированы. Если одновременно итерировать и менять коллекцию разными потоками, можно нарушить целостность данных. Есть два способа этого избежать:
1️⃣ Fail-fast итераторы бросают ConcurrentModificationException при изменениях во время итерации.
2️⃣ Fail-safe итераторы работают с неизменяемой структурой.
Большинство однопоточных коллекций реализуют fail-fast подход.
Метод
❓Почему нельзя использовать траверс по умолчанию?
➡️ Потому что итератор проще и работает быстрее, а условия для пропуска элемента при обходе встречаются редко.
❓Зачем нужно несколько вариантов?
➡️
Итого: при выводе элементов
❗️Cинтаксис
Чтобы ответить на вопросы выше, посмотрим на обход более сложных сущностей, например, графа. Граф - структура данных из вершин и рёбер. В графах часто встречаются циклические пути. Найти гамильтонов путь, то есть обойти вершины графа по одному разу - непростая задача. Алгоритм ведёт учёт посещённых вершин и перебирает разные варианты прежде, чем утвердить один из них. В computer science обход структуры данных называется traverse. Основная идея — выводим текущий элемент, следующий пока неизвестен и считается отдельно.
Вернёмся в мир java. Здесь нет зацикленных графов, все просто — списки, множества, очереди, деревья. При обходе следующий элемент всегда однозначен, а его отсутствие означает конец работы. Поэтому обход выглядит так:
Iterator it=list.iterator();Метод next() возвращает текущий элемент и сразу сдвигает указатель на следующий. Метод hasNext() проверяет, ссылается ли этот указатель куда-нибудь. Этот паттерн повторяется снова и снова и называется Iteration. Важно - указатель на следующий элемент вычисляется заранее. Итератор лежит в основе синтаксиса
while(it.hasNext())
int result = it.next();
for (T e: collection).❌ Так как следующий элемент известен заранее, итератор может показать удалённый элемент. Или не вывести только что добавленный.
ArrayList, HashMap, HashSet и тд. не синхронизированы. Если одновременно итерировать и менять коллекцию разными потоками, можно нарушить целостность данных. Есть два способа этого избежать:
1️⃣ Fail-fast итераторы бросают ConcurrentModificationException при изменениях во время итерации.
2️⃣ Fail-safe итераторы работают с неизменяемой структурой.
Большинство однопоточных коллекций реализуют fail-fast подход.
ConcurrentHashMap потокобезопасен, поэтому изменения во время обхода разрешены. Обход через for реализован через итератор. Указатель на следующий элемент вычисляется заранее. Более подходящий новый элемент не отображается, и выводится 2 элемента.Метод
forEach в ConcurrentHashMap использует подход траверс и вычисляет следующий элемент только когда он запрашивается. Поэтому новый ключ подхватывается и выводится 3 элемента.❓Почему нельзя использовать траверс по умолчанию?
➡️ Потому что итератор проще и работает быстрее, а условия для пропуска элемента при обходе встречаются редко.
❓Зачем нужно несколько вариантов?
➡️
ConcurrentHashMap может перестраиваться во время обхода. Чтобы во время перестройки не выводить пользователю дубликаты, используется траверс со сложной логикой.Итого: при выводе элементов
ConcurrentHashMap через for и forEach используются разные алгоритмы обхода, поэтому результат вывода тоже разный.❗️Cинтаксис
forEach реализован и для однопоточных коллекций, но там используется итератор, поэтому разницы с for нет.Intellij IDEA: пишем код быстрее.
1️⃣ Live templates.
Аббревиатуры для популярных синтаксических конструкций. Вводите и нажимаете Enter: 4 символа разворачиваются в 40, а курсор приходит в нужную позицию.
Полный список сокращений в File/Settings/Editor/Live Templates. Есть для Java, Kotlin, JS, Groovy, для разработки под Android и React.
Самые популярные для Java:
▫️St →
▫️ fori
Дополнение имен классов, методов и полей на основе контекста. Варианты появляются в выпадающем списке. Можно писать только начало:
▫️Int →
▫️NPE →
▫️count == 4.if
1️⃣ Live templates.
Аббревиатуры для популярных синтаксических конструкций. Вводите и нажимаете Enter: 4 символа разворачиваются в 40, а курсор приходит в нужную позицию.
Полный список сокращений в File/Settings/Editor/Live Templates. Есть для Java, Kotlin, JS, Groovy, для разработки под Android и React.
Самые популярные для Java:
▫️St →
String
▫️sout System.out.println();
▫️main public static void main(String[] args){}
▫️prsf private static final
Некоторые сокращения разворачиваются в методы с параметрами для автозаполнения. Перемещаться между ними можно с помощью Tab:▫️ fori
for (int i=0; i< ; i++) {}
▫️ifn if (args == null) {}
▫️mx = Math.max(, );
▫️lazy
if (obj == null)2️⃣ Code completion.
{ obj = new Integer(); }
Дополнение имен классов, методов и полей на основе контекста. Варианты появляются в выпадающем списке. Можно писать только начало:
▫️Int →
Integer
▫️Cust → Customer
Можно писать заглавные буквы в названиях классов:▫️NPE →
NullPointerException
▫️CHM → ConcurrentHashMap
Можно оборачивать код в синтаксические конструкции:▫️count == 4.if
if (count == 4) {}
▫️list.for for(Integer i : list) {}
▫️obj.opt
Optional.of(obj)
▫️answer.switch switch (answer) {}
Полный список таких дополнений в File/Settings/Editor/General/Postfix Completion. Есть варианты не только для Java, но и для Kotlin и JS.🔥2
Что выведется в консоль?
Anonymous Poll
43%
10 20 30
2%
30 20 10
6%
20 10 30
50%
Невозможно определить
HashMap в Java 8.
О работе HashMap спрашивают и джуниоров, и сеньоров. Часто всё сводится к рассказу о бакетах и оценке сложности на базе статей 1998 года. С тех времён много изменилось. Этот пост о текущей структуре, особенностях и недостатках HashMap.
❓Как устроен HashMap?
В основе HashMap лежит массив начальных элементов, которые называют бакетами. Их количество зависит от количества элементов — подбирается ближайшая степень двойки. Если элементов 20, то создаётся 32 бакета.
Если хэш-функция ключа распределяет значения равномерно, то в каждом бакете будет 0 или 1 элемент.
На практике такое происходит редко, и значения хэшей элементов иногда совпадают. Тогда элементы с одинаковым хэш кодом организуются в список. Если их становится больше 8, то элементы перестраиваются в двоичное дерево.
❓Как определяется нужный бакет?
Популярное заблуждение - по остатку деления хэша на количество бакетов. Это не совсем так. Количество бакетов кратно степени двойки, и при вычислении используется логическое И.
В массиве из 16 бакетов последний элемент имеет номер 15, в битовом представлении это 1111. Операция & оставит последние 4 бита числа.
Сравним:
❓Что такое load factor и зачем он нужен?
HashMap быстро работает, когда в бакете 0 или 1 элемент. При добавлении элементов повышается шанс, что в одном бакете их будет несколько, a HashMap станет менее эффективен. Поэтому HashMap периодически расширяется. Параметр load factor определяет, когда это происходит. По умолчанию равен 0.75.
Пример: в HashMap 20 элементов и 32 бакета. Когда элементов будет 32*0.75=24, количество бакетов удвоится и элементы перераспределятся по ним.
❓Какие недостатки у HashMap?
1️⃣ Cильная зависимость от функции распределения хэшей.
2️⃣ Неэкономный расход памяти. При load factor = 0.75 будет пустовать 25% памяти. При неравномерном распределении хэшей — ещё больше.
❓В конструкторе HashMap проставляются свойства, а массив бакетов создаётся только при первой вставке. Зачем нужна ленивая инициализация?
Расширение HashMap — долгая операция. Когда добавляется набор элементов, можно сразу увеличить массив до необходимого размера. Очень популярный случай - когда в пустой HashMap добавляютcя элементы другой map:
А теперь разберём вопрос про порядок элементов:
✅ Обход HashMap происходит последовательно - бакет за бакетом. Поэтому задание сводится к определению бакетов, в которые попадут значения. Это и будет порядок вывода элементов:
Ответ: 20 10 30
О работе HashMap спрашивают и джуниоров, и сеньоров. Часто всё сводится к рассказу о бакетах и оценке сложности на базе статей 1998 года. С тех времён много изменилось. Этот пост о текущей структуре, особенностях и недостатках HashMap.
❓Как устроен HashMap?
В основе HashMap лежит массив начальных элементов, которые называют бакетами. Их количество зависит от количества элементов — подбирается ближайшая степень двойки. Если элементов 20, то создаётся 32 бакета.
Если хэш-функция ключа распределяет значения равномерно, то в каждом бакете будет 0 или 1 элемент.
На практике такое происходит редко, и значения хэшей элементов иногда совпадают. Тогда элементы с одинаковым хэш кодом организуются в список. Если их становится больше 8, то элементы перестраиваются в двоичное дерево.
❓Как определяется нужный бакет?
Популярное заблуждение - по остатку деления хэша на количество бакетов. Это не совсем так. Количество бакетов кратно степени двойки, и при вычислении используется логическое И.
В массиве из 16 бакетов последний элемент имеет номер 15, в битовом представлении это 1111. Операция & оставит последние 4 бита числа.
Сравним:
hash % 16Результат будет одинаковый, но логическое И выполняется в 2 раза быстрее. Вычисление бакета — частая операция в HashMap, и такая микрооптимизация даёт хороший прирост в производительности.
hash & 15
❓Что такое load factor и зачем он нужен?
HashMap быстро работает, когда в бакете 0 или 1 элемент. При добавлении элементов повышается шанс, что в одном бакете их будет несколько, a HashMap станет менее эффективен. Поэтому HashMap периодически расширяется. Параметр load factor определяет, когда это происходит. По умолчанию равен 0.75.
Пример: в HashMap 20 элементов и 32 бакета. Когда элементов будет 32*0.75=24, количество бакетов удвоится и элементы перераспределятся по ним.
❓Какие недостатки у HashMap?
1️⃣ Cильная зависимость от функции распределения хэшей.
2️⃣ Неэкономный расход памяти. При load factor = 0.75 будет пустовать 25% памяти. При неравномерном распределении хэшей — ещё больше.
❓В конструкторе HashMap проставляются свойства, а массив бакетов создаётся только при первой вставке. Зачем нужна ленивая инициализация?
Расширение HashMap — долгая операция. Когда добавляется набор элементов, можно сразу увеличить массив до необходимого размера. Очень популярный случай - когда в пустой HashMap добавляютcя элементы другой map:
Map result=new HashMap();Если бы внутренняя структура result создавалась в конструкторе, пришлось бы тут же её расширять. С отложенной инициализацией можно этого избежать.
...
result.putAll(anotherMap);
А теперь разберём вопрос про порядок элементов:
✅ Обход HashMap происходит последовательно - бакет за бакетом. Поэтому задание сводится к определению бакетов, в которые попадут значения. Это и будет порядок вывода элементов:
10 % 16 = 10
20 % 16 = 4
30 % 16 = 14
Ответ: 20 10 30
👍7❤1🔥1
Вопрос на закрепление: в каком порядке выведутся ключи, если задать HashMap размер 100?
Кодировки: основы.
Компактные строки - важное обновление в java 9. Чтобы лучше понять проблему и новое решение, разберём в этом посте отличия между кодировками. А в среду обсудим компактные строки.
Символы хранятся в памяти в виде чисел. Кодировки отвечают за формат хранения и правила перевода символов в числа и обратно. Кодировки можно условно поделить на две группы:
1️⃣ ASCII-based
2️⃣ Unicode-based
1️⃣ ASCII
В большинстве ASCII кодировок символ занимает 1 байт и содержит число от 0 до 256. Первые 128 значений транслируются одинаково во всех кодировках:
0-31: управляющие последовательности - перенос строки, конец файла и т.д.
32-127: латинский алфавит, цифры, знаки препинания.
Специфичные символы языков отображаются на значения 128-255. Разные кодировки - разные наборы символов:
🔸Кириллица: ISO-8859-5, Windows-1251.
🔸Греческий алфавит: ISO-8859-7, Windows-1253.
🔸Исландские символы: OEM 861.
200 символ может стать Ш, Θ, È или чем-то ещё. Фраза «Я люблю Java” в другой кодировке отобразится как «Ď ŰîŃŰî Java».
Итого:
✅ Один символ занимает 1 байт — компактно.
❌ Всего 256 значений — нет места для эмодзи.
❌ Неоднозначность трактовки.
❌ Нельзя использовать украинский и норвежский язык в одном тексте.
2️⃣ Unicode
В основе преобразований лежит таблица с большинством символов, которые используются в мире. Но не со всеми, многие азиатские иероглифы записываются в памяти как комбинация 2-3 символов. Или эмоджи, например,👩❤️💋👩 - это сочетание 8 Unicode-кодов.
Изначально Unicode использовал 2 байта для записи символа и кодировку UCS-2. После 256 символов ASCII казалось, что диапазона 0-65536 хватит навсегда. Эта кодировка использовалась в ранних версиях java для типа char.
Со временем в таблицу добавилось больше символов и встал вопрос об эффективном хранении данных. Сегодня, чтобы однозначно представить символ юникода нужно 32 бита — так символы хранятся в UTF-32.
✅ Прямое отображение.
✅ Простота обработки.
❌ Неэффективный расход памяти — если использовать только латиницу с кодами типа 0..045 и 0...077, ¾ памяти будет занято нулями.
На смену UCS-2 с фиксированными 2 байтами пришёл UTF-16 с переменной длиной. Если значение символа превышает 65536, то оно занимает 4 байта. Java перешла на UTF-16.
Кодировка UTF-8 тоже использует переменное количество памяти. Для каждого символа задаётся, сколько он занимает места — 1, 2, 3 или 4 байта.
✅ Экономный расход памяти для латинских символов.
❌ Обработка и поиск происходят чуть медленнее.
❌ Отметка длины находится в первых 2 битах и уменьшает диапазон значений.
Если в структуре 2 или больше байтов, то одни процессоры быстрее считывают их в прямом порядке, а другие — в обратном. Поэтому у UTF-16 и UTF-32 могут быть приставки LE или BE: Little/big endian.
ASCII-кодировки не умеют читать символы больше 255, поэтому когда они встречают юникод-символы, то показывают ❓ и текст «я люблю Java” превращается в “? ????? Java”.
Компактные строки - важное обновление в java 9. Чтобы лучше понять проблему и новое решение, разберём в этом посте отличия между кодировками. А в среду обсудим компактные строки.
Символы хранятся в памяти в виде чисел. Кодировки отвечают за формат хранения и правила перевода символов в числа и обратно. Кодировки можно условно поделить на две группы:
1️⃣ ASCII-based
2️⃣ Unicode-based
1️⃣ ASCII
В большинстве ASCII кодировок символ занимает 1 байт и содержит число от 0 до 256. Первые 128 значений транслируются одинаково во всех кодировках:
0-31: управляющие последовательности - перенос строки, конец файла и т.д.
32-127: латинский алфавит, цифры, знаки препинания.
Специфичные символы языков отображаются на значения 128-255. Разные кодировки - разные наборы символов:
🔸Кириллица: ISO-8859-5, Windows-1251.
🔸Греческий алфавит: ISO-8859-7, Windows-1253.
🔸Исландские символы: OEM 861.
200 символ может стать Ш, Θ, È или чем-то ещё. Фраза «Я люблю Java” в другой кодировке отобразится как «Ď ŰîŃŰî Java».
Итого:
✅ Один символ занимает 1 байт — компактно.
❌ Всего 256 значений — нет места для эмодзи.
❌ Неоднозначность трактовки.
❌ Нельзя использовать украинский и норвежский язык в одном тексте.
2️⃣ Unicode
В основе преобразований лежит таблица с большинством символов, которые используются в мире. Но не со всеми, многие азиатские иероглифы записываются в памяти как комбинация 2-3 символов. Или эмоджи, например,👩❤️💋👩 - это сочетание 8 Unicode-кодов.
Изначально Unicode использовал 2 байта для записи символа и кодировку UCS-2. После 256 символов ASCII казалось, что диапазона 0-65536 хватит навсегда. Эта кодировка использовалась в ранних версиях java для типа char.
Со временем в таблицу добавилось больше символов и встал вопрос об эффективном хранении данных. Сегодня, чтобы однозначно представить символ юникода нужно 32 бита — так символы хранятся в UTF-32.
✅ Прямое отображение.
✅ Простота обработки.
❌ Неэффективный расход памяти — если использовать только латиницу с кодами типа 0..045 и 0...077, ¾ памяти будет занято нулями.
На смену UCS-2 с фиксированными 2 байтами пришёл UTF-16 с переменной длиной. Если значение символа превышает 65536, то оно занимает 4 байта. Java перешла на UTF-16.
Кодировка UTF-8 тоже использует переменное количество памяти. Для каждого символа задаётся, сколько он занимает места — 1, 2, 3 или 4 байта.
✅ Экономный расход памяти для латинских символов.
❌ Обработка и поиск происходят чуть медленнее.
❌ Отметка длины находится в первых 2 битах и уменьшает диапазон значений.
Если в структуре 2 или больше байтов, то одни процессоры быстрее считывают их в прямом порядке, а другие — в обратном. Поэтому у UTF-16 и UTF-32 могут быть приставки LE или BE: Little/big endian.
ASCII-кодировки не умеют читать символы больше 255, поэтому когда они встречают юникод-символы, то показывают ❓ и текст «я люблю Java” превращается в “? ????? Java”.