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