Calling Java methods expecting Generic types in your CFML

Sunday, September 12, 2010

On Twitter, Jason Dean asked: “Is there a way for me to create an ArrayList<String> in ColdFusion for passing into a Java constructor?”

I responded saying I thought he should only need to create a regular old ArrayList and that the jvm should accept it, even though there’s technically no way to do “Generics” in java.


Your CFML code needn’t care if a java method expects a Collection of  Generic types. You can create objects of the “raw” type and pass those. However, If your collection object contains objects of a type that the original programmer didn’t expect, your code will most likely fail. Thus, consider the Generic type declaration as an instruction to you, the programmer, instead of an instruction to the compiler.

If you want to know why, read further.

If you’d like to play along, you can get all the code here:

What are Generics?

What he’s talking about are Java Generics. That’s the little angle bracket thing in there.

Given this code:

public String echoGenericStringList(List<String> list){
	return list.toString();

And then some driver code:

ArrayList list1 = new ArrayList();

ArrayList<String> list2 = new ArrayList<String>();
list2.add("G: Hi");
list2.add("G: Hi2");

System.out.println(gm.echoGenericStringList(  list1   ));
System.out.println(gm.echoGenericStringList(  list2   ));

1) The echoGenericStringList method declares an input of type List. Additionally, it expects that list to contain only Strings.

2) The first ArrayList – list1 – is a “raw” ArrayList. I’ve declared no additional type requirements on it. It can contain any types: Strings, Integers, Foos, Bars, Unicorns. Whatever.

3) The second ArrayList – list2 – is a a “typed” ArrayList. It instructs the compiler to fail if any attempt is made to add something to the list that isn’t a String. Thus, list2.add(“one”) is fine, but list2.add(1) is not.

Raw vs Generic; Compile-time vs. Runtime

Neither of these last two lines will fail compilation. The second one is obvious: the method declares an input of type List<String> and gets it. But the first one… why doesn’t it fail? It’s getting a “raw” list, not a generic list.

The answer is: Because that’s how generics work.  The compiler will issue a warning, but it will not fail. A method declaring inputs with Generic types must accept inputs of raw types. The downside is that when using raw types, you lose the type safety which Generic types confer.

The key to understanding this behavior is this: Currently, generics are a compile-time type check. The typing of that list: ArrayList<String>, is erased upon compilation, and during run-time no check is made. That’s why, for example, the compiler would not complain if you added an Integer to list1 above and passed it to echoGenericStringList(List<String> list), but it would complain if you attempted to add an Integer to list 2 – since that list has been declared to only accept Strings.

Importantly, if you were to try this, the compiler does not complain at the line of the actual method, the public String echoGenericStringList(List<String> list), but at the point where you attempt to create a List that does not honor the contract of that list’s declaration. See this screenshot:


Notice that the compilation failure – the line of code with the red x beside it – happens at list2.add(1), not on the method itself.

If none of this makes sense, well, that’s expected if you don’t know Java. The bottom line here is that Generics are inspected at compile time and erased prior to runtime. The contents of the Generic Collection only matter at compile time; at runtime, you’re on your own.

This compile-time check doesn’t eliminate the possibility of type errors at runtime… but it greatly mitigates it. If you’re a CF developer, you probably don’t care at all about compile time type checks, which brings us to…

What does this mean for CFML developers?

Quite simply, it means that we can basically ignore generics in our CFML and treat methods that expect generic collections as if it expected a “raw” type collection. So, for us, a method declared thusly:

public String foo(List<String> list)

is the same as

public String foo(List list)

The reason is that by the time our CFML calls that foo() method from some Java class, that java code has already been compiled. The byte code does not have any notion of Genrics at this point. So when we create a non-Generic list in our CFML, the JVM will gladly accept it since it’s the same as if we were passing in a raw list, as in the example above. Java developers may bemoan the fact that Generics are erased at runtime, but we CFML developers can take advantage of that fact.

Thus, this CFML works fine:

<h2>Using Java Generics</h2>
<cfset genericMethods = loader.create("generics.GenericMethods")>

	<cfset list = createObject("java", "java.util.ArrayList")>
	<cfset list.add("hi")>
	<cfset list.add("boo")>
	<cfset ilist = createObject("java", "java.util.ArrayList")>
	<cfset ilist.add(1)>
	<cfset ilist.add(2)>
	Echo a list: #genericMethods.echoList(list)#
	Same list, passed to method expecting ArrayList<String>: #genericMethods.echoGenericStringList(list)#
	New List with only ints, passed to method expecting ArrayList<Integer>: #genericMethods.echoGenericIntegerList(ilist)#
	<cfset iList.add("foo")>
	Method expecting int list gladly takes list with non-ints: #genericMethods.echoGenericIntegerList(iList)#



Jason Dean said...

Marc, that is awesome. Thank you for looking into that. We just started discussing generics in my Java class and I had an idea about the runtime vs. compile time stuff, but you definitely cleared it up.

The problem I was having looked like it was about passing the wrong type into the constructor, but it turned out to be something else (I don't recall what). But when I got it figured out, the regular ArrayList worked just fine.

Thanks again. It's good to know I have such experts on retainer ;) (Your check is in the mail)

Marc Esher said...

This was a fun exercise. Thanks for the motivation!