Java for Beginner
688 subscribers
586 photos
161 videos
12 files
900 links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Особенности и подводные камни

Погрешность и потеря точности
Каждое присваивание или операция с float может сопровождаться потерей точности. Например:
float a = 1_000_000;
float b = a + 0.0001f;
System.out.println(a == b); // true — потерялась дробная часть


Нормализованные и денормализованные числа

float поддерживает очень маленькие значения, но при этом точность сильно страдает. Денормализованные значения позволяют представлять числа ближе к нулю, но с меньшей точностью.

NaN, Infinity и -Infinity

float поддерживает специальные значения:
Float.NaN — результат недопустимых операций (например, 0.0f / 0.0f)
Float.POSITIVE_INFINITY и Float.NEGATIVE_INFINITY — результат переполнения или деления на 0
Эти значения не вызывают исключений, и с ними можно работать, но это требует осторожности.


Сравнение на равенство

Из-за округлений не следует использовать == для сравнения двух float.


Вместо этого используют допустимую погрешность:
float a = 0.1f + 0.2f;
float b = 0.3f;
if (Math.abs(a - b) < 1e-6) {
System.out.println("Равны с учетом погрешности");
}


Приведение типов

При смешанных операциях с float и целочисленными типами Java автоматически приводит меньший тип к float.

Например:
int x = 3;
float y = 2.5f;
float result = x + y; // x преобразован в float
Это не вызывает проблем, но может повлиять на точность, если целое число очень большое.


Двоичное представление и неожиданное округление

Некоторые десятичные дроби (например, 0.1, 0.2) не могут быть точно представлены в двоичной системе. Это приводит к накапливающимся погрешностям, особенно при работе с циклами или большими массивами данных.


Когда использовать float, а когда — double


Используй float, если:
Работаешь в среде с ограниченной памятью или производительностью (например, Android, микроконтроллеры)
Требуется снизить объем данных (например, передача координат в 3D-движке)
Максимальная точность не критична


Используй double, если:
Точность важна (финансовые расчеты, физические симуляции)
Объёмы данных позволяют использовать больше памяти
Не хочешь постоянно контролировать потерю точности


#Java #для_новичков #beginner #float
👍3
Продолжаем выбирать темы для разбора и голосовать за рассмотрение предложенных! 🤓

Голосуем за тему к рассмотрению в эти выходные!

Выбираем новую тему!
(можете предложить что-то из того что предлагали на прошлой неделе и что проигрывает в голосовании!)

Не стесняемся! ✌️
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Что выведет код?

public class Task280525 {
public static void main(String[] args) {
float f = 16777216f;
System.out.println(f == (f + 1f));
}
}


#Tasks
👍1
Как я память искал (Часть I)

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

Причинами и способами решения, сегодня я решил поделиться с Вами
.

Вводные данные:
🔵Основная сущность (к примеру User) и связанная с ней через one-to-many, вторичная сущность (пусть будет Car).
🔵Сущности описаны по стандарту Spring JPA в коде.
🔵По запросу бизнеса, при получении списка основных сущностей должна применяться пагинация, фильтрация и сортировка (для корректного отображения на web-странице).
🔵Само собой при получении основных сущностей, нужно подгружать все связанные, а так же иметь возможность фильтрации и сортировки по содержащихся в них данных.

Предложенное решение (как показала практика - неверное):
🔵Для решения подобного запроса использовать стандартные средства Spring, такие как Pageable и для формирования сложного SQL запроса средствами Java - Specification

Примерный код:

🧍 User
@Entity
public class User {
//стандартные поля
@OneToMany(mappedBy = "owner", fetch = FetchType.LAZY)
private List<Car> cars = new ArrayList<>();
}


🚗 Car
@Entity
public class Car {
//стандартные поля
@ManyToOne
@JoinColumn(name = "owner_id")
private User owner;
}


Пагинация + fetch join через Specification (без фильтров)

Page<User> users = userRepository.findAll(specification, PageRequest.of(0, 10));


настройка Specification
return (root, query, cb) -> {
root.fetch("cars", JoinType.LEFT);
query.distinct(true);
return cb.conjunction();
};


Проблема:
🔵Хотя предложенное выше решение и выглядит очевидным при озвученном кейсе требований и решении проблемы N+1, оно создает проблему о которой Вам не расскажут на хайповых курсах и видео уроках.

