Привет! Сегодня разберем топ вопросов о коллекциях в Java, которые ежедневно гуглят тысячи разработчиков. Но не так, как в сухих учебниках — а так, как это реально работает в продакшене.
Когда я только начинал программировать, коллекции казались мне какой-то магией.
ArrayList,
LinkedList,
HashMap — зачем столько вариантов для хранения данных? Не проще ли использовать везде массивы? Спойлер: нет, не проще. И сейчас объясню почему.
1. ArrayList или LinkedList — что выбрать?
Это как вопрос "Что лучше — внедорожник или спорткар?". Зависит от того, куда едешь.
ArrayList — это обычный массив под капотом. Представьте книжную полку: все книги стоят рядом, пронумерованы, и вы мгновенно достаете нужную. Именно так работает доступ по индексу —
O(1), то есть почти мгновенно.
Но если нужно вставить книгу в середину полки? Придется сдвинуть половину книг. Это уже
O(n) — чем больше книг, тем дольше.
LinkedList — это цепочка элементов, где каждый "знает" только соседей. Как вагоны поезда: чтобы дойти до середины, нужно пройти все предыдущие вагоны. Зато добавить новый вагон — раз плюнуть, просто перецепил пару связей.
Вот сравнительная таблица:
| Операция | ArrayList | LinkedList |
| 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. Вы будете использовать их каждый день, в каждом проекте. Чем лучше вы их понимаете, тем чище и эффективнее будет ваш код.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