Java: fill the gaps
12.9K subscribers
7 photos
215 links
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк

🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt

Комплименты, вопросы, предложения: @utki_letyat
Download Telegram
Сколько элементов выведется в консоль?
Сколько элементов выведется в консоль?
Anonymous Poll
64%
2
36%
3
Теперь заменим for на forEach. Сколько элементов выведется в консоль?
Сколько элементов выведется в консоль при использовании forEach?
Anonymous Poll
44%
2
56%
3
Разница между for и forEach

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

В этом посте разберём разницу между ними. Цель — чисто познавательная: как компьютер саенс проявляется в языке программирования.

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

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

Обойти все вершины графа по одному разу (найти гамильтонов путь) — непростая задача. Алгоритм ведёт учёт посещённых вершин и перебирает множество вариантов. В computer science обход структуры данных называется traverse.
🔸Самое важное: следующий элемент вычисляется только, когда понадобится.

Вернёмся в мир java. В стандартных структурах царят списки, множества, очереди и деревья. Циклов здесь нет, следующий элемент при обходе однозначен, а его отсутствие означает конец работы. Поэтому обход выглядит так:
Iterator it=list.iterator();
while(it.hasNext())
int result = it.next();

Метод next возвращает текущий элемент и сдвигает указатель на следующий. Метод hasNext проверяет, ссылается ли этот указатель куда-нибудь. Этот паттерн повторяется снова и снова и называется Iteration.

Итератор лежит в основе синтаксиса for (T e: collection)

🔸Самое важное: указатель на следующий элемент вычисляется заранее.

Возможный минус: если мы удалили или поменяли элемент, который должен вывестись следующим, то итератор не подхватит изменений и выведет старое значение.

Что по задаче:

Обход через for использует итератор. Указатель на следующий элемент вычисляется заранее. Более подходящий новый элемент не отображается, и выводится 2 элемента.

Метод forEach использует траверс и вычисляет следующий элемент только когда он запрашивается. Поэтому новый ключ подхватывается, и в консоль попадают 3 элемента.

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

Зачем нужно несколько вариантов?
— ConcurrentHashMap может перестраиваться во время обхода. Чтобы во время перестройки не выводить дубликаты, используется траверс со сложной логикой.

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

❗️Для однопоточных коллекций между for и forEach нет никакой разницы, в обоих случаях используется итератор.
👍2
Надо ли добавлять джуна в ревьюеры?

Во многих командах такой вопрос даже не стоит. Понятно, что новичок
▪️ мало разбирается в предметной области
▪️ плохо ориентируется в коде проекта

А значит ничего не поймёт, ничего не скажет, и нет смысла его добавлять в ревьюеры.

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

1️⃣ Проверка читаемости

Применяете SOLID, следуете принципам clean code, но понять вас способен только разработчик с 10-летним опытом? Что-то тут не так🙂

Возможно вы пишете запутанную дичь в стиле 2000-х, пул-реквест занимает 50 файлов, вы вышли за пределы задачи и заодно провернули сложный рефакторинг. Это сильно усложняет понимание кода. Но коллеги-сеньоры привыкли к вашему стилю, поэтому не жалуются.

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

2️⃣ Быстрее получаете фидбэк

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

3️⃣ Многоуровневый фидбэк

Каждый сделает ваш код лучше:
🔸 Джуниор заметит опечатку в названии переменной
🔸 Мидл подскажет удобный метод из java 11
🔸 Сеньор укажет на непокрытый кейс
🔸 Тимлид заметит несоответствие корпоративным стандартам

4️⃣ Обмен знаниями

Материалы в разделе онбординга устаревают. Пул-реквесты показывают то, что происходит здесь и сейчас. Код-ревью быстро вводит нового сотрудника в курс дела:
🔹 Над чем вообще работает команда
🔹 Как писать код и как его оформлять
🔹 Какие тесты и в каком объёме писать
🔹 Как работать с БД и накатывать апдейты
🔹 Что обновлять в документации и CI

