Stream.skip: примеры (JAVA)
Stream.skip(long n): StreamОписание и сигнатура
Метод Stream.skip в Java пропускает первые n элементов входного потока и возвращает поток, содержащий оставшиеся элементы в том же порядке (при наличии порядка). Это промежуточная ленивaя операция, применяемая к объектам типа java.util.stream.Stream и примитивным потокам: IntStream, LongStream, DoubleStream.
Сигнатуры:
Streamskip(long n) IntStream skip(long n) LongStream skip(long n) DoubleStream skip(long n)
Параметры и возврат:
- n - количество элементов для пропуска. Тип long.
- Возвращает новый поток, представляющий входной поток без первых n элементов. Если n >= длины потока, возвращается пустой поток.
Поведение и особенности:
- Если n меньше нуля, выбрасывается IllegalArgumentException.
- Операция является промежуточной и ленивой - вычисления происходят при выполнении терминальной операции.
- Это состояние-зависимая операция: для корректного пропускания требуется отслеживание количества пропущенных элементов.
- Если исходный поток имеет характеристику SIZED, то размер результирующего потока уменьшается на min(n, size). Для бесконечных потоков пропуск выполняется корректно (пример: Stream.generate или Stream.iterate).
- При работе с упорядоченными потоками порядок сохраняется. Для неупорядоченных потоков смысл пропуска связан с любым упорядочением, которое может применяться движком, и результат может отличаться от ожидаемого при предположении о фиксированном порядке.
- В параллельных потоках реализация может использовать специальные оптимизации для сплитераторов; тем не менее пропуск большого количества элементов может быть дорог по времени и памяти в зависимости от источника и порядка.
Краткое замечание о совместимости: метод появился в Java 8 и доступен во всех последующих релизах; аналогичные версии существуют для примитивных потоков.
Простые примеры использования
Базовые сценарии работы с методами Stream.skip и примитивными вариантами.
Пример 1: пропуск первых 2 элементов
List list = Arrays.asList("a", "b", "c", "d");
List res = list.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(res);
["c", "d"]
Пример 2: skip больше размера - пустой поток
List nums = Arrays.asList(1, 2, 3);
List res = nums.stream()
.skip(10)
.collect(Collectors.toList());
System.out.println(res);
[]
Пример 3: отрицательный аргумент - исключение
Stream.of("x", "y").skip(-1).forEach(System.out::println);
Exception in thread "main" java.lang.IllegalArgumentException
at java.base/java.util.stream.ReferencePipeline.skip(ReferencePipeline.java:...)
...
Пример 4: примитивный поток
IntStream.range(0, 6)
.skip(3)
.forEach(System.out::println);
3 4 5
Альтернативы и сопоставление внутри Java
В Java есть несколько похожих средств, которые могут быть предпочтительнее в разных сценариях:
- dropWhile (Java 9+) - отбрасывает элементы, пока выполняется предикат; полезно, когда пропуск зависит от значения, а не от фиксированного числа.
- limit - противоположная операция: выбирает первые n элементов. Часто применяется вместе с skip для пагинации:
Stream.skip(offset).limit(size). - Для списков предпочтительнее использовать List.subList(from, to) - это более эффективная операция для коллекций с константным доступом, чем stream.skip в плане выделения поддиапазона.
- Для массивов - Arrays.copyOfRange, дающий сразу новый массив без создания потока.
Когда выбирать:
- Для коллекций с известным индексированием и небольшой аллокации лучше subList/copyOfRange.
- Для ленивой обработки, конвейеров преобразований и бесконечных источников - Stream.skip.
- Если пропуск зависит от предиката - dropWhile предпочтительнее.
Аналоги в других языках
Краткое сравнение и примеры эквивалентов в распространённых языках.
JavaScript (массивы и функциональные утилиты)
// Array.prototype.slice
const arr = ['a','b','c','d'];
console.log(arr.slice(2));
// Lodash: _.drop(arr, 2)
[ 'c', 'd' ]
Python
# список
lst = [1,2,3,4]
print(lst[2:])
# генераторы: itertools.islice
from itertools import islice
print(list(islice((x for x in range(10)), 3, None)))
[3, 4] [3, 4, 5, 6, 7, 8, 9]
PHP
$arr = ['a','b','c','d'];
print_r(array_slice($arr, 2));
Array ( [0] => c [1] => d )
SQL
-- PostgreSQL, MySQL
SELECT * FROM table ORDER BY id OFFSET 20 LIMIT 10;
(записи с 21 по 30 в соответствии с ORDER BY)
C# (LINQ)
var res = new[]{1,2,3,4}.Skip(2);
Console.WriteLine(string.Join(",", res));
3,4
Kotlin
val list = listOf("a","b","c")
println(list.drop(1))
// Для Sequence: sequence.drop(n)
[b, c]
Go
s := []int{1,2,3,4}
fmt.Println(s[2:])
[3 4]
Lua
local t = {"a","b","c","d"}
local function drop(tbl, n)
local res = {}
for i = n+1, #tbl do table.insert(res, tbl[i]) end
return res
end
print(table.concat(drop(t,2),","))
c,d
Комментарии по отличиям:
- Во многих языках для массивов/списков существует синтаксис среза, который более эффективен и прямолинеен, чем потоковые операции.
- В JavaScript и Python срезы возвращают новый объект быстро; в Java skip обычно ленив и удобен для цепочек преобразований и бесконечных потоков.
- В SQL OFFSET выражает ту же семантику на уровне запросов и применяется совместно с ORDER BY для детерминированного результата.
Типичные ошибки и примеры
Наиболее распространённые ошибки при использовании skip с примерами и пояснениями.
Ошибка 1: отрицательное значение
Stream.of(1,2).skip(-5).collect(Collectors.toList());
Exception in thread "main" java.lang.IllegalArgumentException
at java.base/java.util.stream.ReferencePipeline.skip(ReferencePipeline.java:...)
...
Ошибка 2: повторное использование потока
Stream s = Stream.of("a","b","c");
s.skip(1).forEach(System.out::println);
// попытка повторно использовать тот же поток
s.skip(2).count();
a
b
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:...)
...
Ошибка 3: некорректные ожидания порядка в неупорядоченных потоках
Set set = new HashSet<>(Arrays.asList("a","b","c"));
List res = set.stream().skip(1).collect(Collectors.toList());
System.out.println(res);
["b","c"] // порядок зависит от внутреннего хеширования и может отличаться
Ошибка 4: производительность при пагинации
// пагинация большого источника
stream.skip(1000000).limit(10) ...
// при каждом запросе с большим offset может происходить дорогое пропускание элементов; для коллекций лучше subList или индексированный доступ
История изменений
Краткая хронология и заметные изменения, связанные с операцией пропуска.
- Java 8 - добавлен API Stream, включая метод skip для ссылочных и примитивных потоков.
- Java 9 - добавлены методы dropWhile и другие улучшения потокового API, расширяющие возможности отбора и пропуска по условию.
- Дальнейшие релизы не вносили принципиальных изменений в семантику skip. Могли производиться внутренние оптимизации реализации, особенно для параллельных потоков и сплитераторов.
Расширенные и редко встречающиеся примеры
Несколько сложных сценариев применения skip с пояснениями и результатами.
Пример 1: пагинация с потоками (эффект и ограничение)
int page = 5; int size = 10;
List pageData = bigList.stream()
.skip((long)page * size)
.limit(size)
.collect(Collectors.toList());
System.out.println(pageData);
// Работает, но при большом offset и источнике без индексированного доступа каждый вызов может быть дорогостоящим.
Пример 2: использование с бесконечным потоком
List res = Stream.iterate(0, n -> n + 1)
.skip(100)
.limit(5)
.collect(Collectors.toList());
System.out.println(res);
[100, 101, 102, 103, 104]
Пример 3: комбинирование skip и sorted - влияние на результат
List list = Arrays.asList(3,1,4,2,5);
List r1 = list.stream().skip(2).sorted().collect(Collectors.toList());
List r2 = list.stream().sorted().skip(2).collect(Collectors.toList());
System.out.println(r1);
System.out.println(r2);
[4,5] // сначала пропущены первые 2 исходных элемента [3,1] --> остаются [4,2,5], затем сортировка [3,4,5] // сначала сортировка [1,2,3,4,5], затем пропуск первых 2 -> [3,4,5]
Пример 4: пропуск по индексу внутри map (имитация skip с условием)
AtomicInteger idx = new AtomicInteger();
List res = Stream.of("a","b","c","d","e")
.filter(x -> idx.getAndIncrement() >= 2)
.collect(Collectors.toList());
System.out.println(res);
["c","d","e"]
Комментарий: такой подход небезопасен для параллельных потоков, поэтому пригоден только для последовательных конвейеров.
Пример 5: эффективная замена для коллекций с доступом по индексу
// Для ArrayList эффективнее использовать subList
List page = arrayList.subList(Math.min(offset, arrayList.size()),
Math.min(offset + size, arrayList.size()));
System.out.println(page);
// Быстрый поддиапазон без обхода первых offset элементов
Пример 6: параллельный поток и unordered источник
List data = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List res = data.parallelStream()
.unordered()
.skip(100)
.limit(10)
.collect(Collectors.toList());
System.out.println(res);
// Результат может быть в произвольном порядке и содержать произвольные элементы, отличные от ожидаемого при сохранении порядка