Paul King created GROOVY-12585:
----------------------------------

             Summary: 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
             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