Чем быстрее новички вольются в проект, тем быстрее возьмут на себя ваши задачки.
IDEA training

Я часто пишу посты про фичи Intellij IDEA, и такие посты всегда пользуются бешеной популярностью. IDEA классная, но мало кто знает обо всех возможностях.

Я уже писала, как выучить полезные именно вам шорткаты через Productivity Guide или плагин Key Promoter X.

Более основательный способ - это плагин IDE Features Trainer.

Это целый мини-курс, который охватывает написание кода, рефакторинг, навигацию и тд. Для каждой темы открывается учебный проект, и вам по шагам рассказывают, что и куда нажимать🥰

Как поставить: File → Settings → Plugins → MarketPlace → IDE Features Trainer → Install

Потом идёте в Help → IDE Features Trainer
и следуете инструкциям.

Если вы новичок или редко пользуетесь горячими клавишами, но очень хотите начать, то это лучший вариант🔥
👍4
Лямбды и разрыв шаблона

Лямбда-выражение - анонимная функция, которую можно передавать в методы как параметр:

Function<Integer,Integer> sum = (a,b) → a+b;

В чём преимущество лямбда-выражений?

Очевидный ответ - в краткости. Раньше чтобы "передать поведение" нужен был отдельный или анонимный класс, а теперь есть переменная с анонимной функцией. Удобно и понятно.

Но у лямбд есть ещё одно интересное свойство. Посмотрим на него на примере обхода коллекций.

В далёкие времена обойти коллекцию можно было двумя способами:
▫️С помощью цикла
▫️Через итератор

Этот подход называется external iteration или внешний обход. Последовательно идём по элементам коллекции и применяем к ним некоторую логику. С коллекцией общаемся через интерфейс. Главный путь оптимизации - оптимизация логики обработки.

Лямбда-выражения поменяли сценарий работы. Теперь мы как бы передаём функцию внутрь структуры данных. Структура данных сама решает, как применить функцию исходя из деталей своей реализации.

Теперь уже логика обработки скрыта за функциональным интерфейсом, а структура данных ей пользуется. Это называется internal iteration или внутренний обход.

Итого у нас разрыв шаблона. Двадцать лет обход коллекций шёл по сценарию
🔸Логика обработки - ведущий игрок, все оптимизации здесь
🔸Структура данных - абстракция, скрыта за интерфейсом

С появлением лямбда выражений и Stream API субъект и объект поменялись местами:
🔹Логика обработки доступна через интерфейс
🔹Структура данных - главная, оптимизации происходят на этом уровне

Теперь работу над коллекцией легко делить между потоками. Раньше это было невозможно - логика обработки могла быть любой и параллелить в каждом случае нужно по-разному. Деление на подзадачи на уровне структуры данных гораздо проще, и теперь у нас есть библиотечный метод parallel() в Stream API.

Понятно, что такой подход применим не только к обходу элементов в коллекции, но и к взаимодействию любых компонентов в целом. Смена ролей часто приводит к свежему взгляду на привычные действия. И в жизни, и в написании кода🙂
👍3
Как НЕ реализованы лямбда-выражения

Давно хотела написать о том, как выглядят лямбды внутри JVM.

Во-первых, мало кто об этом знает. Есть несколько статей и видео с конференций, но они достаточно сложные.

Во-вторых, это интересно. Для ответа на вопрос нужно представлять, как работает JVM и собрать вместе несколько концептов.

В-третьих, время не стоит на месте. Java развивается, а многие до сих пор не в курсе изменений 10летней давности.

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

Часть 1: внешний вид. Как лямбды будут выглядеть в коде?

Первая догадка - использовать опыт других языков и ввести в JVM тип function:

fun(s: Integer): boolean { return s<3; }

Легко для понимания, сложно для реализации. Java - ООП язык, всё в JVM заточено под работу объектов и примитивов. Добавить новый тип данных - чудовищно огромная работа. Как функция будет хранится, вызываться и взаимодействовать с другими объектами? Будет ли она работать с дженериками? Тысячи вопросов и сложностей.

Второй вариант: пусть лямбда реализует интерфейс с одним методом:

Predicate p = i -> i<3;

Выглядит по-джавовски и отлично впишется в текущую систему. Существующие библиотеки будут работать с лямбдами по умолчанию.

Но какой объект будет присвоен переменной p?

Часть 2: реализация

Здесь тоже два варианта.

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

С учётом того, что лямбды станут основой Stream API и использоваться для обработки данных - решение ужасное и непроизводительное.

Второй вариант - компилировать код лямбды в отдельный метод и подставлять в исходный код MethodHandle.

Мы рассмотрим MethodHandle на следующей неделе, но если кратко - это указатель на метод. Компилируем исходный пример в:

public boolean lambda(Integer i) { return i<3; }
MethodHandle mh = LDC["lambda"];

И подставляем mh в исходный код:

...stream().filter(mh)...

В теории решение отличное, на практике - неподходящее.

Теряется информация о входных и выходных типах, приходится проверять их в рантайме
Библиотечные методы не принимают на вход MethodHandle

Как же сделаны лямбды?

Об этом поговорим на следующей неделе:
▫️ Сначала вернёмся во времена java 6 и посмотрим как вызываются методы в JVM
▫️ Изучим новую байткод инструкцию java 7 и чуть глубже обсудим MethodHandle

После этого говорить про лямбды будет легко и понятно.
👍8
Method Handle: краткий обзор

В java 7 появилось новое апи для вызова методов - MethodHandle. В этом посте обсудим его составные части, разницу с Reflection, и где MethodHandle может пригодится.

В чём разница с Reflection API?

MethodHandle выполняет проверки типов, доступов и тд всего один раз - во время создания. Собственно для этого метод хэндл и создавался - оптимизировать вызов методов, неизвестных во время компиляции.

Reflection делает все операции при каждом вызове.

Чем больше вызовов - тем сильнее разница. В OpenJDK даже есть JEP "Переписать реализацию Reflection API на MethodHandle", что говорит о многом.

Как им пользоваться?

1️⃣ Получаем фабрику для будущих метод хэндлов

Lookup pl = MethodHandles.publicLookup();

2️⃣ Задаём сигнатуру метода

MethodType mt = MethodType.methodType(String.class, Integer.class, Long.class);

Первый аргумент (String.class) - это возвращаемое значение, остальные - входные параметры.

3️⃣ Получаем указатель на метод

MethodHandle mh = all.findVirtual(User.class, "findName", mt);

Это ссылка на метод findName в классе User, который принимает на вход два параметра и возвращает String.

4️⃣ Подставляем аргументы и вызываем

String res = mh.invoke(1, 2L);

Выбор методов на каждом этапе гораздо шире, но разбирать мы их, конечно, не будем.

Где пригодится MethodHandle?

Конечно же в библиотеках и фреймворках, которые работают с аннотациями. Но для энтерпрайза у MethodHandle тоже есть микро-кейс: объявление логгера.

Ну вы знаете, в классах бизнес-логики часто добавляют логгирование:

LoggerFactory.getLogger(AccountService.class);

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

Если использовать MethodHandle:

Logger.getLogger(MethodHandles.lookup().lookupClass());

то можно спокойно копировать строчку и ничего не менять.

В целом основная миссия MethodHandle - работать в паре с invokedynamic. Но об этом чуть позже🙂
👍41
Как устроены лямбды: вызов методов внутри JVM (2/4)

Одним летним утром мы обсудили Method Handle API.

А в первом осеннем посте сделаем полшага назад и посмотрим, как вызываются методы в JVM. Эта история тоже непростая, поэтому начнём с древних времён java 6.

Для разных методов компилятор генерирует разные байткод инструкции:

🍂 invokespecial - приватные методы, конструкторы и вызовы через super
🍂 invokestatic - статические методы
🍂 invokevirtual - public и protected методы
🍂 invokeinterface - вызов методов интерфейса

Байткод можно посмотреть, вызвав команду для скомпилированного файла:

javap -c -v UserInfo.class

При первом использовании файл с байткодом загружается с диска и регистрируется внутри JVM. Нам интересны четыре сущности:

1️⃣ ConstantPool - табличка из class-файла с именами полей, методов и аргументов. Выглядит как-то так:

#14=Utf8        Autumn
#16=Class #18
#17=NameAndType #19:#20

2️⃣ Список байткод инструкций

29: getstatic     #39 
32: aload_3
33: invokevirtual #45

Все конкретные методы и аргументы обозначаются через ссылки из Constant Pool.

3️⃣ Табличка виртуальных методов для класса

4️⃣ Табличка с интерфейсами и реализациями

Таблицы 3 и 4 лежат внутри JVM в особой структуре, чтобы по ним было легко перемещаться.

Каждый из invoke* методов работает по своему алгоритму. Понятно, что для protected метода нужно походить по иерархии, а для приватного это не нужно.

Что тут важно: все структуры строятся при загрузке класса и больше не меняются.

В Java 7 в JVM добавилась инструкция invokedynamic для поддержки динамических языков.

Зачем в JVM поддержка других языков? А потому что JVM - очень развитая и крутая:

🍁 Кроссплатформенность - берёт на себя работу с операционной системой и даёт заняться чистейшим программированием
🍁 Изоляция - можно безопасно запускать несколько приложений
🍁 JIT компиляторы
🍁 Поддержка многопоточности, сборщики мусора, рефлекшн

Думаете, у всех такое есть? А вот и нет.

Но есть один недостаток. JVM совершенно не приспособлена под динамические языки - JavaScript, Python, Ruby. Все invoke* методы должны заранее знать все классы и типы аргументов.

Поэтому в JVM добавили новую инструкцию invokedynamic. А о том, как она работает, поговорим в пятницу.
1
Как устроены лямбды: invokedynamic (3/4)

10 лет назад в java было 4 байткод инструкции для вызова методов. В Java 7 добавили invokedynamic для поддержки динамических языков.

invokedynamic - это лишь небольшая часть Da Vinci Machine Project. У проекта очень красивое описание:

a multi-language renaissance for the Java Virtual Machine architecture

Среди других фич Interface injection, Lightweight bytecode loading, Tail calls and tail recursion и прочие интересные штуки.

Но вернёмся к invokedynamic. Разберём по шагам, как он работает:

1️⃣ JVM встречает в class файле такой байткод

invokedynamic #7, 0

2️⃣ В пуле констант находим такую запись

#7 = InvokeDynamic #0:#8

3️⃣ Идём по ссылкам и попадаем в специальную секцию .class файла под названием BootstrapMethods.

Если компилятор использует invokedynamic, то он обязан прописать такой метод.

4️⃣ Там мы находим набор инструкций, который по заданным аргументам укажет на конкретный метод. В итоге всё сводится к получению и вызову MethodHandle.

В чём фишка:
В других invoke* алгоритм связывания жёстко задан внутри JVM, а для invokedynamic логика связывания прописывается в отдельном методе, который может быть любым.

Такая вот общая схема. Дальше идут оптимизации. Например, бутстрап метод возвращает не сам MethodHandle, а более гибкий CallSite. У него три реализации: одна с неизменным MethodHandle и две с переменным

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

Так что на следующей неделе соберём всё вместе и погрузимся в детали реализации лямбда-выражений🤓
👍3
Как устроены лямбды: собираем всё вместе (4/4)

В последней части наконец-то перейдём к лямбда-выражениям.

Что нам уже известно:

🔸 Лямбды не приводят к созданию анонимных классов
🔸 В java 7 появились invokedynamic и MethodHandle API для поддержки динамических языков. Но эти штуки отлично подошли для некоторых java фич, в том числе лямбда-выражений.

Если не читали - прочитайте, тогда этот пост будет понятней.

Итак, разработчик пишет код с лямбда-выражением

stream().filter(x -> x<3)

В байткоде на месте вызова лямбды появляется инструкция

