The Nashorn JavaScript Engine - Java SE 8 for the Really Impatient (2014)

Java SE 8 for the Really Impatient (2014)

Chapter 7. The Nashorn JavaScript Engine

Topics in This Chapter

Image 7.1 Running Nashorn from the Command Line

Image 7.2 Running Nashorn from Java

Image 7.3 Invoking Methods

Image 7.4 Constructing Objects

Image 7.5 Strings

Image 7.6 Numbers

Image 7.7 Working with Arrays

Image 7.8 Lists and Maps

Image 7.9 Lambdas

Image 7.10 Extending Java Classes and Implementing Java Interfaces

Image 7.11 Exceptions

Image 7.12 Shell Scripting

Image 7.13 Nashorn and JavaFX

Image Exercises

For many years, Java bundled the Rhino JavaScript interpreter, an open source JavaScript interpreter written in Java. It is called Rhino because a well-regarded JavaScript book has the image of a rhinoceros on its cover. Rhino works just fine, but it isn’t particularly fast. Oracle’s engineers realized that they could build a much more efficient JavaScript interpreter using the new JVM instructions designed for dynamic languages. Thus, the Nashorn project was born. Nashorn is the German word for rhinoceros—literally, nose-horn. (You get extra karma for pronouncing it nas-horn, not na-shorn.) Nashorn is very fast, and it lets you integrate Java with JavaScript on a highly performant virtual machine. It is also incredibly compliant with the ECMAScript standard for JavaScript. If you are thinking of giving your users the ability to script your application, or if you are intrigued by the ease of use of reactive programming environments such as node.js, check out Nashorn in Java 8. Not only do you get the benefits of a reasonably well-designed scripting language (i.e., JavaScript), but you have the full power of the JVM behind it.

The key points of this chapter are:

• Nashorn is the successor to the Rhino JavaScript interpreter, with greater performance and fidelity to the JavaScript standard.

• Nashorn is a pleasant environment for experimenting with the Java API.

• You can run JavaScript through the jjs interpreter, or from Java via the scripting API.

• Use the predefined JavaScript objects for the most common packages, or the Java.type function to access any package.

• Beware of intricacies in the conversion of strings and numbers between JavaScript and Java.

• JavaScript offers a convenient syntax for working with Java lists and maps, as well as JavaBeans properties.

• You can convert JavaScript functions to Java interfaces in a way that is very similar to using lambda expressions.

• You can extend Java classes and implement Java interfaces in JavaScript, but there are limitations.

• Nashorn has good support for writing shell scripts in JavaScript.

• You can write JavaFX programs in JavaScript, but the integration is not as good as it might be.

7.1. Running Nashorn from the Command Line

Java 8 ships with a command-line tool called jjs. Simply launch it, and issue JavaScript commands.

$ jjs
jjs> 'Hello, World'
Hello, World

You get what’s called a “read-eval-print” loop, or REPL, in the world of Lisp, Scala, and so on. Whenever you enter an expression, its value is printed.

jjs> 'Hello, World!'.length
13


Image NOTE

As a reminder, in JavaScript, strings can be delimited by '...' or "...". In this chapter, I will use single quotes for JavaScript strings to give you a visual clue that the code is JavaScript, not Java.


You can define functions and call them:

jjs> function factorial(n) { return n <= 1 ? 1 : n * factorial(n - 1) }
function factorial(n) { return n <= 1 ? 1 : n * factorial(n - 1) }
jjs> factorial(10)
3628800

You can call Java methods:

var input = new java.util.Scanner(
new java.net.URL('http://horstmann.com').openStream())
input.useDelimiter('$')
var contents = input.next()

Now, when you type contents, you see the contents of the web page.

Look how refreshing this is. You didn’t have to worry about exceptions. You can make experiments dynamically. I wasn’t quite sure whether I could read the entire contents by setting the delimiter to $, but I tried it out and it worked. And I didn’t have to write public static void main. I didn’t have to compile a thing. I didn’t have to make a project in my IDE. The REPL is the easiest way to explore an API. It is a bit odd that one drives Java from JavaScript, but it is also convenient. Note how I didn’t have to define the types for the input and contents variables.


