[ 
https://issues.apache.org/jira/browse/GROOVY-8101?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15893354#comment-15893354
 ] 

Paul King commented on GROOVY-8101:
-----------------------------------

The phrase "you are tied to plus() relying on clone()" isn't quite correct. The 
plus() method is the backing method for the ' + ' operator and most people when 
they see {{newCollection = oldCollection + addedThings}} expect the ' + ' 
operator to be type preserving, i.e. {{newCollection}} will have the same type 
as {{oldCollection}}. The current implementation is our best (and by no means 
perfect) compromise on achieving that given that the Java collection api 
doesn't have rigid enough conventions in place to allow us to enforce that. The 
{{oldCollection}} could be a Set, List, Stack etc. or some arbitrary custom 
extension class. It may or may not have a no-arg constructor. Indeed, it might 
have no public constructors and use factory methods. Relying on a clone() 
method which obeys the method's stated conventions has been a reasonable 
approach for us to adopt.
{quote}
By convention, the object returned by [ the clone() ] method should be 
independent
of this object (which is being cloned).  To achieve this independence,
it may be necessary to modify one or more fields of the object [ ...snip... ] 
before returning it.
{quote}
While we could try to code around poorly designed libraries, it isn't our first 
preference. But if you can think of some improvements, we welcome any 
suggestions or PRs.

> DefaultGroovyMethods.plus() copies internal state of the left Map
> -----------------------------------------------------------------
>
>                 Key: GROOVY-8101
>                 URL: https://issues.apache.org/jira/browse/GROOVY-8101
>             Project: Groovy
>          Issue Type: Bug
>          Components: groovy-runtime
>    Affects Versions: 2.4.8
>            Reporter: Nick Knowlson
>            Priority: Minor
>
> When upgrading Tomcat to a newer version (8.0.39) we discovered a bunch of 
> new exceptions being thrown in a Grails application we were using with it. 
> The root cause ended up being that plus() was copying private internal state 
> of the specific Map implementation, and so couldn't be used to combine two 
> Maps together.
> Here is an example that shows the problem.
> *Setup:*
> {code}
> // Simpler stand-in for classes like org.apache.catalina.util.ParameterMap
> // Important part is: it is a specific implemenation of a map with private 
> fields
> class ParameterMap<K,V> extends LinkedHashMap<K,V> {
>     private boolean locked = false
>     public void setLocked(boolean locked) {
>         this.locked = locked
>     }
>      @Override
>     public V put(K key, V value) {
>         if (locked) {
>             throw new IllegalStateException('class is locked');
>         }
>         return (super.put(key, value));
>     }
>     public void putAll(Map<? extends K,? extends V> map) {
>         if (locked) {
>             throw new IllegalStateException('class is locked');
>         }
>         super.putAll(map);
>     }
> }
> // Stand-in for javax.servlet.ServletRequest.getParameterMap
> // Important part is: it is a method that returns a generic (as far as the 
> caller can tell) Map of values
> Map<String, String> getParameterMap() {
>     def internalMap = new ParameterMap<String, String>(['1': 'one', '2': 
> 'wrong'])
>     internalMap.setLocked(true)
>     return internalMap
> }
> {code}
> *Example Scenario:*
> {code}
> // We have some existing data that we don't want to change, but we also have 
> some more specific information that should override that existing data.
> // We need to combine two maps and return a new one - this is what plus() was 
> created for, right?
> Map<String, String> parameterMap = getParameterMap()
> Map<String, String> newData = ['2': 'two', '3': 'three']
> // Not this time! This line fails with an IllegalStateException, message 
> 'class is locked'
> // Even though (from calling code's perspective) we are just adding two 
> generic Maps together
> def combinedMaps1 = parameterMap.plus(newData) // will fail
> assert combinedMaps1.get('2') == 'two'
> {code}
> Why? plus() isn't supposed to mutate the original collection. Turns out it 
> internally calls cloneSimilarMap(), which clones the whole object including 
> the class right down to the state of private variables. 
> Code using plus() should not have to account for plus() doing this - plus() 
> accepts generic Maps as its arguments, cloning the internal state of objects 
> like this breaks its contract and prevents encapsulation from working well.
> *Workaround for now:*
> {code}
> // Need to manually create a new Map<String, String> from the existing 
> Map<String, String> and then add the new data 
> def combinedMaps2 = parameterMap.collectEntries({key, value -> [key, value] 
> }).plus(newData)
> assert combinedMaps2.get('2') == 'two'
> // Can't just change order they are combined in - we want values from right 
> map to take precedence over values from left map with same key
> def combinedMaps3 = newData.plus(parameterMap) 
> assert combinedMaps3.get('2') == 'two' // will fail
> {code}



--
This message was sent by Atlassian JIRA
(v6.3.15#6346)

Reply via email to