Getting Started with Eclipse Collections — Part 4

Donald Raab
20 min readApr 25, 2023

--

Processing information in collections

Photo by Xavi Cabrera on Unsplash

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:

Basic operations (aka iteration patterns) available as methods on 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.

Eclipse Collections and Java Stream equivalent operation names compared

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.

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.

Back to Section Links

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.

Back to Section Links

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.

Back to Section Links

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());
}

Back to Section Links

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);
}

Back to Section Links

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);
}

Back to Section Links

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.

--

--

Donald Raab
Donald Raab

Written by Donald Raab

Java Champion. Creator of the Eclipse Collections OSS Java library (https://github.com/eclipse/eclipse-collections). Inspired by Smalltalk. Opinions are my own.