Java for Beginner
870 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

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

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Варианты ответа:
Anonymous Quiz
50%
0
21%
1
21%
-1
7%
Exception
👍3
Почему finalize() опасен даже если он “работает”? 🤓

Ответ:

finalize вызывается не сразу и не гарантирован вообще.

Объект может долго оставаться в памяти, даже если он логически мёртв. Ошибка в finalize может полностью блокировать сборку мусора для объекта. Кроме того, finalize выполняется в отдельном потоке GC, что усложняет отладку.

Именно поэтому finalize признан deprecated, а управление ресурсами должно выполняться явно через try-with-resources.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
История IT-технологий сегодня — 27 января

ℹ️ Кто родился в этот день

Лью́ис Кэ́рролл (англ. Lewis Carroll, настоящее имя Чарльз Лютвидж До́джсон, или Чарльз Латуидж До́джсон (традиционная русская передача; сам Кэрролл произносил свою фамилию Додсон, ˈdɒdsən[11], ряд современных словарей даёт произношение ˈdɒdʒsən[12][13]), Charles Lutwidge Dodgson; 27 января 1832 — 14 января 1898) — математик и логик, работал над логическими диограммами, алгеброй логики и теорией вероятностей; его логические идеи и подход к формализации рассуждений — часть исторического фундамента логики, на которой стоит теоретическая информатика. Наиболее известные произведения — «Приключения Алисы в Стране чудес» и «Алиса в Зазеркалье», а также юмористическая поэма «Охота на Снарка».


🌐 Знаковые события

1994 — Джим Кларк уходит из Silicon Graphics и создаёт Mosaic Communications (позже Netscape): компания стала одной из первых, кто коммерциализировал веб‑браузер, что сильно ускорило развитие и распространение интернета и веб‑технологий. Доля пользователей Netscape достигла пика в середине 1990-х годов, но к концу 2006 года упала до менее чем одного процента.


#Biography #Birth_Date #Events #27Января
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Глава 4: Эффективный поиск. Бинарный и не только

Практика

Сегодня мы применим знания о эффективном поиске на проекте «Библиотека».

Мы отсортируем список книг по названию, реализуем бинарный поиск для нахождения первой книги заданного автора (в отсортированном списке), и проведём сравнение времени выполнения линейного и бинарного поиска на коллекциях разных размеров. Это поможет наглядно увидеть преимущества логарифмического поиска O(log n) над линейным O(n), понять предпосылки (сортировка данных) и границы применимости (когда сортировка окупается).

Подготовка к уроку


Перед началом убедитесь, что проект готов, и вспомните ключевые концепции:
Бинарный поиск работает только на отсортированных данных, делит интервал пополам.
Линейный поиск — перебор O(n), всегда работает.
Сортировка O(n log n) — предпосылка для бинарного.

Откройте проект «Библиотека»: Убедитесь, что List<Book> books содержит достаточно книг (добавьте метод для генерации тестовых данных).
Импортируйте пакеты: java.util.Arrays (для бинарного поиска), java.util.Random (для генерации данных).
Генерация больших данных: Создайте метод generateBooks(int size) для создания списков размером 100, 10 000, 1 000 000 (используйте Random для title/author/year).


Отсортировать книги по названию

Обновите Book для Comparable (если не сделано): Реализуйте compareTo по title (this.title.compareTo(other.title)).
Создайте метод sortByTitle(): Используйте Collections.sort(books) — сортирует по Comparable (названию).
Вывод: После сортировки вызовите printAllBooks() для проверки.


Реализовать бинарный поиск для нахождения первой книги заданного автора

Отсортируйте по автору: Создайте Comparator<Book> byAuthor = Comparator.comparing(Book::getAuthor);, затем books.sort(byAuthor).

Реализуйте метод findFirstBookByAuthor(String author):
Используйте Arrays.binarySearch, но поскольку books — List, преобразуйте в массив или реализуйте вручную.

Вручную:
int low = 0, high = books.size() - 1; 

while (low <= high) {
mid = (low + high) / 2; cmp = books.get(mid).getAuthor().compareTo(author);

if (cmp < 0) low = mid + 1;
else if (cmp > 0) high = mid - 1;
else { // Найден, найти первый:
while (mid > 0 && books.get(mid-1).getAuthor().equals(author))
mid--;
return books.get(mid);
}
}


Если не найден — return null.

Проверка: После сортировки вызовите метод, выведите найденную книгу.


Сравнить время линейного и бинарного поиска на разных размерах

