Привет! Сегодня разберем топ вопросов о коллекциях в Java, которые ежедневно гуглят тысячи разработчиков. Но не так, как в сухих учебниках — а так, как это реально работает в продакшене. Когда я только начинал программировать, коллекции казались мне какой-то магией. ArrayList, LinkedList, HashMap — зачем столько вариантов для хранения данных? Не проще ли использовать везде массивы? Спойлер: нет, не проще. И сейчас объясню почему.

1. ArrayList или LinkedList — что выбрать?

Это как вопрос "Что лучше — внедорожник или спорткар?". Зависит от того, куда едешь. ArrayList — это обычный массив под капотом. Представьте книжную полку: все книги стоят рядом, пронумерованы, и вы мгновенно достаете нужную. Именно так работает доступ по индексу — O(1), то есть почти мгновенно. Но если нужно вставить книгу в середину полки? Придется сдвинуть половину книг. Это уже O(n) — чем больше книг, тем дольше. LinkedList — это цепочка элементов, где каждый "знает" только соседей. Как вагоны поезда: чтобы дойти до середины, нужно пройти все предыдущие вагоны. Зато добавить новый вагон — раз плюнуть, просто перецепил пару связей. Вот сравнительная таблица:
ОперацияArrayListLinkedList
get(index)O(1)O(n)
add(element)O(1)*O(1)
add(index, element)O(n)O(n)
remove(index)O(n)O(n)
*О(1) для добавления в конец, когда не нужно расширять массив Важная деталь про память: каждый элемент LinkedList тащит с собой две ссылки (на предыдущий и следующий). Если у вас миллион элементов — это миллион лишних объектов. ArrayList просто хранит массив ссылок. Мой совет: в 95% случаев используйте ArrayList. LinkedList имеет смысл только если вы постоянно добавляете/удаляете элементы из середины списка. Но даже тогда подумайте дважды.

2. Как правильно удалять элементы во время итерации?

О, это классика! Каждый начинающий разработчик хоть раз словил ConcurrentModificationException. Я помню, как в первый раз увидел эту ошибку и думал: "Но я же не использую многопоточность!". Неправильно (словим ошибку):
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));

for (Integer num : numbers) {
    if (num > 3) {
        numbers.remove(num); // БАХ! ConcurrentModificationException
    }
}
Правильно — Iterator:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
Iterator<Integer> iterator = numbers.iterator();

while (iterator.hasNext()) {
    Integer num = iterator.next();
    if (num > 3) {
        iterator.remove(); // Вот так работает!
    }
}
Еще лучше — современный Java (8+):
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
numbers.removeIf(num -> num > 3); // Одна строка вместо пяти!
Метод removeIf() появился в Java 8 и делает код чище. За кулисами он использует тот же Iterator, но вам не нужно об этом думать.

3. Как конвертировать List в массив примитивов?

Вот тут начинается веселье с автобоксингом. Java различает Integer и int, и это важно.
List<Integer> list = List.of(1, 2, 3, 4, 5);
int[] array = list.stream()
                  .mapToInt(Integer::intValue)
                  .toArray();
Stream API — это вообще отдельная тема для восхищения. Одна строка вместо цикла, и код читается как предложение на английском: "возьми список, преобразуй в поток целых чисел, собери в массив". Для объектов еще проще:
List<String> list = List.of("Java", "Python", "JavaScript");
String[] array = list.toArray(new String[0]);
// Или совсем короткий вариант:
String[] array2 = list.toArray(String[]::new);

4. Как конвертировать массив в List?

Внимание, ловушка! Многие делают так:
Integer[] array = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.asList(array);
list.add(6); // УПС! UnsupportedOperationException
Arrays.asList() возвращает неизменяемый список фиксированного размера. Это обертка над массивом, а не настоящий ArrayList. Правильные способы:
// Способ 1: через конструктор
Integer[] array = {1, 2, 3, 4, 5};
List<Integer> list = new ArrayList<>(Arrays.asList(array));

