Java:根据特定属性创建包含两个给定列表中所有非交叉元素的列表的正确方法?

问题描述 投票:5回答:6

给定两个对象列表,我将能够根据其中一个属性判断哪些项目不在其交叉点中。我们来看下面的例子:

我有一个类Foo有两个属性:booplaceholder

class Foo {
    private int boo;
    private int placeholder = 1;

    public Foo(int boo) {
        this.boo = boo;
    }

    public int getBoo() {
        return boo;
    }
}

现在我正在创建两个列表(让我们说这是我的输入)

    List<Foo> list1 = new ArrayList<Foo>();
    list1.add(new Foo(1));
    list1.add(new Foo(2));
    list1.add(new Foo(3));

    List<Foo> list2 = new ArrayList<Foo>();
    list2.add(new Foo(0));
    list2.add(new Foo(1));
    list2.add(new Foo(2));

现在我想说明哪些物品在list1而不是list2list2而不是list1基于它们的属性boo。所以在上面的例子中,我想要一个包含一个List<Foo> notInIntersectList和一个Foo(0)Foo(3)

    List<Foo> notInIntersectList = new ArrayList<Foo>();
    list1.forEach(li1foo -> {
        boolean inBothLists = false;
        list2.forEach(li2foo -> {
            if (li1foo.getBoo() == li2foo.getBoo()) {
                inBothLists = true;
            }
        });
        if (!inBothLists) {
            notInIntersectList.add(li1foo);
        }
    });
    //now I covered all items in list1 but not in list2. Now do this again with lists swapped, so I can also cover those.
    //...

可悲的是,我将Local variable inBothLists defined in an enclosing scope must be final or effectively final视为错误。这个问题是如何正确解决的,因为这似乎不是“正确的”解决方案?

java
6个回答
6
投票

你不能改变lambda表达式中的变量(参见:Variable used in lambda expression should be final or effectively final

这是一种修复代码的方法(Streams很有趣)

List<Foo> notInIntersectList = list1.stream()
        .filter(fooElementFromList1 -> list2
                .stream()
                .noneMatch(fooElementFromList2 -> fooElementFromList2.getBoo() == fooElementFromList1.getBoo()))
        .collect(Collectors.toCollection(ArrayList::new));

list2.stream()
        .filter(fooElementFromList2 -> list1
            .stream()
            .noneMatch(fooElementFromList1 -> fooElementFromList1.getBoo() == fooElementFromList2.getBoo()))
        .forEach(notInIntersectList::add);

其复杂性是O(n*m)(其中nm分别是list1和list2中元素的数量)。

要在O(n+m)中执行此操作,您可以使用Set。为此,你需要在equals类上使用hashcodeFoo方法。这假设两个Foo实例仅基于实例变量boo的值相等。

class Foo {
    ....

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Foo other = (Foo) obj;
        return boo == other.boo;
    }

    @Override
    public int hashCode() {
        return boo;
    }
}

并使用Set作为

Set<Foo> fooSet1 = new HashSet<>(list1);
Set<Foo> fooSet2 = new HashSet<>(list2);

fooSet1.removeAll(list2);
fooSet2.removeAll(list1);

List<Foo> notInIntersectList = Stream.concat(fooSet1.stream(), fooSet2.stream())
            .collect(Collectors.toList());

3
投票

如果你不能在你的班级上创建一个equalshashCode(也许他们已经有了但不是基于boo),我会做的是:

  • 创建一个包含列表1中所有Set<Integer>值的BitSet(或boo)。称之为set1
  • 创建一个包含列表2中所有Set<Integer>值的BitSet(或boo)。称之为set2
  • 使用set1.retainAll(set2)获取两组的交集。
  • 使用以下命令创建我的列表: Stream.concat(list1.stream(),list2.stream()) .filter(item-> ! set1.contains(item.getBoo())) .collect(Collectors.toList);

这是O(m + n)并且还确保维护原始列表中的项目的顺序,以及任何重复项目(具有相同boo的项目)。


3
投票

首先,你应该将方法equalshashCode添加到你的班级Foo(参见Why do I need to override the equals and hashCode methods in Java?

class Foo {
    private int boo;
    private int placeholder = 1;

    public Foo(int boo) {
        this.boo = boo;
    }

    public int getBoo() {
        return boo;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + boo;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof Foo))
            return false;
        Foo other = (Foo) obj;
        return boo == other.boo;
    }

}

