2017-02-21 - Sequence v2.3 is released, with optimizations to
size
and the ability to tell whether a sequence has a defined size or is infinite or unsized through the new
sizeIfKnown
and
sizeType
methods.
Adds
mapLeft
/
mapRight
to
BiSequence
and
mapKeys
/
mapValues
to
EntrySequence
.
Adds argument checking for fail-fast behaviour under incorrect
Sequence
usage.
Brings test coverage of the entire project up to 100% on codecov.io.
Adds
limitTail
to all sequences.
Adds
groupBy
operations to Sequence
, and
toGroupedMap
to EntrySequence
and BiSequence
.
Adds
sum
,
average
and
statistics
to
LongSequence
,
DoubleSequence
and
IntSequence
.
Performance optimizations, improved strictness checks, and improved error condition behaviour in primitive collections.
- See the Sequence wiki for older news.
- Follow @SequenceLibrary on Twitter to receive updates.
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 has no external dependencies and will not slow down your build.
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 every day data processing tasks on moderately sized collections. Sequences
go to
great lengths to be as lazy and late-evaluating as possible, with minimal overhead.
Sequence
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 Pairs
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 peeking at previous or next elements during traversal.
Not being parallel in nature allows more advanced operations on the sequence which rely on traversing elements in
Iterator
order. 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(T...), Sequence#from(Iterable), Sequence#filter(Predicate), Sequence#map(Function), Sequence#toList()
Unlike Stream
, Sequences
are directly backed by the underlying storage, allowing direct manipulation of the
Collection
the sequence is based on to the extent permitted by the combination of operations applied on the sequence,
as well as directly reflecting outside changes to the underlying collection. See Updating for more
information.
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));
The Sequence
library is protected by over 5000 tests providing 100% branch and line coverage of all classes in the
project. Javadoc for the entire project is available at the
Sequence javadoc.io Page.
The Sequence library is available for manual install or as a maven central dependency for maven and gradle.
For manually installable releases, check out the GitHub Releases Page.
To install in maven, use the maven central dependency:
<dependency>
<groupId>org.d2ab</groupId>
<artifactId>sequence</artifactId>
<version>[2.3,3.0)</version>
</dependency>
To install in gradle, use the maven central dependency:
repositories {
mavenCentral()
}
dependencies {
compile 'org.d2ab:sequence:[2.3,3.0)'
}
Javadoc for the entire project is available at the Sequence javadoc.io Page.
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> digits = Sequence.ints(); // all integer digits starting at 1
// using sequence of ints first time to get 5 odd numbers
Sequence<Integer> odds = digits.step(2).limit(5);
assertThat(odds, contains(1, 3, 5, 7, 9));
// re-using the same sequence of digits again to get squares of numbers between 4 and 8
Sequence<Integer> squares = digits.startingFrom(4).endingAt(8).map(i -> i * i);
assertThat(squares, contains(16, 25, 36, 49, 64));
See also: Sequence#range(int, int), Sequence#ints(), Sequence#intsFromZero(), Sequence#filter(Predicate), Sequence#map(Function), Sequence#step(long), Sequence#limit(long), Sequence#skip(long), Sequence#startingFrom(T), Sequence#endingAt(T)
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 iterator()
method of Iterable
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, through Iterator#remove()
, by
modifying the underlying collection directly (in between iterations), and by using Collection
methods directly on
the Sequence
itself.
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<>(Lists.of(1, 2, 3, 4, 5));
Sequence<String> evens = Sequence.from(list)
.filter(x -> x % 2 == 0)
.biMap(Object::toString, Integer::parseInt);
assertThat(evens, contains("2", "4"));
evens.add("6"); // biMap allows adding back to underlying collection
expecting(IllegalArgumentException.class, () -> evens.add("7")); // cannot add filtered out item
assertThat(evens, contains("2", "4", "6"));
assertThat(list, contains(1, 2, 3, 4, 5, 6));
Since Sequence
implements Collection
it provides a fully functioning size()
method, however note that this method
may degrade to O(n)
performance if the size is not known a-priori and thus needs to be calculated by traversing the
Sequence
. In some cases, the size can be calculated in advance and Sequence
makes full use of this:
Sequence<Integer> repeated = Sequence.of(1, 2, 3).repeat().limit(5);
assertThat(repeated, contains(1, 2, 3, 1, 2));
assertThat(repeated.size(), is(5)); // immediate return value
assertThat(repeated.sizeIfKnown(), is(5)); // would return -1 if unknown in advance
While Sequence
goes to great lengths to be able to detect the size in advance, in many cases the size can not be
calculated in advance and so Sequence
must traverse the list of elements to calculate the size, degrading to O(n)
performance for the size()
operation:
List<Integer> growingList = new ArrayList<Integer>() {
@Override
public Iterator<Integer> iterator() {
add(size() + 1);
return super.iterator();
}
};
Sequence<Integer> repeated = Sequence.from(growingList).repeat().limit(10);
assertThat(repeated, contains(1, 1, 2, 1, 2, 3, 1, 2, 3, 4));
assertThat(repeated.size(), is(10)); // O(n) traversal of elements required
assertThat(repeated.sizeIfKnown(), is(-1)); // cannot determine size in advance as collections can mutate
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), Sequence#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);
assertThat(fibonacci.endingAt(34), 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)));
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: BiSequence, BiSequence#recurse(L, R, BiFunction), BiSequence#toSequence(BiFunction), Pair, Pair#of(T, U), Sequence#recurse(T, UnaryOperator), Sequence#generate(Supplier), Sequence#repeat(), Sequence#repeat(long), Sequence#until(T), Sequence#endingAt(T), Sequence#untilNull(T), Sequence#until(Predicate), Sequence#endingAt(Predicate)
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(T, 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#adjacentPairs(), Pair, Sequence#toMap(), Sequence#toMap(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(pair -> pair.test((s, i) -> i != 2))
.map(pair -> pair.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, Pair#of(T, U), Pair#from(Entry), Pair#test(BiPredicate), Pair#map(BiFunction)
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, EntrySequence#from(Map), EntrySequence#filter(BiPredicate), EntrySequence#map(BiFunction), EntrySequence#toSequence(BiFunction), Maps#entry(K, V), EntrySequence#toMap()
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, BiSequence#from(Map), BiSequence#filter(BiPredicate), BiSequence#map(BiFunction), BiSequence#toSequence(BiFunction), BiSequence#toMap()
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 Sequence#toChars(ToCharFunction) Sequence#toInts(ToIntFunction) Sequence#toLongs(ToLongFunction) Sequence#toDoubles(ToDoubleFunction)
Sequences
also have mapping and filtering methods that peek on the previous and next elements:
CharSeq titleCase = CharSeq.from("hello_lexicon")
.mapBack('_', (prev, x) -> prev == '_' ? toUpperCase(x) : x)
.map(c -> (c == '_') ? ' ' : c);
assertThat(titleCase.asString(), is("Hello Lexicon"));
See also: Sequence#peekBack(BiConsumer), Sequence#peekForward(BiConsumer), Sequence#filterBack(BiPredicate), Sequence#filterForward(BiPredicate), Sequence#mapBack(BiFunction), Sequence#mapForward(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 binary predicate to determine which 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) < 0) != (vowels.indexOf(b) < 0))
.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(T), 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', (prev, x) -> isWhitespace(prev) ? toUpperCase(x) : x)
.split('\n')
.map(phrase -> phrase.append('!'))
.map(CharSeq::asString);
assertThat(titleCase, contains("Hello World!", "Goodbye World!"));
reader.close(); // sequence does not close reader
String original = "hello world\ngoodbye world\n";
BufferedReader transformed = new BufferedReader(CharSeq.from(original).map(Character::toUpperCase).asReader());
assertThat(transformed.readLine(), is("HELLO WORLD"));
assertThat(transformed.readLine(), is("GOODBYE WORLD"));
transformed.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(Reader), IntSequence#read(InputStream)
Go ahead and give it a try and experience a leaner way to Stream
your Sequences
!
Copyright © 2016-2017 Daniel Skogquist Åborg (d2ab.org). Licensed under the Apache License, Version 2.0.
Your feedback is welcome! For comments, feature requests or bug reports, use the GitHub Issues Page or email me at [email protected].
Developed with IntelliJ IDEA. ❤️