Вот примеры:
Пример 1
Пример 2

🔵Суть проблемы в том, что когда вы используете JOIN FETCH (например, через Specification), Hibernate подгружает связанные сущности в один SQL-запрос (чтобы избежать N+1).

Однако в сочетании с пагинацией (Pageable) Hibernate теряет корректность подсчёта количества строк и может загрузить всю таблицу в память, чтобы затем вручную "отрезать" нужную страницу на уровне Java, тем самым использовав ВСЮ выделенную JVM память для хранения
.

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


Что происходит, подробно?

Когда вы вызываете, например:
Page<User> users = userRepository.findAll(specification, PageRequest.of(0, 10));


Hibernate должен выполнить:
SELECT COUNT(*) ... — чтобы узнать общее количество строк.
SELECT ... LIMIT 10 OFFSET 0 — чтобы получить только первую страницу.


❗️Но fetch join меняет семантику запроса

Когда вы пишете JOIN FETCH, например:
root.fetch("cars", JoinType.LEFT);

Или:
criteriaQuery.distinct(true);


Hibernate генерирует SQL примерно такого вида:
SELECT u.*, r.* FROM users u
LEFT JOIN roles r ON r.user_id = u.id


При этом:
Если у одного пользователя 3 cars, то он появится 3 раза в результате SQL-запроса.
Hibernate потом вручную собирает дубликаты в одну сущность User, у которой будет List<Car> с 3 элементами.
LIMIT/OFFSET применяются к строкам SQL, а не к "собранным" сущностям — и это вызывает проблемы.


⚠️ Проблема: LIMIT работает до агрегации

Hibernate не может корректно объединить дубликаты после применения LIMIT, потому что:

При использовании fetch join, результат SQL-разворачивается в несколько строк (по связям).
Но LIMIT обрезает эти строки до того, как Hibernate агрегирует их в объекты Java.
Поэтому Hibernate игнорирует LIMIT в SQL, чтобы корректно собрать сущности → он загружает все строки в память, затем отрезает нужную страницу на уровне Java.


🤯 Что я получил в результате использования неверного решения

🔜 Упавший сервис на проде 😨 (так как на тестовых средах, объем загружамых данных был в разы меньше).
🔜 Бесценный опыт, которым сейчас делюсь с Вами ☺️

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

Понравился стиль подачи материала?

Отправь другу и ставь -
🔥

#Java #fetch #autor
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9
В чем разница между ArrayList и LinkedList? 🤓

Ответ:

ArrayList использует динамический массив, быстрый доступ по индексу (O(1)), но медленные вставки/удаления в середине (O(n)).

LinkedList использует двухсвязный список, быстрые вставки/удаления (O(1)), но медленный доступ по индексу (O(n)).


#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
📌 Факт дня:

А вы знали, что термин "алгоритм" назван в честь персидского математика?

Слово "algorithm" происходит от имени Аль-Хорезми, персидского учёного IX века, чьи труды по математике легли в основу современных вычислений. Его имя латинизировали как "Algoritmi".


Proof

#facts
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
📌 Цитата дня: Джеймс Кларк

"Интернет — это конец посредников."


Джим Кларк, один из основтелей Netscape, сказал это в 1995 году в интервью журналу Wired.


Почитать короткую биографию

#Citation #Biography
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Структура и основные команды Liquibase

1. Файл конфигурации (liquibase.properties)

Файл liquibase.properties содержит настройки для подключения к БД и управления Liquibase.

Основные параметры:
# Подключение к БД  
url=jdbc:postgresql://localhost:5432/mydb
username=user
password=pass
driver=org.postgresql.Driver

# Настройки changelog
changeLogFile=db/changelog/db.changelog-master.xml

# Дополнительные параметры
liquibase.hub.mode=off # Отключение Liquibase Hub (если не используется)


Разбор параметров:
url – JDBC-URL базы данных (зависит от СУБД).
username и password – учетные данные для подключения.
driver – класс JDBC-драйвера (например, org.postgresql.Driver для PostgreSQL).
changeLogFile – путь к главному файлу changelog.


Где размещается?

В корне проекта (рядом с pom.xml/build.gradle).

Или указывается явно при запуске:
liquibase --defaults-file=config/liquibase.properties update


2. Основные команды CLI


Liquibase предоставляет консольные команды для управления миграциями.

update – применение изменений

Применяет все невыполненные changeSet’ы из changelog.
liquibase update