Image TIP

There are two annoyances that keep the JavaScript REPL from being as refreshing as its equivalent in Scala. The Scala REPL has command completion. When you press the Tab key, you get a list of possible completions of the current expression. Admittedly, that is a difficult trick to pull off for dynamically typed languages such as JavaScript. A more fundamental omission is command-line recall. Pressing the ↑ key should get you the previous command. If it doesn’t, try installing rlwrap and run rlwrap jjs. Alternatively, you can run jjsinside Emacs. Don’t worry—this won’t hurt a bit. Start Emacs and hit M-x (i.e., Alt+x or Esc x) shell Enter, then type jjs. Type expressions as usual. Use M-p and M-n to recall the previous or next line, and the left and right arrow keys to move within a line. Edit a command, then press Enter to see it executed.


7.2. Running Nashorn from Java

In the preceding section, you saw one use case for Nashorn scripting: to experiment with Java APIs from the jjs REPL. Another use case is to allow users of your programs to run scripts. In the desktop world, this is quite common. For example, all Microsoft Office applications can be scripted with a language called VB Script that is a descendant of the Basic language. Quite a few people write such scripts, and this capability leads to a form of vendor lock. It is difficult to adopt an alternate office suite that won’t run those scripts. If you want to lock in the users of your Java desktop or server app, you can provide the same capabilities.

Running a Nashorn script from Java uses the script engine mechanism that has been introduced in Java 6. You can use that mechanism to execute scripts in any JVM language with a script engine, such as Groovy, JRuby, or Jython. There are also script engines for languages that run outside the JVM, such as PHP or Scheme.

To run a script, you need to get a ScriptEngine object. If the engine is registered, you can simply get it by name. Java 8 includes an engine with name "nashorn". Here is how to use it:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Object result = engine.eval("'Hello, World!'.length");
System.out.println(result);

You can also read a script from a Reader:

Object result = engine.eval(Files.newBufferedReader(path));

To make a Java object available to your scripts, use the put method of the ScriptEngine interface. For example, you can make a JavaFX stage visible, so that you can populate it using JavaScript code:

public void start(Stage stage) {
engine.put("stage", stage);
engine.eval(script); // Script code can access the object as stage
}

Instead of putting variables into the global scope, you can collect them in an object of type Bindings and pass that object to the eval method:

Bindings scope = engine.createBindings();
scope.put("stage", stage);
engine.eval(script, scope);

This is useful if a set of bindings should not persist for future calls to the eval method.

7.3. Invoking Methods

In the preceding section, you saw how the script engine can make Java objects accessible to JavaScript. You can then invoke methods on the provided variables. For example, if the Java code calls

engine.put("stage", stage);

then the JavaScript code can call

stage.setTitle('Hello')

In fact, you can also use the syntax

stage.title = 'Hello'

Nashorn supports a convenient property syntax for getters and setters. If the expression stage.title occurs to the left of the = operator, it is translated to an invocation of the setTitle method. Otherwise it turns into a call stage.getTitle().

You can even use the JavaScript bracket notation to access properties:

stage['title'] = 'Hello'

Note that the argument of the [] operator is a string. In this context, that isn’t useful, but you can call stage[str] with a string variable and thereby access arbitrary properties.


Image NOTE

In JavaScript, semicolons at the end of a line are optional. Many JavaScript programmers put them in anyway, but in this chapter, I omit them so that you can easily distinguish between Java and JavaScript code snippets.


JavaScript has no concept of method overloading. There can be only one method with a given name, and it can have any number of parameters of any type. Nashorn attempts to pick the correct Java method, following the number and types of the parameters.

In almost all cases, there is only one Java method that matches the supplied parameters. If there is not, you can manually pick the correct method with the following rather strange syntax:

list['remove(Object)'](1)

Here, we specify the remove(Object) method that removes the Integer object 1 from the list. (There is also a remove(int) method that removes the object at position 1.)

7.4. Constructing Objects

