我有一个X
类。它具有两个属性a
和b
。在某些情况下,X
对象的相等性将基于a
的相等性,而在某些情况下将基于b
的相等性。我想知道建模此数据的最佳方法。
我不能简单地基于某个标志具有两个equals函数,因为我使用了很多集合和列表,因此必须重写equals()
。这就是我的想法:
X
,具有两种实现Xa
和Xb
。问题是,我将需要在Xa
和Xb
之间进行转换,并且我希望有数百个实例,因此创建新副本将非常昂贵。 a
进行相等操作,因此请实现equals()
与a
进行比较。当需要基于b
的相等性时,只需为其编写一个单独的方法。问题是,我必须重新发明轮子来比较集合和列表。 上面的优点和缺点是什么?还有其他选择吗?
X
类是否首先存在缺陷?我可以以更好的方式实现它吗? 最佳答案
解决方案0:重构
我想建议的第一件事是考虑重新设计对象层次结构。尽管从提供的信息中我们对您试图建模的实际问题知之甚少,但您所描述的情况听起来不太干净。
解决方案1:“切换”多态性
将您所说的作为严格的要求,我可以想到以下(不是很漂亮)的解决方案。基本思想是X
对象的每个实例都获得一个标志,以告知其“性别”。因此,在性别之间进行转换仅仅是分配一个单词的问题。但是请注意,这也会使对象大小增加一个单词。如果您有许多小对象,那么额外的开销可能会很大。 (在下面的玩具示例中,它高达三分之一,在这种情况下,我绝对希望仅在需要时创建Xa
或Xb
类型的新对象。)取决于其他相等比较和哈希码计算的成本是,尽管可能可以接受,但案例选择的额外开销也可能很明显。
下面的类经过精心设计,可以满足我所知道的所有合同,并且可以在任何集合中使用并可以自由地来回转换。但是,当对象的性别包含在任何集合中时,不得触摸该对象的性别,并且一个集合只能包含特定性别的X
。如您所见,我们正在慢慢偏离面向对象的方向,必须管理自己的不变式。编译器无法帮助我们强制执行它们。这应该足以引起一个大的危险信号。
public final class X implements Comparable<X> {
public static enum Genders { A, B };
private Genders gender;
private final String a;
private final Integer b;
public X(final String a, final Integer b, final Genders gender) {
if (a == null) {
throw new NullPointerException("a");
}
if (b == null) {
throw new NullPointerException("b");
}
if (gender == null) {
throw new NullPointerException("gender");
}
this.a = a;
this.b = b;
this.gender = gender;
}
public Genders getGender() {
return this.gender;
}
public void setGender(final Genders gender) {
if (gender == null) {
throw new NullPointerException("gender");
}
this.gender = gender;
}
@Override
public boolean equals(final Object other) {
if (other instanceof X) {
final X otherX = (X) other;
if (this.gender == otherX.gender) {
switch (this.gender) {
case A:
return this.a.equals(otherX.a);
case B:
return this.b.equals(otherX.b);
default:
throw new AssertionError("unexpected gender");
}
}
}
return false;
}
@Override
public int hashCode() {
switch (this.gender) {
case A:
return this.a.hashCode();
case B:
return this.b.hashCode();
default:
throw new AssertionError("unexpected gender");
}
}
@Override
public int compareTo(final X other) {
// It seems acceptable to allow the case that
// this.gender != other.gender here.
switch (this.gender) {
case A:
return this.a.compareTo(other.a);
case B:
return this.b.compareTo(other.b);
default:
throw new AssertionError("unexpected gender");
}
}
@Override
public String toString() {
return String.format("{a: \"%s\", b: %d, gender: %s}",
this.a, this.b, this.gender);
}
}
这是一个小示范如何使用类型。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
public final class Main {
public static void main(final String[] args) {
final Set<X> theAs = new HashSet<>();
final Set<X> theBs = new TreeSet<>();
theAs.add(new X("alpha", 1, X.Genders.A));
theAs.add(new X("beta", 1, X.Genders.A));
theAs.add(new X("gamma", 2, X.Genders.A));
theAs.add(new X("delta", 2, X.Genders.A));
System.out.println("These are the As:\n");
for (final X x : theAs) {
System.out.println(x);
}
System.out.println();
{
final Iterator<X> iter = theAs.iterator();
while (iter.hasNext()) {
final X x = iter.next();
iter.remove(); // remove before changing gender
x.setGender(X.Genders.B);
theBs.add(x);
}
}
theBs.add(new X("alpha", 3, X.Genders.B));
theBs.add(new X("alpha", 4, X.Genders.B));
System.out.println("These are the Bs:\n");
for (final X x : theBs) {
System.out.println(x);
}
}
}
输出:
These are the As:
{a: "alpha", b: 1, gender: A}
{a: "delta", b: 2, gender: A}
{a: "beta", b: 1, gender: A}
{a: "gamma", b: 2, gender: A}
These are the Bs:
{a: "alpha", b: 1, gender: B}
{a: "delta", b: 2, gender: B}
{a: "alpha", b: 3, gender: B}
{a: "alpha", b: 4, gender: B}
解决方案2:装饰器模式
如果您可以使用每个对象“转换”的单个
new
的开销来生活(而且我很确定可以),那么使用decorators会更简洁,更不易出错。让我们开始为您的类型定义一个接口。 (您可能会比这个玩具示例复杂得多。)
public interface X {
public String getA();
public Integer getB();
}
接下来,我们提供该接口的基本实现,除了支持比较之外,它会执行所有操作。请注意,该类是(可以是)不可变的(尤其是
final
)。因为我没有重写equals
和hashCode
,甚至不费心去实现Comparable
,所以这个“基”类的实例将具有从Object
继承的身份比较语义。这就是我们想要的(请参阅下文)。public final class BasicX implements X {
private final String a;
private final Integer b;
public BasicX(final String a, final Integer b) {
if (a == null) {
throw new NullPointerException("a");
}
if (b == null) {
throw new NullPointerException("b");
}
this.a = a;
this.b = b;
}
@Override
public String getA() {
return this.a;
}
@Override
public Integer getB() {
return this.b;
}
@Override
public String toString() {
return String.format("{a: \"%s\", b: %d}", this.a, this.b);
}
// Note: No implementation of equals() and hasCode().
}
有了所有业务逻辑之后,我们现在可以转向装饰器。我们将定义其中两个:
Xa
和Xb
。他们将把所有东西(在这个人为的例子中没有多少)委托给它们包含的X
实例,除了他们将提供适当的equals
和hashCode
实现并实现Comparable
。由于两个装饰器的委派逻辑相同,因此我将把通用代码分解为中间包私有类。
abstract class DecoratedX implements X {
private final X x;
protected DecoratedX(final X x) {
if (x == null) {
throw new NullPointerException("x");
}
this.x = x;
}
protected final X getX() {
return this.x;
}
@Override
public final String getA() {
return this.x.getA();
}
@Override
public final Integer getB() {
return this.x.getB();
}
@Override
public final String toString() {
return this.x.toString();
}
}
这将
Xa
和Xb
内部的代码减少到只是比较逻辑,这在每个类中都是唯一的。请注意,Xa
和Xb
可以是final
。public final class Xa extends DecoratedX implements X, Comparable<Xa> {
public Xa(final X x) {
super(x);
}
@Override
public boolean equals(final Object other) {
if (other instanceof Xa) {
final Xa otherXa = (Xa) other;
return this.getA().equals(otherXa.getA());
}
return false;
}
@Override
public int hashCode() {
return this.getA().hashCode();
}
@Override
public int compareTo(final Xa other) {
return this.getA().compareTo(other.getA());
}
}
我可能会用
Xb
的(虽然有些重复)代码来惹恼您,但是为了完整起见,这里是。final class Xb extends DecoratedX implements X, Comparable<Xb> {
public Xb(final X x) {
super(x);
}
@Override
public boolean equals(final Object other) {
if (other instanceof Xb) {
final Xb otherXb = (Xb) other;
return this.getB().equals(otherXb.getB());
}
return false;
}
@Override
public int hashCode() {
return this.getB().hashCode();
}
@Override
public int compareTo(final Xb other) {
return this.getB().compareTo(other.getB());
}
}
然后我们走了。放在一起,我们可以做比以前更酷的事情。请注意,我们现在如何在三个不同的集合中使用不同的比较语义同时拥有相同的对象(尽管在两种情况下进行了包装(装饰))。
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public final class Main {
public static void main(final String[] args) {
final List<X> theXs = new ArrayList<>();
final Set<Xa> theXas = new HashSet<>();
final Set<Xb> theXbs = new TreeSet<>();
theXs.add(new BasicX("alpha", 1));
theXs.add(new BasicX("alpha", 1));
theXs.add(new BasicX("beta", 2));
theXs.add(new BasicX("beta", 3));
theXs.add(new BasicX("gamma", 2));
theXs.add(new BasicX("delta", 3));
for (final X x : theXs) {
theXas.add(new Xa(x));
theXbs.add(new Xb(x));
}
System.out.println("These are the As:\n");
for (final X x : theXas) {
System.out.println(x);
}
System.out.println();
System.out.println("These are the Bs:\n");
for (final X x : theXbs) {
System.out.println(x);
}
}
}
输出:
These are the As:
{a: "alpha", b: 1}
{a: "delta", b: 3}
{a: "beta", b: 2}
{a: "gamma", b: 2}
These are the Bs:
{a: "alpha", b: 1}
{a: "beta", b: 2}
{a: "beta", b: 3}
还要注意,这种设计是类型安全的:编译器只是不允许我们在
Xb
的集合中使用Xa
对象。在示例中,我直接从Xa
创建了Xb
和BasicX
。如果要“将Xa
转换为Xb
”,反之亦然,则该代码当然是Xb a2b(final Xa xa) {
return new Xb(xa.getX());
}
和
Xa b2a(final Xb xb) {
return new Xa(xb.getX());
}
相反。您必须使
DecoratedX.getX()
方法public
才能真正起作用。 (从技术上讲,您也可以将Xa
粘贴到Xb
中:毕竟是X
。虽然这完全可以工作,并且适用于装饰器模式的其他应用程序,但在这种情况下,无用的间接层很快会变得令人讨厌并且很容易避免。)关于java - Java中数据的双重性质,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/29334638/