Что происходит?
Liquibase проверяет таблицу DATABASECHANGELOG.
Находит changeSet’ы, которых нет в этой таблице.
Применяет их в порядке указания в changelog.

rollback – откат изменений

Возвращает БД к предыдущему состоянию.
# Откат до тега v1.0  
liquibase rollback v1.0

# Откат последнего changeSet’а
liquibase rollbackCount 1

# Откат до определенной даты
liquibase rollbackToDate 2024-01-01


status – проверка состояния БД

Показывает, какие changeSet’ы не применены.
liquibase status


Вывод:
2 changesets have not been applied to mydb@jdbc:postgresql://localhost:5432/mydb


validate – проверка корректности changelog

Проверяет синтаксис changelog без применения изменений.
liquibase validate


Если есть ошибки, выведет сообщение, например:
ERROR: ChangeSet db/changelog/changes/001-create-table.xml::1::alex failed. Reason: Table 'users' already exists


3. Жизненный цикл изменений


Как Liquibase применяет changeSet’ы?

Парсинг changelog:
Liquibase читает главный файл (например, db.changelog-master.xml).
Загружает все вложенные changeSet’ы.


Проверка DATABASECHANGELOG:
Сравнивает список changeSet’ов с теми, что уже выполнены (хранятся в таблице DATABASECHANGELOG).

Применение изменений:
Невыполненные changeSet’ы применяются в порядке их объявления.
После успешного выполнения информация о changeSet’е записывается в DATABASECHANGELOG.


Контроль версий и порядок выполнения

Уникальность changeSet’а определяется по:
id (например, "1").
author (например, "alex").
Путь к файлу changelog.


Порядок выполнения:
<databaseChangeLog>
<include file="db/changelog/changes/001-create-users.xml"/>
<include file="db/changelog/changes/002-add-email.xml"/>
</databaseChangeLog>


Liquibase выполнит 001-create-users.xml раньше, чем 002-add-email.xml.


Атрибуты, влияющие на выполнение:

runOnChange="true" – повторно выполнит changeSet, если его содержимое изменилось.
failOnError="false" – пропустит ошибку (например, если таблица уже существует).


#Java #middle #Liquibase
👍4
Что выведет код?

public class Task290525 {
public static void main(String[] args) {
System.out.println(1.0 / 0.0);
}
}


#Tasks
👍4
😱1
Как я память искал (Часть II)

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

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

Решения 🤓

🟡Первое и самое очевидное:

Избегать fetch join при пагинации в Hibernate

Да так просто.

Но вы скажете: "А как же проблема N+1"? Ее я предлагаю решить всем известной аннотацией
@BatchSize.
@Entity
public class User {
//стандартные поля
@OneToMany(mappedBy = "owner")
@BatchSize(size = 50)
private List<Car> cars = new ArrayList<>();
}
Количество запросов существенно уменьшится — на тот размер size, который мы задали. Это значит, что проблема N+1 решена.


Да, запросов станет точно больше чем 1. Но такова цена использования Spring Hibernate 🤷‍♀️

🟡Второе и не очень хорошее, а при увеличении количества связанных сущностей в запросе - опасное:

разделение запроса на две части с использованием @EntityGraph

@EntityGraph(attributePaths = {"cars"})
Page<User> findAll(Pageable pageable);


При таком решении в первой части запроса мы запрашиваем id наших User с определенной страницы. А во второй части запроса с помощью @EntityGraph и JOIN мы получаем все Car которые им принадлежат.

Но как показали тестовые запуски с использованием @EntityGraph над множеством связанных сущностей в основной, проведенные автором статьи(спасибо ему) :
"При пяти загружаемых коллекциях производительность разделенного запроса с @EntityGraph хуже в 2 раза. При десяти — в 45 раз. А при загрузке 15 коллекций — в 1401 раз! На графике нет данных по разделенному запросу для 20 коллекций, так как я просто-напросто получил ошибку OutOfMemoryError."

С @BatchSize же никаких JOIN не происходит, и при добавлении дополнительной коллекции с ассоциацией @OneToMany просто добавляются дополнительные select для этой коллекции.


🟡И третье, элементарное как свет солнца 😏

Использовать Нативный SQL - запрос.

