简体   繁体   中英

extract uncommon elements from list based on attribute in another list

My Structure

   A {
       String id;
       String bid;
   }

   B {
       String id;
   }

Given

List<A> aList = Arrays.asList(
   new A (1,2),
   new A (2,5),
   new A (3,9),
   new A (4,10),
   new A (5, 20),
   new A (6, 8),
   new A (7, 90)
   new A (8, 1)
);

List<B> bList = Arrays.asList(
   new B (2),
   new B (9),
   new B (10)
);

Now i want the elements of A which don't match with any B's element should be collected in another collection and these elements should be deleted from A collection itself.

Result

 List<A> aList = Arrays.asList(
       new A (1,2),
       new A (3,9),
       new A (4,10)
    );

    List<A> aListBin = Arrays.asList(
       new A (2,5),
       new A (5, 20),
       new A (6, 8),
       new A (7, 90)
       new A (8, 1)
    );

MY take

I can think of iterating A using iterator and for each element in A iterate through B and if found keep else keep adding to separate list and delete using iterator remove.

Is there a better way to do this using stream magic? Thanks

Collectors#partitionBy is your friend.

First, we'll extract the id s from the list of B s into a bare Set<Integer> , so we can use that for lookup:

Set<Integer> bSet = bList.stream()
    .map(b -> b.id)
    .collect(Collectors.toSet());

As mentioned by JB Nizet, a HashSet is fit for the job.

Then it's just as simple as this – we'll partition by a given predicate. The predicate is whether A.bid is contained in any B.id (which we stored into bSet for convenience).

Map<Boolean, List<A>> map = aList.stream()
    .collect(Collectors.partitioningBy(a -> bSet.contains(a.bid)));

Now map.get(true) contains all items contained in B , map.get(false) all others.

In order to replace aList , simply reassign aList :

aList = map.get(true);

You can't remove elements from a list created with Arrays.asList(). It returns a view of the array you pass as argument, so calling remove will throw UnsupportedOperationException.

Assuming you have ArrayList instead, I still don't think you can achieve this on a single step, and certainly not with "stream magic", since streams won't allow you to modify the original collection.

In two steps, something like:

List<A> newList = aList.stream()
    .filter(a -> !bList.contains(a.bid))
    .collect(Collectors.toList());
aList.removeAll(newList);

If performance is an issue, use Set or Map(with id as key) instead of List in order to perform the contains() and the removeAll() in O(1) and O(n) respectively.

Yes, you can use Java 8 Streams .

Here's the full example from your input:

import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.toList;

public class MyClass {
    public static void main(String args[]) {

        class A {
            public int id;
            public int bid;
            public A(int id, int bid) { this.id = id; this.bid = bid; }
            public String toString() { return "(" + id + "," + bid + ")"; }
        };

        class B {
            public int id;
            public B(int id) { this.id = id; }
            public String toString() { return "" + id; }
        };

        List<A> aList = Arrays.asList(
                new A (1,2),  // not removed
                new A (2,5),  // removed
                new A (3,9),  // not removed
                new A (4,10), // not removed
                new A (5, 20),// not removed
                new A (6, 8), // not removed
                new A (7, 90),// not removed
                new A (8, 1)// not removed
        );


        List<B> bList = Arrays.asList(
                new B (2),
                new B (9),
                new B (10)
        );


        List<A> aListBin = new ArrayList<>();
        aList.stream()
            .forEach( a -> {
                if (bList.stream().noneMatch(b -> b.id == a.bid )) {
                    aListBin.add(a);        
                }
            });

        aList = aList.stream()
        .filter( a -> bList.stream().anyMatch(b -> b.id == a.bid))
        .collect(toList());

        System.out.println("Alist-> " + aList);
        System.out.println("Blist-> " + bList);
        System.out.println("Removed-> " + aListBin);
    }
}

Output:

Alist-> [(1,2), (3,9), (4,10)]
Blist-> [2, 9, 10]
Removed-> [(2,5), (5,20), (6,8), (7,90), (8,1)]

You can use Collectors.partitioningBy . Is it better? Depends on your definition of better. It is much more concise code, however it is not as efficient as the simple iterator loop you described.

I cannot think of a more efficient way than the iterator route, except maybe using a string hashset for lookup of the B class id.

However, if you prefer concise code, here is the code using partitioningBy:

class A {
    int id;
    int bid;

    public A(int id, int bid){
        this.id = id;
        this.bid = bid;
    }

    public boolean containsBId(List<B> bList) {
        return bList.stream().anyMatch(b -> bid == b.id);
    }
}

class B {
    int id;

    public B(int id){
        this.id = id;
    }
}

class Main {

public static void main(String[] args) {
    List<A> aList = Arrays.asList(
        new A (1,2),
        new A (2,5),
        new A (3,9),
        new A (4,10),
        new A (5, 20),
        new A (6, 8),
        new A (7, 90),
        new A (8, 1)
    );
    List<B> bList = Arrays.asList(
        new B (2),
        new B (9),
        new B (10)
    );
    Map<Boolean, List<A>> split = aList.stream()
        .collect(Collectors.partitioningBy(a -> a.containsBId(bList)));

    aList = split.get(true);
    List<A> aListBin = split.get(false);
}

Since you are dealing with two different classes you can't compare them directly. So you need to reduce to the least common denominator which is the integer ID's.

   // start a stream of aList.
   List<A> aListBin = aList.stream()

   // Convert the bList to a collection of
   // of ID's so you can filter.       
   .filter(a -> !bList.stream()

         // get the b ID
         .map(b->b.id)

         // put it in a list         
        .collect(Collectors.toList())

         // test to see if that list of b's ID's
         // contains a's bID   
         .contains(a.bid))

    //if id doesn't contain it, then at to the list.
    .collect(Collectors.toList());

To finish up, remove the newly created list from the aList.

        aList.removeAll(aListBin);

They are displayed as follows:

        System.out.println("aListBin = " + aListBin);
        System.out.println("aList = " + aList);
        aListBin = [[2, 5], [5, 20], [6, 8], [7, 90], [8, 1]]
        aList = [[1, 2], [3, 9], [4, 10]]

Note:

  • To reverse the contents of each final list, remove the bang(!) from the filter.
  • I added toString methods in the classes to allow printing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM