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.