Stream.skip: примеры (JAVA)

Метод Stream.skip: объяснение и примеры
Раздел: Потоки данных (Stream API) - промежуточные операции
Stream.skip(long n): Stream

Описание и сигнатура

Метод Stream.skip в Java пропускает первые n элементов входного потока и возвращает поток, содержащий оставшиеся элементы в том же порядке (при наличии порядка). Это промежуточная ленивaя операция, применяемая к объектам типа java.util.stream.Stream и примитивным потокам: IntStream, LongStream, DoubleStream.

Сигнатуры:

Stream    skip(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: пагинация с потоками (эффект и ограничение)

Пример java
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: использование с бесконечным потоком

Пример java
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 - влияние на результат

Пример java
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 с условием)

Пример java
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: эффективная замена для коллекций с доступом по индексу

Пример java
// Для ArrayList эффективнее использовать subList
List page = arrayList.subList(Math.min(offset, arrayList.size()),
                                     Math.min(offset + size, arrayList.size()));
System.out.println(page);
// Быстрый поддиапазон без обхода первых offset элементов

Пример 6: параллельный поток и unordered источник

Пример java
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);
// Результат может быть в произвольном порядке и содержать произвольные элементы, отличные от ожидаемого при сохранении порядка

джава Stream.skip function comments

En
Stream.skip Returns a stream consisting of the remaining elements after discarding the first n elements