Реализуйте линейный поиск: Метод linearSearchByAuthor(String author) — for-each, если совпадение — return book.
Измерение времени: Используйте System.nanoTime() before/after.

Тест на размерах:
Для 100: generateBooks(100), sortByAuthor, time linear vs binary (поиск рандомного автора).
Для 10 000 и 1 000 000: Аналогично, усредните по 100 запускам.
Выводите: "Для n=[size]: Линейный: [time ns], Бинарный: [time ns]".

Анализ: Точка окупаемости сортировки + бинарный поиск vs множественный линейный поиск
Точка окупаемости — момент, когда стоимость сортировки + m бинарных поисков становится меньше m линейных поисков.

Расчёт:
Линейный: O(n) per search → m * n
Бинарный: O(n log n) sort + m * log n
Окупаемость: n log n + m log n < m n → m > (n log n) / (n - log n) ≈ log n (для больших n)
Пример: n = 1000, log n ≈ 10 — окупаемость после ~10 поисков.
n = 1 млн, log n ≈ 20 — после ~20 поисков.
Граница: Для малого m или n — линейный дешевле (нет сортировки).
В библиотеке: Если поиски редки — линейный; если часты — sort + binary.

#Java #для_новичков #beginner #algorithm #sorted #binary #практика
👍4
Что выведет код?

public class Task270126 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("12345");
sb.append(sb.append("6"));
System.out.println(sb.toString());
}
}


#Tasks
👍2
Варианты ответа:
Anonymous Quiz
19%
"123456"
30%
"123456123456"
19%
"1234566"
33%
"12345123456"
👍2
Что такое String, StringBuilder и StringBuffer? 🤓

Ответ:

String
— неизменяемый (immutable) класс. Любая операция (конкатенация, замена) создает новый объект.

StringBuilder и StringBuffer — изменяемые классы для работы со строками. StringBuilder (появился в Java 5) работает быстрее, но не потокобезопасен.


StringBuffer
— потокобезопасный (все методы синхронизированы), но медленнее.

Для однопоточных программ выбирают StringBuilder.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История IT-технологий сегодня — 28 января

ℹ️ Кто родился в этот день

Уильям Сьюард Берроуз (28 января 1857, Рочестер, Нью-Йорк — 14 сентября 1898) — американский изобретатель механического счётного устройства; его «adding machine» стала предшественником кассовых аппаратов и деловых калькуляторов, а сама компания Burroughs в XX веке вошла в число производителей компьютеров (часть истории мейнфреймов и деловых ЭВМ).


🌐 Знаковые события

1724 — указом Петра I основаны Петербургская академия наук и Петербургский университет.


#Biography #Birth_Date #Events #28Января
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Как то совсем незаметно канал преодолел отметку в 800 подписчиков 😎.

Спасибо всем кто присоединяется ❤️, значит все таки, что-то интересное делаю)))

А тем кто с нами давно двойная благодарность, что не уходите 👏
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11🍾7
Раздел 7. Алгоритмы

Глава 5: Рекурсия, деревья и введение в динамическое программирование


Принцип рекурсии и её опасности

Рекурсия — это методология решения задач, при которой функция вызывает сама себя для обработки подзадачи меньшего размера. В математике это называется рекуррентным соотношением, в программировании — рекурсивным вызовом. Главная идея состоит в том, чтобы свести решение сложной проблемы к решению аналогичной, но более простой проблемы, плюс некоторый шаг объединения результатов.

Представьте задачу вычисления суммы чисел от 1 до n. Итеративный подход использует цикл с аккумулятором. Рекурсивный подход говорит: сумма от 1 до n равна n плюс сумма от 1 до n-1. Это определение ссылается на само себя, но с меньшим аргументом. Такое самоподобие лежит в основе рекурсивного мышления.


Два столпа корректности: базовый случай и рекурсивный шаг

Любая корректная рекурсивная функция строится на двух неразрывно связанных компонентах:
Базовый случай (base case) — это условие, при котором рекурсия останавливается и функция возвращает конкретное значение без дальнейших вызовов самой себя. Это точка выхода из бесконечного цикла вызовов. Без базового случая функция будет вызывать себя вечно, пока не исчерпает системные ресурсы. Для суммы чисел от 1 до n базовым случаем является ситуация, когда n равно 0 или 1 — сумма пустого множества или одного элемента тривиальна.
Рекурсивный шаг (recursive step) — это логика, связывающая результат текущего вызова с результатом вызова функции от модифицированных аргументов. Здесь кроется математическая индукция: мы предполагаем, что функция работает корректно для меньших входных данных (индукционная гипотеза), и доказываем, что тогда она работает для текущих данных.

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

