Basics Without Boilerplate: Java the Pain-free Way, Part II
In the last post, I talked a bit in general about we taught introductory Java without advanced constructs in a recent Udacity course. Now let's get to the finer detail of how it all works.
The need for JVM magic
I had a few non-negotiable requirements:
- The student must be able to use real constructs and not mocked classes.
- The student must write syntatically correct Java code for the portions they write.
The first requirement meant that we needed to look beyond the Java language for testing student code. Java is statically typed so all its classes and functions can't be changed after compile time. That presents a problem when you are trying to check student code that uses Random which by definition shouldn't be reproducible. And we didn't want to do something like having students use a FakeRandom class because they might try to use it outside our course and encounter errors.
Luckily there's a language for the Java Virtual Machine(JVM) called Groovy. Most valid Java is also valid Groovy but a major plus is Groovy's Meta Object Protocol(MOP). It's a mechanism that controls how methods, properties, and attributes are resolved and allowing an application to intelligently respond to method calls. This next bit is impossible in regular Java but is valid Groovy code:
Database.findByAgeAndIncomeAndCity(25, 50000, "Baltimore")
The database object doesn't know anything about the underlying request before I run it. The first time the code is invoked, the lookup fails and methodMissing
is called. In methodMissing, the library uses the name to create the query and caches it as a named function. Any future invocations will skip the lookup and use the cached version. The MOP feature we used the most was rewriting the implementation of Math.random() to something that is reproducible.
def randomNumberGenerator
Math.metaClass.'static'.random = {
if (randomNumberGenerator == null)
randomNumberGenerator = new Random(42)
return randomNumberGenerator.nextDouble()
}
Using Groovy also enabled us to combine what would be separate compilation and execution phases into a single phase.
Generating a class from inside a Java class
The first programming quiz, PrintALine, where a student inputs a single line of code was actually the hardest to create.
There's a library called JavaPoet that allows you to embrace your inner inception and construct functions and classes around student code.
Here's the code that constructs a class for the PrintALine quiz:
def codeBlocksString = """
System.out.println("Java Rocks!");\n
"""
def javaRocks = MethodSpec.methodBuilder("solutionCode")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addCode(codeBlocksString)
def studentSolution = MethodSpec.methodBuilder("studentCode")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
TypeSpec codeClass = TestUtils.createClass(true, studentCode, javaRocks.build(), studentSolution)
JavaFile javaFile = JavaFile.builder("com.udacity.javand", codeClass).build()
And the output class looks like this:
package com.udacity.javand;
public class CodeClass1471932125479 {
public static void studentCode() {
System.out.println("Hello, World!");
}
public static void solutionCode() {
System.out.println("Java Rocks!");
}
}
When Groovy isn't so helpful
One feature of Groovy that was initially helpful was joint-compilation. The Groovy compiler can use Java class files as is. But Groovy is too helpful in some cases. Groovy doesn't require semicolons and parentheses are optional when the compiler can infer the intent. In the absence of a return statement, Groovy returns the last statement as a return value. As a result it's easy to accidentally create valid Groovy that isn't valid Java. It doesn't matter if the class was labeled as ClassName.java, in Groovy's mind, it's Groovy code.
All of these features are great if you know the fundamentals and want to save time, not so great if you are a beginner trying to learn as these Groovyisms can sow bad habits. The best and only way we've found to check Java syntax is to compile it and see what happens. From about Java 6, you can invoke the compiler and run it inside an app.
So we receive a String from the student, programmatically create a Java class, run the output through a compile and load it if compilation passes.
Check out the Java Programming Basics course and our Android Developer Nanodegree.