[ 
https://issues.apache.org/jira/browse/GROOVY-12585?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12585:
-------------------------------
    Description: 
h3. Problem

Groovy turns a collection into another collection type when it is assigned or 
cast (\{{T x = value}}, \{{(T) value}}, through 
\{{DefaultTypeTransformation.castToType}}), and when it is coerced with \{{as}} 
(through \{{DefaultGroovyMethods.asType(Collection, Class)}}). The two paths 
know different default implementations, and neither covers the common 
collection interfaces. So:
* some assignments fail where the equivalent \{{as}} works;
* some \{{as}} coercions return a proxy that does not behave like the requested 
type;
* an array source and a collection source give different results for the same 
target.

Results below are the same with indy and classic call sites, on JDK 17 and 21.

h4. 1. Assignment fails where \{{as}} works

||Target||{{T x = [3, 1, 2, 1]}}||{{[3, 1, 2, 1] as T}}||
|{{SortedSet}}|{{GroovyCastException}}|{{TreeSet [1, 2, 3]}}|
|\{{Queue}}|\{{GroovyCastException}}|\{{LinkedList [3, 1, 2, 1]}}|
|\{{Stack}}|\{{GroovyCastException}}|\{{Stack [3, 1, 2, 1]}}|
|\{{List}} (source \{{[3, 1, 2] as 
LinkedHashSet}})|\{{GroovyCastException}}|\{{ArrayList [3, 1, 2]}}|

{\{asType}} has a table of defaults for \{{List}}, \{{Set}}, \{{SortedSet}}, 
\{{Queue}} and \{{Stack}}. \{{castToType}} has none for a \{{Collection}} 
source: it gives a \{{LinkedHashSet}} when the target accepts one. Otherwise it 
tries the target's constructor with the elements, then with the collection, 
which fails for an interface and for a class without a copy constructor such as 
\{{Stack}}.

An array source goes through a different branch: a no-arg constructor plus 
\{{addAll}}. So \{{Stack s = new Integer[]{3, 1, 2}}} works while \{{Stack s = 
[3, 1, 2]}} fails.

h4. 2. Assignment fails, and \{{as}} returns a proxy that does not behave like 
the type

||Target||\{{T x = value}}||\{{value as T}}||
|\{{NavigableSet}}|\{{GroovyCastException}}|proxy over the list: \{{([3, 1, 2, 
1] as NavigableSet).size() == 4}}, \{{first() == 3}}|
|\{{Deque}}|\{{GroovyCastException}}|proxy over the list: \{{peekFirst()}} 
throws \{{MissingMethodException}}|
|\{{BlockingQueue}}|\{{GroovyCastException}}|proxy over the list|
|\{{SortedMap}}, \{{NavigableMap}}|\{{GroovyCastException}}|dynamic proxy over 
the map: \{{firstKey()}} throws \{{UnsupportedOperationException}}|
|\{{ConcurrentMap}}|\{{GroovyCastException}}|dynamic proxy over the map|
|\{{Iterable}} (array source)|\{{GroovyCastException}}|proxy over the array|

h4. 3. Array and collection sources give different result types

||Expression||Result||
|\{{Set s = new Integer[]{3, 1, 2}}}|\{{LinkedHashSet [3, 1, 2]}}|
|\{{new Integer[]{3, 1, 2} as Set}}|\{{HashSet [1, 2, 3]}} (order lost)|
|\{{[3, 1, 2] as Set}}|\{{LinkedHashSet [3, 1, 2]}}|
|\{{HashSet h = [3, 1, 2]}}|\{{LinkedHashSet [3, 1, 2]}}|
|\{{[3, 1, 2] as HashSet}}|\{{HashSet [1, 2, 3]}}|

h3. Already working (no change intended)

{\{Collection}}, \{{Iterable}} (collection source), \{{Set}}, \{{AbstractSet}}, 
\{{List}} (list or array source), \{{HashSet}}, \{{LinkedHashSet}}, 
\{{TreeSet}}, \{{ArrayList}}, \{{LinkedList}}, \{{Vector}}, \{{ArrayDeque}}, 
\{{PriorityQueue}}, \{{ConcurrentLinkedQueue}}, \{{CopyOnWriteArrayList}}, 
\{{Map}}, \{{HashMap}}, \{{LinkedHashMap}}, \{{AbstractMap}}, \{{TreeMap}}, 
\{{ConcurrentHashMap}}, \{{ConcurrentSkipListMap}}. On JDK 21, also 
\{{SequencedCollection}}, \{{SequencedSet}} and \{{SequencedMap}}.

h3. Proposal

One table of default implementations, used by both \{{castToType}} and 
\{{asType}}. A value that is already an instance of the target is returned 
unchanged, as now.

||Target||Default||
|\{{List}}, \{{Collection}}, \{{Iterable}}|\{{ArrayList}}|
|\{{Set}}|\{{LinkedHashSet}}|
|\{{SortedSet}}, \{{NavigableSet}}|\{{TreeSet}}|
|\{{Queue}}, \{{Deque}}|\{{LinkedList}} (accepts \{{null}} elements, unlike 
\{{ArrayDeque}}; matches today's \{{as Queue}})|
|\{{BlockingQueue}}|\{{LinkedBlockingQueue}}|
|\{{SortedMap}}, \{{NavigableMap}}|\{{TreeMap}}|
|\{{ConcurrentMap}}|\{{ConcurrentHashMap}}|

For a concrete target class without a copy constructor, fall back to its no-arg 
constructor plus \{{addAll}} for a collection source too, as is already done 
for an array source.

h3. Behaviour changes

* Group 1 assignments that now fail would succeed.
* \{{as NavigableSet}}, \{{as Deque}}, \{{as BlockingQueue}}, \{{as 
SortedMap}}, \{{as NavigableMap}} and \{{as ConcurrentMap}} would return a real 
collection instead of a proxy. Code relying on the proxy keeping a reference to 
the original collection would see a copy.
* \{{array as Set}} would keep element order (\{{LinkedHashSet}} instead of 
\{{HashSet}}).
* \{{TreeSet}}/\{{TreeMap}} targets throw \{{ClassCastException}} or 
\{{NullPointerException}} for non-\{{Comparable}} or \{{null}} elements and 
keys, as \{{as SortedSet}} and \{{TreeSet x = ...}} already do.

Open question: should \{{HashSet h = list}} keep giving a \{{LinkedHashSet}} (a 
subclass, so order is kept), or give the exact class as \{{list as HashSet}} 
does?

h3. Out of scope

Static compilation rejects a list literal assigned to most concrete collection 
classes, for example \{{@CompileStatic ... LinkedList l = [3, 4, 5]}} fails 
with "Cannot find matching constructor java.util.LinkedList(int, int, int)", 
following the list-constructor rule in the type-checking docs. That is a 
separate type-checker question, to be raised in its own issue.

  was:
h3. Problem

Groovy turns a collection into another collection type when it is assigned or 
cast (\{{T x = value}}, \{{(T) value}}, through 
\{{DefaultTypeTransformation.castToType}}), and when it is coerced with \{{as}} 
(through \{{DefaultGroovyMethods.asType(Collection, Class)}}). The two paths 
know different default implementations, and neither covers the common 
collection interfaces. So:
* some assignments fail where the equivalent \{{as}} works;
* some \{{as}} coercions return a proxy that does not behave like the requested 
type;
* an array source and a collection source give different results for the same 
target.

Results below are the same with indy and classic call sites, on JDK 17 and 21.

h4. 1. Assignment fails where \{{as}} works

||Target||\{{T x = [3, 1, 2, 1]}}||\{{[3, 1, 2, 1] as T}}||
|\{{SortedSet}}|\{{GroovyCastException}}|\{{TreeSet [1, 2, 3]}}|
|\{{Queue}}|\{{GroovyCastException}}|\{{LinkedList [3, 1, 2, 1]}}|
|\{{Stack}}|\{{GroovyCastException}}|\{{Stack [3, 1, 2, 1]}}|
|\{{List}} (source \{{[3, 1, 2] as 
LinkedHashSet}})|\{{GroovyCastException}}|\{{ArrayList [3, 1, 2]}}|

{\{asType}} has a table of defaults for \{{List}}, \{{Set}}, \{{SortedSet}}, 
\{{Queue}} and \{{Stack}}. \{{castToType}} has none for a \{{Collection}} 
source: it gives a \{{LinkedHashSet}} when the target accepts one. Otherwise it 
tries the target's constructor with the elements, then with the collection, 
which fails for an interface and for a class without a copy constructor such as 
\{{Stack}}.

An array source goes through a different branch: a no-arg constructor plus 
\{{addAll}}. So \{{Stack s = new Integer[]{3, 1, 2}}} works while \{{Stack s = 
[3, 1, 2]}} fails.

h4. 2. Assignment fails, and \{{as}} returns a proxy that does not behave like 
the type

||Target||\{{T x = value}}||\{{value as T}}||
|\{{NavigableSet}}|\{{GroovyCastException}}|proxy over the list: \{{([3, 1, 2, 
1] as NavigableSet).size() == 4}}, \{{first() == 3}}|
|\{{Deque}}|\{{GroovyCastException}}|proxy over the list: \{{peekFirst()}} 
throws \{{MissingMethodException}}|
|\{{BlockingQueue}}|\{{GroovyCastException}}|proxy over the list|
|\{{SortedMap}}, \{{NavigableMap}}|\{{GroovyCastException}}|dynamic proxy over 
the map: \{{firstKey()}} throws \{{UnsupportedOperationException}}|
|\{{ConcurrentMap}}|\{{GroovyCastException}}|dynamic proxy over the map|
|\{{Iterable}} (array source)|\{{GroovyCastException}}|proxy over the array|

h4. 3. Array and collection sources give different result types

||Expression||Result||
|\{{Set s = new Integer[]{3, 1, 2}}}|\{{LinkedHashSet [3, 1, 2]}}|
|\{{new Integer[]{3, 1, 2} as Set}}|\{{HashSet [1, 2, 3]}} (order lost)|
|\{{[3, 1, 2] as Set}}|\{{LinkedHashSet [3, 1, 2]}}|
|\{{HashSet h = [3, 1, 2]}}|\{{LinkedHashSet [3, 1, 2]}}|
|\{{[3, 1, 2] as HashSet}}|\{{HashSet [1, 2, 3]}}|

h3. Already working (no change intended)

{\{Collection}}, \{{Iterable}} (collection source), \{{Set}}, \{{AbstractSet}}, 
\{{List}} (list or array source), \{{HashSet}}, \{{LinkedHashSet}}, 
\{{TreeSet}}, \{{ArrayList}}, \{{LinkedList}}, \{{Vector}}, \{{ArrayDeque}}, 
\{{PriorityQueue}}, \{{ConcurrentLinkedQueue}}, \{{CopyOnWriteArrayList}}, 
\{{Map}}, \{{HashMap}}, \{{LinkedHashMap}}, \{{AbstractMap}}, \{{TreeMap}}, 
\{{ConcurrentHashMap}}, \{{ConcurrentSkipListMap}}. On JDK 21, also 
\{{SequencedCollection}}, \{{SequencedSet}} and \{{SequencedMap}}.

h3. Proposal

One table of default implementations, used by both \{{castToType}} and 
\{{asType}}. A value that is already an instance of the target is returned 
unchanged, as now.

||Target||Default||
|\{{List}}, \{{Collection}}, \{{Iterable}}|\{{ArrayList}}|
|\{{Set}}|\{{LinkedHashSet}}|
|\{{SortedSet}}, \{{NavigableSet}}|\{{TreeSet}}|
|\{{Queue}}, \{{Deque}}|\{{LinkedList}} (accepts \{{null}} elements, unlike 
\{{ArrayDeque}}; matches today's \{{as Queue}})|
|\{{BlockingQueue}}|\{{LinkedBlockingQueue}}|
|\{{SortedMap}}, \{{NavigableMap}}|\{{TreeMap}}|
|\{{ConcurrentMap}}|\{{ConcurrentHashMap}}|

For a concrete target class without a copy constructor, fall back to its no-arg 
constructor plus \{{addAll}} for a collection source too, as is already done 
for an array source.

h3. Behaviour changes

* Group 1 assignments that now fail would succeed.
* \{{as NavigableSet}}, \{{as Deque}}, \{{as BlockingQueue}}, \{{as 
SortedMap}}, \{{as NavigableMap}} and \{{as ConcurrentMap}} would return a real 
collection instead of a proxy. Code relying on the proxy keeping a reference to 
the original collection would see a copy.
* \{{array as Set}} would keep element order (\{{LinkedHashSet}} instead of 
\{{HashSet}}).
* \{{TreeSet}}/\{{TreeMap}} targets throw \{{ClassCastException}} or 
\{{NullPointerException}} for non-\{{Comparable}} or \{{null}} elements and 
keys, as \{{as SortedSet}} and \{{TreeSet x = ...}} already do.

Open question: should \{{HashSet h = list}} keep giving a \{{LinkedHashSet}} (a 
subclass, so order is kept), or give the exact class as \{{list as HashSet}} 
does?

h3. Out of scope

Static compilation rejects a list literal assigned to most concrete collection 
classes, for example \{{@CompileStatic ... LinkedList l = [3, 4, 5]}} fails 
with "Cannot find matching constructor java.util.LinkedList(int, int, int)", 
following the list-constructor rule in the type-checking docs. That is a 
separate type-checker question, to be raised in its own issue.


> Casting a collection to SortedSet, Queue, Deque and similar interfaces fails, 
> or with "as" gives a broken proxy
> ---------------------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12585
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12585
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>             Fix For: 7.x
>
>
> h3. Problem
> Groovy turns a collection into another collection type when it is assigned or 
> cast (\{{T x = value}}, \{{(T) value}}, through 
> \{{DefaultTypeTransformation.castToType}}), and when it is coerced with 
> \{{as}} (through \{{DefaultGroovyMethods.asType(Collection, Class)}}). The 
> two paths know different default implementations, and neither covers the 
> common collection interfaces. So:
> * some assignments fail where the equivalent \{{as}} works;
> * some \{{as}} coercions return a proxy that does not behave like the 
> requested type;
> * an array source and a collection source give different results for the same 
> target.
> Results below are the same with indy and classic call sites, on JDK 17 and 21.
> h4. 1. Assignment fails where \{{as}} works
> ||Target||{{T x = [3, 1, 2, 1]}}||{{[3, 1, 2, 1] as T}}||
> |{{SortedSet}}|{{GroovyCastException}}|{{TreeSet [1, 2, 3]}}|
> |\{{Queue}}|\{{GroovyCastException}}|\{{LinkedList [3, 1, 2, 1]}}|
> |\{{Stack}}|\{{GroovyCastException}}|\{{Stack [3, 1, 2, 1]}}|
> |\{{List}} (source \{{[3, 1, 2] as 
> LinkedHashSet}})|\{{GroovyCastException}}|\{{ArrayList [3, 1, 2]}}|
> {\{asType}} has a table of defaults for \{{List}}, \{{Set}}, \{{SortedSet}}, 
> \{{Queue}} and \{{Stack}}. \{{castToType}} has none for a \{{Collection}} 
> source: it gives a \{{LinkedHashSet}} when the target accepts one. Otherwise 
> it tries the target's constructor with the elements, then with the 
> collection, which fails for an interface and for a class without a copy 
> constructor such as \{{Stack}}.
> An array source goes through a different branch: a no-arg constructor plus 
> \{{addAll}}. So \{{Stack s = new Integer[]{3, 1, 2}}} works while \{{Stack s 
> = [3, 1, 2]}} fails.
> h4. 2. Assignment fails, and \{{as}} returns a proxy that does not behave 
> like the type
> ||Target||\{{T x = value}}||\{{value as T}}||
> |\{{NavigableSet}}|\{{GroovyCastException}}|proxy over the list: \{{([3, 1, 
> 2, 1] as NavigableSet).size() == 4}}, \{{first() == 3}}|
> |\{{Deque}}|\{{GroovyCastException}}|proxy over the list: \{{peekFirst()}} 
> throws \{{MissingMethodException}}|
> |\{{BlockingQueue}}|\{{GroovyCastException}}|proxy over the list|
> |\{{SortedMap}}, \{{NavigableMap}}|\{{GroovyCastException}}|dynamic proxy 
> over the map: \{{firstKey()}} throws \{{UnsupportedOperationException}}|
> |\{{ConcurrentMap}}|\{{GroovyCastException}}|dynamic proxy over the map|
> |\{{Iterable}} (array source)|\{{GroovyCastException}}|proxy over the array|
> h4. 3. Array and collection sources give different result types
> ||Expression||Result||
> |\{{Set s = new Integer[]{3, 1, 2}}}|\{{LinkedHashSet [3, 1, 2]}}|
> |\{{new Integer[]{3, 1, 2} as Set}}|\{{HashSet [1, 2, 3]}} (order lost)|
> |\{{[3, 1, 2] as Set}}|\{{LinkedHashSet [3, 1, 2]}}|
> |\{{HashSet h = [3, 1, 2]}}|\{{LinkedHashSet [3, 1, 2]}}|
> |\{{[3, 1, 2] as HashSet}}|\{{HashSet [1, 2, 3]}}|
> h3. Already working (no change intended)
> {\{Collection}}, \{{Iterable}} (collection source), \{{Set}}, 
> \{{AbstractSet}}, \{{List}} (list or array source), \{{HashSet}}, 
> \{{LinkedHashSet}}, \{{TreeSet}}, \{{ArrayList}}, \{{LinkedList}}, 
> \{{Vector}}, \{{ArrayDeque}}, \{{PriorityQueue}}, \{{ConcurrentLinkedQueue}}, 
> \{{CopyOnWriteArrayList}}, \{{Map}}, \{{HashMap}}, \{{LinkedHashMap}}, 
> \{{AbstractMap}}, \{{TreeMap}}, \{{ConcurrentHashMap}}, 
> \{{ConcurrentSkipListMap}}. On JDK 21, also \{{SequencedCollection}}, 
> \{{SequencedSet}} and \{{SequencedMap}}.
> h3. Proposal
> One table of default implementations, used by both \{{castToType}} and 
> \{{asType}}. A value that is already an instance of the target is returned 
> unchanged, as now.
> ||Target||Default||
> |\{{List}}, \{{Collection}}, \{{Iterable}}|\{{ArrayList}}|
> |\{{Set}}|\{{LinkedHashSet}}|
> |\{{SortedSet}}, \{{NavigableSet}}|\{{TreeSet}}|
> |\{{Queue}}, \{{Deque}}|\{{LinkedList}} (accepts \{{null}} elements, unlike 
> \{{ArrayDeque}}; matches today's \{{as Queue}})|
> |\{{BlockingQueue}}|\{{LinkedBlockingQueue}}|
> |\{{SortedMap}}, \{{NavigableMap}}|\{{TreeMap}}|
> |\{{ConcurrentMap}}|\{{ConcurrentHashMap}}|
> For a concrete target class without a copy constructor, fall back to its 
> no-arg constructor plus \{{addAll}} for a collection source too, as is 
> already done for an array source.
> h3. Behaviour changes
> * Group 1 assignments that now fail would succeed.
> * \{{as NavigableSet}}, \{{as Deque}}, \{{as BlockingQueue}}, \{{as 
> SortedMap}}, \{{as NavigableMap}} and \{{as ConcurrentMap}} would return a 
> real collection instead of a proxy. Code relying on the proxy keeping a 
> reference to the original collection would see a copy.
> * \{{array as Set}} would keep element order (\{{LinkedHashSet}} instead of 
> \{{HashSet}}).
> * \{{TreeSet}}/\{{TreeMap}} targets throw \{{ClassCastException}} or 
> \{{NullPointerException}} for non-\{{Comparable}} or \{{null}} elements and 
> keys, as \{{as SortedSet}} and \{{TreeSet x = ...}} already do.
> Open question: should \{{HashSet h = list}} keep giving a \{{LinkedHashSet}} 
> (a subclass, so order is kept), or give the exact class as \{{list as 
> HashSet}} does?
> h3. Out of scope
> Static compilation rejects a list literal assigned to most concrete 
> collection classes, for example \{{@CompileStatic ... LinkedList l = [3, 4, 
> 5]}} fails with "Cannot find matching constructor java.util.LinkedList(int, 
> int, int)", following the list-constructor rule in the type-checking docs. 
> That is a separate type-checker question, to be raised in its own issue.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to