// Способ 2: через Stream (Java 8+)
List<Integer> list2 = Arrays.stream(array).collect(Collectors.toList());

// Способ 3: через List.of (Java 9+) - тоже неизменяемый!
List<Integer> list3 = List.of(array);

// Способ 4: для изменяемого списка (Java 10+)
List<Integer> list4 = new ArrayList<>(List.of(array));
Для примитивов:
int[] primitives = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.stream(primitives)
                           .boxed()
                           .collect(Collectors.toList());

5. Как фильтровать коллекции?

Раньше для этого использовали библиотеки типа Guava или Apache Commons. Сейчас в этом нет необходимости — Stream API рулит!
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> result = numbers.stream()
                              .filter(num -> num > 5)
                              .collect(Collectors.toList());
Еще круче — с несколькими условиями:
List<Integer> result = numbers.stream()
    .filter(num -> num > 3)
    .filter(num -> num % 2 == 0)
    .map(num -> num * 2)
    .collect(Collectors.toList());
Читается как текст: "Возьми числа больше 3, оставь четные, умножь на 2". Красота!

6. List в Set — как убрать дубликаты?

Это один из самых частых вопросов на собеседованиях. Простой способ:
List<String> listWithDuplicates = List.of("Java", "Python", "Java", "JavaScript", "Python");
Set<String> uniqueSet = new HashSet<>(listWithDuplicates);
Порядок элементов? Забудьте. HashSet не гарантирует порядок. Если порядок важен:
Set<String> orderedSet = new LinkedHashSet<>(listWithDuplicates);
// Сохранит порядок первого появления элемента
Если нужен отсортированный Set:
Set<String> sortedSet = new TreeSet<>(listWithDuplicates);
// Элементы будут отсортированы по алфавиту (или по Comparator)
С помощью Stream (и сразу обратно в List):
List<String> uniqueList = listWithDuplicates.stream()
                                            .distinct()
                                            .collect(Collectors.toList());

7. Удаление дубликатов из ArrayList

Это продолжение предыдущего вопроса, но с нюансом — хотим изменить существующий список. Если порядок не важен:
List<String> list = new ArrayList<>(List.of("A", "B", "A", "C", "B"));
Set<String> set = new HashSet<>(list);
list.clear();
list.addAll(set);
Если порядок важен (Java 8+):
List<String> list = new ArrayList<>(List.of("A", "B", "A", "C", "B"));
list = list.stream()
           .distinct()
           .collect(Collectors.toList());
Через LinkedHashSet (сохраняет порядок):
List<String> list = new ArrayList<>(List.of("A", "B", "A", "C", "B"));
list = new ArrayList<>(new LinkedHashSet<>(list));

8. Как поддерживать отсортированную коллекцию?

Есть три основных подхода, и каждый для своей задачи. 1. Collections.sort() — одноразовая сортировка
List<Integer> numbers = new ArrayList<>(List.of(5, 2, 8, 1, 9));
Collections.sort(numbers); // [1, 2, 5, 8, 9]
// Или еще проще:
numbers.sort(null); // null означает естественный порядок
Использовать, когда: нужно отсортировать список один раз. 2. PriorityQueue — всегда упорядоченная очередь
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(5);
queue.add(2);
queue.add(8);
queue.add(1);

System.out.println(queue.poll()); // 1
System.out.println(queue.poll()); // 2
System.out.println(queue.poll()); // 5
Использовать, когда: нужен только минимальный (или максимальный) элемент, обрабатываете задачи по приоритету. 3. TreeSet — всегда отсортированный Set
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(5);
treeSet.add(2);
treeSet.add(8);
treeSet.add(1);

System.out.println(treeSet.first()); // 1
System.out.println(treeSet.last());  // 8
Использовать, когда: нужна отсортированная коллекция без дубликатов с быстрым поиском минимума/максимума. Современный подход — Stream API:
List<String> names = List.of("Анна", "Борис", "Виктор", "Алексей");
List<String> sorted = names.stream()
                           .sorted()
                           .collect(Collectors.toList());