When you want to construct objects in JavaScript (as opposed to having them handed to you from the script engine), you need to know how to access Java packages. There are two mechanisms.

There are global objects java, javax, javafx, com, org, and edu that yield package and class objects via the dot notation. For example,

var javaNetPackage = java.net // A JavaPackage object
var URL = java.net.URL // A JavaClass object

If you need to access a package that does not start with one of the above identifiers, you can find it in the Package object, such as Package.ch.cern.

Alternatively, call the Java.type function:

var URL = Java.type('java.net.URL')

This is a bit faster than java.net.URL, and you get better error checking. (If you make a spelling error such as java.net.Url, Nashorn will think it is a package.) But if you want speed and good error handling, you probably shouldn’t be using a scripting language in the first place, so I will stick with the shorter form.


Image NOTE

The Nashorn documentation suggests that class objects should be defined at the top of a script file, just like you place imports at the top of a Java file:

var URL = Java.type('java.net.URL')
var JMath = Java.type('java.lang.Math')
// Avoids conflict with JavaScript Math object


Once you have a class object, you can call static methods:

JMath.floorMod(-3, 10)

To construct an object, pass the class object to the JavaScript new operator. Pass any constructor parameters in the usual way:

var URL = java.net.URL
var url = new URL('http://horstmann.com')

If you aren’t concerned about efficiency, you can also call

var url = new java.net.URL('http://horstmann.com')


Image CAUTION

If you use Java.type with new, you need an extra set of parentheses:

var url = new (Java.type('java.net.URL'))('http://horstmann.com')


If you need to specify an inner class, you can do so with the dot notation:

var entry = new java.util.AbstractMap.SimpleEntry('hello', 42)

Alternatively, if you use Java.type, use a $, like the JVM does:

var Entry = Java.type('java.util.AbstractMap$SimpleEntry')

7.5. Strings

Strings in Nashorn are, of course, JavaScript objects. For example, consider the call

'Hello'.slice(-2) // Yields 'lo'

Here, we call the JavaScript method slice. There is no such method in Java.

But the call

'Hello'.compareTo('World')

also works, even though in JavaScript there is no compareTo method. (You just use the < operator.)

In this case, the JavaScript string is converted to a Java string. In general, a JavaScript string is converted to a Java string whenever it is passed to a Java method.

Also note that any JavaScript object is converted to a string when it is passed to a Java method with a String parameter. Consider

var path = java.nio.file.Paths.get(/home/)
// A JavaScript RegExp is converted to a Java String!

Here, /home/ is a regular expression. The Paths.get method wants a String, and it gets one, even though it makes no sense in this situation. One shouldn’t blame Nashorn for this. It follows the general JavaScript behavior to turn anything into a string when a string is expected. The same conversion happens for numbers and Boolean values. For example, 'Hello'.slice('-2') is perfectly valid. The string '-2' is silently converted to the number –2. It is features such as this one that make programming in a dynamically typed language an exciting adventure.

7.6. Numbers

JavaScript has no explicit support for integers. Its Number type is the same as the Java double type. When a number is passed to Java code that expects an int or long, any fractional part is silently removed. For example, 'Hello'.slice(-2.99) is the same as'Hello'.slice(-2).

For efficiency, Nashorn keeps computations as integers when possible, but that difference is generally transparent. Here is one situation when it is not:

var price = 10
java.lang.String.format('Price: %.2f', price)
// Error: f format not valid for java.lang.Integer

The value of price happens to be an integer, and it is assigned to an Object since the format method has an Object... varargs parameter. Therefore, Nashorn produces a java.lang.Integer. That causes the format method to fail, since the f format is intended for floating-point numbers. In this case, you can force conversion to java.lang.Double by calling the Number function:

java.lang.String.format('Unit price: %.2f', Number(price))

7.7. Working with Arrays

To construct a Java array, first make a class object:

var intArray = Java.type('int[]')
var StringArray = Java.type('java.lang.String[]')

Then call the new operator and supply the length of the array:

var numbers = new intArray(10) // A primitive int[] array
var names = new StringArray(10) // An array of String references

Then use the bracket notation in the usual way:

numbers[0] = 42
print(numbers[0])

You get the length of the array as numbers.length. To iterate through all values of the names array, use

for each (var elem in names)
Do something with elem

This is the equivalent of the enhanced for loop in Java. If you need the index values, use the following loop instead:

for (var i in names)
Do something with i and names[i]


Image CAUTION

Even though this loop looks just like the enhanced for loop in Java, it visits the index values. JavaScript arrays can be sparse. Suppose you initialize a JavaScript array as

var names = []
names[0] = 'Fred'
names[2] = 'Barney'

Then the loop for (var i in names) print(i) prints 0 and 2.


Java and JavaScript arrays are quite different. When you supply a JavaScript array where a Java array is expected, Nashorn will carry out the conversion. But sometimes, you need to help it along. Given a JavaScript array, use the Java.to method to obtain the equivalent Java array:

var javaNames = Java.to(names, StringArray) // An array of type String[]

Conversely, use Java.from to turn a Java array into a JavaScript array:

var jsNumbers = Java.from(numbers)
jsNumbers[-1] = 42

You need to use Java.to to resolve overload ambiguities. For example,

java.util.Arrays.toString([1, 2, 3])

is ambiguous since Nashorn can’t decide whether to convert to an int[] or Object[] array. In that situation, call

java.util.Arrays.toString(Java.to([1, 2, 3], Java.type('int[]')))

or simply

java.util.Arrays.toString(Java.to([1, 2, 3], 'int[]'))

7.8. Lists and Maps

Nashorn provides “syntactic sugar” for Java lists and maps. You can use the bracket operator with any Java List to invoke the get and set methods:

var names = java.util.Arrays.asList('Fred', 'Wilma', 'Barney')
var first = names[0]
names[0] = 'Duke'

The bracket operator also works for Java maps:

var scores = new java.util.HashMap
scores['Fred'] = 10 // Calls scores.put('Fred', 10)

To visit all elements in the map, you can use the JavaScript for each loops:

for (var key in scores) ...
for each (var value in scores) ...

If you want to process keys and values together, simply iterate over the entry set:

for each (var e in scores.entrySet())
Process e.key and e.value


Image NOTE

The for each loop works for any Java class that implements the Iterable interface.


7.9. Lambdas

JavaScript has anonymous functions, such as

var square = function(x) { return x * x }
// The right-hand side is an anonymous function
var result = square(2)
// The () operator invokes the function

Syntactically, such an anonymous function is very similar to a Java lambda expression. Instead of an arrow after the parameter list, you have the keyword function.

You can use an anonymous function as a functional interface argument of a Java method, just like you could use a lambda expression in Java. For example,

java.util.Arrays.sort(words,
function(a, b) { return java.lang.Integer.compare(a.length, b.length) })
// Sorts the array by increasing length

Nashorn supports shorthand for functions whose body is a single expression. For such functions, you can omit the braces and the return keyword:

java.util.Arrays.sort(words,
function(a, b) java.lang.Integer.compare(a.length, b.length))

Again, note the similarity with a Java lambda expression (a, b) -> Integer.compare(a.length, b.length).


Image NOTE

That shorthand notation (called an “expression closure”) is not part of the official JavaScript language standard (ECMAScript 5.1), but it is also supported by the Mozilla JavaScript implementation.


7.10. Extending Java Classes and Implementing Java Interfaces

To extend a Java class, or to implement a Java interface, use the Java.extend function. Supply the class object of the superclass or interface and a JavaScript object with the methods that you want to override or implement.

For example, here is an iterator that produces an infinite sequence of random numbers. We override two methods, next and hasNext. For each method, we provide an implementation as an anonymous JavaScript function:

var RandomIterator = Java.extend(java.util.Iterator, {
next: function() Math.random(),
hasNext: function() true
}) // RandomIterator is a class object
var iter = new RandomIterator() // Use it to construct an instance


Image NOTE

When calling Java.extend, you can specify any number of superinterfaces as well as a superclass. Place all class objects before the object with the implemented methods.


