Java for Beginner
778 subscribers
749 photos
220 videos
12 files
1.26K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

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

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Глава 2. List — списки в Java

Интерфейс List и его особенности

В мире программирования, и в Java в частности, необходимость хранить и управлять наборами данных — одна из самых частых задач. Начинающие разработчики знакомы с массивами — простыми и эффективными, но обладающими серьезным недостатком: их размер фиксирован после создания. Что же делать, если количество элементов заранее неизвестно? На помощь приходит интерфейс List (список), который является одной из краеугольных концепций в Java Collections Framework (фреймворке коллекций Java).


Что такое интерфейс List?

List — это интерфейс, который расширяет более общий интерфейс Collection. Если говорить просто, List — это контракт, который обещает определенное поведение для всех классов, которые его реализуют. Сам по себе List не является классом, поэтому вы не можете создать объект типа «List». Он лишь определяет «правила игры», а конкретные классы, такие как ArrayList или LinkedList, уже следуют этим правилам, реализуя функционал по-своему.

Главная идея List — это упорядоченная последовательность. Это его ключевая особенность, отличающая его от других коллекций, например, Set (множества).


Ключевые особенности интерфейса List

Гарантированный порядок элементов.
Это самый важный принцип. Когда вы добавляете элементы в список, система запоминает именно ту последовательность, в которой вы их добавили. Элемент «A», добавленный первым, всегда будет находиться на позиции 0 (если только его не удалили или не переместили). Элемент «B», добавленный вторым, будет на позиции 1, и так далее. Этот порядок сохраняется на протяжении всей жизни списка (если только вы сами его не измените). Это кардинально отличает список от, например, мешка с яблоками, где порядок не важен.

Доступ по индексу.
Благодаря строгому порядку, List предоставляет возможность работать с элементами по их целочисленному индексу (позиции). Индексация всегда начинается с 0, как и в массивах. Вы можете «спросить» список: «Дай мне элемент, который находится на пятой позиции», и он его вернет. Эта операция является одной из базовых и высокооптимизированной в большинстве реализаций списков.

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

Динамический размер.
В отличие от массива, список не имеет фиксированной длины. Он является динамической структурой данных. Когда вы создаете пустой список, он занимает немного памяти. При добавлении каждого нового элемента список самостоятельно заботится о том, чтобы для него хватило места, при необходимости выделяя дополнительную память «про запас». Это избавляет программиста от необходимости заранее знать точное количество элементов и вручную управлять памятью.

Null-допустимость.
Список разрешает хранение специального значения null, которое обозначает отсутствие объекта. Это означает, что вы можете добавить null в список в качестве валидного элемента. Однако с этим нужно быть осторожным, так как при попытке выполнить какие-либо операции с этим null-элементом (например, вызвать его метод) может быть выброшено исключение NullPointerException.


#Java #для_новичков #beginner #List
👍2
Абстракция и полиморфизм

Одна из сильных сторон использования интерфейса List — принцип полиморфизма. В своем коде вы можете объявить переменную типа List, а затем присвоить ей любой объект, который реализует этот интерфейс.

List<String> myList; // Объявление переменной интерфейсного типа
myList = new ArrayList<>(); // Работаем с динамическим массивом
// ... позже в коде ...
myList = new LinkedList<>(); // Теперь работаем со связным списком


Это позволяет писать гибкий и слабосвязанный код. Основная логика вашей программы, которая использует методы add, get, remove, будет работать с любой реализацией
List. А вы, в зависимости от конкретных требований к производительности (например, если чаще нужен быстрый доступ по индексу или частое добавление в начало), можете легко подменить одну реализацию на другую, не переписывая весь код.

#Java #для_новичков #beginner #List
👍2
Глава 2. List — списки в Java

Реализации: ArrayList и LinkedList. Сравнение производительности


ArrayList: динамический массив под капотом

Самая популярная и часто используемая реализация List. Её название раскрывает всю суть: ArrayList — это список, реализованный на основе массива.

Внутреннее устройство:
Массив как основа. Когда вы создаете ArrayList, внутри него создается обычный массив типа Object[] (или E[] после дженериков). Изначально этот массив имеет некоторый начальный размер (емкость, capacity), часто по умолчанию это 10 элементов.

// Примерно так выглядит внутри ArrayList
public class ArrayList<E> {
private Object[] elementData; // Внутренний массив
private int size; // Текущее количество реальных элементов
// ...
}


