Getting Started with Eclipse Collections — Part 4
Processing information in collections
Getting Started with Eclipse Collections
In part 1 of this series, I explained how to download the Eclipse Collections library from Maven Central and create collections using Eclipse Collections factories. In part 2, I explained how to add items to and remove items from different collection types. In part 3, I explained how to use converter methods to convert any RichIterable
type to another Collection
or Map
type. In part 4, I will explain how to use several of the most commonly used methods to process information in collections.
Processing Information in Collections
In Eclipse Collections there are several ways to process information in collections. Information contained in collections may be processed eagerly, lazily, serially or in parallel. The default approach available in methods directly on collections is Eager Serial. Eager Serial methods process and return a result immediately executing on the current thread. Lazy Serial and Lazy Parallel processing options are both available by calling either asLazy
or asParallel
on a collection. There is also Eager Parallel processing available, but that is only available via utility classes. Neither Lazy Parallel nor Eager Parallel processing options will be covered in this blog. There is some discussion of both of these approaches in the following blog.
In order to understand the difference between eager and lazy methods, I recommend reading the following blog. This blog requires no knowledge of Eclipse Collections types or method names and presents eager alternatives to Java Stream methods using the same names as the lazy Java Stream methods (e.g. filter
, map
).
Eager processing is easier to learn and understand than lazy processing. If you can write a for
loop or while
loop that does something for each element of a collection, then you likely already understand eager processing.
Basic Collection Processing
The following are the basic set of collection operations developers use often with Eclipse Collections:
The basic operations, or iteration patterns, are provided by the method names in the middle, above, in Eclipse Collections. The types on the right are the parameter types accepted by the methods in the middle. The names used in Eclipse Collections are inspired by the method names used in the collections framework in the Smalltalk programming language.
Section Links
The following links will take you to different sections in this blog. There is a link back to here at the end of each of the sections.
What’s in a name?
There are an alternate set of names provided for the equivalent set of operations on Java Streams. If you are familiar with the methods available on Java Streams, this translation guide should help you.
Procedures, Predicates, and Functions
Each of the methods above on a collection type will take some Functional Interface. A Functional Interface in Java is defined as an interface that has at most a single abstract method. A Functional Interface can be represented syntactically as a lambda, a method reference, or as a named or anonymous inner class.
The three primary Functional Interface types used by the basic collection operations in Eclipse Collections are Procedure
, Predicate
, and Function
.
The following interoperability exists between Eclipse Collections Functional Interface types and JDK Functional Interface types.
Procedure
extendsjava.util.function.Consumer
Predicate
extendsjava.util.function.Predicate
Function
extendsjava.util.function.Function
The interop only works in one direction. Eclipse Collections Functional Interface types can be used with any methods in Java Stream that take Consumer
, Predicate
, or Function
. The reverse is not true. JDK Functional Interface Types will not work directly with methods that require the Eclipse Collections Functional Interface types.
Operation: Do
The most basic iteration pattern is the one named forEach
, where you do something specified in the form of a Procedure
for every element in a collection. The method forEach
returns void
, so most often this method will cause some form of mutation to occur. The simplest example leveraging forEach
is printing elements of a collection to System.out
.
Examples of forEach
// Object Collections
Lists.mutable.with(1, 2, 3).forEach(System.out::println);
Sets.mutable.with(1, 2, 3).forEach(System.out::println);
Bags.mutable.with(1, 2, 3).forEach(System.out::println);
Stacks.mutable.with(1, 2, 3).forEach(System.out::println);
// Primitive Collections
IntLists.mutable.with(1, 2, 3).forEach(System.out::println);
IntSets.mutable.with(1, 2, 3).forEach(System.out::println);
IntBags.mutable.with(1, 2, 3).forEach(System.out::println);
IntStacks.mutable.with(1, 2, 3).forEach(System.out::println);
Examples of each
There is a shorter synonym for forEach
, named each
.
// Object Collections
Lists.mutable.with(1, 2, 3).each(System.out::println);
Sets.mutable.with(1, 2, 3).each(System.out::println);
Bags.mutable.with(1, 2, 3).each(System.out::println);
Stacks.mutable.with(1, 2, 3).each(System.out::println);
// Primitive Collections
IntLists.mutable.with(1, 2, 3).each(System.out::println);
IntSets.mutable.with(1, 2, 3).each(System.out::println);
IntBags.mutable.with(1, 2, 3).each(System.out::println);
IntStacks.mutable.with(1, 2, 3).each(System.out::println);
Handling Exceptions
Sometimes when you do something, something bad can happen, in the form of a checked Exception
being thrown. The method named throwing
in the Procedures
class in Eclipse Collections can help you adapt a Procedure
implementation to handle a check Exception
and re-throw a RuntimeException
if an exception occurs.
Examples of handling checked exceptions
@Test
public void forEachPrintToSystemOut()
{
// Object Collections
Appendable builder = new StringBuilder();
// Note: Appendable append throws IOException
Procedure<String> throwing = Procedures.throwing(builder::append);
Lists.mutable.with("1", "2", "3").each(throwing);
Sets.mutable.with("1", "2", "3").forEach(throwing);
Bags.mutable.with("1", "2", "3").each(throwing);
Stacks.mutable.with("1", "2", "3").forEach(throwing);
Assertions.assertEquals(
"111122223333",
Strings.asChars(builder.toString())
.toSortedList()
.makeString(""));
}
You can also customize the RuntimException
that is thrown using an overloaded form of Procedures.throwing
.
Example returning a custom RuntimeException
@Test
public void forEachPrintToSystemOut()
{
// Object Collections
Appendable builder = new StringBuilder();
// Note: Appendable append throws IOException
Procedure<String> throwing = Procedures.throwing(
builder::append,
(each, exception) -> new RuntimeException(exception));
Lists.mutable.with("1", "2", "3").forEach(throwing);
Sets.mutable.with("1", "2", "3").each(throwing);
Bags.mutable.with("1", "2", "3").forEach(throwing);
Stacks.mutable.with("1", "2", "3").each(throwing);
Assertions.assertEquals(
"111122223333",
Strings.asChars(builder.toString())
.toSortedList()
.makeString(""));
}
The following blog goes into more detail about handling exceptions during iteration with Eclipse Collections.
FizzBuzz (🥤🐝) with forEach
using CaseProcedure
There is a Procedure
named CaseProcedure
that can be used for routing elements to different Procedures based on Predicates. The following example demonstrates a solution to the classic FizzBuzz problem using forEach
with CaseProcedure
. To make the code more fun, and easier to read, I have replaced Fizz with the Soda emoji (🥤), and Buzz with the Bee emoji (🐝).
@Test
public void forEachFizzBuzz()
{
MutableList<String> list = Lists.mutable.empty();
Interval.oneTo(15).forEach(
new CaseProcedure<Integer>(e -> list.add(e.toString()))
.addCase(i -> i % 15 == 0, e -> list.add("🥤🐝"))
.addCase(i -> i % 3 == 0, e -> list.add("🥤"))
.addCase(i -> i % 5 == 0, e -> list.add("🐝")));
Assertions.assertEquals(
"1, 2, 🥤, 4, 🐝, 🥤, 7, 8, 🥤, 🐝, 11, 🥤, 13, 14, 🥤🐝",
list.makeString());
}
FizzBuzz (🥤🐝) with forEach
using an IntCaseProcedure
In this blog I will occasionally demonstrate how a method works on primitive collections. There is a type IntInterval
, which extends ImmutableIntList
and defines a forEach
method which takes an IntProcedure
as a parameter. There is also an IntCaseProcedure
type, which can be used to solve the FizzBuzz problem without boxing any primitive int
values as Integer
objects.
@Test
public void forEachFizzBuzzECPrimitive()
{
MutableList<String> list = Lists.mutable.empty();
IntInterval.oneTo(15).forEach(
new IntCaseProcedure(i -> list.add(Integer.toString(i)))
.addCase(i -> i % 15 == 0, e -> list.add("🥤🐝"))
.addCase(i -> i % 3 == 0, e -> list.add("🥤"))
.addCase(i -> i % 5 == 0, e -> list.add("🐝")));
Assertions.assertEquals(
"1, 2, 🥤, 4, 🐝, 🥤, 7, 8, 🥤, 🐝, 11, 🥤, 13, 14, 🥤🐝",
list.makeString());
}
Symmetry between object and primitive collections
There is pretty good symmetry between the object and primitive collections in Eclipse Collections. When we discover an asymmetry in an API, we sometimes work to correct it. Other times we do not. There is a cost/benefit to providing good symmetry. Sometimes the cost is worth the benefit. Sometimes, it is not. In the case of IntCaseProcedure
, we thought there was a benefit to providing the same Predicate
-> Procedure
routing capability for both object and primitive forEach
.
Other forms of forEach
There are other forms of forEach
available in Eclipse Collections. Any time you see forEach
in a method name, there are a common set of traits shared with other forEach
methods. These traits are that 1) the return type of forEach
is void
, and 2) all elements of the collection will be visited.
The following blog has more details and many more examples of forEach
and another method named injectInto
.
I will not be covering injectInto
in this blog. There is another blog dedicated to injectInto
. It demonstrates how injectInto
can be used to define many other methods on the Eclipse Collections API.
Operation: Filter
There are three methods that you can use to filter data in Eclipse Collections. The methods are select
, reject
, and partition
. Each of these eager methods have covariant overrides on each of the types in the type hierarchy. Most of the eager methods that return some Collection type will have a covariant override in Eclipse Collections. This means if you use select
or reject
on a MutableList
, you get back a MutableList
. If you use them on an ImmutableList
, you get back an ImmutableList
. MutableSet
will return MutableSet
. MutableBag
will return MutableBag
. This is one of the many benefits of having eager methods directly on collection types. The collection type knows its type and can determine the best type to return and how to optimize any algorithms.
The method select
filters inclusively. This means elements which respond true
when the specific Predicate
is evaluated will be included in the result collection.
The method reject
filters exclusively. This means elements which respond false when the specific Predicate
is evaluated will be included in the result collection.
The method partition
splits the results into a PartitionIterable
which contains both selected
and rejected
elements. The return type for partition
will be more specific based on the context type. If partition
is called on a MutableList
, it will return a PartitionMutableList
. MutableSet
returns a PartitionMutableSet
. MutableBag
returns a PartitionMutableBag
.
There are other methods that perform filtering, all prefixed with select
. For example, there are methods named selectInstancesOf
, selectByOccurrences
, selectUnique
, selectDuplicates
.
LegoBrick Use Case
I built a simple and fun use case to demonstrate filtering and the other remaining collection operations. I am using Java 20 with Java records, Pattern Matching for Switch, Java Text blocks, and Java Enums in this use case. The use case is generating and filtering Lego Bricks. A LegoBrick
has a BrickType
, Color
, and Dimensions
. I use emojis to display the topView
, side widthView
, and side lengthView
.
public enum BrickType
{
BRICK(Sizes.MULTIPLE),
PLATE(Sizes.MULTIPLE),
CORNER_BRICK(Sizes.ONE),
CORNER_PLATE(Sizes.ONE),
GRILL(Sizes.MULTIPLE),
SLOPE_BRICK(Sizes.MULTIPLE),
SLOPE_BRICK_OUTSIDE_CORNER(Sizes.ONE),
TILE(Sizes.MULTIPLE),
PLATE_ROUND(Sizes.MULTIPLE);
private Sizes sizes;
BrickType(Sizes sizes)
{
this.sizes = sizes;
}
public boolean hasMultipleSizes()
{
return this.sizes == Sizes.MULTIPLE;
}
public enum Sizes
{
ONE, MULTIPLE;
}
}
import org.eclipse.collections.api.factory.Sets;
import org.eclipse.collections.api.set.ImmutableSet;
public enum Color
{
RED("🟥", "🔴"),
YELLOW("🟨", "🟡"),
BLUE("🟦", "🔵"),
GREEN("🟩", "🟢"),
WHITE("⬜️", "⚪️"),
BLACK("⬛", "⚫️");
private static final ImmutableSet<Color> ALL =
Sets.immutable.with(Color.values());
private final String square;
private final String circle;
Color(String square, String circle)
{
this.square = square;
this.circle = circle;
}
public static ImmutableSet<Color> all()
{
return ALL;
}
public String getSquare()
{
return square;
}
public String getCircle()
{
return circle;
}
}
public record Dimensions(int width, int length)
{
}
import org.eclipse.collections.api.bag.Bag;
import org.eclipse.collections.api.bag.MutableBag;
import org.eclipse.collections.api.factory.Bags;
import org.eclipse.collections.api.set.SetIterable;
import org.eclipse.collections.impl.factory.Sets;
import org.eclipse.collections.impl.list.primitive.IntInterval;
public record LegoBrick(BrickType type, Color color, Dimensions dimensions)
{
public LegoBrick(BrickType type, Color color, int width, int length)
{
this(type, color, new Dimensions(width, length));
}
public static Bag<LegoBrick> generateMultipleSizedBricks(
int count,
SetIterable<Color> colors,
SetIterable<Dimensions> sizes)
{
MutableBag<LegoBrick> bag = Bags.mutable.empty();
var cartesianProduct = Sets.immutable.with(BrickType.values())
.select(BrickType::hasMultipleSizes)
.cartesianProduct(colors);
cartesianProduct.each(pair -> sizes.forEach(dimensions ->
bag.addOccurrences(
new LegoBrick(pair.getOne(), pair.getTwo(), dimensions),
count)));
return bag;
}
public String topView()
{
return IntInterval.oneTo(this.dimensions.width())
.collect(i -> this.topViewShape().repeat(this.length()))
.makeString(System.lineSeparator());
}
private String topViewShape()
{
return switch (this.type)
{
case TILE, SLOPE_BRICK, SLOPE_BRICK_OUTSIDE_CORNER, GRILL ->
this.color.getSquare();
default -> this.color.getCircle();
};
}
@Override
public String toString()
{
return this.topView();
}
public String lengthView()
{
return switch (this.type)
{
case BRICK -> IntInterval.oneTo(2)
.collect(i -> this.sideViewShape().repeat(this.length()))
.makeString(System.lineSeparator());
default -> this.sideViewShape().repeat(this.length());
};
}
public String widthView()
{
return switch (this.type)
{
case BRICK -> IntInterval.oneTo(2)
.collect(i -> this.sideViewShape().repeat(this.width()))
.makeString(System.lineSeparator());
default -> this.sideViewShape().repeat(this.width());
};
}
private String sideViewShape()
{
return this.color.getSquare();
}
public int length()
{
return this.dimensions.length();
}
public int width()
{
return this.dimensions.width();
}
}
For each code example in the rest of this blog, I will include JUnit tests. The setup code for each of the LegoBrickTest
tests is as follows. Each unit test will use the bricks
field which is an ImmutableBag<LegoBrick>
. The setUp
code generates LegoBrick
instances for all of the multiple sized BrickType
and for all instances of the Color
enum.
public class LegoBrickTest
{
private ImmutableBag<LegoBrick> bricks;
@BeforeEach
void setUp()
{
ImmutableSet<Dimensions> sizes =
Sets.immutable.with(
new Dimensions(1, 2),
new Dimensions(2, 2),
new Dimensions(1, 3),
new Dimensions(2, 3),
new Dimensions(2, 4));
Bag<LegoBrick> bag =
LegoBrick.generateMultipleSizedBricks(5, Color.all(), sizes);
this.bricks = bag.toImmutableBag();
}
}
Examples of select
If you want to filter a collection inclusively with a Predicate
, use the select
method. Here’s the first example from the LegoBrick
class, where I generate and filter LegoBrick
instances using select
followed by cartesianProduct
to combine BrickType
with Color
.
public static Bag<LegoBrick> generateMultipleSizedBricks(
int count,
SetIterable<Color> colors,
SetIterable<Dimensions> sizes)
{
MutableBag<LegoBrick> bag = Bags.mutable.empty();
var cartesianProduct = Sets.immutable.with(BrickType.values())
.select(BrickType::hasMultipleSizes)
.cartesianProduct(colors);
cartesianProduct.each(pair -> sizes.forEach(dimensions ->
bag.addOccurrences(
new LegoBrick(pair.getOne(), pair.getTwo(), dimensions),
count)));
return bag;
}
I first create an ImmutableSet<BrickType>
using all the values in the BrickType
enum. Then I filter inclusively all of the BrickType
instances that have multiple sizes using select(BrickType::hasMultipleSizes)
. The call to cartesianProduct
takes the filtered BrickType
instances and combines them with all the Color
instances. The final each call generates LegoBrick
instances for all of the specified Dimensions
and the requested count
.
The next example uses multiple calls to select
, in order to filter a specific set of LegoBrick
instances.
@Test
public void selectRedPlateBricksWidthTwo()
{
ImmutableBag<LegoBrick> select =
this.bricks.select(brick -> brick.width() == 2)
.select(brick -> brick.color() == Color.RED)
.select(brick -> brick.type() == BrickType.PLATE);
MutableSortedSet<LegoBrick> set =
select.toSortedSetBy(LegoBrick::length);
String expectedBricks = """
🔴🔴
🔴🔴,
🔴🔴🔴
🔴🔴🔴,
🔴🔴🔴🔴
🔴🔴🔴🔴""";
Assertions.assertEquals(expectedBricks, set.makeString(",\n"));
}
This code filters out all bricks with width size 2, color RED
, that have a BrickType
of PLATE
. The return result of this is an ImmutableBag<LegoBrick>
, which is the same type as the source this.bricks
. Since select on ImmutableBag
is eager, each call to select
here creates a new ImmutableBag
. The excess copying could be avoided by either using asLazy
, or combining all of the individual select calls into a single Predicate
using ands. The following code shows how asLazy
can be used instead.
LazyIterable<LegoBrick> select =
this.bricks.asLazy()
.select(brick -> brick.width() == 2)
.select(brick -> brick.color() == Color.RED)
.select(brick -> brick.type() == BrickType.PLATE);
MutableSortedSet<LegoBrick> set =
select.toSortedSetBy(LegoBrick::length);
Notice the return type for select here now returns LazyIterable
. The code here does not do any computation until the call to toSortedSetBy
.
Examples of reject
If you want to filter a collection exclusively with a Predicate
, use the reject
method. The following code shows reject
being used to exclusively filter some LegoBrick
instances.
@Test
public void selectTilesAndRejectLessThanLengthFour()
{
ImmutableBag<LegoBrick> select = this.bricks
.select(brick -> brick.type() == BrickType.TILE);
ImmutableBag<LegoBrick> reject = select
.reject(brick -> brick.length() < 4);
MutableSortedSet<LegoBrick> bricks =
reject.toSortedSetBy(LegoBrick::color);
String expectedBricks = """
🟥🟥🟥🟥
🟥🟥🟥🟥,
🟨🟨🟨🟨
🟨🟨🟨🟨,
🟦🟦🟦🟦
🟦🟦🟦🟦,
🟩🟩🟩🟩
🟩🟩🟩🟩,
⬜️⬜️⬜️⬜️
⬜️⬜️⬜️⬜️,
⬛⬛⬛⬛
⬛⬛⬛⬛""";
Assertions.assertEquals(expectedBricks, bricks.makeString(",\n"));
}
In this code, any LegoBrick
instances less than four in length are excluded. The reject
method returns an ImmutableBag
when called on an ImmutableBag
. The reject
method is covariantly overridden just like select
.
This code can be made lazy by calling asLazy
. Notice what happens to the return type when you call asLazy
. The result of the code is the same whether it is done eagerly or lazily. The amount of computation and performance may differ.
@Test
public void selectTilesAndRejectLessThanLengthFour()
{
LazyIterable<LegoBrick> select = this.bricks.asLazy()
.select(brick -> brick.type() == BrickType.TILE);
LazyIterable<LegoBrick> reject = select
.reject(brick -> brick.length() < 4);
MutableSortedSet<LegoBrick> bricks =
reject.toSortedSetBy(LegoBrick::color);
String expectedBricks = """
🟥🟥🟥🟥
🟥🟥🟥🟥,
🟨🟨🟨🟨
🟨🟨🟨🟨,
🟦🟦🟦🟦
🟦🟦🟦🟦,
🟩🟩🟩🟩
🟩🟩🟩🟩,
⬜️⬜️⬜️⬜️
⬜️⬜️⬜️⬜️,
⬛⬛⬛⬛
⬛⬛⬛⬛""";
Assertions.assertEquals(expectedBricks, bricks.makeString(",\n"));
}
Example of partition
If you want to split a collection based on Predicate
, use the method partition
. This has the effect of simultaneously filtering inclusively and exclusively in a single pass iteration. Note that partition
is a terminal operation and must return a result, so it cannot be accomplished lazily.
The following code shows how to partition LegoBrick
instances using a switch
expression with two groups of three colors each.
@Test
public void selectRejectPartition()
{
PartitionMutableSortedSet<LegoBrick> bricks =
this.bricks.select(brick -> brick.type() == BrickType.TILE)
.reject(brick -> brick.length() < 4)
.toSortedSetBy(LegoBrick::color)
.partition(brick -> switch (brick.color())
{
case GREEN, WHITE, YELLOW -> true;
case BLUE, RED, BLACK -> false;
});
String selectedBricks = """
🟨🟨🟨🟨
🟨🟨🟨🟨,
🟩🟩🟩🟩
🟩🟩🟩🟩,
⬜️⬜️⬜️⬜️
⬜️⬜️⬜️⬜️""";
Assertions.assertEquals(
selectedBricks,
bricks.getSelected().makeString(",\n"));
String rejectedBricks = """
🟥🟥🟥🟥
🟥🟥🟥🟥,
🟦🟦🟦🟦
🟦🟦🟦🟦,
⬛⬛⬛⬛
⬛⬛⬛⬛""";
Assertions.assertEquals(
rejectedBricks,
bricks.getRejected().makeString(",\n"));
}
The return type of the method partition
is covariant. So for the MutableSortedSet
that is returned by toSortedSetBy
, the method partition
retuns a PartitionMutableSortedSet
. Every PartitionIterable
subtype has getSelected
and getRejected
methods which are also covariant.
I’ve covered several forms of filtering using Eclipse Collections. You can find more examples of filtering in the following blog.
The following blog has additional examples about partitioning.
Operation: Transform
The method name for a transformation operation in Eclipse Collections is collect
. In Java Streams, the equivalent method name is map
. The method collect
takes a Function
as a parameter and will transform one type (e.g. Integer
) to another type (e.g. String
). I’m going to start off by showing how to use collect to solve the FizzBuzz problem I solved earlier in this blog with forEach
.
FizzBuzz (🥤🐝) with collect
using CaseFunction
FizzBuzz is a transformation problem. You need to convert a range of 100 int
values to String
values. The result for each int will depend on if it is divisible by 3, divisible by 5, divisible by both 3 and 5, or divisible by neither. The following code example shows how to use a CaseFunction
with the collect
method on an Interval
to solve the FizzBuzz problem.
@Test
public void collectFizzBuzz()
{
LazyIterable<String> iterable = Interval.oneTo(15)
.collect(new CaseFunction<Integer, String>(Object::toString)
.addCase(i -> i % 15 == 0, e -> "🥤🐝")
.addCase(i -> i % 3 == 0, e -> "🥤")
.addCase(i -> i % 5 == 0, e -> "🐝"));
Assertions.assertEquals(
"1, 2, 🥤, 4, 🐝, 🥤, 7, 8, 🥤, 🐝, 11, 🥤, 13, 14, 🥤🐝",
iterable.makeString());
}
The collect
method on Interval
is lazy, and it returns LazyIterable
.
FizzBuzz (🥤🐝) with collect
using IntCaseFunction
The following code example shows how to use a IntCaseFunction
with the collect
method on an IntInterval
to solve the FizzBuzz problem.
@Test
public void collectFizzBuzz()
{
ImmutableList<String> list = IntInterval.oneTo(15)
.collect(new IntCaseFunction<>(Integer::toString)
.addCase(i -> i % 15 == 0, e -> "🥤🐝")
.addCase(i -> i % 3 == 0, e -> "🥤")
.addCase(i -> i % 5 == 0, e -> "🐝"));
Assertions.assertEquals(
"1, 2, 🥤, 4, 🐝, 🥤, 7, 8, 🥤, 🐝, 11, 🥤, 13, 14, 🥤🐝",
list.makeString());
}
Notice that the collect method on IntInterval
is eager, and it returns an ImmutableList
. This difference between Interval
and IntInterval
was an evolutionary design decision. Interval
was created well before Eclipse Collections had ImmutableCollection
and primitive collection types. We wanted Interval
to have a rich set of methods like the other MutableCollection
types at the time, without implementing those types directly, as it is an immutable type.
FizzBuzz (🥤🐝) with collect
using Pattern Matching for Switch
Since I am using Java 20 to compile and run the code examples in this blog, I thought it would be interesting to share another example of Pattern Matching for Switch. I learned how to solve FizzBuzz using this approach from Vladimir Zakharov who tweeted a similar solution using IntStream
.
@Test
public void collectFizzBuzzUsingPatternMatchingForSwitch()
{
ImmutableList<String> list = IntInterval.oneTo(15)
.collect(each -> switch ((Integer) each)
{
case Integer j when j % 15 == 0 -> "🥤🐝";
case Integer j when j % 3 == 0 -> "🥤";
case Integer j when j % 5 == 0 -> "🐝";
default -> String.valueOf(each);
});
Assertions.assertEquals(
"1, 2, 🥤, 4, 🐝, 🥤, 7, 8, 🥤, 🐝, 11, 🥤, 13, 14, 🥤🐝",
list.makeString());
}
This was the first example I have tried where I used the when
clause in a switch
expression. There is a clever trick that Vlad came up with here, which was the cast the int
value named each
to Integer
in the switch so it could be matched in each case
. The code for this winds up being slightly simpler using Interval
, which already has boxed Integer
values, so doesn’t require a cast in the switch
.
@Test
public void collectFizzBuzzUsingPatternMatchingForSwitch()
{
LazyIterable<Object> iterable = Interval.oneTo(15)
.collect(each -> switch (each)
{
case Integer j when j % 15 == 0 -> "🥤🐝";
case Integer j when j % 3 == 0 -> "🥤";
case Integer j when j % 5 == 0 -> "🐝";
default -> String.valueOf(each);
});
Assertions.assertEquals(
"1, 2, 🥤, 4, 🐝, 🥤, 7, 8, 🥤, 🐝, 11, 🥤, 13, 14, 🥤🐝",
iterable.makeString());
}
Examples of collect
with LegoBrick use case
Now that I have solved FizzBuzz with forEach
and collect
, let’s move on to using collect
with the LegoBrick
use case.
This is the field and setup code for the source collection we built in the LegoBrickTest
. The type the bricks are stored in is an ImmutableBag<LegoBrick>
.
private ImmutableBag<LegoBrick> bricks;
@BeforeEach
void setUp()
{
ImmutableSet<Dimensions> sizes = Sets.immutable.with(
new Dimensions(1, 2),
new Dimensions(2, 2),
new Dimensions(1, 3),
new Dimensions(2, 3),
new Dimensions(2, 4));
Bag<LegoBrick> bag =
LegoBrick.generateMultipleSizedBricks(5, Color.all(), sizes);
this.bricks = bag.toImmutableBag();
}
The first thing I will do with the bricks is collect
all of the colors of the bricks. ImmutableBag
allows duplicate instances, so there will be duplicate Color
instances.
@Test
public void collectColors()
{
ImmutableBag<Color> colors =
this.bricks.collect(LegoBrick::color);
MutableBag<Color> expected = Bags.mutable.empty();
expected.addOccurrences(Color.BLUE, 150);
expected.addOccurrences(Color.BLACK, 150);
expected.addOccurrences(Color.GREEN, 150);
expected.addOccurrences(Color.WHITE, 150);
expected.addOccurrences(Color.YELLOW, 150);
expected.addOccurrences(Color.RED, 150);
Assertions.assertEquals(expected, colors);
}
The collect
method is eager and covariant on an ImmutableBag
and the return result is an ImmutableBag<Color>
. A Bag
is very useful for counting, and we can see based on the LegoBrick
instances we created there are 150 bricks of each Color
.
We can also collect
all of the Dimensions
of the bricks.
@Test
public void collectDimensions()
{
ImmutableBag<Dimensions> dimensions =
this.bricks.collect(LegoBrick::dimensions);
MutableBag<Dimensions> expected = Bags.mutable.empty();
expected.addOccurrences(new Dimensions(1, 3), 180);
expected.addOccurrences(new Dimensions(1, 2), 180);
expected.addOccurrences(new Dimensions(2, 2), 180);
expected.addOccurrences(new Dimensions(2, 3), 180);
expected.addOccurrences(new Dimensions(2, 4), 180);
Assertions.assertEquals(expected, dimensions);
}
There are 5 unique Dimensions
that were generated, with a count of 180
of each.
The final transformation using collect
I will demonstrate will be converting from an Object
type to a primitive type.
@Test
public void collectWidthTimesLength()
{
ImmutableIntBag dimensions =
this.bricks.collectInt(each -> each.width() * each.length());
MutableIntBag expected = IntBags.mutable.empty();
expected.addOccurrences(2, 180);
expected.addOccurrences(3, 180);
expected.addOccurrences(4, 180);
expected.addOccurrences(6, 180);
expected.addOccurrences(8, 180);
Assertions.assertEquals(expected, dimensions);
}
In this code I used a method named collectInt
to transform each LegoBrick
to its width()
* length()
. There are primitive forms of collect
for every primitive type (boolean
, byte
, char
, short
, int
, float
, long
, double
) . Each primitive form return type is also covariant, so you will notice the return type for collectInt
on an ImmutableBag<LegoBrick>
is an ImmutableIntBag
.
There are a large number of possible variations for return types for collect
, if a target collection is used as a second parameter with the available method overloads.
For example, I can collect
the Dimensions
from each LegoBrick
instance in the ImmutableBag<LegoBrick>
into an empty MutableSet<Dimensions>
.
@Test
public void collectDimensionsToTargetSet()
{
MutableSet<Dimensions> dimensions =
this.bricks.collect(
LegoBrick::dimensions,
Sets.mutable.empty());
Set<Dimensions> expected = Sets.mutable.with(
new Dimensions(1, 3),
new Dimensions(1, 2),
new Dimensions(2, 2),
new Dimensions(2, 3),
new Dimensions(2, 4));
Assertions.assertEquals(expected, dimensions);
}
Here we can see there are 5 unique dimensions as the expected result.
Method overloads with target collection parameters are also available for the primitive collect
methods. I can collect the width()
* length()
results from each LegoBrick
instance in the ImmutableBag<LegoBrick>
to an empty MutableIntSet
.
@Test
public void collectWidthTimesLengthToTargetSet()
{
MutableIntSet dimensions =
this.bricks.collectInt(
each -> each.width() * each.length(),
IntSets.mutable.empty());
MutableIntSet expected = IntSets.mutable.with(2, 3, 4, 6, 8);
Assertions.assertEquals(expected, dimensions);
}
I’ve covered a number of ways to transform using variations of collect
. There is a special form of collect
which combines filtering with transforming and the method is named collectIf
. I blog about collectIf
and some more examples of collect
in the following blog.
Operation: Find
The method for finding an element of a collection matching a Predicate in Eclipse Collections is detect
. There are forms of detect
that handle situations where there are no matching elements. For these kinds of situations, there are methods named detectIfNone
and detectOptional
.
Examples using detect
The following examples demonstrate using detect
where an element is found that matches the Predicate
, and where no element is found.
@Test
public void detect()
{
LegoBrick detect =
this.bricks.detect(brick -> brick.width() == 1);
Assertions.assertNotNull(detect);
LegoBrick detectMissing =
this.bricks.detect(brick -> brick.width() == 5);
Assertions.assertNull(detectMissing);
}
As can be seen above, detect
returns null
if there is no match.
Examples using detectIfNone
The method detectIfNone
takes a Predicate
and a Function0
as parameters. If no element is found matching the Predicate
, the Function0
is evaluated and the result is returned instead.
@Test
public void detectIfNone()
{
LegoBrick detect =
this.bricks.detectIfNone(
brick -> brick.width() == 1,
() -> new LegoBrick(BrickType.BRICK, Color.RED, 1, 1));
Assertions.assertNotNull(detect);
Assertions.assertNotEquals(
new LegoBrick(BrickType.BRICK, Color.RED, 1, 1),
detect);
LegoBrick detectMissing =
this.bricks.detectIfNone(
brick -> brick.width() == 5,
() -> new LegoBrick(BrickType.BRICK, Color.RED, 1, 1));
Assertions.assertEquals(
new LegoBrick(BrickType.BRICK, Color.RED, 1, 1),
detectMissing);
}
Examples using detectOptional
The method detectOptional
takes a Predicate
and returns an Optional
. I generally prefer detectIfNone
to this method, but this was added for developers who prefer to use Optional
coding patterns.
@Test
public void detectOptional()
{
Optional<LegoBrick> detectOptionalFound =
this.bricks.detectOptional(brick -> brick.width() == 1);
Assertions.assertTrue(detectOptionalFound.isPresent());
Assertions.assertFalse(detectOptionalFound.isEmpty());
Optional<LegoBrick> detectOptionalMissing =
this.bricks.detectOptional(brick -> brick.width() == 5);
Assertions.assertFalse(detectOptionalMissing.isPresent());
Assertions.assertTrue(detectOptionalMissing.isEmpty());
}
Operation: Test
Sometimes it is useful to test whether any, all or none of the elements of a collection match the conditions of a Predicate
. In Eclipse Collections, the methods that support this testing functionality are named anySatisfy
, allSatisfy
, and noneSatisfy
. All three are terminal operations and execute eagerly because their return result is boolean
.
Examples of anySatisfy
The method anySatisfy
will answer true
if any element of the collection satisfies a given condition. The condition is specified in the form of a Predicate
.
The following example tests to see if any bricks match the specified circle colors.
@Test
public void anySatisfy()
{
boolean anySatisfy1 =
this.bricks.anySatisfy(
each -> "🔴".equals(each.color().getCircle()));
Assertions.assertTrue(anySatisfy1);
boolean anySatisfy2 =
this.bricks.anySatisfy(
each -> "🟣".equals(each.color().getCircle()));
Assertions.assertFalse(anySatisfy2);
}
Examples of allSatisfy
The method allSatisfy
will answer true
if all elements of the collection satisfy a given condition. The condition is specified in the form of a Predicate
.
The following example tests to see if all bricks have a color in the two specified color sets.
@Test
public void allSatisfy()
{
String colors1 = "🔴🟡🔵🟢⚪️⚫️";
String colors2 = "🔴🟡🔵";
boolean allSatisfy1 =
this.bricks.allSatisfy(
each -> colors1.contains(each.color().getCircle()));
Assertions.assertTrue(allSatisfy1);
boolean allSatisfy2 =
this.bricks.allSatisfy(
each -> colors2.contains(each.color().getCircle()));
Assertions.assertFalse(allSatisfy2);
}
Examples of noneSatisfy
The method noneSatisfy
will answer true
if none of the elements of the collection satisfy a given condition. The condition is specified in the form of a Predicate
.
The following example tests to see if none of the bricks match the specified circle colors.
@Test
public void noneSatisfy()
{
boolean noneSatisfy1 =
this.bricks.noneSatisfy(
each -> "🔴".equals(each.color().getCircle()));
Assertions.assertFalse(noneSatisfy1);
boolean noneSatisfy2 =
this.bricks.noneSatisfy(
each -> "🟣".equals(each.color().getCircle()));
Assertions.assertTrue(noneSatisfy2);
}
Operation: Count
The method named count
, returns an int
value representing the total number of elements that return true
when evaluating a specified Predicate
. There is also a countBy
method which counts elements and groups them by some specified Function
.
Example of count
The following example counts the number of elements that match the specified circle colors.
@Test
public void count()
{
int count1 =
this.bricks.count(
each -> "🔴".equals(each.color().getCircle()));
Assertions.assertEquals(150, count1);
int count2 =
this.bricks.count(
each -> "🟣".equals(each.color().getCircle()));
Assertions.assertEquals(0, count2);
}
Example of countBy
The following example counts the number of elements by each circle color.
@Test
public void countBy()
{
ImmutableBag<String> countBy =
this.bricks.countBy(each -> each.color().getCircle());
MutableBag<String> expected = Bags.mutable.empty();
expected.addOccurrences("🔵", 150);
expected.addOccurrences("🟢", 150);
expected.addOccurrences("🟡", 150);
expected.addOccurrences("⚪️", 150);
expected.addOccurrences("🔴", 150);
expected.addOccurrences("⚫️", 150);
Assertions.assertEquals(expected, countBy);
}
This series will help you get started
In this four part series, I have introduced topics that will help any developer get started using Eclipse Collections. There is a lot more to learn about using Eclipse Collections. Eclipse Collections has been evolving for almost two decades, and has had features added to meet the needs of production use cases in many Financial Services applications and other application domains. Some developers use Eclipse Collections for the performance optimization and primitive collections. Others use it for the fluent and functional API. Others use it for non-standard collection types like Bag, Multimap, and BiMap. There are many reasons to use Eclipse Collections in an application.
Thank you for reading this blog series. I hope you find this Getting Started series helpful. I hope it will be a good reference for your application development needs with Eclipse Collections. If you’d like to learn more about what Eclipse Collections contains that you will not find in the JDK, then check out the following blog series.
Enjoy!
I am the creator of and a Committer for the Eclipse Collections OSS project which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions.