Groovy: Working with Collections - Part 1

Abstract

Groovy is a dynamic language on Java platform. Groovy provides extensive support for working with collection, with native support for list and map literals.In this article, I will explore the options for working with collections effectively. We will explore various collection types, internal iterators, map reduce methods, method chaining to using Java 8 streams API. Also we would see how Java collections get that extra power when they enter the Groovy world.

Working with index based collections

Creating collection instances

Groovy provides native support for list types.

def numbers = [3, 2, 4, 1]
def developers = ['Raj', 'Reena', 'John']
assert numbers.class == java.util.ArrayList
assert developers.class == ArrayList
def emptyList = []

In the above example, we created a variable numbers and assigned a list value to it. In Groovy, lists are created using [] syntax. The assert statements prove that by default, Groovy creates instances of java.util.ArrayList. If you want to work with traditional Array type, the following code shows you how to achieve it.

def numbersArray = numbers as int[]
assert numbersArray.class == int[]
def namesArray = ['Raj', 'Reena', 'John'] as String[]
assert namesArray.class == String[]

The as keyword can also be used in situations where instead of ArrayList, you want to create other collection types, such as Set or alternate List implementations, such as LinkedList.

def setOfNumbers = [1, 2, 3, 1] as Set
println setOfNumbers // [1, 2, 3]
assert setOfNumbers.class == LinkedHashSet
def numbersLinkedList = numbers as LinkedList
assert numbersLinkedList.class == LinkedList

Working with elements

The following code snippet shows how to add elements, remove elements and access an element of a collection.

def numbers = [1, 2, 3]
numbers.add(4) 
println numbers // [1, 2, 3, 4]
numbers << 5
println numbers // [1, 2, 3, 4, 5]
println numbers.get(1) // 2
println numbers[1] // 2
println numbers.remove(1) // 2 - removes item at index 1
println numbers // [1, 3, 4, 5]

Groovy supports adding elements to a collection using << operator. Similarly one can use [] operator to access the element at a specified index.

Iterating through elements

Groovy provides internal iterators each() and eachWithIndex() for collections.

def numbers = [3, 2, 4, 1]
numbers.each{ number ->
	println number
}
numbers.eachWithIndex { number, index ->
	println "${index + 1}. $number"
}

The method each accepts a closure with single argument. For every element in the collection, the closure will be invoked with the current element as the argument. In the above example, I have used an anonymous closure. However you could use the closure that you have already created too.

def printNumber = { number ->
	println number
}
def printNumberWithIndex = { number, index ->
	println "${index + 1}. $number"
}
numbers.each(printNumber)
numbers.eachWithIndex(printNumberWithIndex)

We have already realised that Groovy uses Java collections API under the hood. In the above example, numbers is an instance of java.util.ArrayList. However ArrayList does not have methods each and eachWithIndex defined within. Groovy enriches Java classes with additional methods for the convenience of developers.

Merging collections

Java's Collection interface declares a method addAll, which accepts a Collection object as an argument. Groovy improves on this by providing 3 additional flavours of addAll, which accept an instance of java.lang.Iterable, java.util.Iterator, and array of Object respectively.

def numbers = [1, 2, 3]
numbers.addAll([4,5])
println numbers // [1, 2, 3, 4, 5]
def moreNumbers = [6, 7] as Integer[]
numbers.addAll(moreNumbers) 
println numbers // [1, 2, 3, 4, 5, 6, 7]

In the above example, the original list is getting modified, which might not be always desirable. In such cases, you could write your code as follows.

def list1 = [1, 2, 3]
def list2 = [4, 5, 6]
def combined = list1 + list2
println list1
println list2
println combined

Another interesting aspect of + operator is that you could also use this to add an element to a list, without altering the original list.

def original = [1, 2, 3]
def newList = original + 4
println newList // [1, 2, 3, 4]
println original // [1, 2, 3

Groovy also provides - operator, which can be used to create a collection with few elements removed from the original collection.

Search items in a collection

Often we need to check if our collection contains a specific element. Groovy provides some convenience methods like find and findAll. Let us go ahead and explore them.

def numbers = [2, 3, 1, 4]
println numbers.find({ it == 1}) // 1
println numbers.find({ it == 10}) // null

Since the find method takes a closure an the argument, we need not restrict ourselves to equality checks.

def numbers = [2, 3, 1, 4]
def even = { it % 2 == 0}
def greaterThan2 = { it > 2 }
println numbers.find(even) // 2
println numbers.find(greaterThan2) // 3

Note that here, find method returned the first element from the list that matched the criterion. If you want to find all the matching elements, use findAll method.

