Skip to content

Java Type Objects

Attila Szegedi edited this page Mar 16, 2021 · 3 revisions

Nashorn represents Java types with a special class of objects, we call these "Java type objects", or just "type objects". They are used as constructors with the new operator, they expose static fields, properties, and methods, and they can be used with the ECMAScript instanceof operator as well.

Obtaining type objects

A typical way to obtain a type object from a type name is using the Java.type method:

 var arrayListType = Java.type("java.util.ArrayList")
 var intType = Java.type("int")
 var stringArrayType = Java.type("java.lang.String[]")
 var int2DArrayType = Java.type("int[][]")

Note that the name of the type is always a string for a fully qualified name.

Using type objects as constructors

You can use any of these types to create new instances, e.g.:

 var anArrayList = new (Java.type("java.util.ArrayList"))

or

 var ArrayList = Java.type("java.util.ArrayList")

 var anArrayList = new ArrayList
 var anArrayListWithSize = new ArrayList(16)

When array types are used as constructors, they take one parameter that is the array size:

var intArrType = Java.type("int[]")
var intArr5 = new intArrType(5)

is equivalent to new int[5] in Java.

Type objects for inner classes

In the special case of inner classes, you can either use the JVM fully qualified name, meaning using $ sign in the class name, or you can use the dot:

 var ftype = Java.type("java.awt.geom.Arc2D$Float")

and

 var ftype = Java.type("java.awt.geom.Arc2D.Float")

both work. Note however that using the dollar sign is faster, as Java.type first tries to resolve the class name as it is originally specified, and the internal JVM names for inner classes use the dollar sign. If you use the dot, Java.type will internally get a ClassNotFoundException and subsequently retry by changing the last dot to dollar sign. It'll keep replacing dots with dollar signs until it either successfully loads the class or runs out of all dots in the name. This way it can correctly resolve and load even multiply nested inner classes with the dot notation. Again, this will be slower than using the dollar signs in the name.

Finally, you can also access the type object for an inner class as a property of the type object for the outer class:

 var arctype = Java.type("java.awt.geom.Arc2D")
 var ftype = arctype.Float

You can access both static and non-static inner classes. If you want to create an instance of a non-static inner class, remember to pass an instance of its outer class as the first argument to the constructor.

Accessing static members

Type objects expose all static fields and methods on the Java class they represent as properties. Examples:

 var File = Java.type("java.io.File")
 var pathSep = File.pathSeparator
 var tmpFile1 = File.createTempFile("abcdefg", ".tmp")
 var tmpFile2 = File.createTempFile("abcdefg", ".tmp", new File("/tmp"))

You can even assign static methods to variables and invoke them on their own, so the above example can be rewritten as:

 var File = Java.type("java.io.File")
 var createTempFile = File.createTempFile
 var tmpFile1 = createTempFile("abcdefg", ".tmp")
 var tmpFile2 = createTempFile("abcdefg", ".tmp", new File("/tmp"))

If you need to access the actual java.lang.Class object for the type, you can use the class property on the object representing the type, just as you would do with a type name in Java:

 var File = Java.type("java.io.File")
 var someFile = new File("blah")
 print(File.class === someFile.getClass()) // prints true

Nashorn also exposes a synthetic property named static on any java.lang.Class object to retrieve its type-representing object:

 var File = Java.type("java.io.File")
 print(File.class.static === File) // prints true

Note that this functionality doesn't have a Java equivalent as in Java, types are not objects themselves.

Instanceof operator

Type objects work with the ECMAScript instanceof operator:

 var File = Java.type("java.io.File")
 var aFile = new File("foo")
 print(aFile instanceof File) // prints true

Type objects vs. Class objects

It is a good time to clarify the difference between type objects and java.lang.Class objects.

In Nashorn, type objects are distinct from Class objects. Class objects are treated as ordinary Java objects. You can call getClass() (it also works as a class property) on any object to get its Class object, and call any methods on it such as getName(), or getSuperclass() etc. However, you can not use them as constructors, and you can not retrieve static members of the class through them. This distinction exists in Java too: you don't write new File.class("/tmp") or File.class.pathSeparator, or aFile instanceof File.class but rather use only the type name ("File") in these places that gets resolved by the compiler. The difference is that in ECMAScript, there's no compiler involved, and everything is an object, so we introduce a separate class of objects to represent types for purposes of construction, subclassing, and static access.

For those curious about details, type objects are not actually implemented in Nashorn. They are instead provided by the JDK built-in jdk.dynalink module that Nashorn heavily relies on; they are instances of jdk.dynalink.beans.StaticClass.

Instantiating abstract classes and interfaces

Similar to how you can create anonymous inner classes in Java, you can use the type objects of abstract classes and interfaces to create anonymous subclasses. If the type is abstract, you can instantiate an anonymous subclass of it using an argument list that is applicable to any of its public or protected constructors and adding a ECMAScript object with functions properties that provide ECMAScript implementations of the abstract methods. E.g.:

 var TimerTask =  Java.type("java.util.TimerTask")
 var task = new TimerTask({ run: function() { print("Hello World!") } })

