Nashorn, коллекции Java, как реализовать equals и hashCode в чистом javascript

я столкнулся со следующей проблемой. Я хотел бы использовать java.util.HashMap и java.util.PriorityQueue на Насхорн скрипт, где мне нужно использовать конкретный пользовательский объект в качестве клавиша в HashMap, а также использовать HashMap.containsKey() чтобы проверить, есть ли ключ на карте (другой вариант-проверить, есть ли объект в коллекции.содержит(объект o)).

итак, очевидно, мне нужно реализовать equals и hashCode в моем объекте на основе некоторых значений полей.

для пример:

  1. попытка использовать JavaScript. Не работает из-за того, что JavaScript не имеет этих методов. Пожалуйста, смотрите Пример 1 и Пример 2

  2. расширение java.ленг.Объект. Пример 3. Работает частично, вызываются методы. Но!--24-->

    • Как подключить конструктор с параметрами?
    • как сделать бросок из этого: [объект Объект] для другого: jdk.нэшорн.javaadapters.Ява.ленг.Объект@0, или наоборот?
  3. реализация моего пользовательского класса в Java и расширение его в JavaScript. Пример 4. Завод. Но нужен ли мне Nashorn, если мне нужно использовать 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)));

правка 1

@Tomasz, спасибо. Видели все упомянутые ссылки. Но хотя это несколько недокументировано существует. Почти сдался с Насхорн. Пришел к следующему частичному решению, вызываются методы, используется конструктор, но С other.from на equals способ для того, чтобы получить доступ к from поле исходного объекта (этот код создает разные классы для каждого экземпляра вершины):

//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)));

Изменить 2

Спасибо за Tomasz, как он указал, каждый вызов Java.функция extend () с реализацией класса объект создает новый класс адаптера Java. Таким образом, мы должны иметь один расширитель объектов и создавать объекты с этим типом, как он показал в своем примере. Я немного изменил его, поэтому он создает экземпляры с тем же классом с помощью factory или direct constructor, так как мы используем один и тот же расширитель объектов

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

тем не менее, есть еще проблема, тип параметра equals и jdk.nashorn.javaadapters.java.lang.Object, т. е. other и this внутри equals несколько различные формы. есть ли способ, чтобы бросить или сделать _from значение из объекта, переданного в equals?

РЕШЕНИЕ

см. решение проблемы в ответе Томаша.

отличная работа Томаш! Спасибо.

PS: очень грустно, что нет аккуратного и простого способа реализовать equals и hashCode в Насхорн. Было бы полезно для прототипирования. Просто сравнить что с этим:)

import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode(excludes="cost")
class Vertex {
   int from, cost
}

1 ответов


в Rhino вы бы использовали:

var vertex = new JavaAdapter(java.lang.Object, new Vertex(1, 10));
hm.put(vertex, 10);

чтобы методы JavaScript переопределяли одноименные методы Java из java.ленг.Объект (см. ссылку https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Scripting_Java#The_JavaAdapter_Constructor)

может есть похожая конструкция в Насхорн.

EDIT:

вы можете использовать синтаксис Rhino в Nashorn. Просто поставь точку. :

load("nashorn:mozilla_compat.js");

посмотреть: https://wiki.openjdk.java.net/display/Nashorn/Rhino+миграции+руководство

EDIT: (СНОВА)

С Nashorn, кажется, гораздо сложнее:

// 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);

посмотреть http://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/javascript.html

что более интересно, если вы создаете несколько экземпляров, как ниже:

var v1 = createVertex(1, 10);
var v2 = createVertex(1, 20);

тогда они одного класса (я ожидал, что они будут экземплярами двух анонимных sunclasses Object).

var classEquals = (v1.class === v2.class); // produces : true

ТРЮК:

хотя в Nashorn вы не можете расширить не абстрактные классы на лету, как:

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.

вы можете расширить таким образом любые абстрактные классы или интерфейсы. (И как любой анонимный класс, реализующий интерфейс, также расширяет объект, чтобы вы могли перезаписать equals или hashCode методов).

чтобы проиллюстрировать это, рассмотрим, что у вас есть 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);
    }
};

теперь вы можете создать его экземпляры "Java-wrapped", как показано ниже:

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

зная, что вы можете создать пустой интерфейс Java, чтобы включить такие оболочки без необходимости в дополнительных реализациях методов (например,compare в Примере с Comparable выше).

Как Вы указывалось, что объекты, созданные обоими способами, представленными выше, являются объектами Java с фиксированным "интерфейсом". Таким образом, любой метод или поле из обернутого объекта JavaScript, который не был явно указан имплантированными интерфейсами, или классы не будут доступны из javascript.

РЕШЕНИЕ

после некоторой возни я нашел решение вышеуказанной проблемы. Ключ к нему -jdk.nashorn.api.scripting.AbstractJSObject класс из Nashorn scripting API.

рассмотрим мы у JSVertex "класс javascript" (очень похожий на уже представленный выше):

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;
    }
};

давайте создадим функцию, которая позволит нам обернуть объект Java над любым объектом JavaScript таким образом, что любой одноименный метод из объекта JavaScript будет "расширять" соответствующий метод объекта Java.

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);
    };
})();

наконец-то написав, что все давайте посмотрим, как это работает.

создайте оболочку над объектом JSVertex и выполните некоторые тесты это:

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);

вывод:

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