println numbers.findAll(even) // [2, 4]
println numbers.findAll(greaterThan2) // [3, 4]

Map operations

Groovy provides a method named collect for performing map operations. The collect method applies the specified transformation opertaion on each of the items in the collection and produces a new collection.

def numbers = [3, 2, 1, 4]
println numbers.collect {
	it + 1
} // [4, 3, 2, 5]
def multiplyBy2 = { number ->
	number * 2
} 
println numbers.collect(multiplyBy2)
// [6, 4, 2, 8]

Another common use case for collect method is when you want to obtain a collection of a property from the collection of objects. For example, you might want get the list of names from a list of Person objects.

class Person{
	String name
	int age
}
def people = [
	new Person(name: 'Raj', age: 30),
	new Person(name:'Reema', age: 25),
	new Person(name: 'Mark', age: 40)
]
println people.collect { it. name }
// [Raj, Reema, Mark]

Groovy provides a syntactic sugar for performing such operations through its spread operator *.

println people*.name

collect method by default returns a list. This might not be desirable always. Consider the following example.

def numbers = [ -2, -1, 1, 2] as Set
println numbers.collect { it * it } // [4, 1, 1, 4]

Here, numbers is a Set and if we intend the result to be a Set too, then we are in trouble. Because the resulting set has fewer elements (since a Set does not contain duplicate elements). Groovy provides another flavour of collect method, which accepts a Collection in addition to a closure. We could apply it as follows.

println numbers.collect(new HashSet()) { it * it }
// [1, 4]

// OR

println numbers.collect([] as Set) { it * it }
// [4, 1]

Since we have passed an empty HashSet object to collect, the transformed values are added to that object instead of creating a new List object.

Reduce Operations

Groovy supports the most common reduce operations such as sum, min, max out of the box. Let us see them in action.

def numbers = [3, 2, 1, 4]
println numbers.sum() // 10
println numbers.min() // 1
println numbers.max() // 4

join is another common reduce operation, which is often helpful is deriving comma separated values from a collection.

def developers = ['Raj', 'Reena', 'John']
println developers.join(", ")
// Raj, Reena, John

No language can provide all the possible reduce operations out of the box. But a simpler way to implement custom reduce operations will be appreciated by the developer community. Groovy provides inject method, which is popular as 'fold left' in many languages.

Let us implement sum operation using inject method. Also let us define a custom reduce operation 'product'.

println numbers.inject(0){result, number ->
	result + number
} // 10

def product = { result, number ->
	result * number
}
println numbers.inject(product) // 24

While implementing sum, we have provided a default value 0. So even if numbers was an empty collection, we would have got the result 0. However while implementing product, we did not specify an initial value and hence collection cannot be empty here. We can overcome this problem by specifying the initial value as 1, but that would not make sense as default value for product, when no elements are present. So ensuring collection isn't empty seems like the right choice.

Groovy collections have a method any to check if the collection has at least one element that satisfies the specified criterion.

def numbers = [3, 2, 1, 4]
println numbers.any { it > 3 } // true
println numbers.any { it < 0 } // false

Similarly, every method checks if all the elements in a collection satisfy the specified criterion.

println numbers.every { it > 0 } // true
println numbers.every { it % 2 == 0 } // false

Stack operations

Groovy provides push and pop methods on a List out of the box.

def numbers = []
numbers.push(10)
numbers.push(20)
println numbers // [10, 20]
println numbers.pop() // 20
println numbers // [10]

Working with Maps

Creating a map

Groovy has native support for maps too. Following examples illustrate how to create maps.

def emptyMap = [:]
assert emptyMap.getClass() == java.util.LinkedHashMap
def capitals = [India: 'New Delhi', France: 'Paris']
assert capitals.getClass() == java.util.LinkedHashMap

Note that when you specify the keys, you don't have to enclose them in quotes.

Accessing elements

println capitals.get('India')
println capitals['India']
println capitals.India

You can access value corresponding to a key using either subscript notation or using '.' notation.

Add elements

capitals.put('Nepal', 'Kathmandu')
capitals['Singapore'] = 'Singapore'
capitals.UAE = 'Abu Dhabi'

Map Iteration

Similar to lists, Groovy provides each method on maps. Interestingly it can be used in two ways.

capitals.each{ entry ->
	println "${entry.key} - ${entry.value}"
}

capitals.each { key, value ->
	println "$key -> $value"
}

In the first example, the closure we passed takes one argument, while in the second example, closure takes two arguments.
each method checks the number of arguments the closure can take and accordingly sends the data to that closure. If you provide a closure that accepts single argument, it would be called with Map.Entry; otherwise closure is called with key and value as two distinct arguments.

Show Comments