Groovy null safety with multiple assignment - groovy

Is there a way to "apply" null safety to a multiple assignment from a null? For example, this piece of code would obviously throw:
// verbosity is for clarity
def (a,b) = new HashMap<String, List<String>>().get("nothing")
but it would be neat to have it define a and b with null value.
So far I came up with
def (a,b) = new HashMap<String, List<String>>().get("nothing").with {[it?.get(0), it?.get(1)]}
But that's really ugly...

This is the safe bet, if you have both missing keys and null values:
def (a,b) = new HashMap<String, List<String>>().get("nothing") ?: []
The next one is from Groovy, but modifies the underlying map (so only
works for mutable maps and whether you are fine with modifying it) and
it only gives the default for missing keys:
def (a,b) = new HashMap<String, List<String>>().get("nothing", [])
Similar (also from Groovy and also modifying the underlying map):
withDefault. This option is great if you plan to pass the map around
to other places, that would have to deal with the default all over again
(also only defaults for missing keys):
def (a,b) = new HashMap<String, List<String>>().withDefault{[]}.get("nothing")
And the next one is from Java, but that also only falls back, if the key
is missing:
def (a,b) = new HashMap<String, List<String>>().getOrDefault("nothing",[])

Related

Remove map entries based on collection of values - how to do it in a Groovy way?

Is there a Groovy way of dropping elements from a that match values in b?
def a = [1:"aa", 2:"bb", 3:"cc", 4:"dd"]
def b = [ "bb", "dd"]
expected output : [1:"aa", 3:"cc"]
I am currently using 2 nested for loops to solve this. I am wondering if Groovy has a better way of doing it?
For Groovy < 2.5.0
You can use a single Map.findAll() method to do that:
a.findAll { k,v -> !(v in b) }
However, keep in mind that this method does not modify existing a map, but it creates a new one instead. So if you want to modify map stored in a variable you will have to reassign it.
a = a.findAll { k,v -> !(v in b) }
For Groovy >= 2.5.0
Groovy version 2.5.x introduced a new default method for Map - removeAll which takes a predicate and removes elements from input map based on this predicate.
a.removeAll { k,v -> v in b}

Injecting key/value into HashMap

I'm trying to generate HashMap object that will have properties and values set from parsed text input. Working fine with simple assigned, but wanted to make it more clever and use inject.
def result = new HashMap();
def buildLog = """
BuildDir:
MSBuildProjectFile:test.csproj
TargetName: test
Compile:
Reference:
""".trim().readLines()*.trim()
buildLog.each {
def (k,v) = it.tokenize(':')
result."${k.trim()}"=v?.trim()
}
println "\nResult:\n${result.collect { k,v -> "\t$k='$v'\n" }.join()}"
generates expected output:
Result:
Reference='null'
MSBuildProjectFile='test.csproj'
BuildDir='null'
TargetName='test'
Compile='null'
after replacing the insides of .each { } closure with injection:
it.tokenize(':').inject({ key, value -> result."${key}" = value?.trim()})
the results generated are missing unset values
Result:
MSBuildProjectFile='test.csproj'
TargetName='test'
Am I doing something wrong, tried with inject ("", {...}) but it seems to push may keys into values.
inject is basically a reduce. The reducing function takes two arguments, the result of the previous iteration or the initial value (e.g. the accumulator) and the next value from the sequence. So it could be made to work, but since you only expect one sequence value, it just convolutes the code.
I do see a great use for collectEntries here, as it allows you to create a Map using either small key/values map, or lists of two elements. And the latter you have:
result = buildLog.collectEntries {
it.split(":",2)*.trim()
}
should work for your code instead of buildLog.each

Create a new map in groovy by taking the values of one map and making it keys of another map

I have two map in the following way
def map1 = ['a':1,'b':2]
def map2 = ['a':345,'c':10,'b':1]
I would like to create a result map which would look basically look to match the keys of the two maps and would make the value of map1 as the key and value of map2 as the value itself. The output would look like this:
map3=[1:345,2:1]
You can do this easily with a simple loop:
map3 = map1.collectEntries { key, val -> [(val): map2[key]] }

lazy instantiation of the map value

Is there a way to instantiate the value of map lazy?
For example
class MapTest {
#Lazy(soft = true) HashMap<String, List<String>> map
}
Doing like this I can use this call and get null without recieving NullPointerException
new MapTest().map.key1
However attempt to call
map.key1.remove(1)
will lead to NullPointerException due the value being null. (it would be fine if it threw IndexOutOfBounds exception)
Is there a way to instantiate the list value of the map?
try map.withDefault :
def map = [:].withDefault { [] }
assert map.key1.isEmpty()
Some explanation :
[:] is the groovy way to instantiate an empty hash map
withDefault is a groovy method on a map wich take a closure. this closure is call every time a key is requested to initialize the value if it doesn't exist. this closure take one parameter (the key) and should the value
[] is the groovy way to create an empty list - { [] } is a closure wich return an empty list for every key
see others examples here

Creating Map using withDefault causing null when putting element

I'm trying to use the Groovy way of creating a TreeMap<String, List<Data>> with default values so I easily add data to a new list if the key isn't already present.
TreeMap<String, List<Data>> myData = (TreeMap<String, List<Data>>) [:].withDefault { [] }
As you can see, I have the requirement to use a TreeMap and withDefault only returns a Map instance, so I need to cast.
When I attempt to add a new list to the map,
myData[newKey].add(newData)
myData[newKey] is null. However, if I change my Map initilization to remove the TreeMap cast (and change the type to just Map instead of TreeMap), myData[newKey].add(newData) works as expected.
What's the reasoning for this? Can I not use withDefault if I cast the map?
The problem isn't just about the cast. It also has to do with the declared type. The problem can be simplified to something like this:
def map1 = [:].withDefault { 0 }
TreeMap map2 = map1
When that is executed map1 is an instance of groovy.lang.MapWithDefault and map2 is an instance of java.util.TreeMap. They are 2 separate objects on the heap, not just 2 references pointing to the same object. map2 will not have any default behavior associated with it. It is as if you had done this:
def map1 = [:].withDefault { 0 }
TreeMap map2 = new TreeMap(map1)
That is what is happening with your code. The cast and the generics just makes it less clear with your code.
This:
TreeMap<String, List<Data>> myData = (TreeMap<String, List<Data>>) [:].withDefault { [] }
Can be broken down to this:
def tmpMap = [:].withDefault { [] }
TreeMap<String, List<Data>> myData = (TreeMap<String, List<Data>>)tmpMap
I hope that helps.
EDIT:
Another way to see the same thing happening is to do something like this:
Set names = new HashSet()
ArrayList namesList = names
When the second line executes a new ArrayList is created as if you had done ArrayList namesList = new ArrayList(names). That looks different than what you have in your code, but the same sort of thing is happening. You have a reference with a static type associated with it and are pointing that reference at an object of a different type and Groovy is creating an instance of your declared type. In this simple example above, that declared type is ArrayList. In your example that declared type is TreeMap<String, List<Data>>.

Resources