Ian Competent
Ian Competent

Reputation: 93

Merge two ArrayLists with no duplicate subclasses

Given:

public abstract class Cars {}

...

public class Ford extends Cars {}

...

public class Dodge extends Cars {}

...

public class Volkswagen extends Cars {}

...

If I have two ArrayList objects:

List<Cars> dealer1 = new ArrayList<>;
List<Cars> dealer2 = new ArrayList<>;

dealer1.addAll(asList(new Ford("ford1"), new Dodge("dodge1")));
dealer2.addAll(asList(new Dodge("dodge2"), new Volkswagen("vw1")));

I then want to create a merged list from the two with only one instance of each subclass, such that:

dealerMerged = ["ford1", "dodge1", "vw1"]

OR

dealerMerged = ["ford1", "dodge2", "vw1"]

It doesn't matter which instance makes it into the merged list.

Is this possible? I had a search through and saw something about using Set but that seems to only ensure unique references, unless I've badly misunderstood something.

Upvotes: 2

Views: 291

Answers (4)

Shafin Mahmud
Shafin Mahmud

Reputation: 4071

Overriding equals() will work but DON'T

You can always make your collection distinctful converting it to a Set (as @Arun states in comment) or using distinct operation over the Stream of your collections. But remember those approaches use the equal() methods for that. So a quick thinking would be overriding equals() and return its Class type. But wait ! If you do so you will end up having all Dodge objects equals to each other despite they have different properties like name dodge1, dodge2. You may not only handle a single business in read world. equal() method has lots of other significances. So stay away of doing so.

If you are thinking a Java 8 way, Stateful Filter is perfect

We have a choice to use the filter operation for our concatenated stream. filter operation works pretty straight forward. It takes a predicate and decide which element to take or ignore. This a commonly used function that you will find all over the blogs that solves this problem.

public static <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) {
    Map<Object, Boolean> seen = new ConcurrentHashMap<>();
    return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

Here the distinctBy function returns a predicate (that will be used in filter operation). It maintains state about what it's seen previously and returns whether the given element was seen for the first time. (You can read further explanation about this here)

You can use this Stateful Filter like

Stream.of(dealer1, dealer2)
        .flatMap(Collection::stream)
        .filter(distinctBy(Cars::getClass))
        .collect(Collectors.toList())
        .forEach(cars -> System.out.println(cars));

So What we actually did here ?

  • We concatenated the 2 ArrayList with flatmap that will give us a single stream of the merged elements (If you are new to Stream API. See this Stream Concatenation article
  • We then exploits the filter() operation that is feed with the distinctBy method which return a predicate.
  • And you see a ConcurrentHashMap is maintained to track which element satisfies the predicate or not by a boolean flag.
  • And the predicate uses the getClass() method which returns the full class name, that distinguise the elements as subclasses
  • We then can collect or iterate over the filtered list.

Upvotes: 2

Uday Fokane
Uday Fokane

Reputation: 101

Try using Map instead of List. You may please try following solution. This will let you put Car instances by their types. Thereby you will always have only one entry per class (this will be the latest entry in your map by the way).

public class CarsCollection {
    Map<Class<? extends Cars>, ? super Cars> coll = new HashMap<>();

    public <T extends Cars> void add(Class<T> cls, T obj) {
        coll.put(cls, obj);
    }
}

public class Temp {

    public static void main(String[] args)  {

        CarsCollection nos1 = new CarsCollection();

        cars.add(Ford.class, new Ford("ford1"));
        cars.add(Dodge.class, new Dodge("dodge1"));

        cars.add(Dodge.class, new Dodge("dodge2"));
        cars.add(Volkswagen.class, new Volkswagen("vw1"));

        System.out.println(cars);
    }

}

Upvotes: 1

tsolakp
tsolakp

Reputation: 5948

Just use class of the object as key in the map. This example with Java stream does exactly that:

List<Cars> merged = Stream.of(dealer1, dealer2)
                    .flatMap(Collection::stream)
                    .collect( Collectors.toMap( Object::getClass, Function.identity(), (c1, c2) -> c1 ) )
                    .values()
                        .stream().collect( Collectors.toList() );

Upvotes: 0

olivierhsta
olivierhsta

Reputation: 103

You could add all the element of the first list into the result list (assuming there is no duplicate in the first list) and then loop through the second list and add the elements to the resulting list only if there is no instance of the same class in the first list.

That could look something like this :

dealerMerged = dealer1;
boolean isAlreadyRepresented;
for (car2 : dealer2) {
    isAlreadyRepresented = false;
    for (car1 : dealer1) {
        if (car1.getClass().equals(car2.getClass())) {
            isAlreadyRepresented = true;
        }
    }
    if (!isAlreadyRepresented) {
        dealerMerged.add(car2);
    }
}

Upvotes: 0

Related Questions