Рассмотрим классический пример вычисления факториала. Математически n! определен как произведение всех натуральных чисел от 1 до n, с дополнительным условием 0! = 1.
public class RecursionFundamentals {

/**
* Вычисляет факториал числа n.
*
* @param n неотрицательное целое число
* @return n!
* @throws IllegalArgumentException если n отрицательно
*/
public static long factorial(int n) {
// Базовый случай: факториал 0 или 1 равен 1
// Это математическое определение, служащее якорем рекурсии
if (n <= 1) {
return 1;
}

// Защита от некорректного использования
if (n < 0) {
throw new IllegalArgumentException("Factorial undefined for negative numbers");
}

// Рекурсивный шаг: n! = n * (n-1)!
// Здесь мы полагаемся на то, что factorial(n-1) вернет правильный результат
// и умножаем его на n для получения текущего значения
return n * factorial(n - 1);
}
}


Инвариант рекурсии — условие, которое остается истинным на каждом уровне вызовов. Для факториала это утверждение, что при вызове factorial(k) мы находимся в процессе вычисления произведения чисел от k до n, где n — исходный аргумент верхнего уровня. Поддержание инварианта гарантирует корректность результата при возврате из глубины стека.


#Java #для_новичков #beginner #algorithm #recursion
👍4
Стек вызовов: механизм жизнеобеспечения рекурсии

Чтобы понять опасности рекурсии, необходимо заглянуть под капот JVM и понять, как работает стек вызовов (call stack).

Стек вызовов — это область памяти, выделенная каждому потоку выполнения в Java. Он организован по принципу LIFO (Last In, First Out — последним пришел, первым вышел).

Каждый раз, когда метод вызывается, в стек помещается фрейм активации (stack frame), содержащий:
- Локальные переменные метода (примитивы и ссылки)
- Параметры метода
- Адрес возврата (куда передать управление после завершения метода)
- Ссылку на объект this (для нестатических методов)
- Служебную информацию для отладки (номера строк в таблице символов)

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

Размер стека вызовов ограничен. В Java по умолчанию он составляет 1 мегабайт для каждого потока (значение может варьироваться в зависимости от архитектуры и версии JVM, обычно от 256KB до 2MB). Этот размер фиксируется при создании потока и не может быть динамически расширен во время выполнения.


StackOverflowError: когда глубина превышает высоту

Если рекурсия слишком глубока, стек вызовов переполняется. JVM обнаруживает попытку записи за пределы выделенной области и выбрасывает java.lang.StackOverflowError. Это не checked exception и не unchecked runtime exception в традиционном смысле — это ошибка (Error), наследник VirtualMachineError, сигнализирующая о критическом состоянии ресурсов.

Важно понимать, что StackOverflowError — это неизлечимая в рамках текущего потока ситуация. После её возникновения стек разрушен, состояние объектов может быть неопределенным, и продолжать выполнение потока нельзя. Единственное разумное действие — завершить поток или всю программу.

Какова максимальная глубина рекурсии?

Это зависит от нескольких факторов:
- Размера стека (-Xss параметр JVM)
- Количества и размера локальных переменных в методе (чем больше данных во фрейме, тем быстрее заполнится стек)
- Архитектуры (64-битные системы имеют больший оверхед на ссылки)

Для простого метода типа факториала с одним параметром типа int глубина обычно составляет от нескольких тысяч до десятков тысяч вызовов. Например, попытка вычислить factorial(100000) практически гарантированно вызовет StackOverflowError.
public class StackDepthDemo {

private static int depth = 0;

public static void recursiveCall() {
depth++;
if (depth % 1000 == 0) {
System.out.println("Current depth: " + depth);
}
recursiveCall(); // Бесконечная рекурсия без базового случая
}

public static void main(String[] args) {
try {
recursiveCall();
} catch (StackOverflowError e) {
System.err.println("Stack overflow at depth: " + depth);
// Вывод примерно: Stack overflow at depth: 10800 (зависит от -Xss)
}
}
}


#Java #для_новичков #beginner #algorithm #recursion
👍4
Отсутствие Tail-Call Optimization: архитектурный долг JVM

