Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is this Map of String to String possible as a Map of String to a list of String in Groovy?

Tags:

groovy

Saw some working code in a project that is like the one below (of course that's just an example) but I'm extremely confused at how this is possible as a Map<String, String> and not Map<String, List<String>>?

Looked it up in the Groovy doc but I didn't see anything that explains this concept. Could someone explain if this is indeed a legit concept in Groovy?

static final Map<String, String> NAMES = [
   "Male": ["Bill", "Bryant", "Jack"],
   "Female": ["Lily", "Haley", "Mary"]
]

like image 644
isoplayer Avatar asked Dec 08 '25 08:12

isoplayer


1 Answers

It happens because of Java's Generic Type Erasure. If you look at the internal type signatures, you will see that NAMES field is just a type of java.util.Map. Take a look at the descriptor of that field.

$ javap -s -p SomeClass
Compiled from "SomeClass.groovy"
public class SomeClass implements groovy.lang.GroovyObject {
  private static final java.util.Map<java.lang.String, java.lang.String> NAMES;
    descriptor: Ljava/util/Map;
  private static org.codehaus.groovy.reflection.ClassInfo $staticClassInfo;
    descriptor: Lorg/codehaus/groovy/reflection/ClassInfo;
  public static transient boolean __$stMC;
    descriptor: Z
  private transient groovy.lang.MetaClass metaClass;
    descriptor: Lgroovy/lang/MetaClass;
  private static java.lang.ref.SoftReference $callSiteArray;
    descriptor: Ljava/lang/ref/SoftReference;
  public SomeClass();
    descriptor: ()V

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V

  protected groovy.lang.MetaClass $getStaticMetaClass();
    descriptor: ()Lgroovy/lang/MetaClass;

  public groovy.lang.MetaClass getMetaClass();
    descriptor: ()Lgroovy/lang/MetaClass;

  public void setMetaClass(groovy.lang.MetaClass);
    descriptor: (Lgroovy/lang/MetaClass;)V

  static {};
    descriptor: ()V

  public static java.util.Map<java.lang.String, java.lang.String> getNAMES();
    descriptor: ()Ljava/util/Map;

  private static void $createCallSiteArray_1(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V

  private static org.codehaus.groovy.runtime.callsite.CallSiteArray $createCallSiteArray();
    descriptor: ()Lorg/codehaus/groovy/runtime/callsite/CallSiteArray;

  private static org.codehaus.groovy.runtime.callsite.CallSite[] $getCallSiteArray();
    descriptor: ()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
}

Now, that' not all. If you decompile the Groovy bytecode to the Java readable code, you will find something like this.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import groovy.transform.Generated;
import java.util.Map;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class SomeClass implements GroovyObject {
    private static final Map<String, String> NAMES;

    @Generated
    public SomeClass() {
        CallSite[] var1 = $getCallSiteArray();
        super();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].callStatic(SomeClass.class, var1[1].call(NAMES));
    }

    static {
        Map var0 = ScriptBytecodeAdapter.createMap(new Object[]{"Male", ScriptBytecodeAdapter.createList(new Object[]{"Bill", "Bryant", "Jack"}), "Female", ScriptBytecodeAdapter.createList(new Object[]{"Lily", "Haley", "Mary"})});
        NAMES = var0;
    }

    @Generated
    public static Map<String, String> getNAMES() {
        return NAMES;
    }
}

At this level, NAMES field signature matches Map<String, String>. But look at the static constructor and how this field gets initialized. The NAMES field of type Map<String, String> gets initialized with a raw Map type. Also, the ScriptBytecodeAdapter.createMap method returns a raw map, but it could return a parameterized map as well - you will see the same effect. If that map wasn't a raw map, the compiler would complain and throw an error because of incompatible types. But a raw map essentially allows you to assign a map that stores values that are incompatible with the parameters.

You can get the same effect in pure Java. Take a look at the following example:

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

final class SomeJavaClass {

    private static final Map<String, String> NAMES;

    static {
        final Map map = new HashMap();
        map.put("Male", Arrays.asList("Bill", "Brian", "Jack"));

        NAMES = map;
    }

    public static void main(String[] args) {
        System.out.println(NAMES);
        System.out.println(((Object) NAMES.get("Male")).getClass());
    }
 }

When we execute its main method, we will get the following output:

{Male=[Bill, Brian, Jack]}
class java.util.Arrays$ArrayList

In this example, we had to cast NAMES.get("Male") to Object, because otherwise, we would get a ClassCastException - an ArrayList cannot be cast to String. But when you cast to Object explicitly, you can get an ArrayList from a map that, by definition, should keep only string values.

This is known Java behavior - generic types get erased so the raw types can be compatible with pre Java 1.5 versions. Groovy for its dynamic capabilities operates often on raw classes like Map, or List, and thus it can silently overcome some of those limitations.

If you want to use more restricted type checks, you can use @groovy.transform.TypeChecked annotation to keep Groovy's dynamic behavior and add more restrictive type checks. With this annotation added, your class won't compile anymore.

like image 137
Szymon Stepniak Avatar answered Dec 12 '25 14:12

Szymon Stepniak



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!