How to detect if a list is changed?

Viewed 8348

I have a List field in a class managed by a little-known proprietary framework.

The annotation @BindMagic is managed by the framework, so the underlying list mutates sometimes: it could be recreated or its elements could change.

class SharedEntity{

  @BindMagic // this annotation does a magic that we cannot control
  private List<Map<String,Object>> values;

  public boolean isChangedSincePreviousCall(){
    // check if "values" have changed since the previous call of this method          
  }
}

I'm agree that it is a poor design, but let's suppose there's no possibility to affect it.

Time to time (not on every mutation) it's needed to check if the list is changed. For instance, I want do it with the method isChangedSincePreviousCall. Probably, something like a hash sum would be good. But I'm curious are there better ways.

What is the best practice to detect if the list is changed?

4 Answers

The problem is probably the Thread accessing the list. It is most likely not supposed to be caught up in some kind of Listener-resolution, which is why there is no proper way of attaching a listener to the list.

However, if you have control over the SharedEntiry class, you could 'hack' into the list's access by using synchronized. However you expressly stated, that the list could be recreated, so I assume the instance stored behind values can actually be replaced.

Basically you have three cases:

1 The values-List is replaced by a new List:

Solve this by making a second reference on List:

private List<Map<String,Object>> valuesPrevious;

Whenever you check for change, check for identity of the lists first. If they are a mismatch, you can be sure the list changed (at least the instance, if not the content).

if (values != valuesPrevious) {
    // handle change.
}

Yes, you still need to periodically check, but an identity-comparison is relatively cheap, and therefore an affordable thread to run in the background.

2 The values-List is replaced by a new List (of a type you did not set):

If that occures, move all values from the API's list to an instance of your observable list (described below), set values to that instance and wait for the next change to occure.

3 The values changed, but the instance is the same:

Solve this by using an ObservableList (if you are implementing in Java10+ https://docs.oracle.com/javase/10/docs/api/javafx/collections/ObservableList.html) or by implementing such a List yourself (probably by extending an existing List type).

Then, that listener only sets a 'dirty' flag, and your method knows that a change occured (and resets the flag).

In any way, my suggestion would be to ensure, that the Thread handling the change only triggers another Thread to handle the change, rather than lock the accessing thread, since I suspect, that your @BindMagic-API has some sort of runtime-relevant factor (for example, it is a network or database related shadow of something). If you simply lock the thread, until you have handled your reaction, you might get weird effects, disconnects or end up accidentally blocking the server you are accessing.

Using a hash is not definitive, because the same hash can be produced from different inputs, albeit with a very small chance.

"Being changed" and "being different" mean different things. Consider an entry in one of the maps that is changed from "A" -> 1 to "A" -> 2 then back to "A" -> 1 again between calls to your method - it was changed but isn't different. I'll assume you mean "different".

Make a copy when checking and compare that with the current state. Assuming that the map values are immutable:

class SharedEntity {

    @BindMagic
    private List<Map<String, Object>> values;
    private List<Map<String, Object>> valuesCopy;

    public boolean isChangedSincePreviousCall() {
        newCopy = new ArrayList<>(values);
        boolean result = !Objects.equals(valuesCopy, newCopy);
        valuesCopy = newCopy;
        return result;
    }
}

If the Map values are (or contain) mutable objects, you'll have to make a deep copy of them when creating the copy.

FYI Objects#equals() returns true if both parameters are null.

I would try to use PropertyChangeListener objects. Here is an example for SharedEntity class. You can apply the same for the objects stored in list.

class SharedEntity {
  private List<Map<String,Object>> values;
  private PropertyChangeSupport pcs = new PropertyChangeSupport();

  public void setValues(List<Map<String,Object>> values) {
   List<Map<String,Object>> oldValues = this.values;
   this.values= values;
   pcs.firePropertyChange("values",oldValues, values); 
  }

  public void addValue(Map<String, Object> value) {
   // store old
   // add new element
   // fire change   
  }

  public void removeValue(Map<String, Object> value) {
   // store old
   // remove value
   // fire change
  }

  public void addPropertyChangeListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }
}

You can use Observer Pattern to detect change in values.

You need to create Observable.

package com.psl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;

public class MyList extends Observable{

      private List<Map<String,Object>> values;    
    public List<Map<String, Object>> getValues() {
        return values;
    }

    public void setValues(List<Map<String, Object>> values) {

        if(getValues()==null && values!=null){

            setChanged();
            notifyObservers();
        }

        else if( !this.values.equals(values)){
            setChanged();
            notifyObservers();
        }

        this.values = values;
    }

    public static void main(String[] args) {

        MyList myList = new MyList();
        List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
        Notify notify = new Notify();
        myList.addObserver(notify);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("string_value", null);
        myList.setValues(values);                       
    }


}

You have to create observer which will observe changes in MyList

package com.psl;

import java.util.Observable;
import java.util.Observer;

public class Notify implements Observer{

    @Override
    public void update(Observable o, Object arg) {
            System.out.println("List has been changed");                
    }

}

For more information about Observable Pattern https://springframework.guru/gang-of-four-design-patterns/observer-pattern/

Related