Динамическое расширение.
Когда вы добавляете новый элемент с помощью add(), ArrayList проверяет, осталось ли место во внутреннем массиве.
Если место есть, элемент просто помещается в первую свободную ячейку elementData[size], и значение size увеличивается на 1. Это очень быстрая операция, comparable с работой с массивом.


Если массив полон, происходит следующее:
Создается новый массив большего размера. Стандартная логика увеличения — (старый_размер * 1.5) + 1.
Все элементы из старого массива копируются в новый.
Старый массив удаляется сборщиком мусора, а ссылка elementData начинает указывать на новый массив.
Только после этого новый элемент добавляется в конец.

Этот процесс пересоздания и копирования массива является относительно медленным, поэтому, если вы заранее знаете примерное количество элементов, лучше создать ArrayList с нужной начальной емкостью через конструктор new ArrayList<>(1000). Это позволит избежать или минимизировать количество операций расширения.



LinkedList: цепочка связанных элементов

LinkedList подходит к задаче иначе. Его название также прямо говорит о структуре: LinkedList — это связный список.

Внутреннее устройство:
Узлы (Node). LinkedList не использует массив. Вместо этого он построен на основе узлов.

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


// Примерная структура узла
private static class Node<E> {
E item; // Данные
Node<E> next; // Ссылка на следующий узел
Node<E> prev; // Ссылка на предыдущий узел
// ...
}


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

Отсутствие массива. Элементы не хранятся в непрерывной области памяти. Они разбросаны по куче (Heap), а связаны между собой лишь этими ссылками-«ниточками». Голова списка — это поле first, а хвост — last.



#Java #для_новичков #beginner #List #ArrayList #LinkedList
👍2
Сравнение производительности

Время выполнения операций принято описывать в нотации "Big O", которая показывает, как время работы растет с увеличением объема данных (n).

1. Доступ к элементу по индексу (get(index))

ArrayList: O(1) — константное время.
Это его сильнейшая сторона. Поскольку внутри обычный массив, чтобы получить элемент по индексу 5, система просто делает одну операцию: берет начальный адрес массива и смещается на 5 ячеек в памяти. Это происходит мгновенно, независимо от размера списка.


// Внутренняя логика ArrayList.get(index)
public E get(int index) {
// ... проверка индекса ...
return (E) elementData[index]; // Прямое обращение по индексу массива
}


LinkedList: O(n) — линейное время.
Это его главный недостаток для данной операции. У списка нет индексов в памяти. Чтобы найти элемент с индексом 5, ему приходится начинать с начала (или с конца, если индекс ближе к нему) и последовательно переходить по ссылкам next (или prev).


// Примерная логика (упрощенно). Чтобы найти узел с индексом 5:
Node<E> x = first;
for (int i = 0; i < 5; i++) { // Нужно сделать 5 итераций
x = x.next;
}
return x.item;



Для доступа к первому или последнему элементу (get(0) или get(last)) скорость будет высокой O(1), так как есть прямые ссылки first и last. Но для элемента в середине — очень низкой.


2. Вставка элемента (add(element)) и удаление с конца

ArrayList: В среднем O(1), но в худшем случае O(n).
Добавление в конец (add(element)) обычно очень быстрое (O(1)), так как это запись в свободную ячейку. Однако, если массив полон, требуется дорогостоящая операция копирования всего массива (O(n)).

LinkedList: O(1) — константное время.
Добавление в конец всегда выполняется за константное время. Для этого нужно просто создать новый узел, сделать его prev ссылку на старый последний узел, и обновить ссылку last. Это несколько операций, но их количество не зависит от размера списка.


3. Вставка/удаление в произвольной позиции (add(index, element), remove(index))

ArrayList: O(n) — линейное время.
Это его слабое место. Представьте, что вы вставляете элемент в начало списка (индекс 0). ArrayList вынужден сдвинуть все существующие элементы на одну позицию вправо, чтобы освободить место для нового.


// При вставке в середину/начало в ArrayList
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = newElement;
size++;


Эта операция arraycopy требует времени, пропорционального количеству сдвигаемых элементов (n). Удаление из начала/середины имеет ту же проблему, так как требует сдвига всех последующих элементов влево.

LinkedList: В среднем O(n), но само изменение ссылок — O(1).
Время операции здесь определяется не самим добавлением/удалением, а поиском нужной позиции. Как мы помним, поиск по индексу в LinkedList занимает O(n). Однако, как только узел найден, вставка или удаление выполняются очень быстро: нужно всего лишь поменять несколько ссылок у соседних узлов. Не нужно перемещать половину списка!