Да, больше мороки с формированием динамического SQL (если у тебя 10 разных фильтров, ты сам будешь собирать SQL строку).
@Query(value = """
SELECT u.id AS userId, u.name AS userName, c.model AS carModel
FROM user u
LEFT JOIN car c ON c.owner_id = u.id
WHERE (:name IS NULL OR u.name ILIKE %:name%)
ORDER BY
CASE WHEN :sortBy = 'name' THEN u.name
WHEN :sortBy = 'id' THEN CAST(u.id AS TEXT)
ELSE u.name
END ASC
LIMIT :limit OFFSET :offset
""", nativeQuery = true)
List<UserCarDTO> findWithFilters(
@Param("name") String name,
@Param("sortBy") String sortBy,
@Param("limit") int limit,
@Param("offset") int offset
);


Да, придется использовать дополнительный count-запрос: SELECT COUNT(*) , для обеспечения пагинации
@Query(value = """
SELECT COUNT(DISTINCT u.id)
FROM user u
WHERE (:name IS NULL OR u.name ILIKE %:name%)
""", nativeQuery = true)
long countUsers(@Param("name") String name);


но, ты сам полностью управляешь процессом и скорее всего не встретишь неочевидного поведения 💪

И на сладкое

Какие ещё конструкции в JPA/Hibernate могут привести к аналогичным проблемам, когда ты используешь fetch join и неожиданно получаешь избыточную загрузку в память, ломающую пагинацию или вызывающую дублирование/нагрузку?

1. Pageable + @Query (с JPQL) и JOIN FETCH
Даже если ты не используешь Specification, а пишешь JPQL с @Query, проблема остаётся.

2. EntityManager + JPQL с fetch и пагинацией
Даже ручной вызов через EntityManager может привести к тому же эффекту.

3. Criteria API с fetch + .setMaxResults()/.setFirstResult()
Даже если ты используешь чистый JPA Criteria API, проблема может проявиться.

4. Spring Data REST + @RepositoryRestResource + fetch join
Если ты используешь Spring Data REST, и в репозитории включён fetch join, например через @Query, то Spring сам применяет Pageable, и может попасть в эту ловушку. Опять же — всё сломается.


И личный совет: изучайте матчасть! Читайте наш канал и шанс, что Ваш прод упадет - существенно снизится!

Понравился стиль подачи материала?

Отправь другу и ставь -
🔥

#Java #fetch #autor
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6
Чем отличается HashSet от TreeSet? 🤓

Ответ:

HashSet хранит элементы в хэш-таблице, не упорядочивает их, но работает быстрее (O(1) для добавления/поиска).

TreeSet хранит элементы в
отсортированном виде (на основе красно-черного дерева), но операции медленнее (O(log n)).


#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
📌 Факт дня:

А вы знали, что первый "онлайн-банк" появился в 1995 году?

Security First Network Bank (SFNB), запущенный в 1995 году, был первым банком, предоставляющим услуги через интернет. Однако он потерпел неудачу из-за низкого доверия к подобным сервисам со стороны клиентов, которые были не готовы к таким инновациям. Другие ранние попытки создания онлайн-банкинга, такие как Chemical Bank и Chase Manhattan Bank, также показывали ограниченный успех, но заложили основу для последующего развития онлайн-банкинга. Позже другие банки, такие как Bank of America, стали успешными в этой области, что привело к широко распространенному внедрению онлайн-банкинга.


Proof

#facts
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
📌 Цитата дня: Дуглас Крокфорд

"Социальные сети — это не технология, это психология."


Крис Сакка, инвестор Twitter, сказал это в 2010 году на конференции TechCrunch.


Почитать короткую биографию

#Citation #Biography
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Глубокое изучение типа данных char в Java

Тип char в Java — это единственный примитивный тип, предназначенный для хранения символов. Однако под «символом» в контексте Java подразумевается не просто буква или цифра, а 16-битное значение, соответствующее одному коду Unicode. Это делает char более универсальным, чем аналогичные типы в других языках (например, char в C/C++ — это просто байт).

Тем не менее, с учетом особенностей кодировки Unicode, поведения в выражениях и сравнении с другими примитивами, char — не такой уж простой тип.

Что представляет собой char

В Java char — это целое беззнаковое значение, хранящееся в 16 битах (2 байта). Диапазон значений — от 0 до 65 535. Это соответствует возможным значениям Unicode Code Units, используемым для представления символов в кодировке UTF-16.