В функциональных языках программирования (Scheme, Haskell, Scala с определенными флагами) существует механизм, называемый оптимизацией хвостовой рекурсии (Tail-Call Optimization, TCO). Он позволяет преобразовать рекурсивный вызов в цикл на уровне машинного кода, если рекурсивный вызов является последней операцией в методе (хвостовой позицией).

Рассмотрим хвостовую версию факториала:
public static long factorialTail(int n, long accumulator) {
if (n <= 1) {
return accumulator;
}
// Рекурсивный вызов — последняя операция перед return
// Теоретически JVM могла бы освободить текущий фрейм перед вызовом
return factorialTail(n - 1, n * accumulator);
}


В языках с TCO такой код выполнялся бы с постоянным размером стека O(1), так как каждый новый вызов переиспользовал бы фрейм предыдущего. Однако JVM не поддерживает TCO. Это архитектурное решение, связанное с необходимостью сохранения точных трасс стека для отладки, профилирования и механизма SecurityManager. Каждый вызов factorialTail создает новый фрейм, и при достаточно большом n произойдет StackOverflowError.

Последствия отсутствия TCO:

- Невозможность безопасной рекурсии для больших n: Даже правильно написанная хвостовая рекурсия не спасает от переполнения стека. Вы не можете использовать рекурсию для обработки списка из миллиона элементов, даже если алгоритмически это хвостовой вызов.
- Предпочтение итерации: В Java культуре рекурсия считается менее идиоматичной, чем в функциональных языках. Циклы while и for предпочтительны для глубоких итераций, так как они используют постоянное количество памяти O(1).
- Ручное управление стеком: Для задач, естественно выражаемых через рекурсию (обход деревьев), но требующих обработки больших глубин, Java-разработчики вынуждены использовать явный стек (класс java.util.Stack или ArrayDeque), перекладывая управление памятью из стека вызовов в кучу (heap), где ограничения гораздо мягче.
- Накладные расходы:
Каждый рекурсивный вызов требует:
Выделения фрейма в стеке
Сохранения регистров процессора
Переключения контекста выполнения
Проверок безопасности (в некоторых JVM)
Это делает рекурсию медленнее итерации даже для малых глубин, хотя разница измеряется в наносекундах.


#Java #для_новичков #beginner #algorithm #recursion
👍4
Когда рекурсия естественна: домены применения

Несмотря на ограничения, рекурсия остается незаменимым инструментом для задач с древовидной структурой или редукцией к подзадачам.
Иерархические структуры данных: Файловая система, DOM-дерево HTML, абстрактные синтаксические деревья компилятора, организационные структуры компании — все это примеры данных, где каждый узел содержит ссылки на подчиненные узлы. Рекурсивная функция может обработать текущий узел и вызвать себя для каждого потомка, естественно следуя топологии данных.

Разделение задач: Алгоритмы типа "разделяй и властвуй" (merge sort, quick sort, бинарный поиск) естественно выражаются через рекурсию. Рекурсивный шаг здесь — это применение того же алгоритма к половинам массива.
Математические определения: Функции, определенные рекуррентно (числа Фибоначчи, комбинаторика, фракталы), читаются проще в рекурсивной форме, хотя эффективная реализация часто требует мемоизации или перехода к итерации.

Рассмотрим пример обхода файловой системы — классический случай, где рекурсия проявляет свою силу:
import java.io.File;

public class FileSystemTraversal {

/**
* Рекурсивно подсчитывает общий размер всех файлов в директории.
*
* @param directory корневая директория для анализа
* @return суммарный размер в байтах
*/
public static long calculateDirectorySize(File directory) {
// Базовый случай 1: несуществующий путь
if (!directory.exists()) {
return 0;
}

// Базовый случай 2: это файл, а не директория
// Возвращаем его размер, рекурсия останавливается
if (directory.isFile()) {
return directory.length();
}

// Базовый случай 3: пустая директория (опционально, обработается циклом)

// Рекурсивный шаг: получаем список содержимого
File[] children = directory.listFiles();
if (children == null) {
// Защита от null при отсутствии прав доступа
return 0;
}

long totalSize = 0;
// Для каждого элемента в директории вызываем себя рекурсивно
for (File child : children) {
// Рекурсивный вызов обрабатывает поддиректории на любую глубину
totalSize += calculateDirectorySize(child);
}

return totalSize;
}
}


В этом примере глубина рекурсии равна глубине вложенности директорий. Для типичной файловой системы это 10-20 уровней — безопасно для стека. Но при обработке архивов с глубокой вложенностью или символических ссылок, создающих циклы, необходима защита от бесконечной рекурсии (например, через Set посещенных путей).