// Вставка `newNode` между `prevNode` и `currentNode`
newNode.prev = prevNode;
newNode.next = currentNode;
prevNode.next = newNode;
currentNode.prev = newNode;


Поэтому, если у вас уже есть ссылка на узел (например, вы находитесь в середине итерации), вставка и удаление рядом с этим узлом будут исключительно быстрыми (O(1)).


Когда использовать ArrayList (в 95% случаев):
Когда преобладают операции чтения и получения элементов по индексу.
Когда вы в основном добавляете элементы в конец.
Когда память несколько критична, и вы хотите минимизировать overhead.

Когда использовать LinkedList:
Когда преобладают операции вставки и удаления в начале или середине списка, и при этом у вас нет частой необходимости в быстром доступе по индексу.
Когда вы активно используете структуры типа "стек" (LIFO) или "очередь" (FIFO) (хогда для этого есть более специализированные классы, как ArrayDeque).


#Java #для_новичков #beginner #List #ArrayList #LinkedList
👍2
Глава 2. List — списки

Метод add

Философия добавления элементов в List

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


ArrayList: динамический массив

ArrayList представляет собой реализацию списка на основе динамического массива. Его внутренняя структура построена вокруг массива Object[], который служит хранилищем элементов.

Ключевыми характеристиками этой архитектуры являются:
Прямой доступ по индексу за O(1) время
Необходимость периодического расширения массива при достижении предела емкости
Высокая пространственная локальность данных, благоприятная для кэширования процессора
Эффективность последовательного доступа при итерации



Процесс добавления в конец списка

Когда вызывается метод add(element) для добавления элемента в конец ArrayList, происходит следующая последовательность действий:


1. Проверка емкости:
Система сначала проверяет, достаточно ли места в внутреннем массиве для размещения нового элемента. Эта проверка включает сравнение текущего размера списка (количество фактически содержащихся элементов) с емкостью массива (его физической длиной).

2. Расширение массива при необходимости:

Если массив заполнен, запускается процесс расширения — одна из самых затратных операций в ArrayList:
Создается новый массив большего размера (обычно в 1.5 раза больше текущего)
Все существующие элементы копируются из старого массива в новый
Старый массив становится доступным для сборки мусор
Ссылка на внутренний массив обновляется на новый массив


3. Непосредственное добавление элемента:
Новый элемент помещается в первую свободную позицию массива (индекс, равный текущему размеру списка).

4. Обновление метаданных:
Увеличивается счетчик размера списка и инкрементируется счетчик модификаций (modCount) для поддержки fail-fast итераторов.


Механизм расширения емкости

