TransformingCollections

Nicolai Parlog edited this page May 28, 2015 · 3 revisions

Transforming collections are a view onto another collection, making it appear to be of a different parametric type. This can be used to remove optionality from a collection or substitute equals and hashCode.

The feature's concept is described below before examples demonstrates its use. A set of small, self-contained demos showcases it. The comment on the package org.codefx.libfx.collection.transform provides further documentation. The main types to start using this feature are:

  • TransformingList, TransformingSet, TransformingMap and more: let you specify a transformation E <~> X to make, e.g., a Set<E> into a Set<X>
  • EqualityTransformingSet, EqualityTransformingMap: let you substitute equals and hashCode
  • OptionalTransformingCollection, OptionalTransformingList, OptionalTransformingSet: make, e.g., a List<Optional<E>> into a List<E>

I also wrote a blog post about it which presents the general idea, some details and examples. But note that - unlike this article - it will not be updated.

Concept

A transforming collection is a view onto another collection (e.g. list onto list, map onto map, …), which appears to contain elements of a different type (e.g. integers instead of strings).

The view elements are created from the original elements by applying a transformation. This happens on demand so the transforming collection itself is stateless. Being a proper view, all changes to the inner collection as well as to the transforming view are reflected in the other (like, e.g., Map and its entrySet).

Nomenclature

A transforming collection can also be seen as a decorator. I will refer to the decorated collection as the inner collection and it’s generic type accordingly as the inner type. The transforming collection and its generic type are referred to as outer collection and outer type, respectively.

Important Details

Make sure to read the package comment on org.codefx.libfx.collection.transform before using the feature. Doing that now before continuing with the examples would be helpful.

Examples

Transforming collections can be used to achieve different goals. We will cover three distinct possibilities.

Comments like // "[0, 2] ~ [0, 2]" are the output of System.out.println(innerSet + " ~ " + tranformingSet);.

Transforming The Element Type

Let's use a TransformingSet to view a set of strings, which we know to only contain natural numbers, as a set of integers:

Set<String> innerSet = new HashSet<>();
Set<Integer> transformingSet = TransformingCollectionBuilder
	.forInnerAndOuterType(String.class, Integer.class)
	.toOuter(Integer::parseInt)
	.toInner(Object::toString)
	.transformSet(innerSet);
// both sets are initially empty: "[] ~ []"

// now let's add some elements to the inner set	
innerSet.add("0");
innerSet.add("1");
innerSet.add("2");
// these elements can be found in the view: "[0, 1, 2] ~ [0, 1, 2]"

// modifying the view reflects on the inner set
transformingSet.remove(1);
// again, the mutation is visible in both sets: "[0, 2] ~ [0, 2]"

Replacing equals and hashCode

Let’s say you want to use strings as set elements but for comparison you are only interested in their length:

lengthSet = EqualityTransformingCollectionBuilder
	.forType(String.class)
	.withEquals((a, b) -> a.length() == b.length())
	.withHash(String::length)
	.buildSet();

lengthSet.add("a");
lengthSet.add("b");
System.out.println(lengthSet); // "[a]"

Removing Optionality From A Collection

Maybe you’re working with someone who took the idea of using Optional everywhere, ran wild with it and now you’re stuck with a Set<Optional<String>>.

Set<Optional<String>> innerSet = // ... existing set
Set<String> transformingSet =
	new OptionalTransformingSet<String>(innerSet, String.class);

innerSet.add(Optional.empty());
innerSet.add(Optional.of("A"));

// "[Optional.empty, Optional[A]] ~ [null, A]"

Note how the empty Optional is represented as null. This is the default behavior but you can also specify another string as a value for empty optionals:

Set<String> transformingSet =
	new OptionalTransformingSet<String>(innerSet, String.class, "DEFAULT");

innerSet.add(Optional.empty());
innerSet.add(Optional.of("A"));

// "[Optional.empty, Optional[A]] ~ [DEFAULT, A]"