invokedynamic #2, 0

Следуем по указателям из Constant Pool и попадаем в секцию бутстрап-методов. Видим там вызов

LambdaMetafactory.metafactory(…)

Этот фабричный метод возвращает объект CallSite, внутри которого спрятался MethodHandle.

Больше в байткоде нет информации. Понятно, что работа с лямдой - это какое-то перенаправление, а вся магия описана в классе LambdaMetafactory.

Класс лежит в JDK, идём туда и видим:

1️⃣ С помощью ASM библиотечки создаётся внутренний класс. Он реализует интерфейс Predicate и называется как-то так:

class Smth$$Lambda$14/0x0000000800c02460

Класс динамический и существует только в памяти.

2️⃣ Создаётся объект этого класса и привязывается к MethodHandle внутри ConstantCallSite.

3️⃣ Все обращения к лямбде переадресуется этому объекту.

Чем это лучше анонимных классов?

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

Почему нельзя вынести лямбду в отдельный метод и просто вызывать его?

Чтобы встроить решение в остальной java код. Если в коде пишется

Predicate p = i -> x<3;

То ожидается, что p - это объект. Его можно куда-то передать и вызвать его методы.

Логика вроде "назовём это объектом, но на самом деле это метод" - это слишком большое усложнение. Так не работает. Новые фичи должны вписываться в систему, а не обходить её.

Зачем столько переадресаций? Почему из бутстрап метода надо идти в другой класс? Нельзя было сделать встроенную инструкцию?

Текущий код LambdaMetaFactory со временем изменится. Добавится больше кэширования, семантика лямбд расширится в следующих версиях java. Если вся логика описана в JDK, то гораздо легче поддерживать совместимость версий.

Такая вот эпопея. Я весь процесс поняла не с первого раза и даже не со второго. Поэтому решила провести вас маленькими шажками, и описать всё максимально просто.
👍7
О простоте.

Недавно один подписчик написал "канал топ, но зачем ты его ведёшь?". Вопрос понятный: хорошие посты действительно отнимают много времени и сил.

Когда я была джуниором и мидлом, мне не хватало ясности. По каждой теме приходилось читать тонны документации и фильтровать огромное количество мусора. Хотелось узнать, что имел в виду автор, и как это использовать на практике. Когда в целом понятно, но вопросы "почему" и "зачем так сложно" повисают в воздухе.

Быть разработчиком - значит держать в голове тысячи абстракций и десяток технологий. И это количество только растёт.

Поэтому в обучении хорошо заходят принципы хорошего кода: KISS, читаемость, инкапсуляция. Любая тема раскладывается на простые шаги: 1-2-3. Некоторые темы выглядят как 1-2-3-...-99-100, но каждый шаг логичен и понятен.

Этот подход я применяю в постах и в своём любимом курсе по многопоточке.

Возьмём последний набор постов про лямбды. Пришлось перерыть кучу материалов, чтобы понять основную идею. Она в итоге оказалась несложной, но как долго пришлось добираться до сути!

За простыми вещами всегда стоит много работы. Чтобы другие люди оценили её, используются сложные описания. Так в резюме появляется "комплексный рефакторинг ключевого компонента системы интеграции аналитики".

Но чем проще, тем лучше для всех.

Я стараюсь писать посты чётко, понятно и без воды. Радуюсь, когда получаю сообщения с благодарностью, что кто-то наконец увидел суть.

Горжусь своим курсом по java.util.concurrent. У последнего потока доходимость около 80%. У других попсовых IT курсов редко превышает 10%. Чувствую, что сделала что-то на порядок выше рынка.

Рада, что такой подход откликается подписчикам и ученикам. Спасибо, что разделяете идею о простоте, стремитесь понимать технологии лучше и хорошо делать свою работу.

