See: Description
The Sequence library is a leaner alternative to sequential Java 8 Streams, used in similar ways but with a lighter step, and with better integration with the rest of Java.
It aims to be roughly feature complete with sequential Streams, with additional convenience methods for advanced traversal and transformation. In particular it allows easier collecting into common Collections without Collectors, better handling of Maps with Pair and Map.Entry as first-class citizens, tighter integration with the rest of Java by being implemented in terms of Iterable, and advanced partitioning, mapping and filtering methods, for example allowing you to peek at previous or next elements to make decisions during traversal. Sequences go to great lengths to be as lazy and late-evaluating as possible, with minimal overhead.
Sequences use Java 8 lambdas in much the same way as Streams do, but is based on readily available Iterables instead of a black box pipeline, and is built for convenience and compatibility with the rest of Java. It's for programmers wanting to perform common data processing tasks on moderately sized collections. If you need parallel iteration or are processing over 1 million or so entries, you might benefit from using a parallel Stream instead.
List<String> evens = Sequence.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(x -> x % 2 == 0)
.map(Object::toString)
.toList();
assertThat(evens, contains("2", "4", "6", "8"));
See also: Sequence.of(Object...)
, Sequence.from(Iterable)
The main Sequence package is org.d2ab.sequence
where all
the sequences reside. There are seven kinds of Sequences, each dealing with a different type of entry. The first is
the regular Sequence
which is the general purpose stream of items. EntrySequence
and BiSequence
work directly on the constituent
components of Map.Entry
and Pair
objects. The last four are primitive sequences dealing
with char
, int
, long
and double
primitives; CharSeq
,
IntSequence
, LongSequence
, and DoubleSequence
. These work much the same as the regular Sequence
except
they're adapted to work directly on primitives.
Because each Sequence
is an
Iterable
you can re-use
them safely after you have already traversed them, as long as they're not backed by an
Iterator
or
Stream
which can only be traversed once.
Sequence<Integer> singulars = Sequence.range(1, 9); // Digits 1..9
// using sequence of ints 1..9 first time to get odd numbers between 1 and 9
Sequence<Integer> odds = singulars.step(2);
assertThat(odds, contains(1, 3, 5, 7, 9));
// re-using the same sequence again to get squares of numbers between 4 and 8
Sequence<Integer> squares = singulars.startingFrom(4).endingAt(8).map(i -> i * i);
assertThat(squares, contains(16, 25, 36, 49, 64));
Also because each Sequence
is an Iterable
they work beautifully in foreach
loops:
Sequence<Integer> sequence = Sequence.ints().limit(5);
int expected = 1;
for (int each : sequence)
assertThat(each, is(expected++));
assertThat(expected, is(6));
Because Sequence is a
FunctionalInterface
requiring only the
Iterable#iterator()
method to be implemented, it's very easy to create your own full-fledged Sequence
instances that can be operated on like any other Sequence
through the default methods on
the interface that carry the bulk of the burden. In fact, this is how `Sequence's` own factory methods work. You
could consider all of `Sequence` to be a smarter version of `Iterable`.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// Sequence as @FunctionalInterface of list's Iterator
Sequence<Integer> sequence = list::iterator;
// Operate on sequence as any other sequence using default methods
Sequence<String> transformed = sequence.map(Object::toString);
assertThat(transformed.limit(3), contains("1", "2", "3"));
Sequences can be created from Iterators or Streams but can then only be passed over once.
Iterator<Integer> iterator = Arrays.asList(1, 2, 3, 4, 5).iterator();
Sequence<Integer> sequence = Sequence.once(iterator);
assertThat(sequence, contains(1, 2, 3, 4, 5));
assertThat(sequence, is(emptyIterable()));
See also: Sequence.once(Iterator)
, Sequence.once(Stream)
If you have an Iterator or Stream and wish to convert it to a full-fledged multi-iterable Sequence, use the caching methods on Sequence.
Iterator<Integer> iterator = Arrays.asList(1, 2, 3, 4, 5).iterator();
Sequence<Integer> cached = Sequence.cache(iterator);
assertThat(cached, contains(1, 2, 3, 4, 5));
assertThat(cached, contains(1, 2, 3, 4, 5));
See also: Sequence.cache(Iterable)
, Sequence.cache(Iterator)
,
Sequence.cache(Stream)
Sequences have full support for updating the underlying collection where possible, both through Iterator#remove() and by modifying the underlying collection directly in between iterations.
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Sequence.from(list).filter(x -> x % 2 != 0).clear();
assertThat(list, contains(2, 4));
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Sequence<Integer> sequence = Sequence.from(list).filter(x -> x % 2 == 0);
assertThat(sequence, contains(2, 4));
list.add(6);
assertThat(sequence, contains(2, 4, 6));
See also: IterableCollection.clear()
Sequences interoperate beautifully with Stream, through the once(Stream) and .stream() methods.
Sequence<String> paired = Sequence.once(Stream.of("a", "b", "c", "d")).pairs().flatten();
assertThat(paired.stream().collect(Collectors.toList()), contains("a", "b", "b", "c", "c", "d"));
See also: Sequence.once(Stream)
, Sequence.cache(Stream)
, Collection.stream()
There is full support for infinite recursive Sequences, including termination at a known value.
Sequence<Integer> fibonacci = BiSequence.recurse(0, 1, (i, j) -> Pair.of(j, i + j))
.toSequence((i, j) -> i)
.endingAt(34);
assertThat(fibonacci, contains(0, 1, 1, 2, 3, 5, 8, 13, 21, 34));
Exception exception = new IllegalStateException(new IllegalArgumentException(new NullPointerException()));
Sequence<Throwable> exceptionAndCauses = Sequence.recurse(exception, Throwable::getCause).untilNull();
assertThat(exceptionAndCauses, contains(instanceOf(IllegalStateException.class),
instanceOf(IllegalArgumentException.class),
instanceOf(NullPointerException.class)));
StringBuilder builder = new StringBuilder();
exceptionAndCauses.last(IllegalArgumentException.class).ifPresent(builder::append);
assertThat(builder.toString(), is("java.lang.IllegalArgumentException: java.lang.NullPointerException"));
Iterator<String> delimiter = Sequence.of("").append(Sequence.of(", ").repeat()).iterator();
StringBuilder joined = new StringBuilder();
for (String number : Arrays.asList("One", "Two", "Three"))
joined.append(delimiter.next()).append(number);
assertThat(joined.toString(), is("One, Two, Three"));
CharSeq hexGenerator = CharSeq.random("0-9", "A-F").limit(8);
String hexNumber1 = hexGenerator.asString();
String hexNumber2 = hexGenerator.asString();
assertTrue(hexNumber1.matches("[0-9A-F]{8}"));
assertTrue(hexNumber2.matches("[0-9A-F]{8}"));
assertThat(hexNumber1, is(not(hexNumber2)));
See also:
Sequence.recurse(Object, UnaryOperator)
Sequence.recurse(Object, Function, Function)
Sequence.generate(Supplier)
Sequence.until(Object)
Sequence.until(Predicate)
Sequence.untilNull()
Sequence.endingAt(Object)
Sequence.endingAt(Predicate)
Sequence.endingAtNull()
The standard reduction operations are available as per Stream:
Sequence<Long> thirteen = Sequence.longs().limit(13);
long factorial = thirteen.reduce(1L, (r, i) -> r * i);
assertThat(factorial, is(6227020800L));
See also: Sequence.reduce(BinaryOperator)
, Sequence.reduce(Object,
BinaryOperator)
Maps are handled as Sequences of Entry, with special transformation methods that convert to/from Maps.
Sequence<Integer> keys = Sequence.of(1, 2, 3);
Sequence<String> values = Sequence.of("1", "2", "3");
Map<Integer, String> map = keys.interleave(values).toMap();
assertThat(map, is(equalTo(Maps.builder(1, "1").put(2, "2").put(3, "3").build())));
See also:
Sequence.interleave(Iterable)
Sequence.pairs()
Sequence.toMap()
Sequence.toMap(Function, Function)
Sequence.toMap(Supplier)
Sequence.toMap(Supplier, Function, Function)
Sequence.toSortedMap()
Sequence.toSortedMap(Function, Function)
You can also map Entry Sequences to Pairs which allows more expressive transformation and filtering.
Map<String, Integer> map = Maps.builder("1", 1).put("2", 2).put("3", 3).put("4", 4).build();
Sequence<Pair<String, Integer>> sequence = Sequence.from(map)
.map(Pair::from)
.filter(p -> p.test((s, i) -> i != 2))
.map(p -> p.map((s, i) -> Pair.of(s + " x 2", i * 2)));
assertThat(sequence.toMap(), is(equalTo(Maps.builder("1 x 2", 2).put("3 x 2", 6).put("4 x 2", 8).build())));
See also: Pair
You can also work directly on Entry keys and values using EntrySequence.
Map<String, Integer> original = Maps.builder("1", 1).put("2", 2).put("3", 3).put("4", 4).build();
EntrySequence<Integer, String> oddsInverted = EntrySequence.from(original)
.filter((k, v) -> v % 2 != 0)
.map((k, v) -> Maps.entry(v, k));
assertThat(oddsInverted.toMap(), is(equalTo(Maps.builder(1, "1").put(3, "3").build())));
See also: EntrySequence
When iterating over sequences of Pairs of item, BiSequence provides native operators and transformations:
BiSequence<String, Integer> presidents = BiSequence.ofPairs("Abraham Lincoln", 1861, "Richard Nixon", 1969,
"George Bush", 2001, "Barack Obama", 2005);
Sequence<String> joinedOffice = presidents.toSequence((n, y) -> n + " (" + y + ")");
assertThat(joinedOffice, contains("Abraham Lincoln (1861)", "Richard Nixon (1969)", "George Bush (2001)",
"Barack Obama (2005)"));
See also: BiSequence
There are also primitive versions of Sequence for char, int, long and double processing: CharSeq, IntSequence, LongSequence and DoubleSequence.
CharSeq snakeCase = CharSeq.from("Hello Lexicon").map(c -> (c == ' ') ? '_' : c).map(Character::toLowerCase);
assertThat(snakeCase.asString(), is("hello_lexicon"));
IntSequence squares = IntSequence.positive().map(i -> i * i);
assertThat(squares.limit(5), contains(1, 4, 9, 16, 25));
LongSequence negativeOdds = LongSequence.negative().step(2);
assertThat(negativeOdds.limit(5), contains(-1L, -3L, -5L, -7L, -9L));
DoubleSequence squareRoots = IntSequence.positive().toDoubles().map(Math::sqrt);
assertThat(squareRoots.limit(3), contains(sqrt(1), sqrt(2), sqrt(3)));
See also: CharSeq
, IntSequence
, LongSequence
, DoubleSequence
Sequences also have mapping and filtering methods that peek on the previous and next elements:
CharSeq titleCase = CharSeq.from("hello_lexicon")
.mapBack('_', (p, c) -> p == '_' ? toUpperCase(c) : c)
.map(c -> (c == '_') ? ' ' : c);
assertThat(titleCase.asString(), is("Hello Lexicon"));
See also:
Sequence.peekBack(BiConsumer)
Sequence.peekBack(Object, BiConsumer)
Sequence.peekForward(BiConsumer)
Sequence.peekForward(Object, BiConsumer)
Sequence.filterBack(BiPredicate)
Sequence.filterBack(Object, BiPredicate)
Sequence.filterForward(BiPredicate)
Sequence.filterForward(Object, BiPredicate)
Sequence.mapBack(BiFunction)
Sequence.mapBack(Object, BiFunction)
Sequence.mapForward(BiFunction)
Sequence.mapForward(Object, BiFunction)
Both regular and primitive Sequences have advanced windowing and partitioning methods, allowing you to divide up Sequences in various ways, including a partitioning method that uses a BiPredicate to determine which two elements to create a batch between.
Sequence<Sequence<Integer>> batched = Sequence.of(1, 2, 3, 4, 5, 6, 7, 8, 9).batch(3);
assertThat(batched, contains(contains(1, 2, 3), contains(4, 5, 6), contains(7, 8, 9)));
String vowels = "aeoiuy";
Sequence<String> consonantsVowels = CharSeq.from("terrain")
.batch((a, b) -> (vowels.indexOf(a) == -1) !=
(vowels.indexOf(b) == -1))
.map(CharSeq::asString);
assertThat(consonantsVowels, contains("t", "e", "rr", "ai", "n"));
See also:
Sequence.window(int)
Sequence.window(int, int)
Sequence.batch(int)
Sequence.batch(BiPredicate)
Sequence.split(Object)
Sequence.split(Predicate)
Primitive sequences can be read from `Readers` or `InputStreams` into a `CharSeq` or `IntSequence` respective. These can also be converted back to `Readers` and `InputStreams` respectively, allowing for filtering or transformation of these streams.
Reader reader = new StringReader("hello world\ngoodbye world\n");
Sequence<String> titleCase = CharSeq.read(reader)
.mapBack('\n',
(p, n) -> p == '\n' || p == ' ' ?
Character.toUpperCase(n) : n)
.split('\n')
.map(phrase -> phrase.append('!'))
.map(CharSeq::asString);
assertThat(titleCase, contains("Hello World!", "Goodbye World!"));
reader.close();
Reader original = new StringReader("hello world\ngoodbye world\n");
BufferedReader transformed = new BufferedReader(CharSeq.read(original).map(Character::toUpperCase).asReader());
assertThat(transformed.readLine(), is("HELLO WORLD"));
assertThat(transformed.readLine(), is("GOODBYE WORLD"));
transformed.close();
original.close();
InputStream inputStream = new ByteArrayInputStream(new byte[]{0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF});
String hexString = IntSequence.read(inputStream)
.toSequence(Integer::toHexString)
.map(String::toUpperCase)
.join();
assertThat(hexString, is("DEADBEEF"));
inputStream.close();
See also: CharSeq.read(java.io.Reader)
, IntSequence.read(java.io.InputStream)