В отличие от byte, short, int и других числовых типов, char:
Не поддерживает отрицательные значения.
Не используется в арифметике напрямую (но может быть преобразован в int).
Представляет не число, а код символа, хотя технически это просто целое значение.


Хранение в памяти

Как и все примитивные типы, char хранится в стеке, если используется как локальная переменная, и в куче — если является полем объекта. В обоих случаях он занимает ровно 2 байта (16 бит).

Символ и кодировка Unicode

Java изначально проектировалась с поддержкой международного текста, поэтому char хранит значение по спецификации Unicode. Однако важно понимать: char хранит одну кодовую единицу UTF-16, а не обязательно один графический символ.

Некоторые символы Unicode (например, эмодзи или древние алфавиты) имеют коды за пределами 16 бит. Такие символы представлены в UTF-16 двумя char, известными как суррогатная пара.
Это значит, что один char — это не всегда "один символ", что может привести к ошибкам при разборе строк, итерации по символам или определении длины строки.

Пример:
String s = "😀";
System.out.println(s.length()); // 2, а не 1
Здесь length() возвращает 2, потому что символ представлен двумя char.


Экранирование символов

Литералы char записываются в одиночных кавычках:
char c = 'A';


Java поддерживает экранированные символы, например:
'\n' — перевод строки
'\t' — табуляция
'\'' — апостроф
'\\' — обратный слеш
'\u0041' — Unicode-последовательность (в данном случае это символ 'A')


Unicode-последовательности (\uXXXX) можно использовать даже в имени переменной или в коде, до начала компиляции — они обрабатываются на уровне лексического анализатора, еще до построения синтаксического дерева.

#Java #для_новичков #beginner #char
👍3
Поведение в выражениях и преобразования

Несмотря на то, что char — это не числовой тип, он может участвовать в арифметике. В любых выражениях с участием char он неявно преобразуется в int.
char c = 'A';
int x = c + 1; // x == 66


Но обратное требует явного приведения:
char next = (char)(c + 1); // 'B'
Аналогично, попытка присвоить результат арифметики напрямую переменной типа char вызовет ошибку компиляции без приведения.


Сравнение с другими примитивными типами

В отличие от byte, short, int, char — беззнаковый.
Он занимает 2 байта, как short, но short — знаковый (-32 768 до 32 767), а char — 0 до 65 535.
В арифметике char ведет себя как int, автоматически повышая тип.
В отличие от String, char представляет одну кодовую единицу, а не последовательность символов.
В отличие от boolean, char может участвовать в арифметических выражениях и преобразованиях.


Инициализация и значения по умолчанию

Локальные переменные char должны быть явно инициализированы.
Поля классов по умолчанию получают значение \u0000, что соответствует нулевому символу Unicode (не отображается в консоли).


Частые ошибки и подводные камни

Суррогатные пары. При работе со строками, содержащими символы за пределами BMP (Basic Multilingual Plane), важно использовать методы вроде codePointAt() и Character.toChars(), чтобы не потерять часть символа.
Арифметика с char. Все выражения приводятся к int, что может привести к неожиданному переполнению или необходимости явного приведения.
Сравнение символов. Операции c1 < c2 работают корректно, но сравниваются числовые значения Unicode, а не "лексикографическое положение" в каком-либо языке.
Обработка управляющих символов. Символы вроде '\0', '\n', '\r' видны в коде, но не обязательно видны в выводе — их эффект зависит от среды (консоль, IDE, файл и т.д.).


#Java #для_новичков #beginner #char
👍2
Что выведет код?

public class Task300525 {
public static void main(String[] args) {
char c = 'A';
c += 1.5;
System.out.println(c);
}
}


#Tasks
👍1
Варианты ответа:
Anonymous Quiz
4%
'A'
11%
'B'
11%
'C'
74%
Ошибка компиляции
👍2
Продолжаем выбирать темы для разбора и голосовать за рассмотрение предложенных! 🤓

Голосуем за тему к рассмотрению в эти выходные!

Выбираем новую тему!
(можете предложить что-то из того что предлагали на прошлой неделе и что проигрывает в голосовании!)

Не стесняемся! ✌️
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
В чем разница между throw и throws? 🤓

Ответ:

throw используется для явного выброса исключения, например: throw new IOException();.

throws указывается в сигнатуре метода, чтобы сообщить, какие проверяемые исключения он может выбросить, например: void method() throws IOException.


#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1