Спасибо, что читаете❤️
5
Java выходит каждые полгода, а сегодня вышла 17 версия. Если Age - ваш возраст прямо сейчас, через сколько лет ваш возраст и версия java будут совпадать?
Anonymous Poll
27%
Age - 17
33%
(Age - 17) /2
18%
(Age - 17) *2
21%
Age - 17 + Age/2
Ваш прогноз - когда хотя бы один сервис вашего проекта перейдёт на Java 17?
Anonymous Poll
8%
До конца года
10%
В следующем году
21%
Года через 2-3
61%
Грустно думать об этом😔
Вышла Java 17🥳

Это версия с долгосрочной поддержкой, а значит у неё ненулевой шанс оказаться однажды в вашем проекте.

Самые обсуждаемые фичи:

🔸 Новые сборщики мусора: ZGC и Shenandoah
🔸 Текстовые блоки и стрелочки в switch
🔹 Sealed classes
🔹 Records

А теперь ответ на вопрос - через сколько лет ваш возраст совпадёт с версией java:

Пусть Age – возраст в 2021
x – количество лет, через которое версия = возраст.
Age + x = 17 + 2*x
x = Age - 17
Личный опыт: как я изучаю информацию

Периодически получаю вопрос от подписчиков вроде

Что почитать по джаве, чтобы также чётко всё понимать?

Источники знаний давно известны - это интернет, свой опыт и чужой опыт. Интернет доступен всем, поэтому начну не с вопроса ЧТО, а с вопроса КАК. Как из огромного количества информации извлечь знание? Как сформировать свою призму восприятия?

Чтобы не уходить в абстракции, расскажу пару историй из личного опыта, которые дали мне старт к более осознанному потреблению контента.

История 1: A-Club

В епаме чтобы получить новую должность, нужно пройти assessment. Это внутреннее собеседование раз в полгода, где ты рассказываешь незнакомым коллегам свой опыт и отвечаешь на вопросы. Если всё ок, тебя рекомендуют к повышению.

Это старая и стандартная процедура, список тем и вопросов давным-давно известен. Поэтому надо просто подготовиться.

И вот я и два моих коллеги решили готовиться к ассесменту вместе. Придумали такой формат:

Допустим, я беру себе тему Agile. Обязуюсь изучить тему вдоль и поперёк и к пятнице подготовить докладик на 10-20 минут. Ребята что-то знают про Agile и заранее накидывают мне "глупые вопросы про аджайл":

🔸 А почему скрам-мастер бывает, а канбан-мастера нет?
🔸 Кто-нибудь вообще измеряет задачи НЕ в сторипойнтах?
🔸 А обязательно выделять на ретро целый день?

"Глупые" вопросы принесли больше всего пользы для понимания. Чтобы найти ответ, нужно серьёзно углубиться в кейсы и исходные идеи.

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

История 2: помощь другу

Один мой коллега очень хотел сменить работу, но волновался, что недостаточно хорош.

Я решила ему помочь и за обедом провела небольшое собеседование. Коллега был крепкий мидл: разбирался в технологиях своего проекта, но с кругозором было не очень.

Нашли несколько пробелов, и я сказала "домашнее задание - к среде ответить на X, Y и почётче рассказать про Z".

Он серьёзно подошёл к делу и в среду на всё ответил. Я задала ещё несколько вопросов, он снова ушёл с ними разбираться.

Мы увлеклись этим процессом на 2.5 месяца. Под конец я специально искала сложные вопросы и пыталась его завалить. И "глупыми" вопросами и серьёзными:

🔹 В чём разница между Kafka и ActiveMQ?
🔹 А в базах данных реально всё в таблицах хранится?
🔹 Почему в джаве нельзя напрямую вызвать деструктор?

Иногда к нам присоединялся коллега джуниор, и я просила объяснить ему простыми словами какой-нибудь паттерн, принцип или идею.

Нам всем эти беседы принесли огромную пользу.

Резюме

Объясняешь другому - сам понимаешь лучше
"Глупые" и неожиданные вопросы помогают увидеть тему глубже
Готовишь доклад - структурируешь знания и видишь пробелы

Всё это помогает перейти от пассивного потребления информации к её осознанию.
👍3