Another Nashorn syntax extension lets you define anonymous subclasses of interfaces or abstract classes. When new JavaClassObject is followed by a JavaScript object, an object of the extended class is returned. For example,

var iter = new java.util.Iterator {
next: function() Math.random(),
hasNext: function() true
}

If the supertype is abstract and has only one abstract method, you don’t even have to give the method name. Instead, pass the function as if it was a constructor parameter:

var task = new java.lang.Runnable(function() { print('Hello') })
// task is an object of an anonymous class implementing Runnable


Image CAUTION

When extending a concrete class, you cannot use that constructor syntax. For example, new java.lang.Thread(function() { print('Hello') }) calls a Thread constructor, in this case the constructor Thread(Runnable). The call to new returns an object of class Thread, not of a subclass of Thread.


If you want instance variables in your subclass, add them to the JavaScript object. For example, here is an iterator that produces ten random numbers:

var iter = new java.util.Iterator {
count: 10,
next: function() { this.count--; return Math.random() },
hasNext: function() this.count > 0
}

Note that the JavaScript methods next and hasNext refer to the instance variable as this.count.

It is possible to invoke a superclass method when overriding a method, but it is quite finicky. The call Java.super(obj) yields an object on which you can invoke the superclass method of the class to which obj belongs, but you must have that object available. Here is a way to achieve that:

var arr = new (Java.extend(java.util.ArrayList)) {
add: function(x) {
print('Adding ' + x);
return Java.super(arr).add(x)
}
}

When you call arr.add('Fred'), a message is printed before the value is added to the array list. Note that the call Java.super(arr) requires the arr variable, which is being set to the value returned by new. Calling Java.super(this) does not work—that only gets the JavaScript object that defines the method, not the Java proxy. The Java.super mechanism is only useful for defining individual objects, not subclasses.


Image NOTE

Instead of calling Java.super(arr).add(x), you can also use the syntax arr.super$add(x).


7.11. Exceptions

When a Java method throws an exception, you can catch it in JavaScript in the usual way:

try {
var first = list.get(0)
...
} catch (e) {
if (e instanceof java.lang.IndexOutOfBoundsException)
print('list is empty')
}

Note that there is only one catch clause, unlike in Java where you can catch expressions by type. That, too, is in the spirit of dynamic languages where all type inquiry happens at runtime.

7.12. Shell Scripting

If you need to automate a repetitive task on your computer, chances are that you have put the commands in a shell script, a script that replays a set of OS-level commands. I have a directory ~/bin filled with dozens of shell scripts: to upload files to my web site, my blog, my photo storage, and to my publisher’s FTP site; to convert images to blog size; to bulk-email my students; to back up my computer at two o’clock in the morning.

For me, these are bash scripts, but in the olden days when I used Windows they were batch files. So what is wrong with that? The problem comes once you have a need for branches and loops. For some reason, most implementors of command shells are terrible at programming language design. The way variables, branches, loops, and functions are implemented in bash is simply awful, and the batch language in Windows is even worse. I have a few bash scripts that started out modest but have over time accreted so much cruft that they are unmanageable. This is a common problem.

Why not just write these scripts in Java? Java is quite verbose. If you call external commands via Runtime.exec, you need to manage standard input/output/error streams. The Nashorn designers want you to consider JavaScript as an alternative. The syntax is comparatively lightweight, and Nashorn offers some conveniences that are specifically geared towards shell programmers.

7.12.1. Executing Shell Commands

To use the scripting extensions in Nashorn, run

jjs -scripting

or

jrunscript

Now you can execute shell commands by including them in backquotes, for example

`ls -al`

The standard output and standard error streams of the last command are captured in $OUT and $ERR. The exit code of the command is in $EXIT. (By convention, an exit code of zero means success, and non-zero codes describe error conditions.)

You can also capture the standard output by assigning the result of the backquoted command to a variable:

var output = `ls -al`

If you want to supply standard input for a command, use

$EXEC(command, input)

For example, this command passes the output of ls -al to grep -v class:

$EXEC('grep -v class', `ls -al`)

It’s not quite as pretty as a pipe, but you can easily implement a pipe if you need it—see Exercise 6.