9. Collections.emptyList() или new ArrayList<>()?

Вопрос кажется простым, но есть нюансы. Collections.emptyList():
List<String> empty = Collections.emptyList();
// empty.add("что-то"); // УПС! UnsupportedOperationException
Это неизменяемый пустой список. И он всегда один и тот же (синглтон). JVM не создает новые объекты, а возвращает ссылку на существующий. Когда использовать:
  • Нужно вернуть пустой результат из метода
  • Список точно не будет изменяться
  • Хотите сэкономить память
new ArrayList<>():
List<String> empty = new ArrayList<>();
empty.add("можно добавлять"); // Работает!
Создается новый изменяемый список. Занимает память (хоть и немного). Современная альтернатива (Java 9+):
List<String> empty = List.of(); // Тоже неизменяемый
Мой совет: если метод возвращает коллекцию, но нечего вернуть — используйте Collections.emptyList() или List.of(). Никогда не возвращайте null — это источник NullPointerException.

10. Collections.copy() — копирование списков

Тут важно понять разницу между shallow copy (поверхностное копирование) и deep copy (глубокое копирование). Конструктор ArrayList (самый простой способ):
List<String> original = new ArrayList<>(List.of("Java", "Python", "JavaScript"));
List<String> copy = new ArrayList<>(original);
Это быстро и просто. Используйте в 99% случаев. Collections.copy() (странный способ):
List<String> source = List.of("Java", "Python", "JavaScript");
List<String> dest = new ArrayList<>(Arrays.asList(new String[source.size()]));
Collections.copy(dest, source);
Зачем так сложно? Collections.copy() требует, чтобы целевой список уже был нужного размера! Это сделано для производительности — метод не перевыделяет память, работает за O(n). Когда использовать Collections.copy():
  • Вы переиспользуете существующий список и хотите избежать выделения памяти
  • Очень редко. Серьезно.
Современные способы (Java 10+):
// Неизменяемая копия
List<String> copy1 = List.copyOf(original);

// Изменяемая копия через Stream
List<String> copy2 = original.stream()
                             .collect(Collectors.toList());
Важно понимать: все эти способы делают shallow copy. Копируются ссылки на объекты, а не сами объекты!
class Person {
    String name;
    Person(String name) { this.name = name; }
}

List<Person> original = new ArrayList<>();
original.add(new Person("Иван"));

List<Person> copy = new ArrayList<>(original);
copy.get(0).name = "Петр";

System.out.println(original.get(0).name); // "Петр"! Оба списка указывают на одного Person
Если нужна глубокая копия — придется копировать объекты вручную или использовать специальные библиотеки.

Бонус: современные фишки коллекций

Раз уж мы заговорили о современной Java, вот вам десерт — фичи, которые упростят вам жизнь. Неизменяемые коллекции (Java 9+):
List<String> list = List.of("один", "два", "три");
Set<String> set = Set.of("красный", "синий", "зеленый");
Map<String, Integer> map = Map.of("один", 1, "два", 2, "три", 3);
Короче, безопаснее, быстрее классических вариантов. Удобные операции со Stream:
List<String> names = List.of("Анна", "Борис", "Виктор");

// Преобразование
List<String> uppercase = names.stream()
                              .map(String::toUpperCase)
                              .collect(Collectors.toList());

// Группировка
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

// Поиск
Optional<String> firstLongName = names.stream()
    .filter(name -> name.length() > 5)
    .findFirst();
Работа с null-значениями:
List<String> names = Arrays.asList("Иван", null, "Петр", null, "Сидор");

// Убрать все null
List<String> notNull = names.stream()
                            .filter(Objects::nonNull)
                            .collect(Collectors.toList());

Что дальше?

Коллекции — это основа основ в Java. Вы будете использовать их каждый день, в каждом проекте. Чем лучше вы их понимаете, тем чище и эффективнее будет ваш код.