Практические рекомендации по безопасной рекурсии

Анализ глубины перед реализацией:
Перед написанием рекурсивного метода оцените максимальную глубину вызовов. Если данные могут содержать тысячи уровней вложенности (например, парсинг JSON с глубокой вложенностью объектов), используйте итеративный подход с явным стеком.

Проверка базового случая: Всегда убедитесь, что базовый случай достижим. Для числовых аргументов это обычно проверка на ноль или единицу. Для структур данных — проверка на null или пустоту.
Защита от циклов: При обходе графов или файловых систем с символическими ссылками используйте ThreadLocal<Set> или передавайте Set<VisitedNode> через параметры для отслеживания посещенных узлов.

Размер стека: Для специфических задач с умеренной, но значительной глубиной (например, 5000 уровней) можно увеличить размер стека через флаг -Xss2m (2 мегабайта). Однако это лечит симптом, а не причину, и не масштабируется.

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


#Java #для_новичков #beginner #algorithm #recursion
👍5
Что выведет код?

public class Task280126 {
public static void main(String[] args) {

System.out.println(f(7));

}

static int f(int n) {

if (n <= 1) return 1;
return f(n - 2) + f(n - 1);

}
}


#Tasks
👍2
Варианты ответа:
Anonymous Quiz
18%
8
12%
13
53%
21
18%
36
👍1
Что такое static? 🤓

Ответ:

Ключевое слово static означает, что поле или метод принадлежит самому классу, а не конкретному экземпляру (объекту) этого класса.

Статическое поле (переменная класса) является общим для всех объектов этого класса. Статический метод может быть вызван без создания объекта, но он не имеет доступа к нестатическим полям и методам.

Статический блок инициализации (static {}) выполняется при загрузке класса в память JVM.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История IT-технологий сегодня — 29 января

ℹ️ Кто родился в этот день

Джозеф Бернард Крускал-младший ( / ˈ k r ʌ s k əl / ; 29 января 1928 — 19 сентября 2010) — математик, статистик и компьютерный учёный; автор алгоритма Крускала для поиска минимального остовного дерева (MST), используемого в сетевых задачах, маршрутизации, кластеризации и многих алгоритмах в графах.


🌐 Знаковые события

1992 — начался спор Линуса Торвальдса и Эндрю Таненбаума — «Линукс устарел».

Главный тезис Таненбаума: будущие ОС должны строиться на микроядрах, а монолитные ядра — вчерашний день, «шаг назад в 70‑е», поэтому Linux уже устарел на момент создания; кроме того, он критиковал привязку Linux к x86 и сомневался в его переносимости.

Позиция Торвальдса: монолитное ядро (с модульностью) проще и практичнее для реальной рабочей системы, чем тогдашние экспериментальные микроядра; он подчеркивал, что Linux уже работает на реальном железе и людям удобнее получить доступную, быструю систему, чем идеальную архитектуру «когда‑нибудь».

Задним числом особенно интересно читать аргументы Таненбаума о том, что Linux «не взлетит», на фоне того, как всё сложилось в реальности.


#Biography #Birth_Date #Events #29Января
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Глава 5: Рекурсия, деревья и введение в динамическое программирование

Рекурсия на практике: обход деревьев


Деревья в информатике — это не только природные объекты, но и фундаментальная абстракция для моделирования иерархических связей. Файловая система, DOM-документа, организационная структура компании, дерево разбора выражений — все они являются узлами, связанными отношениями «родитель — дети».

Главное свойство таких структур — самоподобие: каждый узел содержит подструктуру, топологически идентичную всему дереву. Это свойство делает рекурсию естественным способом обхода и обработки.

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


Файловая система — подсчет общего размера

Файловая система — это дерево, где узлами являются каталоги, а листьями — файлы. Каждый каталог содержит массив подчиненных элементов. Задача подсчета общего размера естественно рекурсивна: размер каталога равен сумме размеров всех его дочерних элементов, где для файла возвращается прямое значение, а для подкаталога — рекурсивный вызов.

