Java Arrays are like the Seats in a Car

Donald Raab
7 min readJun 20, 2024

--

You can’t drive the seats very far without the rest of the car.

Photo by Vasile Valcan on Unsplash

Splitting a String into an Array

Split a String in Java and we get an array, or more specifically, a String[], which is an array of String.

Here are two examples of String instances split into arrays of String.

String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

So we can split a String into an array. So what can the array do for us? The answer is, not very much.

Seats won’t take you far

The only methods we can call on an array are toString, equals, and hashCode. None of these methods are useful on array. Support is needed from other classes like java.util.Arrays to provide useful versions of these methods.

The following test shows the uselessness of these methods on arrays, and how java.util.Arrays provides useful versions.

@Test
public void testArrayToStringEqualsHashCodeLength()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

Assertions.assertEquals(
"[Ljava.lang.String;@7193666c", nameArray.toString());
Assertions.assertEquals(
"[Ljava.lang.String;@72057ecf", emojiArray.toString());

Assertions.assertEquals(
"[apple, mango, pear]", Arrays.toString(nameArray));
Assertions.assertEquals(
"[🍎, 🥭, 🍐]", Arrays.toString(emojiArray));

String[] nameArray2 = "apple,mango,pear".split(",");
String[] emojiArray2 = "🍎,🥭,🍐".split(",");

Assertions.assertNotEquals(nameArray, nameArray2);
Assertions.assertNotEquals(emojiArray, emojiArray2);

Assertions.assertNotEquals(
nameArray.hashCode(), nameArray2.hashCode());
Assertions.assertNotEquals(
emojiArray.hashCode(), emojiArray2.hashCode());

Assertions.assertTrue(
Arrays.equals(nameArray, nameArray2));
Assertions.assertTrue(
Arrays.equals(emojiArray, emojiArray2));

Assertions.assertEquals(
Arrays.hashCode(nameArray), Arrays.hashCode(nameArray2));
Assertions.assertEquals(
Arrays.hashCode(emojiArray), Arrays.hashCode(emojiArray2));

Assertions.assertEquals(
nameArray.length, nameArray2.length);
Assertions.assertEquals(
emojiArray.length, emojiArray2.length);
}

While Arrays provides methods for toString, equals, and hashCode, there is not much else Arrays provides beyond sort, fill, and binarySearch. This methods, while occasionally useful, are not frequently used methods. A lot more is necessary to provide useful functionality for arrays.

The Classic Array from Smalltalk

Photo by Hoyoun Lee on Unsplash

In the Smalltalk language, Array is a subtype of Collection. This gives Array a wide variety of useful inherited behaviors, in addition to some Array specific ones. I missed having drivable arrays when I started programming in the Java language. The following shows some of the commonly used iteration methods that were available to the Smalltalk Array class.

Smalltalk Array Iteration Methods in a unit test

When I created Eclipse Collections, it was originally named Caramel, which was roughly an acronym for Collection, Array, Map Enumeration Library. My first goal for Caramel was to provide a rich and consistent set of functional iteration methods for Java Collections, Arrays and Maps. Arrays were the least useful of the Collection-like objects in Java. In the early days of Caramel, I created a utility class named ArrayIterate, which provided useful iteration methods for Java arrays.

Turbo-charging Java Arrays with my favorite Smalltalk methods

ArrayIterate adds support for select, reject, collect, detect, injectInto, anySatisfy, allSatisfy, noneSatisfy to Java arrays. These are some of my favorite collection methods that were inspired by the Smalltalk Collection class.

Photo by Kevin Bhagat on Unsplash

The following test shows examples of each of these methods on ArrayIterate working with Java arrays.

@Test
public void myFavoriteSmalltalkMethodsWorkWithJavaArrays()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

MutableList<String> selected =
ArrayIterate.select(nameArray, "mango"::equals);

Assertions.assertEquals(List.of("mango"), selected);

MutableList<String> rejected =
ArrayIterate.reject(emojiArray, "🥭"::equals);

Assertions.assertEquals(List.of("🍎", "🍐"), rejected);

MutableList<String> collected =
ArrayIterate.collect(nameArray, String::toUpperCase);

Assertions.assertEquals(List.of("APPLE", "MANGO", "PEAR"), collected);

String detected =
ArrayIterate.detect(emojiArray, "🍐"::equals);

Assertions.assertEquals("🍐", detected);

MutableList<Object> injected =
ArrayIterate.injectInto(
Lists.mutable.empty(),
nameArray,
MutableList::with);

Assertions.assertEquals(List.of("apple", "mango", "pear"), injected);

Assertions.assertTrue(ArrayIterate.anySatisfy(nameArray, "mango"::equals));
Assertions.assertFalse(ArrayIterate.allSatisfy(emojiArray, "🥭"::equals));
Assertions.assertTrue(ArrayIterate.noneSatisfy(emojiArray, "🍋"::equals));
}

Missing wheels, doors, windows, steering wheel

Arrays are only as useful as seats in a car in Java, so we have to add everything that is missing. This includes basic Collection methods like contains and forEach. While we were at it, we supplemented arrays with more advanced features like forEachWithIndex, forEachInBoth, zip, and zipWithIndex.

ArrayIterate.contains

The simplest iteration method that is available on all Collections and Maps is contains. There is no contains equivalent for arrays. ArrayIterate lets us check if an array contains an Object.

@Test
public void contains()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

Assertions.assertTrue(ArrayIterate.contains(nameArray, "mango"));
Assertions.assertTrue(ArrayIterate.contains(emojiArray, "🥭"));

Assertions.assertFalse(ArrayIterate.contains(nameArray, "lemon"));
Assertions.assertFalse(ArrayIterate.contains(emojiArray, "🍋"));
}

ArrayIterate.forEach

Since Iterable, Collection and Map all got forms of forEach in Java 8, it makes sense for arrays to have forEach support as well. There is a forEach method on ArrayIterate which takes a Functional Interface named Procedure as an argument.

@Test
public void forEach()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

List<String> result = new ArrayList<>();

ArrayIterate.forEach(nameArray, result::add);
Assertions.assertEquals(
List.of("apple", "mango", "pear"), result);

ArrayIterate.forEach(emojiArray, result::add);
Assertions.assertEquals(
List.of("apple", "mango", "pear", "🍎","🥭","🍐"), result);
}

ArrayIterate.forEachWithIndex

Arrays are indexed data structures. While they don’t have get methods like a list, we can reference elements at indexes using array[index] syntax. ArrayIterate has a method name forEachWithIndex which takes an array and a Functional Interface named ObjectIntProcedure as arguments. The following test shows some examples of using forEachWithIndex.

@Test
public void forEachWithIndex()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

List<String> result = new ArrayList<>();

ArrayIterate.forEachWithIndex(nameArray,
(each, index) -> result.add(index, each));
Assertions.assertEquals(
List.of("apple", "mango", "pear"), result);

ArrayIterate.forEachWithIndex(emojiArray,
(each, index) -> result.add(index, each));
Assertions.assertEquals(
List.of("🍎", "🥭", "🍐", "apple", "mango", "pear"), result);
}

ArrayIterate.forEachInBoth

If we have two arrays of the same size, we can iterate over them together using forEachInBoth. The forEachInBoth method takes two arrays and a Functional Interface named Procedure2 as arguments.

@Test
public void forEachInBoth()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

Map<String, String> expected =
Map.of("apple", "🍎", "mango", "🥭", "pear", "🍐");

Map<String, String> result = new LinkedHashMap<>();

ArrayIterate.forEachInBoth(nameArray, emojiArray, result::put);

Assertions.assertEquals(expected, result);
}

ArrayIterate.zip

An alternative to forEachInBoth is the method zip, which can take two arrays and return a MutableList<Pair>.

@Test
public void zip()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

Map<String, String> expected =
Map.of("apple", "🍎", "mango", "🥭", "pear", "🍐");

MutableList<Pair<String, String>> zipped =
ArrayIterate.zip(nameArray, emojiArray);

Map<String, String> result1 =
zipped.toMap(Pair::getOne, Pair::getTwo);
Assertions.assertEquals(expected, result1);

Map<String, String> result2 =
zipped.toMap(Pair::getOne, Pair::getTwo, new LinkedHashMap<>());
Assertions.assertEquals(expected, result2);
}

ArrayIterate.zipWithIndex

In addition to forEachWithIndex, ArrayIterate has support for zipWithIndex, which zips each element with its corresponding index as a Pair<Element, Integer> in a MutableList.

@Test
public void zipWithIndex()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

MutableList<Pair<String, Integer>> zippedNames =
ArrayIterate.zipWithIndex(nameArray);

MutableList<Pair<String, Integer>> zippedEmojis =
ArrayIterate.zipWithIndex(emojiArray);

List<Triple<String, String, Integer>> result =
Lists.mutable.empty();

zippedNames.forEachInBoth(
zippedEmojis,
(pair1, pair2) ->
result.add(
Tuples.triple(
pair1.getOne(),
pair2.getOne(),
pair1.getTwo() + pair2.getTwo())));

List<Triple<String, String, Integer>> expected = List.of(
Tuples.triple("apple", "🍎", 0),
Tuples.triple("mango", "🥭", 2),
Tuples.triple("pear", "🍐", 4));

Assertions.assertEquals(expected, result);
}

ArrayIterate.makeString

The last method of ArrayIterate we will look at is makeString. We used split on a comma separated String to create two arrays. We will use makeString to create two comma separated String instances.

@Test
public void makeString()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