While not pictured here, it is important to note that when method names are overloaded, a ECMAScript function for a name will provide implementation for all overloads of the name.

Nashorn supports a syntactic extension where a "new" expression followed by an argument is identical to invoking the constructor and passing the argument to it, so you can write the above example also as:

 var task = new TimerTask {
     run: function() {
       print("Hello World!")
     }
 }

which is very similar to Java anonymous inner class definition. On the other hand, if the type is an abstract type with a single abstract method (commonly referred to as a "SAM type" or "lambda") or all abstract methods it has share the same overloaded name, then instead of an object, you can just pass a function, so the above example can become even further simplified to:

 var task = new TimerTask(function() { print("Hello World!") })

Note that in every one of these cases if you are trying to instantiate an abstract class that has constructors that take some arguments, you can invoke those simply by specifying the arguments after the initial implementation object or function.

The use of functions can be taken even further; if you are invoking a Java method that takes a SAM type, you can simply pass in a function object, and Nashorn will know what you meant:

 var timer = new (Java.type("java.util.Timer"))
 timer.schedule(function() { print("Hello World!") }, 0)

Here, Timer.schedule() expects a TimerTask as its argument, so Nashorn creates an instance of a TimerTask subclass and uses the passed function to implement its only abstract method, run(). In this usage though, you can't use non-default constructors; the type must be either an interface, or must have a protected or public no-arg constructor.

Extending classes and implementing interfaces

In addition to the above functionality of creating anonymous subclasses of abstract Java classes and interfaces, you can also subclass non-abstract Java classes and implement an arbitrary number of Java interfaces in a single class using Nashorn; for that you will need to use the Java.extend method. Example:

Example:
 var ArrayList = Java.type("java.util.ArrayList")
 var ArrayListExtender = Java.extend(ArrayList)
 var printSizeInvokedArrayList = new ArrayListExtender() {
     size: function() { print("size invoked!"); }
 }
 var printAddInvokedArrayList = new ArrayListExtender() {
     add: function(x, y) {
       if(typeof(y) === "undefined") {
           print("add(e) invoked!");
       } else {
           print("add(i, e) invoked!");
       }
 }

Note that if the Java method is overloaded (as in the above example List.add()), then your ECMAScript adapter must be prepared to deal with all overloads.

It is also possible to specify an ordinary ECMAScript object as the last argument to extend. In that case, it is treated as a class-level override. extend will return an extender class where all instances will have the methods implemented by functions on that object, just as if that object were passed as the last argument to their constructor. Example:

 var Runnable = Java.type("java.lang.Runnable")
 var R1 = Java.extend(Runnable, {
     run: function() {
         print("R1.run() invoked!")
     }
 })
 var r1 = new R1
 var t = new java.lang.Thread(r1)
 t.start()
 t.join()

As you can see, you don't have to pass any object when you create a new instance of R1 as its run() function was defined already when extending the class. If you also want to add instance-level overrides on these objects, you will have to repeatedly use extend() to subclass the class-level adapter. For such adapters, the order of precedence is instance-level method, class-level method, superclass method, or UnsupportedOperationException if the superclass method is abstract. If we continue our previous example:

 var R2 = Java.extend(R1);
 var r2 = new R2(function() { print("r2.run() invoked!") })
 r2.run()

We'll see it'll print "r2.run() invoked!", thus overriding on instance-level the class-level behavior. Note that you must use Java.extend to explicitly create an instance-override adapter class from a class-override adapter class, as the class-override adapter class is no longer abstract!

To invoke super methods from adapters, call them on the adapter instance prefixing them with super$, or use the special super-adapter created by the Java._super method:

 var cw = new FilterWriterAdapter(sw) {
     write: function(s, off, len) {
         s = capitalize(s, off, len)
         cw_super.write(s, 0, s.length())
     }
 }
 var cw_super = Java._super(cw)

You can pass multiple types to Java.extend. Of those, at most one can represent a class, and the rest must be interfaces. The created adapter class will extend the specified class (or java.lang.Object if no classes are specified) and implement all specified interfaces.

Every specified list of Java types with Java.extend will have one extender subclass in Nashorn -- repeated invocations of extend with the same list of types will yield the same extender type. It's a generic adapter that delegates to whatever ECMAScript functions its implementation object has on a per-instance basis. If you are using a security manager then this is somewhat refined: you will end up with one extender subclass for scripts in the same protection domain. Nashorn must create one extender subclass for every extended type for every protection domain from which an extender subclass is requested.

Java.extend doesn't deduplicate adapter classes for different order of parameters. If you specify

var Runnable = Java.type("java.lang.Runnable")
var Comparable = Java.type("java.lang.Comparable")

var t1 = Java.extend(Runnable, Comparable)
var t2 = Java.extend(Runnable, Comparable)
var t3 = Java.extend(Comparable, Runnable)

then t1 and t2 will be the same type object, but t3 will be distinct from t1 even though they functionally equivalent.