microJUG – Telegram
microJUG
979 subscribers
155 photos
1 video
2 files
237 links
Мысли о Java.
Основной канал: @miniJUG
Буст: https://news.1rj.ru/str/microJUG?boost
Чат: https://news.1rj.ru/str/micro_JUG
Таблица JEP'ов: https://minijug.org/jeps.html
Download Telegram
Кто-нибудь может объяснить, почему в Идее отсутствуют кнопки разворачивания окон на весь экран? Неужели никого это не напрягает? 🤔
#IntelliJIDEA
Что объединяет все эти примеры кода?
Универсальные дженерики уже попали в репозиторий Valhalla. И там стоит релиз Java 18! Естественно, preview. Значит по логике мы сможем писать List<int> в Java 20. Поскорей бы.
#Java18 #Java20
Где-то с год назад я делал разбор поста из одного популярного Telegram-канала по Java, где объяснил, почему автор неправ. Решил вот опять заглянуть туда. Ничего не изменилось: автор (точнее авторка) продолжает сбивать с толку своих читателей и вдалбливать в голову совершенно вредные и, в данном случае, неправильные вещи. А количество подписчиков с того момента увеличилось на две тысячи, так что стало всё только хуже.

Перед началом чтения моего разбора поста можете прочитать оригинальный пост, хотя это необязательно (и может быть даже опасно 🤡).

Итак, автор начинает с двух тестов. В первом приводится кусок кода и спрашивается, сколько элементов он выведет:

Map<Integer, Integer> map = new ConcurrentHashMap<>();
map.put(20, 0);
map.put(30, 0);

for (Integer key : map.keySet()) {
System.out.println(key);
map.put(40, 0);
}

Во втором тесте цикл for заменён на метод forEach и задаётся тот же самый вопрос:

map.keySet().forEach(key -> {
System.out.println(key);
map.put(40, 0);
});

Без чтения дальнейшего поста уже стоило бы напрячься. Во-первых, модифицировать мапу во время итерации в этом же самом потоке уже немного странно. Ну хорошо, бывает, что иногда приводится не очень практичный пример чисто ради педагогических целей. Есть ли какая-то педагогическая ценность в данном вопросе? Я бы сказал, да, если бы среди ответов был вариант: «чувак, поведение не определено и может произойти что угодно». Ведь для ConcurrentHashMap гарантируется только отсутствие вылета ConcurrentModificationException, а будет ли там подхватываться или не подхватываться элемент – это уже деталь реализации. Среди ответов же есть только два варианта «выведется 2 элемента» и «выведется 3 элемента». На самом деле контракт нам про 2 или 3 сказать ничего не может. Гипотетически может произойти любая комбинация:

1. for выведет 2 элемента, forEach выведет 2
2. for выведет 2 элемента, forEach выведет 3
3. for выведет 3 элемента, forEach выведет 2
4. for выведет 3 элемента, forEach выведет 3

Комбинации 2 и 4, например, можно вполне себе легко воспроизвести: на Java 11.0.12 for выведет 20, 30, а forEach 20, 40, 30. Но если взять другие числа, например, 10, 20, 30 вместо 20, 30, 40, то оба выведут одно и то же: 20, 10, 30.

Таким образом, автор, пытаясь объяснить различия между устройством for и forEach, выбрала крайне неудачный (и некорректный пример).

Теперь давайте перейдём к самому посту. Пойдём прямо по тексту:

for использует для обхода итератор, а forEach - траверс.

Не очень понятно, что автор хочет нам этим сказать. И for и forEach оба делают обход. Правильнее было бы сказать: for использует pull-based обход, а forEach – push-based. Отсюда и возможные различия в поведении.

Сразу скажу: всё это имеет смысл только для обхода многопоточных структур данных

Не только. Различия между for и forEach порой существенны и для однопоточных структур. Например, те же стримы могут в некоторых случаях вести себя немного по-разному, если использовать iterator вместо forEach. Кроме того, forEach работает, как правило, быстрее, так как push-based реализация чаще намного проще и эффективнее. Посмотрите, например, как просто реализован ArrayList.forEach и насколько сложнее ArrayList.Itr.

Почему нельзя использовать траверс по умолчанию?
— Итератор проще и работает быстрее, а условия для пропуска элемента при обходе встречаются редко.

Не проще и не быстрее (см. выше). Итератор может быть либо медленнее и сложнее, либо как минимум одинаков по скорости и сложности с forEach (ну если говорить о разумных реализациях, можно конечно специально сделать forEach медленнее).
Итого: при выводе элементов ConcurrentHashMap через for и forEach используются разные алгоритмы обхода, поэтому результат вывода тоже разный.

Как раз-таки для ConcurrentHashMap используется один и тот же алгоритм обхода. Это тот случай, когда for и forEach совпадают. forEach использует вложенный класс Traverser, а for – класс BaseIterator, который наследуется от Traverser. А различия в поведении (for выводит 20, 30, а forEach 20, 40, 30) наблюдаются из-за того, что Traverser более ленив, чем BaseIterator. BaseIterator следующий элемент вычисляет раньше, чем он реально нужен.

Для однопоточных коллекций между for и forEach нет никакой разницы, в обоих случаях используется итератор.
Нет, разница между ними есть. И нет, в обоих случаях не используется итератор. Да, реализация forEach в Iterable по умолчанию делегирует обход к итератору. Но почти все коллекции эту реализацию переопределяют на более эффективную.
Квиз вам на ночь
Марк Рейнолд объявил, что LTS-релизы будут выходить раз в 2 года, а не в 3.
#java21 #lts
Классный вводный доклад про арифметику с плавающей точкой. Не поленитесь и потратьте всего полчаса на просмотр. Гарантирую, что время будет потрачено не зря. Я вот, например, узнал, как вообще на английском читается аббревиатура IEEE 754 😅. Оказывается, её произносят как "I triple-E seven fifty-four".

Ну это не самое полезное знание, конечно. Куда важнее понять, что в double числа дискретные и их множество конечно, в то время как вещественные числа непрерывны и их бесконечное количество. Это значит, double аппроксимирует вещественные числа, т.е. каждое вещественное число конвертируется в double путём нахождения ближайшего. Например, числа 0.1 в double нет, а ближайшее к нему это 0.1000000000000000055511151231257827021181583404541015625. Но почему тогда Double.toString(0.1) возвращает "0.1", а не "0.1000000000000000055511151231257827021181583404541015625"?

А ещё из-за такой аппроксимации нарушаются многие аксиомы. Не работают ассоциативность сложения, ассоциативность умножения, дистрибутивность и т.д. Например:

> 0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1
$1 ==> 0.9999999999999999

Но:

> (0.1+0.1+0.1+0.1)+(0.1+0.1+0.1+0.1)+0.1+0.1
$2 ==> 1.0

(Это кстати отличный пример, демонстрирующий, почему double ни в коем случае нельзя использовать для хранения денег)

В общем, про всё это можно узнать из лекции.

#IEEE754 #double
Думаю, написать кусок кода на Java через стрим или цикл
#юмор
Три самых сложных проблемы в разработке ПО – это именование переменных, инвалидация кешей, и… порядок объявления полей, методов и параметров. Третье особенно болезненно в Java, потому что в ней нет никаких ограничений по порядку. Например, можно объявить поле в классе, потом десяток методов, а потом опять поле. Когда я вижу такое в коде, у меня сильно бомбит. Считаю, что Java не должна вообще такое позволять. Но с другой стороны, в далёких 90-х не было понятно, какой должен быть строгий порядок, поэтому решили не ограничивать вообще. Например, люди, пришедшие из C++ (а это основная целевая группа в то время), привыкли группировать по области видимости:

class C {
private:
// поля, методы
public:
// поля, методы
}

Я же считаю, что надо группировать по типу членов: поля отдельно, методы отдельно.

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

class C {
// статические константы (static final)
// статические поля (static)
// final поля
// поля
// конструкторы
// геттеры/сеттеры
// методы
// вложенные классы
}

Порядок самих полей – вещь сложная, надо решать в каждом индивидуальном случае. Строгого правила нет, но можно исходить из здравого смысла: если, например, есть Text inputText и Button okButton, и поле ввода в окошке находится выше кнопки ОК, то и поле inputText должно быть объявлено выше okButton.

Порядок параметров конструкторов, очевидно, должен соответствовать порядку полей. Если конструкторов несколько, то они должны быть отсортированы по принципу «кто кого дёргает». Если конструктор A вызывает конструктор B, то конструктор A должен быть объявлен выше. Если конструкторы не связаны друг с другом, то выше лучше объявить тот конструктор, у которого поменьше параметров (он более простой и, скорее всего, будет вызываться чаще, поэтому лучше, чтобы он находился читателем кода пораньше).

С методами то же самое: тот, который вызывает, должен быть выше того, кого вызывают. То есть чем ниже мы скроллим класс, тем всё глубже в детали реализации мы погружаемся. Чем ниже, тем более низкоуровневые методы мы видим. Бывает, что методы вызывают друг друга, тогда непонятно, что должно быть выше. Но взаимная рекурсия довольно редкая штука, на самом деле. Сложнее, если методы не связаны друг с другом. В этом случае уже надо подключать здравый смысл и логику: я предпочитаю группировать по функциональности. Связанные вещи должны быть рядом:

// Функциональность a
a() {
aImpl1()
aImpl2()
}

aImpl1() {}

aImpl2() {}

// Функциональность b
b() {
bImpl1()
bImpl2()
}

bImpl1() {}

bImpl2() {}

С порядком параметров методов посложнее – здесь я ещё не до конца выработал правила. Есть только несколько не очень чётких принципов. Один из них: in-параметры, очевидно, должны быть раньше, чем out-параметры. Что такое in- и out-параметры? Суть должна стать понятна на примере:

public void copy(Path from, Path to) {}

Думаю, не стоит пояснять, что in, а что out. Всё очевидно. Что in, а что out, должно вытекать из здравого смысла.

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

public void drawPoint(GC gc, int x, int y) {}
public void drawCircle(GC gc, int x, int y, int r) {}

GC – это graphics context. Он более стабилен, чем координаты, потому что создаётся один раз, а рисуется туда много всего и приходит много разных иксов и игреков. Другими словами, если что-то более постоянное и долгоживущее, тем оно более стабильно и должно идти раньше.

Также я всегда объявляю лямбды последними. Например:

public void addEvent(EventType type, Runnable r);

Иначе будет вот так:

addEvent(() -> {
// 100500 строк кода
}, EventType.MOUSE_CLICK);

Лучше, чтобы MOUSE_CLICK был первым, а после длинной лямбды не шло ничего.

В общем, как-то так. Такой вот набор правил и принципов я применяю.

Вопросы? Замечания?
Кружка на работе. Как вам?
Как же хорошо, когда у тебя заботливые коллеги