Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nashorn, Java collections, How to implement equals and hashCode in pure javascript

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:

  1. Trying to use JavaScript. Doesn’t work due to JavaScript doesn’t have those methods. Please see Sample 1 and Sample 2

  2. Extending java.lang.Object. Sample 3. Works partially, Methods are being invoked. But

    • How to plugin constructor with parameters?
    • How to do the cast from this:[object Object] to other:jdk.nashorn.javaadapters.java.lang.Object@0, or vice versa?
  3. 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
}
like image 374
Arseny Kovalchuk Avatar asked Aug 19 '14 08:08

Arseny Kovalchuk


1 Answers

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
like image 112
Tomasz Gawel Avatar answered Oct 21 '22 13:10

Tomasz Gawel