I’ve faced with the following problem. I’d like to use java.util.HashMap
and java.util.PriorityQueue
in Nashorn script, where I need to use particular custom object as a key in the HashMap, and also use HashMap.containsKey()
to check if there is a key in the Map (another option is to check if the object in a Collection.contains(Object o)).
So, obviously, I need to implement equals and hashCode in my object basing on some field values.
For example:
Trying to use JavaScript. Doesn’t work due to JavaScript doesn’t have those methods. Please see Sample 1 and Sample 2
Extending java.lang.Object. Sample 3. Works partially, Methods are being invoked. But
Implementing my custom class in Java and extend it in JavaScript. Sample 4. Works. But do I need Nashorn if I have to use Java?
var PriorityQueue = java.util.PriorityQueue;
var HashMap = java.util.HashMap;
var Integer = java.lang.Integer;
// Sample 1
// Doesn't work, equals and hashCode are not being invoked
function Vertex1(from, cost) {
this.from = from;
this.cost = cost;
this.equals = function(other) { return this.from == other.from; }
this.hashCode = function() { return Integer.hashCode(this.from); }
}
var hm = new HashMap();
hm.put(new Vertex1(1, 10), 10);
hm.put(new Vertex1(1, 20), 21);
// Prints size is 2, but I'd like to see 1
print("HashMap size: " + hm.size());
// Prints false
print("HashMap1 contains: " + hm.containsKey(new Vertex1(1, 20)));
// ------------------------------------------------------------------
// Sample 2
// Doesn't work, equals and hashCode are not being invoked
function Vertex1(from, cost) {
this.from = from;
this.cost = cost;
}
Vertex1.prototype = {
equals : function(other) { return this.from == other.from; },
hashCode : function() { return Integer.hashCode(this.from); },
}
var hm = new HashMap();
hm.put(new Vertex1(1, 10), 10);
hm.put(new Vertex1(1, 20), 21);
// Prints size is 2, but I'd like to see 1
print("HashMap size: " + hm.size());
// Prints false
print("HashMap1 contains: " + hm.containsKey(new Vertex1(1, 20)));
// ------------------------------------------------------------------
// Sample 3
// Works partially, Methods are being invoked. But
// 1. How to plugin construstor with parameters?
// 2. How to do the cast from this:[object Object] to other:jdk.nashorn.javaadapters.java.lang.Object@0, or vice versa
var JObject = Java.type("java.lang.Object");
var Vertex2 = Java.extend(JObject, {
from : 0,
equals : function(other) { return this.from.equals(other.from); },
hashCode : function() { return Integer.hashCode(this.from); },
});
var hm = new HashMap();
// How to implement constructor for new Vertex2(10, 10)?
hm.put(new Vertex2(), 10);
hm.put(new Vertex2(), 21);
// Prints size is 2, because hashCode is the same and equals returns false
print("HashMap size: " + hm.size());
// Prints false, because equals returns false
print("HashMap1 contains: " + hm.containsKey(new Vertex2()));
// ------------------------------------------------------------------
// Sample 4
// com.arsenyko.MyObject is implemented in Java, Works, but Nashorn is ambiguous then!!!
var MyObject = Java.type("com.arsenyko.MyObject");
var Vertex2 = Java.extend(MyObject, {});
var hm = new HashMap();
hm.put(new Vertex2(1, 10), 10);
hm.put(new Vertex2(1, 20), 21);
print("HashMap size: " + hm.size());
print("HashMap1 contains: " + hm.containsKey(new Vertex2(1, 10)));
EDIT 1
@Tomasz, thanks. Have seen all the mentioned links. But though that somewhat undocumented exists. Almost gave up with Nashorn. Came to the following partial solution, methods are being invoked, constructor is being used, but how to cast other.from
in the equals
method in order to get access to the from
field of the original object (this code produces different classes for each instance of a Vertex):
//load("nashorn:mozilla_compat.js");
var PriorityQueue = java.util.PriorityQueue;
var HashMap = java.util.HashMap;
var Integer = java.lang.Integer;
function Vertex1(from, cost) {
this.from = from;
this.cost = cost;
this.equals = function(other) {
var value1 = this.from;
// How to get other.from here???
var value2 = other.from;
print('value1=' + value1 + ' value2=' + value2);
print(other);
var eq = value1.equals(value2);
print('equals is ' + eq);
return eq;
}
this.hashCode = function() {
var hashCode = Integer.hashCode(this.from);
print('hashCode is ' + hashCode);
return hashCode;
}
var JObject = Java.type("java.lang.Object");
// return Java.extend(JObject, this); // doesn't work
// return this; // doesn't work
// return new JavaAdapter(java.lang.Object, this); // Works! with load("nashorn:mozilla_compat.js");
var Type = Java.extend.apply(Java, [JObject]);
return new Type(this);
}
var hm = new HashMap();
hm.put(new Vertex1(1, 10), 10);
hm.put(new Vertex1(1, 20), 21);
// Prints size is 2, but I'd like to see 1
print("HashMap size: " + hm.size());
// Prints false
print("HashMap contains: " + hm.containsKey(new Vertex1(1, 20)));
EDIT 2
Thanks for Tomasz, as he pointed out, Every invocation of the Java.extend() function with a class-specific implementation object produces a new Java adapter class. Thus we need to have one Object Extender and instantiate objects with that one type, as he showed in his sample. I modified it a little bit, so it produces instances with the same class with either factory or direct constructor, since we are using the same Object Extender
var HashMap = java.util.HashMap;
var JInteger = java.lang.Integer;
var JObject = Java.extend(java.lang.Object);
var createVertex = (function() {
var
_equals = function(other) {
print(this + ' vs ' + other);
return this._from === other.from;
};
_hashCode = function() {
var hashCode = JInteger.hashCode(this._from);
print(hashCode);
return hashCode;
};
return function(from, cost) {
return new JObject() {
_from : from,
_cost : cost,
equals : _equals,
hashCode : _hashCode,
}
}
})();
var JSVertex = function(from, cost) {
return new JObject() {
_from : from,
_cost : cost,
equals : function(other) {
print(this + ' vs ' + other);
return this._from === other._from;
},
hashCode : function() {
var hashCode = JInteger.hashCode(this._from);
print(hashCode);
return hashCode;
}
}
}
var v1 = JSVertex(1, 10);
var v2 = JSVertex(1, 20);
//var v1 = createVertex(1, 10);
//var v2 = createVertex(1, 20);
var v3 = createVertex(1, 20);
print(v1.class === v2.class); // returns true
print(v2.class === v3.class); // returns true
var hm = new HashMap();
hm.put(v1, 10);
hm.put(v2, 21);
print("HashMap size: " + hm.size()); // Prints 2, but I'd like to see 1
print("HashMap contains: " + hm.containsKey(v3)); // Prints false
However, there is a still an issue, the type of the parameter of the equals
is jdk.nashorn.javaadapters.java.lang.Object
, in other words the other
and this
inside equals
are different types. Is there a way, to cast or get _from
value from the object passed to the equals
?
THE SOLUTION
See the solution for the problem is in the Tomasz's answer.
Great work Tomasz! Thanks.
PS : It's very sad that there is no neat and straightforward way to implement equals
and hashCode
in Nashorn. It would be useful for prototyping. Just compare that with this :)
import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode(excludes="cost")
class Vertex {
int from, cost
}
In Rhino you would use :
var vertex = new JavaAdapter(java.lang.Object, new Vertex(1, 10));
hm.put(vertex, 10);
to make the JavaScript methods override same named Java methods from java.lang.Object (see reference https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Scripting_Java#The_JavaAdapter_Constructor)
Maybe there is a similar construct in Nashorn.
EDIT:
You may use Rhino syntax in Nashorn. Just put the line :
load("nashorn:mozilla_compat.js");
See: https://wiki.openjdk.java.net/display/Nashorn/Rhino+Migration+Guide
EDIT: (AGAIN)
With Nashorn it seems to be much more complicated:
// we will need a factory method
var createVertex = (function() { // i hope you are familiar with "inline" function calls
// private variables used in every call of factory method - but initialized once
var
JObjExtender = Java.extend(Java.type("java.lang.Object")),
JInteger = Java.type("java.lang.Integer"),
_equals = function(other) {
return this.from === other.from;
},
_hashCode = function() {
return JInteger.hashCode(+this.from); // leading "+" converts to number
};
// the "actual" factory method
return function(from, cost) {
return new JObjExtender() {
from : from,
cost : cost,
equals : _equals,
hashCode : _hashCode
};
};
})();
var vertex = createVertex(1, 10);
hm.put(vertex, 10);
See http://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/javascript.html
What is more interesting, if you create multiple instances like below:
var v1 = createVertex(1, 10);
var v2 = createVertex(1, 20);
Then they are of the same class (I expected them to be instances of two anonymous sunclasses of Object
).
var classEquals = (v1.class === v2.class); // produces : true
A TRICK:
Although in Nashorn you can NOT extend non-abstract classes on the fly like:
var v1 = new java.lang.Object(new JSVertex(10, 10));
// produces: TypeError: Can not construct java.lang.Object with the passed
// arguments; they do not match any of its constructor signatures.
You may extend in this way any abstract classes or interfaces. (And as any anonymous class implementing an interface also extends Object so you can overwrite equals
or hashCode
methods too).
To illustrate this, consider you have a JavaScript "prototype-class":
var JSVertex = function (from, cost) {
this.from = from;
this.cost = cost;
};
JSVertex.prototype = {
equals : function(other) {
return this.from === other.from;
},
hashCode : function() {
return java.lang.Integer.hashCode(+this.from); // leading "+" converts to number
},
compare : function(other) {
return this.from - (+other.from);
}
};
now you can create its "Java-wrapped" instances as below:
var v1 = new java.lang.Comparable(new JSVertex(10, 10));
print(v1.class);
// produces both: class jdk.nashorn.javaadapters.java.lang.Object and
// class jdk.nashorn.javaadapters.java.lang.Comparable
var v2 = new java.lang.Comparable(new JSVertex(11, 12));
print(v2 instanceof java.lang.Object); // produces true
print(v2 instanceof java.lang.Comparable); // produces true
Knowing that you can create an empty Java interface to enable such wrappers without a need for additional method implementations to be provided (like compare
in the example with Comparable
above).
PROBLEM
As You pointed out the objects created in both ways presented above are Java objects with fixed "interface". Thus any method or field from wrapped JavaScript object, which has not been explicitly specified by implented interfaces, or classes will NOT be accessible from javascript.
THE SOLUTION
After some fiddling I found the solution to the above problem. A key to it is jdk.nashorn.api.scripting.AbstractJSObject
class from Nashorn scripting API.
Consider we have JSVertex "javascript class" (very similar to already presented above):
var JSVertex = function (from, cost) {
this.from = +from;
this.cost = +cost;
};
JSVertex.prototype = {
equals : function(other) {
print("[JSVertex.prototype.equals " + this + "]");
return this.from === other.from;
},
hashCode : function() {
var hash = java.lang.Integer.hashCode(this.from);
print("[JSVertex.prototype.hashCode " + this + " : " + hash + "]");
return hash;
},
toString : function() {
return "[object JSVertex(from: " +
this.from + ", cost: " + this.cost + ")]";
},
// this is a custom method not defined in any Java class or Interface
calculate : function(to) {
return Math.abs(+to - this.from) * this.cost;
}
};
Let's create a function which will allow us to wrap Java Object over any JavaScript object in that way, that any same-named method from the JavaScript object would "extend" corresponding Java Object method.
var wrapJso = (function() {
var
JObjExtender = Java.extend(Java.type(
"jdk.nashorn.api.scripting.AbstractJSObject")),
_getMember = function(name) {
return this.jso[name];
},
_setMember = function(name, value) {
this.jso[name] = value;
},
_toString = function() {
return this.jso.toString();
};
return function(jsObject) {
var F = function() {};
F.prototype = jsObject;
var f = new F();
f.jso = jsObject;
f.getMember = _getMember;
f.setMember = _setMember;
f.toString = _toString; // "toString hack" - explained later
return new JObjExtender(f);
};
})();
Finally having written that all let's see it working.
Create a wrapper over JSVertex object and do some tests on it:
var wrapped = wrapJso(new JSVertex(11,12));
// access custom js property and method not defined in any java class
// or interface.
print(wrapped.from);
print(wrapped.calculate(17));
print("--------------");
// call toString() and hashCode() from JavaScript on wrapper object
print(wrapped.toString());
print(wrapped.hashCode());
print("--------------");
// Use StringBuilder to make Java call toString() on our wrapper object.
print(new java.lang.StringBuilder().append(wrapped).toString() );
// see hack in wrapJso() - for some reason java does not see
// overriden toString if it is defined as prototype member.
// Do some operations on HashMap to get hashCode() mehod called from java
var map = new java.util.HashMap();
map.put(wrapped, 10);
map.get(wrapped);
wrapped.from = 77;
map.get(wrapped);
print("--------------");
// let's show that modyfing any of pair: wrapped or jso touches underlying jso.
var jso = new JSVertex(17,128);
wrapped = wrapJso(jso);
print(wrapped);
jso.from = 9;
wrapped.cost = 10;
print(wrapped);
print(jso);
print(jso == wrapped);
The output:
11
72
--------------
[object JSVertex(from: 11, cost: 12)]
[JSVertex.prototype.hashCode [object JSVertex(from: 11, cost: 12)] : 11]
11
--------------
[object JSVertex(from: 11, cost: 12)]
[JSVertex.prototype.hashCode [object JSVertex(from: 11, cost: 12)] : 11]
[JSVertex.prototype.hashCode [object JSVertex(from: 11, cost: 12)] : 11]
[JSVertex.prototype.hashCode [object JSVertex(from: 77, cost: 12)] : 77]
--------------
[object JSVertex(from: 17, cost: 128)]
[object JSVertex(from: 9, cost: 10)]
[object JSVertex(from: 9, cost: 10)]
false
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With