这是一个使用lambdas和高阶函数的Java 8版本。使用匿名内部类而不是lambdas将其转换为Java 7可能是可能的。(我相信大多数IDE都有一个重构操作来做到这一点。我将把它作为有兴趣的读者的练习。
这里实际上有两个不同的问题:
给定两个不同类型的对象,通过检查每个对象的相应字段来评估它们。这与 JDK 库 API 已经定义的“equals”和“compare”操作不同,因此我将使用术语“等效”来代替。
给定两个包含这些类型元素的集合,确定它们对于该术语的某些定义是否“相等”。这实际上是相当微妙的;请参阅下面的讨论。
1. 等效性
给定两个类型的对象,我们想要确定它们是否等效。结果是一个布尔值。这可以用 类型的函数表示。但是我们不一定能直接检查对象;相反,我们需要从每个对象中提取各自的字段,并相互评估提取结果。如果从中提取的字段是类型,而从中提取的字段是类型,则提取器由函数类型表示T
U
BiPredicate<T,U>
T
TR
U
UR
Function<T, TR>
Function<U, UR>
现在我们已经提取了类型和 的结果。我们可以只调用它们,但这是不必要的限制。相反,我们可以提供另一个等价函数,该函数将被调用以相互评估这两个结果。那是一个.TR
UR
equals()
BiPredicate<TR,UR>
鉴于所有这些,我们可以编写一个高阶函数,该函数接受所有这些函数并为我们生成和等价函数(为了完整性而包含通配符):
static <T,U,TR,UR> BiPredicate<T,U> equiv(Function<? super T, TR> tf,
Function<? super U, UR> uf,
BiPredicate<? super TR, ? super UR> pred) {
return (t, u) -> pred.test(tf.apply(t), uf.apply(u));
}
使用 来评估字段提取结果可能是一种常见情况,因此我们可以为此提供重载:equals()
static <T,U> BiPredicate<T,U> equiv(Function<? super T, ?> tf,
Function<? super U, ?> uf) {
return (t, u) -> equiv(tf, uf, Object::equals).test(t, u);
}
我本可以提供另一个类型变量作为两个函数的结果类型,以确保它们是相同的类型,但事实证明这不是必需的。由于 定义了 on 并且它需要一个参数,我们实际上并不关心函数返回类型是什么,因此通配符。R
equals()
Object
Object
下面介绍如何使用它来仅使用字符串字段来评估 OP 的示例类:
ClassA a = ... ;
ClassB b = ... ;
if (equiv(ClassA::getStrA, ClassB::getStrB).test(a, b)) {
// they're equivalent
}
作为变体,我们可能还需要一个原始的特化,以避免不必要的装箱:
static <T,U> BiPredicate<T,U> equivInt(ToIntFunction<? super T> tf,
ToIntFunction<? super U> uf) {
return (t, u) -> tf.applyAsInt(t) == uf.applyAsInt(u);
}
这使我们能够基于单个字段构造等价函数。如果我们想基于多个字段评估等效性,该怎么办?我们可以通过链接该方法来组合任意数量的 BiPredicates。下面介绍如何创建一个函数,该函数使用 OP 示例中类的 和 字段来评估等价性。为此,最好将函数与使用它分开存储在变量中,尽管这可能都是内联的(我认为这会使它不可读):and()
int
String
BiPredicate<ClassA, ClassB> abEquiv =
equivInt(ClassA::getIntA, ClassB::getIntB)
.and(equiv(ClassA::getStrA, ClassB::getStrB));
if (abEquiv.test(a, b)) {
// they're equivalent
}
作为最后一个示例,在为两个类创建等价函数时,能够为字段提取结果提供等价函数是非常强大的。例如,假设我们要提取两个 String 字段,如果提取的字符串相等,则认为它们是等效的,忽略大小写。下面的代码导致:true
equiv(ClassA::getStrA, ClassB::getStrB, String::equalsIgnoreCase)
.test(new ClassA(2, "foo", true),
new ClassB(3, "FOO", false))
2. 收集“平等”
第二部分是评估两个集合在某种意义上是否“相等”。问题在于,在集合框架中,相等的概念被定义为一个列表只能等于另一个列表,而一个集合只能等于另一个集合。因此,某些其他类型的集合永远不能等于 List 或 Set。有关这一点的一些讨论,请参阅 Collection.equals()
的规范。
这显然与OP想要的不一致。正如OP所建议的那样,我们并不真正想要“平等”,但我们想要一些其他属性,我们需要为其提供定义。根据OP的例子,以及Przemek Gumula和janos在其他答案中的一些建议,似乎我们希望两个集合中的元素以某种方式一对一对应。我称之为双射,在数学上可能并不精确,但它似乎足够接近。此外,每对元素之间的对应关系应为上文定义的等效性。
计算这有点微妙,因为我们有自己的等价关系。我们不能使用许多内置的收集操作,因为它们都使用 .我的第一次尝试是这样的:equals()
// INCORRECT
static <T,U> boolean isBijection(Collection<T> c1,
Collection<U> c2,
BiPredicate<? super T, ? super U> pred) {
return c1.size() == c2.size() &&
c1.stream().allMatch(t -> c2.stream()
.anyMatch(u -> pred.test(t, u)));
}
(这与Przemek Gumula给出的基本上相同。这存在问题,归结为一个集合中有多个元素与另一个集合中的单个元素相对应的可能性,从而使元素不匹配。如果给定两个多集,使用相等性作为等价函数,这将给出奇怪的结果:
{a x 2, b} // essentially {a, a, b}
{a, b x 2} // essentially {a, b, b}
此函数将这两个多集视为双射,但事实显然并非如此。如果等价函数允许多对一匹配,则会出现另一个问题:
Set<String> set1 = new HashSet<>(Arrays.asList("foo", "FOO", "bar"));
Set<String> set2 = new HashSet<>(Arrays.asList("fOo", "bar", "quux"));
isBijection(set1, set2, equiv(s -> s, s -> s, String::equalsIgnoreCase))
结果是 ,但如果集合的顺序相反,则结果为 。这显然是错误的。true
false
另一种算法是创建一个临时结构,并在元素匹配时将其删除。结构必须考虑重复项,因此我们需要减少计数,并且仅在计数达到零时才删除元素。幸运的是,各种Java 8功能使这变得非常简单。这与janos的答案中使用的算法非常相似,尽管我已经将等价函数提取到方法参数中。唉,因为我的等价函数可以有嵌套的等价函数,这意味着我无法探测映射(由相等性定义)。相反,我必须搜索地图的键,这意味着算法是O(N^2)。哦,好吧。
但是,代码非常简单。首先,使用 从第二个集合生成频率图。然后,迭代第一个集合的元素,并搜索频率图的键以查找等效项。如果找到一个,则其发生次数将递减。请注意传递给 Map.compute()
的重新映射函数的返回值。这会产生删除条目的副作用,而不是将映射设置为 。这有点像API黑客,但它非常有效。groupingBy
null
null
对于第一个集合中的每个元素,必须在第二个集合中找到一个等效元素,否则它将被救助。在处理完第一个集合的所有元素后,频率图中的所有元素也应已处理完毕,因此只需对其进行空测试即可。
代码如下:
static <T,U> boolean isBijection(Collection<T> c1,
Collection<U> c2,
BiPredicate<? super T, ? super U> pred) {
Map<U, Long> freq = c2.stream()
.collect(Collectors.groupingBy(u -> u, Collectors.counting()));
for (T t : c1) {
Optional<U> ou = freq.keySet()
.stream()
.filter(u -> pred.test(t, u))
.findAny();
if (ou.isPresent()) {
freq.compute(ou.get(), (u, c) -> c == 1L ? null : c - 1L);
} else {
return false;
}
}
return freq.isEmpty();
}
目前还不完全清楚这个定义是否正确。但从直觉上讲,这似乎是人们想要的。不过,它很脆弱。如果等价函数不对称,将失败。还有一些自由度没有被考虑在内。例如,假设集合是isBijection
{a, b}
{x, y}
并且等价于 和 ,但只等价于 。如果 与 匹配,则的结果为 。但是如果与 匹配,结果将是 。a
x
y
b
x
a
x
isBijection
false
a
y
true
把它放在一起
下面是 OP 的示例,使用 、 和 函数进行编码:equiv()
equivInt()
isBijection
List<ClassA> myList = Arrays.asList(new ClassA(1, "A", true),
new ClassA(2, "B", true));
Set<ClassB> mySet = new HashSet<>(Arrays.asList(new ClassB(1, "A", false),
new ClassB(2, "B", false)));
BiPredicate<ClassA, ClassB> abEquiv =
equivInt(ClassA::getIntA, ClassB::getIntB)
.and(equiv(ClassA::getStrA, ClassB::getStrB));
isBijection(myList, mySet, abEquiv)
这样做的结果是 。true