-
Notifications
You must be signed in to change notification settings - Fork 851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve NativeJavaList to behave more like NativeArray #830
Conversation
listD.add(1.0); | ||
listD.add(2.0); | ||
listD.add(3.0); | ||
assertEquals("1", runScriptAsString("value.indexOf(2)", listD)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this calling Java's indexOf
method?
https://docs.oracle.com/javase/8/docs/api/java/util/List.html#indexOf-java.lang.Object-
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
They differ in the following ways.
- JS can receive
fromIndex
as the second argument.
var list = new java.util.ArrayList();
list.indexOf(1, 2);
// js: Can't find method java.util.ArrayList.indexOf(number,number).
- The equivalence check is different.
list.add(NaN);
list.indexOf(NaN); // 0
[NaN].indexOf(NaN); // -1
https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html#equals-java.lang.Object-
There may be compatibility issues. Sorry, mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, value.indexOf
will call the Java's indexOf method.
Array.indexOf(value,...)
will call the JS indexOf method.
The question is, what to do, if a method is defined by Java and also by the JS-Array-prototype. Which one should we prefer?
Logically, the method in NativeJavaList will override the method of the prototype.
The next question is, what shoud happen if you wrap an instance, that declares other methods:
public class MyList extends ArrayList {
public String join() {
...
}
}
list1 = new ArrayList();
list2 = new MyList();
runScriptAsString("value.join('-')", list1); // call js_join
runScriptAsString("value.join('-')", list2); // will call MyList.join - but arguments will not match
In Rhino, all methods of an instance can be accessed. // java
public static List getList() {
return new ArrayList();
} // js
var list = Foo.getList();
// ArrayList method, not a List method.
list.forEach;
// function forEach() {/*
// void forEach(java.util.function.Consumer)
// */} When calling Java's methods against the return value of |
I think it would be better to implement |
On the one hand I think, that JS methods should have always priority, maybe we should not inherit from NativeJavaObject at all??
Hmm, you mean: var javaList = new java.util.ArrayList()
var jsList = Array.from(javaList) This means, that changes on |
If it don't prioritize Java methods, it may run into problems with code written in the past. |
private List<Object> list; | ||
|
||
@SuppressWarnings("unchecked") | ||
public NativeJavaList(Scriptable scope, Object list) { | ||
super(scope, list, list.getClass()); | ||
assert list instanceof List; | ||
this.list = (List<Object>) list; | ||
setPrototype(ScriptableObject.getClassPrototype(scope, "Array")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes it possible to invoke all/most NativeArray mehtods also an NativeJavaList
Is this a good/acceptable change?
@@ -39,6 +42,13 @@ public boolean has(int index, Scriptable start) { | |||
} | |||
return super.has(index, start); | |||
} | |||
|
|||
public void delete(int index) { | |||
if (isWithValidIndex(index)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delete value[0]
does work, but will return false
as has(0,start)
will still return true
Should we maintain a boolean[]
array to store all undefined
values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO this is a tradeOff, I would not add this complexity to NativeArray to simulate "empty slots"
} | ||
if (y instanceof Wrapper) { | ||
y = ((Wrapper) y).unwrap(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unwrap here was neccessary to make 'includes' work.
I can place the unwrap also here: https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/NativeArray.java#L1766
@@ -3372,6 +3378,12 @@ private static boolean eqString(CharSequence x, Object y) | |||
} | |||
public static boolean shallowEq(Object x, Object y) | |||
{ | |||
if (x instanceof Wrapper) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was neccessary to make 'indexOf'/'lastIndexOf' work.
The unwrap can also be placed here:
https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/NativeArray.java#L1671
https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/NativeArray.java#L1727
// FIXME: This will invoke the java.util.List.indexOf method, which | ||
// is not yet type aware! | ||
// assertEquals("1", runScriptAsString("value.lastIndexOf(2)", listI)); | ||
// assertEquals("-1", runScriptAsString("value.lastIndexOf(4)", listD)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to write a test for each method defined by the Array
prototype.
There are some methods defined by the wrapped object (java.util.List). These methods have precedence.
listD.add(1.0); | ||
listD.add(2.0); | ||
listD.add(3.0); | ||
assertEquals("1", runScriptAsString("value.indexOf(2)", listD)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, value.indexOf
will call the Java's indexOf method.
Array.indexOf(value,...)
will call the JS indexOf method.
The question is, what to do, if a method is defined by Java and also by the JS-Array-prototype. Which one should we prefer?
Logically, the method in NativeJavaList will override the method of the prototype.
The next question is, what shoud happen if you wrap an instance, that declares other methods:
public class MyList extends ArrayList {
public String join() {
...
}
}
list1 = new ArrayList();
list2 = new MyList();
runScriptAsString("value.join('-')", list1); // call js_join
runScriptAsString("value.join('-')", list2); // will call MyList.join - but arguments will not match
My bad. Changes in this way should not be done.
In most cases, there should be no conflicts, as in most cases, ArrayLists are used. And if there are conflicts, it "works as designed", respectively, this should be specified as "design" BTW: Sry. I assume most of my "review" comments were not yet visible for you, because they were "pending" |
I'm sorry -- I've lost track of what we collectively want to do with this collection of this PR. @rPraml I am hoping that we can still resolve this but I'm not sure where you left it on the review comments. Do we still want to pursue this? |
# Conflicts: # src/org/mozilla/javascript/NativeJavaList.java # src/org/mozilla/javascript/ScriptRuntime.java
@gbrail I updated this PR (I had not much time in the past)
|
@@ -17,6 +18,7 @@ public NativeJavaList(Scriptable scope, Object list) { | |||
super(scope, list, list.getClass()); | |||
assert list instanceof List; | |||
this.list = (List<Object>) list; | |||
setPrototype(ScriptableObject.getClassPrototype(scope, "Array")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if you are using instanceof to determine if it is an array?
if (value instanceof Array) {
// some code to operate `value`
}
How about Array.isArray
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello Tuchida, value instanceof Array
will return true
, while Array.isArray(value)
will return false.
I get different results in firefox
class inheritance
class Foo extends Array {};
var y = new Foo();
y instanceof Array; // returns true
Array.isArray(y); // returns true
vs. prototype
var Bar = function(){};
Bar.prototype = Array.prototype;
var x = new Bar();
x instanceof Array; // returns true
Array.isArray(x); // returns false
What do you think, is ok, when we return true for instanceof
and false for isArray
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think javaList instanceof Array
should be false
.
Java's List
and JavaScript's Array
are different, so their detailed behaviors are different.(For example, .indexOf
)
If the existing code is written as follows, a List
will be passed to the code that is expecting an Array
.
if (value instanceof Array) {
// some code to operate `value`
}
I think it may cause compatibility issues.
@tuchida I made JavaList/JavaObject available as prototypes and made some fixes in class inheritance, so all of them is true: var x= new java.util.ArrayList();
x instanceof JavaList;
x instanceof JavaObject;
x instanceof Object;
x instanceof java.util.List To add the array functions, I can define them now on the Object.getOwnPropertyNames(Array.prototype).forEach(function(x) { JavaList.prototype[x] = Array.prototype[x]}) I think this would be the nicest solutuion, when someone wants to handle JavaList as an array. What do you think about that change? |
Haven't followed along here, but you might want to look at how GraalJS handles Java List interoperability, if only to see how they solved things |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To what extent do you want Java's List
and JS's Array
to be the same?
If we make such a change, it would be better to discuss the specification before implementing it.
For example, as p-bakker commented, I think there are issues that should be considered for the specification, such as investigating how GraalJS and Nashorn are doing.
|
||
public Object call( | ||
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { | ||
throw new UnsupportedOperationException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not an EcmaError
and will cause the process to terminate.
js> new JavaMap()
Exception in thread "main" java.lang.UnsupportedOperationException: Function not constructable
at org.mozilla.javascript.NativeJavaObject$1.call(NativeJavaObject.java:79)
at org.mozilla.javascript.BaseFunction.construct(BaseFunction.java:385)
at org.mozilla.javascript.ScriptRuntime.newObject(ScriptRuntime.java:2678)
at org.mozilla.javascript.gen._stdin__8._c_script_0(Unknown Source)
at org.mozilla.javascript.gen._stdin__8.call(Unknown Source)
at org.mozilla.javascript.ContextFactory.doTopCall(ContextFactory.java:380)
at org.mozilla.javascript.ScriptRuntime.doTopCall(ScriptRuntime.java:3872)
at org.mozilla.javascript.gen._stdin__8.call(Unknown Source)
at org.mozilla.javascript.gen._stdin__8.exec(Unknown Source)
at org.mozilla.javascript.tools.shell.Main.processSource(Main.java:497)
at org.mozilla.javascript.tools.shell.Main.processFiles(Main.java:181)
at org.mozilla.javascript.tools.shell.Main$IProxy.run(Main.java:101)
at org.mozilla.javascript.Context.call(Context.java:535)
at org.mozilla.javascript.ContextFactory.call(ContextFactory.java:472)
at org.mozilla.javascript.tools.shell.Main.exec(Main.java:163)
at org.mozilla.javascript.tools.shell.Main.main(Main.java:138)
|
||
private List<Object> list; | ||
|
||
static void init(ScriptableObject scope, boolean sealed) { | ||
NativeJavaList obj = new NativeJavaList(); | ||
obj.exportAsJSClass(scope, "JavaObject", sealed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct to say that new JavaObject
, JavaList
, and JavaMap
will be added to the global variables?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why these three? There are many interfaces and classes in Java. For example, if you want to extend a Set
or Stream
with a prototype, do you have to add more JavaXXX each time? Why List
instead of Collection
?
If you have a problem you want to solve, it is better to fully discuss what specifications you want to use before implementing them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My original problem was, that the Wrappers (JavaList
/ JavaMap
) should provide the same functionality as the equivalent JS objects (Array
/Object
), so that I can use existing javaScript code that can work on the wrappers without modification. This goal has already been achieved very well by other PRs that improve List/Map handling.
I also took a look at GraalJS as suggested by @p-bakker
You can add objects with list[x] = value
, but only if x
points to the end of the list (x == list.length
)
In this PR I think I've lost the focus.
I will investigate in that direction and will add only neccessary features to rhino
@@ -34,6 +34,8 @@ | |||
|
|||
static void init(ScriptableObject scope, boolean sealed) { | |||
JavaIterableIterator.init(scope, sealed); | |||
NativeJavaObject obj = new NativeJavaObject(); | |||
obj.exportAsJSClass(scope, "Object", sealed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct that JavaObject
inherits from JS Object
?
JS Object
prototype defines valueOf
, toLocaleString
, etc. Are there likely to be compatibility issues with these?
I consider Java's var javaList = java.util.ArrayList([1, 2, 3]);
var jsArray = Array.from(javaList); In JavaScript, there are Array-Like objects that look like Arrays but are not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't followed along here, but you might want to look at how GraalJS handles Java List interoperability, if only to see how they solved things
I made some tests with GraalVM - I think this is the way we should also go in
> var ArrayList = Java.type("java.util.ArrayList");
> var List = Java.type("java.util.List")
> var Map = Java.type("java.util.Map")
> typeof ArrayList
function
> var list = new ArrayList();
> typeof list
object
> var arr = []
> typeof arr
object
> arr instanceof Object
true
> list instanceof Object
false
> list instanceof List
true
> list instanceof Map
false
> arr instanceof List
false
> Object.getPrototypeOf(list)
null
> Object.getOwnPropertyNames(arr)
["length"]
> Object.getOwnPropertyNames(list)
(31)["add", "remove", "get", "clone", "indexOf", "clear", "isEmpty", ...
> list[0] = 'a'
> list[1] = 'b'
> list[4] = 'c' // can only add element to the end
> list
(2)["a", "b"]
Array.prototype.includes.call(list, 'b')
true
Array.prototype.includes.call(list, 'c')
false
I will update the PR and remove unneccesary things
|
||
private List<Object> list; | ||
|
||
static void init(ScriptableObject scope, boolean sealed) { | ||
NativeJavaList obj = new NativeJavaList(); | ||
obj.exportAsJSClass(scope, "JavaObject", sealed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My original problem was, that the Wrappers (JavaList
/ JavaMap
) should provide the same functionality as the equivalent JS objects (Array
/Object
), so that I can use existing javaScript code that can work on the wrappers without modification. This goal has already been achieved very well by other PRs that improve List/Map handling.
I also took a look at GraalJS as suggested by @p-bakker
You can add objects with list[x] = value
, but only if x
points to the end of the list (x == list.length
)
In this PR I think I've lost the focus.
I will investigate in that direction and will add only neccessary features to rhino
@@ -33,7 +37,7 @@ public String getClassName() { | |||
@Override | |||
public boolean has(String name, Scriptable start) { | |||
Context cx = Context.getCurrentContext(); | |||
if (cx != null && cx.hasFeature(Context.FEATURE_ENABLE_JAVA_MAP_ACCESS)) { | |||
if (map != null && cx != null && cx.hasFeature(Context.FEATURE_ENABLE_JAVA_MAP_ACCESS)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should be introducing the possibility of map being null.
I would close this PR and create a new one |
…ist-length-support # Conflicts: # src/org/mozilla/javascript/NativeJavaList.java
# Conflicts: # testsrc/org/mozilla/javascript/tests/NativeJavaListTest.java
closing this PR - all changes are in other PRs |
This PR extends the NativeJavaList to behave more like NativeArray.
Some ideas are extracted from #827
@tuchida maybe you can take a look at this.