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)