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