String names = ArrayIterate.makeString(nameArray, ",");
String emojis = ArrayIterate.makeString(emojiArray, ",");

Assertions.assertEquals("apple,mango,pear", names);
Assertions.assertEquals("🍎,🥭,🍐", emojis);
}

Symmetry for the Win

There are many other methods available on ArrayIterate. Instead of listing them all and showing examples, here’s a link to the Javadoc. The symmetry between ArrayIterate and interfaces like MutableList is not perfect, but is very good.

If we want arrays to have the full capabilities of MutableList, we can move from using the ArrayIterate static utility class to wrapping a Java array in an ArrayAdapter. The benefit of the ArrayIterate utility class is that it saves an object creation. ArrayAdapter creates a lightweight wrapper around an array. ArrayAdapter provides perfect symmetry with the MutableList interface, because ArrayAdapter implements the MutableList interface.

Here’s what my favorite Smalltalk methods would look like using an ArrayAdapter instead of ArrayIterate.

@Test
public void myFavoriteSmalltalkMethodsWorkWithArrayAdapters()
{
MutableList<String> nameArray =
ArrayAdapter.adapt("apple,mango,pear".split(","));
MutableList<String> emojiArray =
ArrayAdapter.adapt("🍎,🥭,🍐".split(","));

MutableList<String> selected =
nameArray.select("mango"::equals);

Assertions.assertEquals(List.of("mango"), selected);

MutableList<String> rejected =
emojiArray.reject("🥭"::equals);

Assertions.assertEquals(List.of("🍎", "🍐"), rejected);

MutableList<String> collected =
nameArray.collect(String::toUpperCase);

Assertions.assertEquals(List.of("APPLE", "MANGO", "PEAR"), collected);

String detected =
emojiArray.detect("🍐"::equals);

Assertions.assertEquals("🍐", detected);

MutableList<Object> injected =
nameArray.injectInto(Lists.mutable.empty(), MutableList::with);

Assertions.assertEquals(List.of("apple", "mango", "pear"), injected);

Assertions.assertTrue(nameArray.anySatisfy("mango"::equals));
Assertions.assertFalse(emojiArray.allSatisfy("🥭"::equals));
Assertions.assertTrue(emojiArray.noneSatisfy("🍋"::equals));
}

What about Java Stream?

Since Java 8 was released, we can use Stream to augment Java arrays with useful behaviors. Unfortunately, a Stream can only be used once, and requires more bun methods than ArrayIterate or ArrayAdapter. Using Stream is like renting a new car every time you want to take a drive. Here’s an example of filter and map with Stream to provide the same functionality as select, reject, collect , detect, any/all/noneSatisfy in the example above.

@Test
public void filterMapFindFirstAnyAllNoneWithArrayAsStream()
{
String[] nameArray = "apple,mango,pear".split(",");
String[] emojiArray = "🍎,🥭,🍐".split(",");

List<String> filtered =
Stream.of(nameArray)
.filter("mango"::equals)
.toList();

Assertions.assertEquals(List.of("mango"), filtered);

List<String> filteredNot =
Stream.of(emojiArray)
.filter(Predicate.not("🥭"::equals))
.toList();

Assertions.assertEquals(List.of("🍎", "🍐"), filteredNot);

List<String> mapped =
Stream.of(nameArray)
.map(String::toUpperCase)
.toList();

Assertions.assertEquals(List.of("APPLE", "MANGO", "PEAR"), mapped);

String found =
Stream.of(emojiArray)
.filter("🍐"::equals)
.findFirst()
.orElse(null);

Assertions.assertEquals("🍐", found);

MutableList<String> collected =
Stream.of(nameArray)
.collect(Lists.mutable::empty,
MutableList::with,
MutableList::withAll);

Assertions.assertEquals(List.of("apple", "mango", "pear"), collected);


Assertions.assertTrue(Stream.of(nameArray).anyMatch("mango"::equals));
Assertions.assertFalse(Stream.of(emojiArray).allMatch("🥭"::equals));
Assertions.assertTrue(Stream.of(emojiArray).noneMatch("🍋"::equals));
}

As we can see, for each method call above, we have to rewrap the array in a Stream, and then call a final “bun” method like toList or findFirst to achieve some computed result. ArrayIterate provides the methods directly executing against the array. ArrayAdapter allows for the adapter to be reused as many times as we want.

Final Thoughts

Java arrays may not be the most useful data structures by themselves, but they have gotten a lot of help from some useful friends in Eclipse Collections and Java 8+. The ArrayIterate utility, ArrayAdapter class, and Java Stream all provide useful higher level methods that work with Java arrays.

I hope you find this blog helpful. Next time you have a Java array you need to do something with, remember you have ArrayIterate, ArrayAdapter, and Java Stream available to help you.

Thanks for reading!

I am the creator of and committer for the Eclipse Collections OSS project, which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions.

--

--

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.