7.12.2. String Interpolation

Expressions inside ${...} are evaluated within doubly quoted and backquoted strings. This is called “string interpolation.” For example,

var cmd = "javac -classpath ${classpath} ${mainclass}.java"
$EXEC(cmd)

or simply

`javac -classpath ${classpath} ${mainclass}.java`

injects the contents of the variables classpath and mainclass into the command.

You can use arbitrary expressions inside the ${...}:

var message = "The current time is ${java.time.Instant.now()}"
// Sets message to a string such as The current time is 2013-10-12T21:48:58.545Z

As with the bash shell, string interpolation does not work inside singly quoted strings.

var message = 'The current time is ${java.time.Instant.now()}'
// Sets message to The current time is ${java.time.Instant.now()}

Strings are also interpolated in “here documents”—inline documents in a script. These inline documents are useful when a command reads multiple lines from standard input and the script author doesn’t want to put the input in a separate file. As an example, here is how you can feed commands to the GlassFish administration tool:

name='myapp'
dir='/opt/apps/myapp'
$EXEC("asadmin", <<END)
start-domain
start-database
deploy ${name} ${dir}
exit
END

The <<END construct means: “Insert the string that starts on the next line and is terminated by the line END.” (Instead of END, you can use any identifier that doesn’t appear inside the string.)

Note that the name and location of the application are interpolated.

String interpolation and here documents are only available in scripting mode.

7.12.3. Script Inputs

You can supply command-line arguments to a script. Since it is possible to include multiple script files on the jjs command line, you need to separate the script files and arguments with a --:

jjs script1.js script2.js -- arg1 arg2 arg3


Image NOTE

That is a little ugly. If you have only one script file, you can instead run

jrunscript -f script.js arg1 arg2 arg3



Image TIP

The first line of a script can be a “shebang,” the symbols #! followed by the location of the script interpreter. For example,

#!/opt/java/bin/jjs

or

#!/opt/java/bin/jrunscript -f

Then you can make the script file executable and simply run it as path/script.js.

When a script starts with a shebang, scripting mode is automatically activated.



Image CAUTION

If your script has arguments, and you use jjs in the shebang, script users will need to supply the --: path/script.js -- arg1 arg2 arg3. Users will not love you for that. Use jrunscript instead.


In the script file, you receive the command-line arguments in the arguments array:

var deployCommand = "deploy ${arguments[0]} ${arguments[1]}"

With jjs (but not with jrunscript), you can use $ARG instead of arguments. If you use that variable with string interpolation, you need two dollar signs:

var deployCommand = "deploy ${$ARG[0]} ${$ARG[1]}"

In your script, you can obtain the shell’s environment variables through the ENV object:

var javaHome = $ENV.JAVA_HOME

In scripting mode, you can prompt the user for input with the readLine function:

var username = readLine('Username: ')


Image CAUTION

To prompt for a password, call

var password = java.lang.System.console().readPassword('Password: ')


Finally, to exit a script, use the exit function. You can supply an optional exit code.

if (username.length == 0) exit(1)

7.13. Nashorn and JavaFX

Nashorn provides a convenient way of launching JavaFX applications. Simply put the instructions that you would normally put into the start method of the Application subclass into the script. Use $STAGE for the Stage parameter. You don’t even have to call show on the Stageobject—that is done for you. For example, here is the “Hello” program from Chapter 4 in JavaScript:

var message = new javafx.scene.control.Label("Hello, JavaFX!");
message.font = new javafx.scene.text.Font(100);
$STAGE.scene = new javafx.scene.Scene(message);
$STAGE.title = "Hello";

Run the script with the -fx option, like this:

jjs -fx myJavaFxApp.js

That is all there is to it. A label with a message “Hello, JavaFX!” is displayed in a 100-point font in a window whose title is “Hello”—see Figure 7–1.

Image

Figure 7–1 The Hello JavaFX application in JavaScript

All the boilerplate is gone, and you have the convenient property notation, that is,

message.font = new Font(100)

instead of

message.setFont(new Font(100))