Рекурсивная реализация с полной обработкой ошибок

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class FileSizeCalculator {

/**
* Вычисляет общий размер всех файлов в каталоге и его подкаталогах.
*
* @param rootPath корневой каталог для анализа
* @return суммарный размер в байтах
* @throws IllegalArgumentException если rootPath не является каталогом или не существует
*/
public static long calculateSize(Path rootPath) {
if (rootPath == null) {
throw new IllegalArgumentException("Path cannot be null");
}

// Проверяем существование и тип файловой системы
if (!Files.exists(rootPath)) {
throw new IllegalArgumentException("Path does not exist: " + rootPath);
}

if (!Files.isDirectory(rootPath)) {
throw new IllegalArgumentException("Path is not a directory: " + rootPath);
}

try {
return calculateRecursive(rootPath);
} catch (IOException e) {
// Преобразуем checked exception в unchecked для удобства API
throw new UncheckedIOException("Failed to traverse directory: " + rootPath, e);
}
}

/**
* Внутренний рекурсивный метод.
*
* @param currentPath текущий узел для анализа
* @return суммарный размер поддерева
*/
private static long calculateRecursive(Path currentPath) throws IOException {
// Базовый случай 1: это файл
if (Files.isRegularFile(currentPath)) {
try {
return Files.size(currentPath);
} catch (SecurityException | IOException e) {
// В реальном приложении логгируем ошибку доступа
// и продолжаем, а не прокидываем исключение
System.err.println("Cannot access file: " + currentPath + " - " + e.getMessage());
return 0;
}
}

//случай 2: это директория, обрабатываем её содержимое
long totalSize = 0;

// Используем try-with-resources для безопасного закрытия DirectoryStream
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentPath)) {
for (Path child : stream) {
// Рекурсивный вызов для каждого дочернего элемента
// Глубина рекурсии = уровень вложенности директорий
totalSize += calculateRecursive(child);
}
} catch (NotDirectoryException e) {
// Это не должно случиться из-за проверки выше, но защита от race condition
return 0;
} catch (SecurityException e) {
System.err.println("Access denied to directory: " + currentPath);
return 0;
}

return totalSize;
}
}
👍3
Ключевые детали реализации:
Два базовых случая: файл (возвращаем размер) и нефайл (обрабатываем детей). Операция Files.isRegularFile() требует минимальных системных вызовов и не читает весь файл в память.
Защита от null: Files.newDirectoryStream() может вернуть null или бросить исключение, если доступ запрещен.
Обработка символических ссылок: Текущая реализация следует по символическим ссылкам, что может создать циклы и StackOverflowError.

Для защиты нужно добавить Set<Path> посещенных узлов:
private static long calculateRecursiveSafe(Path currentPath, Set<Path> visited) throws IOException {
// Проверка на цикл
if (!visited.add(currentPath.toRealPath())) {
System.err.println("Cycle detected, skipping: " + currentPath);
return 0;
}

if (Files.isRegularFile(currentPath)) {
return Files.size(currentPath);
}

long totalSize = 0;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentPath)) {
for (Path child : stream) {
totalSize += calculateRecursiveSafe(child, visited);
}
}

// Важно: не удаляем из visited при возврате, так как путь может быть повторен через другую ссылку
return totalSize;
}



Анализ производительности и памяти


Время: O(N), где N — общее количество файлов и каталогов. Каждый узел посещается ровно один раз.
Память (стек): O(h), где h — максимальная глубина вложенности. Для домашней директории h обычно < 50, для серверов — < 500. Безопасно для 1MB стека.
Память (куча): O(h) на множество visited, если используется защита от циклов.

Иерархия жанров библиотеки — поиск в поддереве

Представим модель библиотеки, где жанры организованы в дерево:
Литература
├── Художественная
│ ├── Фантастика
│ │ ├── Космическая
│ │ └── Альтернативная история
│ └── Детектив
└── Научная
└── Программирование
└── Java


Каждый узел содержит название и список поджанров.

Задача: найти все книги, относящиеся к жанру «Фантастика», включая все поджанры.

Модель данных
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class GenreNode {
private final String name;
private final List<GenreNode> subgenres;
private final List<Book> books; // Книги непосредственно этого жанра

public GenreNode(String name) {
this.name = name;
this.subgenres = new ArrayList<>();
this.books = new ArrayList<>();
}

// Методы добавления поджанров и книг...
public void addSubgenre(GenreNode subgenre) {
subgenres.add(subgenre);
}

public void addBook(Book book) {
books.add(book);
}

public String getName() {
return name;
}

public List<GenreNode> getSubgenres() {
return Collections.unmodifiableList(subgenres);
}

public List<Book> getBooks() {
return Collections.unmodifiableList(books);
}
}

class Book {
final String title;
final String author;

Book(String title, String author) {
this.title = title;
this.author = author;
}
}


#Java #для_новичков #beginner #algorithm #recursion
👍3