3

I've written a custom class that I'm using to store label/value pairs for use in lightning component picklists. I've got quite a complex dependency requirement, so I thought I'd use the "retainAll" method on the apex Set to get the intersections.

This is the class:

public with sharing class ListOption implements Comparable {
  public Id value { get; set; }
  public String label { get; set; }

  public ListOption (Id value, String label) {
    this.value = value;
    this.label = label;
  }

  public Boolean equals(Object obj) {
    if (obj == null || !(obj instanceof ListOption)) {
      return false;
    }

    ListOption lo = (ListOption) obj;

    return this.value == lo.value && this.label == lo.label;
  }

  public Integer hashCode() {
    return (31 * String.valueOf(this.value).hashCode())
      ^ this.label.hashCode();
  }

  public Integer compareTo(Object obj) {
    ListOption lo = (ListOption) obj;
    return this.label.compareTo(lo.label);
  }
}

Now, this is the code I'm testing with, and seeing the strange results:

ListOption lo1 = new ListOption('a0C41000000NUU8EAO', '2017 Fall');
ListOption lo2 = new ListOption('a0C41000000NUU8EAO', '2017 Fall');

System.debug(lo1.equals(lo2));

System.debug(lo1.hashCode());
System.debug(lo2.hashCode());

Set<ListOption> s1 = new Set<ListOption>();

s1.add(lo1);

Set<ListOption> s2 = new Set<ListOption>();

s2.add(lo2);

System.debug('Contains: ' + s1.contains(lo2));

System.debug('Contains All: ' + s1.containsAll(s2));

s1.retainAll(s2);

System.debug(s1);

There are a variety of debug statements in here, and they all behave as expected except for the last one.

The two objects are equals, the "contains" and "containsAll" calls both return true.

But the "retainAll" is resulting in my set being emptied. This is unexpected, as the two values in the sets are equal, I would expect the set to retain the value it already contains.

Is my expectation incorrect?

In response to a comment below, here's another angle. I've got a method that I'm using to find the intersection between a list of sets, the method is here:

 public static List<ListOption> findIntersection(List<Set<ListOption>> sets) {
    if (sets.isEmpty()) {
      return new List<ListOption>();
    }

    Integer counter = 0;
    Set<ListOption> temp;
    Set<ListOption> commons = sets[0];
    Iterator<Set<ListOption>> i = sets.iterator();
    List<ListOption> toReturn;

    System.debug('First Commons');
    System.debug(JSON.serialize(commons));

    while (i.hasNext()) {
      temp = i.next();
      System.debug(JSON.serialize(temp));
      commons.retainAll(temp);
      System.debug('Pruned Commons');
      System.debug(JSON.serialize(commons));
    }

    toReturn = new List<ListOption> (commons);
    toReturn.sort();

    return toReturn;
  }

Here's my anonymous block calling the method:

ListOption lo1 = new ListOption('a0C41000000NUU8EAO', '2017 Fall');
ListOption lo2 = new ListOption('a0C41000000NUU8EAO', '2017 Fall');
ListOption lo3 = new ListOption('a0C41000000OKwWEAW', 'Fall 2016');

Set<ListOption> s1 = new Set<ListOption>();

s1.add(lo1);
s1.add(lo3);

Set<ListOption> s2 = new Set<ListOption>();

s2.add(lo2);

System.debug(DependencySelectionController.findIntersection(new List<Set<ListOption>> { s1, s2 }));

The output is showing my set being emptied:

13:34:48.6 (12120220)|USER_DEBUG|[156]|DEBUG|First Commons
13:34:48.6 (12798334)|USER_DEBUG|[157]|DEBUG|[{"value":"a0C41000000NUU8EAO","label":"2017 Fall"},{"value":"a0C41000000OKwWEAW","label":"Fall 2016"}]
13:34:48.6 (13250587)|USER_DEBUG|[161]|DEBUG|[{"value":"a0C41000000NUU8EAO","label":"2017 Fall"},{"value":"a0C41000000OKwWEAW","label":"Fall 2016"}]
13:34:48.6 (14182499)|USER_DEBUG|[163]|DEBUG|Pruned Commons
13:34:48.6 (14908699)|USER_DEBUG|[164]|DEBUG|[{"value":"a0C41000000NUU8EAO","label":"2017 Fall"},{"value":"a0C41000000OKwWEAW","label":"Fall 2016"}]
13:34:48.6 (15184228)|USER_DEBUG|[161]|DEBUG|[{"value":"a0C41000000NUU8EAO","label":"2017 Fall"}]
13:34:48.6 (15296611)|USER_DEBUG|[163]|DEBUG|Pruned Commons
13:34:48.6 (15375673)|USER_DEBUG|[164]|DEBUG|[]
13:34:48.6 (15484370)|SYSTEM_MODE_EXIT|false
13:34:48.6 (15521569)|USER_DEBUG|[14]|DEBUG|()
  • I copied your code into my org and s1 did not get emptied. – Adrian Larson Dec 6 '16 at 21:15
  • @AdrianLarson: That is really strange, the set was being emptied a little while ago. I can pull up another example, that is still being emptied incorrectly and see if that one works, too – Evan Dec 6 '16 at 21:28
  • Because I find it easier to read in debug output than the standard salesforce output. It's only used in debug statements in any case, it shouldn't affect any of the logic. – Evan Dec 6 '16 at 21:42
  • Fun fact, though, I do use serialization frequently when returning large amounts of data to lightning components, because I've found it to be far faster than passing it back normally and letting lightning handle it. – Evan Dec 6 '16 at 21:45
  • I did just fix a potential error, i replaced commons = sets[0] with commons = sets[0].clone(). This resolve an issue of the first set in the parameter being cleared, but still empties my set. – Evan Dec 6 '16 at 21:52
2

This method works fine for me:

public List<ListOption> findIntersection(Set<ListOption> set1, Set<ListOption> set2)
{
    return findIntersection(new List<Set<ListOption>> { set1, set2 });
}
public static List<ListOption> findIntersection(List<Set<ListOption>> sets)
{
    if (sets.isEmpty())  return new List<ListOption>();
    Set<ListOption> intersection = sets.remove(0).clone();
    while (!sets.isEmpty()) intersection.retainAll(sets.remove(0));
    List<ListOption> sorted = new List<ListOption> (intersection);
    sorted.sort();
    return sorted;
}

It also still works if you call it multiple times.

ListOption lo1 = new ListOption('a0C41000000NUU8EAO', '2017 Fall');
ListOption lo2 = new ListOption('a0C41000000NUU8EAO', '2017 Fall');
ListOption lo3 = new ListOption('a0C41000000OKwWEAW', 'Fall 2016');

Set<ListOption> s1 = new Set<ListOption> { lo1, lo3 };
Set<ListOption> s2 = new Set<ListOption> { lo2 };

system.debug(findIntersection(s1, s2));
system.debug(findIntersection(s1, s2));
system.debug(findIntersection(s1, s2));

Results in:

DEBUG|(ListOption:[label=2017 Fall, value=a0C41000000NUU8EAO])
DEBUG|(ListOption:[label=2017 Fall, value=a0C41000000NUU8EAO])
DEBUG|(ListOption:[label=2017 Fall, value=a0C41000000NUU8EAO])

  • I'll try this new method shortly, which looks essentially like using the "remove" and "isEmpty" instead of using an iterator and "next." Although it wouldn't explain why the iterator method doesn't work. – Evan Dec 6 '16 at 23:09
  • Those are the only changes I made besides the addition of clone, but you found that yourself. It works for me, so it should for you as well. – Adrian Larson Dec 6 '16 at 23:11
  • So I think I've found a repeatable pattern. I'll make a screencast of it soon, but it definitely seems like a bug. When I remove all the debug statements from my code, it works, but when I add them back in, it stops working. I know it sounds crazy, but I'll share a video soon. – Evan Dec 6 '16 at 23:31
  • Here is the screencast of the behavior I'm seeing. I'm do some more poking around, but this seems really strange: screencast.com/t/oIRzaign – Evan Dec 7 '16 at 1:03
1

FYI (as your code matches the good case so this does not explain your problem), I've had unpleasant experiences taking the hashCode of Id value as illustrated by the first case here i.e. Id values appear to break the (Java) equals/hashCode object contract:

// Bad - different hashCode values but Id values are equal
System.debug(System.hashCode([select Id from Contact limit 1].Id));
System.debug(System.hashCode([select Id from Contact limit 1].Id));
System.debug(System.hashCode([select Id from Contact limit 1].Id));
System.debug(System.hashCode([select Id from Contact limit 1].Id));

// Good - same hashCode values
System.debug(String.valueOf([select Id from Contact limit 1].Id).hashCode());
System.debug(String.valueOf([select Id from Contact limit 1].Id).hashCode());
System.debug(String.valueOf([select Id from Contact limit 1].Id).hashCode());
System.debug(String.valueOf([select Id from Contact limit 1].Id).hashCode());

So always turn Id values into String values before taking the hashCode.

0

So thank you to Adrian Larson for helping me test this thing out. His solution is a good one, however, I don't think it's a specific answer to the question. I believe there's some sort of bug involved here, and I'll be reporting it to Salesforce.

For a demonstration of the strange behavior I'm seeing, see this short video: http://www.screencast.com/t/oIRzaign

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.