Image NOTE

If you need to override the init or stop lifecycle methods of the Application class in addition to start, include the methods that you need in your script, at the top level. With the -fx option, you then get a subclass of Application with the script methods.


Now let us look at event handling. As you have seen in Chapter 4, most FX events are handled through listeners to FX properties. Here, the JavaScript story isn’t so pretty. Recall that an FX property has two listener interfaces, InvalidationListener and ChangeListener, both added with the addListener method. In Java, you can call addListener with a lambda expression, and the compiler is able to figure out from the parameter types which of the two listeners to add. But in JavaScript, function parameters have no types. Suppose we have a slider to control the font size. We’d like to add a listener that updates the size when the slider value changes:

slider.valueProperty().addListener(function(property)
message.font = new Font(slider.value))
// Error—Nashorn can't determine which listener type to add

That doesn’t work. Nashorn doesn’t know whether you want to add an InvalidationListener or a ChangeListener, and it doesn’t know that you don’t actually care.

To fix this, you need to make the choice:

slider.valueProperty().addListener(
new javafx.beans.InvalidationListener(function(property)
message.font = new Font(slider.value)))

That’s more heavyweight than the Java equivalent—not something one wants to see in a lightweight scripting language. There is nobody to blame for this, really. The JavaFX designers made the decision to overload the addListener method, which was perfectly reasonable in the context of Java 7 and mostly works with Java 8 lambda expressions. Compatibility with scripting languages was perhaps not their major concern, particularly since they had just abandoned another scripting language.

But it should be a cautionary tale. When you design a Java API, remember Atwood’s law: “Any application that can be written in JavaScript will eventually be written in JavaScript.” Design your API so that it can be accessed nicely from JavaScript.

And there is another sad aspect about the JavaFX support in Nashorn. Recall how in the olden days of JavaFX Script, it was easy to describe the layout of a scene like this:

Frame {
title: "Hello"
content: Label {
text: "Hello, World!"
}
}

Doesn’t it almost look like JavaScript? Well, Nashorn/JavaFX developers, tear down that wall and turn it into JavaScript! Then we can write both the UI layout and event handling in JavaScript, and Atwood’s law will be fulfilled.

Exercises

1. Pick some part of the Java API that you want to explore—for example, the ZonedDateTime class. Run some experiments in jjs: construct objects, call methods, and observe the returned values. Did you find it easier than writing test programs in Java?

2. Run jjs and, using the stream library, interactively work out a solution for the following problem: Print all unique long words (> 12 characters) from a file in sorted order. First read the words, then filter the long words, and so on. How does this interactive approach compare to your usual workflow?

3. Run jjs. Call

var b = new java.math.BigInteger('1234567890987654321')

Then display b (simply by typing b and Enter). What do you get? What is the value of b.mod(java.math.BigInteger.TEN)? Why is b displayed so strangely? How can you display the actual value of b?

4. Construct a nonliteral JavaScript string by extracting a substring from another string, and invoke the getClass method. What class do you get? Then pass the object to java.lang.String.class.cast. What happens? Why?

5. At the end of Section 7.10, “Extending Java Classes and Implementing Java Interfaces,” on page 146, you saw how to extend ArrayList so that every call to add is logged. But that only worked for a single object. Write a JavaScript function that is a factory for such objects, so that one can generate any number of logging array lists.

6. Write a JavaScript function pipe that takes a sequence of shell commands and pipes the output of one to the input of the next, returning the final output. For example, pipe('find .', 'grep -v class', 'sort'). Simply call $EXEC repeatedly.

7. The solution of the preceding exercise is not quite as good as a Unix pipe because the second command only starts when the first one has finished. Remedy that by using the ProcessBuilder class.

8. Write a script that prints the values of all environment variables.

9. Write a script nextYear.js that obtains the age of the user and then prints Next year, you will be ..., adding 1 to the input. The age can be specified on the command line or the AGE environment variable. If neither are present, prompt the user.

10. Write a JavaFX program in JavaScript that reads data from a source of your choice and renders a pie chart. Was it easier or harder than developing the program in Java? Why?