现在你可以使用removeAllList方法:

删除同样包含在指定集合中的所有此集合的元素(可选操作)。返回此调用后,此集合将不包含与指定集合相同的元素。

你将不得不建立一个新的List notInIntersectList

    List<Foo> listIntersection = new ArrayList<>(list1);
    listIntersection.removeAll(list2);

    List<Foo> notInIntersectList = new ArrayList<>(list1);
    notInIntersectList.addAll(list2);
    notInIntersectList.removeAll(listIntersection);

1
投票

避免使用Lambda表达式

它们使代码更难阅读,效率更低。

如果你只是使用传统的for循环,你的代码应该工作......另外,你可以使用break来停止搜索第二个匹配。

    List<Foo> notInIntersectList = new ArrayList<Foo>();
    for(Foo li1foo : list1) {
        boolean inBothLists = false;
        for(Foo li2foo : list2) {
            if (li1foo.getBoo() == li2foo.getBoo()) {
                inBothLists = true;
            }
        }
        if (!inBothLists) {
            notInIntersectList.add(li1foo);
        }
    }

你可能仍然认识到你的代码......现在这里是带有命令式编码的专业版:

    List<Foo> notInIntersectList = new ArrayList<Foo>();
    nextfoo: for(Foo li1foo : list1) {
        for(Foo li2foo : list2)
            if (li1foo.getBoo() == li2foo.getBoo())
                continue nextfoo;
        notInIntersectList.add(li1foo);
    }

这实际上有一个非常明确和明确的逻辑(并且它将使功能性粉丝因为有效的“goto”而呕吐)。如果list2很大,它仍然很慢,我不想改变你的算法。


0
投票

已有一个图书馆:

Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");

SetView<String> intersection = Sets.intersection(primes, wordsWithPrimeLength); // contains "two", "three", "seven"
// I can use intersection as a Set directly, but copying it can be more efficient if I use it a lot.
return intersection.immutableCopy();

如果你使用difference(Set<E> set1, Set<?> set2)而不是intersection(Set<E> set1, Set<?> set2),你将得到两者的差异。

使用ImmutableSet.copyOf(Collection<? extends E> elements),您可以创建一个Set。

它被称为番石榴,并提供许多收集操作:https://github.com/google/guava/wiki/CollectionUtilitiesExplained

API:https://google.github.io/guava/releases/19.0/api/docs/com/google/common/collect/ImmutableSet.html


0
投票

我在这看到四种可能的解决方案

1)在@ Anony-Mousse的回答之后避免lambda表达

2)在类级别包含变量(不推荐使用,因为此布尔值适合本地使用):

public class Testing {
 boolean inBothLists = true;

 public static void main(String[] args) {
  List<Foo> notInIntersectList = new ArrayList<Foo>();
  list1.forEach(li1foo -> {
    inBothLists = false;
    list2.forEach(li2foo -> {
        if (li1foo.getBoo() == li2foo.getBoo()) {
            inBothLists = true;
        }
    });
    if (!inBothLists) {
        notInIntersectList.add(li1foo);
    }
  });

  System.out.println("Intersected values:");
  notInIntersectList.forEach(liInFoo -> {
        System.out.println(liInFoo);
  });

 }

3)使用List中的contains方法(避免布尔值):

List<Foo> notInIntersectList = new ArrayList<Foo>();
list1.forEach(li1foo -> {
    if (!list2.contains(li1foo)) 
        notInIntersectList.add(li1foo);
});

4)Java8 Stream API(已经在另一个答案中提到并避免了布尔值):

List<Foo> notInIntersectList = new ArrayList<Foo>();
list1.forEach(li1foo -> {
        Foo result = list2.stream()
                  .filter(li2foo -> li1foo.getBoo() == li2foo.getBoo() )
                  .findAny()
                  .orElse(null);
        if (result == null)
            notInIntersectList.add(li1foo);
});

我会选择1),3)或4)。我只保留2.)来展示一个使用lambda函数内部变量的例子。

更新:我在recent Baeldung post on how to find an Element in a List中找到了3)和4)选项

© www.soinside.com 2019 - 2024. All rights reserved.