Процесс расширения массива следует стратегии геометрического роста, которая обеспечивает амортизированную постоянную стоимость операций добавления:
// Упрощенная логика расширения
private void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length) {
int newCapacity = elementData.length + (elementData.length >> 1); // Увеличение на 50%
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
Эта стратегия гарантирует, что хотя отдельные операции добавления могут быть дорогими (при необходимости расширения), средняя стоимость большого количества операций добавления остается O(1).



#Java #для_новичков #beginner #List #ArrayList #LinkedList #add
👍1
Процесс добавления по индексу

Вставка в произвольную позицию
Метод add(index, element) реализует более сложный сценарий — вставку элемента в конкретную позицию списка:

1. Валидация индекса:
Проверяется, что указанный индекс находится в допустимом диапазоне (от 0 до текущего размера списка включительно).

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

3. Сдвиг элементов:
Все элементы, начиная с указанной позиции, сдвигаются на одну позицию вправо.

Эта операция требует копирования части массива:
System.arraycopy(elementData, index, elementData, index + 1, size - index);


4. Вставка нового элемента:
Новый элемент помещается в освободившуюся позицию.

5. Обновление метаданных:
Увеличивается размер списка и обновляется счетчик модификаций.

Вставка в произвольную позицию имеет временную сложность O(n) в худшем случае, поскольку требует сдвига в среднем n/2 элементов. Стоимость операции максимальна при вставке в начало списка и минимальна при вставке в конец.


Оптимизации и особенности реализации

Ленивая инициализация

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

Стратегии начальной емкости
Разработчики могут указать начальную емкость через конструктор ArrayList(int initialCapacity).

Правильный выбор начальной емкости может значительно уменьшить количество операций расширения:
Слишком маленькая емкость приводит к частым расширениям и копированиям
Слишком большая емкость приводит к неэффективному использованию памяти
Оптимальная емкость зависит от ожидаемого конечного размера коллекции



Обработка больших массивов

При работе с очень большими ArrayList могут возникать дополнительные considerations:

Ограничения размера массива (Integer.MAX_VALUE - 8 в стандартных реализациях)
Проблемы фрагментации памяти кучи
Влияние на паузы сборки мусора



Сравнительный анализ ArrayList и LinkedList

Производительность операций добавления

Добавление в конец:
ArrayList: O(1) амортизированное время (благодаря стратегии геометрического роста)
LinkedList: O(1) постоянное время


Вставка в начало:
ArrayList: O(n) (требует сдвига всех элементов)
LinkedList: O(1) (простое обновление ссылок)


Вставка в произвольную позицию:
ArrayList: O(n) (сдвиг элементов)
LinkedList: O(n) (поиск позиции) + O(1) (вставка)

Потребление памяти
ArrayList:
Основные затраты: массив Object[] + служебные поля
В среднем 25-50% простаивающей емкости
Хорошая пространственная локальность


LinkedList:
Основные затраты: узлы (каждый ~24-32 байта) + служебные поля
Дополнительные 16-24 байта на элемент для ссылок
Плохая пространственная локальность



Специализированные реализации List

CopyOnWriteArrayList

CopyOnWriteArrayList использует стратегию "копирование при записи", которая обеспечивает потокобезопасность без блокировок для операций чтения

Процесс добавления:
Создается полная копия внутреннего массива
Новый элемент добавляется в конец копии
Ссылка на внутренний массив атомарно заменяется на новую копию


Преимущества:

Идеален для сценариев "частое чтение, редкая запись"
Гарантированная consistency итераторов


Недостатки:
Высокая стоимость операций модификации
Дополнительное потребление памяти


Vector

Устаревшая синхронизированная версия ArrayList:
Все методы синхронизированы
Менее эффективна чем Collections.synchronizedList()
Устаревшая стратегия роста (удвоение емкости)



#Java #для_новичков #beginner #List #ArrayList #LinkedList #add
👍1
Факторы, влияющие на производительность

Для ArrayList

Коэффициент роста:
Стандартный коэффициент 1.5 обеспечивает баланс между количеством расширений и использованием памяти. Увеличение коэффициента уменьшает частоту расширений, но увеличивает простаивающую емкость.

Начальная емкость:
Неправильный выбор начальной емкости может значительно повлиять на производительность:
Слишком маленькая: частые расширения и копирования
Слишком большая: избыточное потребление памяти


Размер элементов:
Для крупных объектов стоимость копирования при расширении может быть значительной.

Для LinkedList
Паттерн доступа:
Производительность сильно зависит от паттерна доступа:
Частые вставки в начало/конец: оптимально
Случайный доступ по индексу: неэффективно
Последовательный доступ: эффективно


Размер списка:
Для очень больших списков могут возникать проблемы с производительностью из-за poor locality и большого количества объектов узлов.


Многопоточные considerations

Потокобезопасность

Стандартные реализации ArrayList и LinkedList не являются потокобезопасными.

Concurrent модификации могут привести к:
Потере данных
Повреждению внутренних структур
Бесконечным циклам в итераторах


Thread-safe обертки:
Использование Collections.synchronizedList().

Copy-on-write коллекции:
Использование CopyOnWriteArrayList для сценариев с редкими модификациями.

Concurrent коллекции:
Использование специализированных concurrent реализаций.


Практические рекомендации

Выбор реализации

Выбор ArrayList когда:

Преобладает случайный доступ по индексу
Частые операции получения элементов
Известен приблизительный конечный размер
Память является критическим ресурсом


Выбор LinkedList когда:
Частые вставки/удаления в начале списка
Преобладает последовательный доступ
Размер списка сильно варьируется
Память не является основным ограничением


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

Для ArrayList:
Указание начальной емкости при создании
Минимизация вставок в середину списка
Использование ensureCapacity() для batch добавлений


Для LinkedList:
Предпочтение операций addFirst()/addLast() когда возможно
Избегание частого доступа по индексу
Использование ListIterator для последовательных вставок



Избегание распространенных ошибок


Неэффективные паттерны использования:
Частые вставки в начало ArrayList
Использование LinkedList для случайного доступа
Игнорирование начальной емкости для больших ArrayList


Проблемы многопоточности:
Concurrent модификации без proper синхронизации
Использование небезопасных итераторов в многопоточной среде



#Java #для_новичков #beginner #List #ArrayList #LinkedList #add
👍1🔥1