This library provides the following code-generating annotations:
-
@Instantiate
generates concrete instantiations of generic classes analogous to C++ templates:
/* you write... */
@Instantiate(String.class)
class MyList<T> {
private T[] array;
// ...
} |
/* ... and you'll get */
class MyListString {
private String[] array;
// ...
} |
-
@Derive
generates new classes from existing ones by applying string or regex replacements to the source code:
/* you write... */
@Derive(name = "MyFloatList", replace = @Replace(from = "double", to = "float"))
class MyDoubleList {
private double[] array;
// ...
} |
/* ... and you'll get */
class MyFloatList {
private float[] array;
// ...
} |
The main advantage of this library over generic template engines such as StringTemplate, Velocity or FreeMaker is:
Your template is actual code!
Thus, instead of having to write a placeholder-sprinkled, engine-specific template file, your "template" is a normal Java class (with annotations). The benefits are as follows:
-
The "template" is source code rather than a resource file
-
The "template" can be unit tested
-
The "template" enjoys IDE syntax highlighting - no template engine-specific plugins required
-
The "template" can be auto-formatted, linted and refactored by your IDE
Instantiate generic classes with @Instantiate
Consider the following generic class (which, of course, would require a lot more work before it’s a reasonable list implementation):
package com.kt.codegen.demo.list1;
class MyList<T> {
private T[] array;
MyList(int size) {
this.array = (T[]) new Object[size];
}
T get(int index) {
return array[index];
}
}
You can annotate it with @Instantiate
to e.g. create a concrete String instantiation,
analogous to C++ templates:
package com.kt.codegen.demo.list2;
import com.kt.codegen.Instantiate;
@Instantiate(String.class)
class MyList<T> {
private T[] array;
MyList(int size) {
this.array = (T[]) new Object[size];
}
T get(int index) {
return array[index];
}
}
This will generate the following class:
// generated from com.kt.codegen.demo.list2.MyList
package com.kt.codegen.demo.list2;
class MyListString {
private String[] array;
MyListString(int size) {
this.array = (String[]) new Object[size];
}
String get(int index) {
return array[index];
}
}
Nice, but the annotation processor only operates on a source code level and simply
replaces occurrences of T
with String
. This leads to a guaranteed class cast
exception in the expression (String[]) new Object[size]
. Can we fix this? Yes, with custom
string replacements, see below.
Simply replacing a generic type with a concrete type like we just did doesn’t usually get us all the way, but fret not, there are custom string replacements:
package com.kt.codegen.demo.list3;
import com.kt.codegen.Instantiate;
import com.kt.codegen.Replace;
@Instantiate(value = String.class,
replace = @Replace(from = "(T[]) new Object[size]", to = "new String[size]"))
class MyList<T> {
private T[] array;
MyList(int size) {
this.array = (T[]) new Object[size];
}
T get(int index) {
return array[index];
}
}
Now the generated string list is safe:
// generated from com.kt.codegen.demo.list3.MyList
package com.kt.codegen.demo.list3;
class MyListString {
private String[] array;
MyListString(int size) {
this.array = new String[size];
}
String get(int index) {
return array[index];
}
}
How about adding a primitive version of our list? Simple: just add a double
instantiation:
package com.kt.codegen.demo.list4;
import com.kt.codegen.Instantiate;
import com.kt.codegen.Replace;
@Instantiate(value = String.class,
replace = @Replace(from = "(T[]) new Object[size]", to = "new String[size]"))
@Instantiate(value = double.class,
replace = @Replace(from = "(T[]) new Object[size]", to = "new double[size]"))
class MyList<T> {
private T[] array;
MyList(int size) {
this.array = (T[]) new Object[size];
}
T get(int index) {
return array[index];
}
}
This will additionally geenrate the following class:
// generated from com.kt.codegen.demo.list4.MyList
package com.kt.codegen.demo.list4;
class MyListDouble {
private double[] array;
MyListDouble(int size) {
this.array = new double[size];
}
double get(int index) {
return array[index];
}
}
Note that the class is called MyListDouble
instead of MyListdouble
(note the
different case of the "d") to make the two types explicit in the class name.
If your generic class has more than one type parameter then you’ll simply have to provide the necessary number of concrete types for each instantiation:
package com.kt.codegen.demo.map;
import com.kt.codegen.Instantiate;
import java.time.Instant;
@Instantiate({String.class, Instant.class}) // <-- two concrete types
class MyMap<K, V> { // <-- two type parameters
private K[] keys;
private V[] values;
// ...
}
-
For projects that don’t follow the Maven directory layout you can specify the relative source directory with
@SourceDirectory
on the source class. -
If normal string replacement won’t cut it, you can set
to@Replace
.regextrue
. -
You can specify multiple replacements with
replace = {@Replace(…), @Replace(…), …}
. -
I you prefer prepending the concrete type(s) to the class rather than the default appending style (i.e.,
StringMyList
rather thanMyListString
) then set
to@Instantiate
.appendfalse
.
Generate derived classes with @Derive
Say you are working on a primitive collection library. You have just finished writing
a double
list implementation:
package com.kt.codegen.demo.double1;
public class MyDoubleList {
private double[] array;
MyDoubleList(int size) {
this.array = new double[size];
}
// ...
}
Now you have a couple of options to create lists for other primitive types:
-
You copy and paste the class a couple of times followed by a search/replace frenzy. This is cumbersome, time-consuming, and will eventually lead to implementations drifting apart because you’ll forget to apply that one fix to the
float
implementation. -
You fire up a generic template engine, convert this nice, working, unit-tested, syntax-highlighted, auto-formatted, error-checked class into a template text file that immediately loses all those nice properties, and you start configuring the template engine.
-
Or you annotate the class as follows:
package com.kt.codegen.demo.double2;
import com.kt.codegen.Derive;
import com.kt.codegen.Replace;
@Derive(name = "MyFloatList", replace = @Replace(from = "\\bdouble\\b", to = "float", regex = true))
@Derive(name = "MyLongList", replace = @Replace(from = "\\bdouble\\b", to = "long", regex = true))
public class MyDoubleList {
private double[] array;
MyDoubleList(int size) {
this.array = new double[size];
}
// ...
}
This will generate two derived classes:
// generated from com.kt.codegen.demo.double2.MyDoubleList
package com.kt.codegen.demo.double2;
public class MyFloatList {
private float[] array;
MyFloatList(int size) {
this.array = new float[size];
}
// ...
}
And:
// generated from com.kt.codegen.demo.double2.MyDoubleList
package com.kt.codegen.demo.double2;
public class MyLongList {
private long[] array;
MyLongList(int size) {
this.array = new long[size];
}
// ...
}
-
The relative source directory can also be changed using
@SourceDirectory
. -
Custom string replacements can be specified in
.@Derive
.replace