可变和不可变类

3

我想在Java中创建可变和不可变节点,除了可变性之外,两者应该完全相同。如何实现基类和可变和不可变类的两个派生类?


这是什么类型的“节点”?它的上下文是什么?这些类应该开放继承吗? - Bruno Kim
5个回答

5
可变类和不可变类的区别在于,不可变类没有setter或者其他修改内部状态的方法。状态只能在构造函数中设置。
如果将父类命名为Immutable,这将是不合适的,因为当你有子类时,这个命名就不再准确了。这个名字会给人带来误解。
ImmutableNode node = new MutableNode();
((MutableNode)node).change();

即使没有setter,使用反射仍然可以修改类。只有final变量才能保证不可变性。 - amukhachov
@myx:我同意在不可变类中应该标记字段为final(但原因与您提出的不同)。但是,这在这里行不通,因为他的子类化想法就不可能实现了。可变类也无法修改final字段。虽然老实说,我不喜欢一开始就使用继承来表示可变性的想法。有其他方法可以共享代码,而不必使用继承,例如接口和组合。 - Mark Byers
2
@myx --- bzzztt。您可以通过反射更改最终变量...除了作为编译时常量表达式的静态最终字段。然而,通过反射更改私有和/或最终变量被认为是不好的做法...只有在万不得已的情况下才应该这样做。 - Stephen C

4
你只需要创建一个带有受保护变量的基类。
public class Base{
     protected int foo;
}

可变对象需要能够设置变量。
public class MutableBase extends Base{
     public void setFoo(){}
}

不可变对象需要能够仅设置一次变量。
public class ImmutableBase extends Base{
     public ImmutableBase(int foo){
          this.foo = foo;
     }
}

大多数不可变类都具有在不改变实例的情况下操作变量的方法。String类就是这样做的,你可能需要像这样的东西。

public ImmutableBase add(int bar){
     return new ImmutableBase(this.foo+bar);
}

这很棒的一点是,你可以让你的类的用户对每个实例的内部有更少的控制/担忧。这使得与之工作变得更容易,因为在Java中每个对象都是通过对象引用传递的,所以如果你传递一个字符串或不可改变的基础类 (ImmutableBase),你就不必担心它被改变了。

编译错误。如果您这样做,foo 就不能是私有的。 - Stephen C
将字段本身声明为final怎么样? - nabil
这并不使类成为不可变的,它只是使变量只允许一次赋值。就概念而言,可变性仅适用于对象。一个不可变的类可能有可变的成员,也可能没有。原始类型没有任何可变性——在你不能将4重新分配为“foo”的意义上,它们是不可变的,但这是一个牵强的解释。 - dfb

1

为了使一个类成为不可变的,它必须被声明为final,并且不能有setter方法。final声明确保它不能被扩展和添加额外的可变属性。

class Base {
    protected int var1;
    protected int var2;

    public getVar1() {return var1;}
    public getVar2() {return var2;}
    }

    class Mutable extends Base {
      public setVar1(int var1) {this.var1 = var1}
      public setVar2(int var2) {this.var2 = var2}
    }

    final class Immutable extends Base { //final to avoid being extended and then implement the setters

    }

这是我所能做的,但你为什么需要这样的场景呢?


将字段本身声明为final怎么样? - nabil
因为他想要一个可变版本?如果字段是final,它就不能被设置,然而可变版本需要该字段被设置。 - maress
虽然只有一个finalFoo可以“保证”Foo的实例不会真正成为某个邪恶可变派生Foo的实例,但有时候拥有一个可继承的、甚至是抽象的类被指定为不可变的(意味着任何可变的派生类都将被视为“损坏”)可能是有帮助的。例如,在某些情况下,定义一个ImmutableMatrix抽象类可能是有用的,其派生包括ImmutableArrayMatrix(由与矩阵大小相同的数组支持)、ImmutableConstantMatrix(由... - supercat
...定义宽度和高度的整数,以及一个定义所有元素值的元素),ImmutableMergedMatrix(其中将保存对两个或多个ImmutableMatrix实例的引用,并且对于每个实例,一些整数定义应该应用于最终矩阵的部分)。 如果特定的应用程序将使用内容符合不匹配任何预先存在的实现的某种模式的矩阵,则定义新类可能是有用的。确保,违反不可变性的派生类会破坏事情,但故障... - supercat
...将在未遵守不可变性规范的类的被覆盖版本中产生问题。 - supercat

1
另一个选项是使用与UnmodifiableList相同的策略。首先,创建一个指定类型的接口。
interface List<T>{
    List<T> add(T t);
    T getAt(int i);
    ...
}

然后您使用所有业务逻辑实现可变类:

public class MutableList<T> implements List<T>{
    @Override
    List<T> add(T t){ ... }

    @Override
    T getAt(int i){ ... }

    ...
}

最后,创建一个不可变类来作为可变类的视图。您实现相同的接口,但将所有读取方法调用委托给被查看的对象,并使用异常禁止任何写入访问。
public class UnmodifiableList<T> implements List<T>{
    //This guy will do all hard work
    private List delegate;

    public UnmodifiableList(List<? extends T> delegate){
        this.delegate = delegate;
    }

    //Forbidden mutable operation: throw exception!
    @Override
    List<T> add(T t){ 
        throw new UnsupportedOperationException("List is unmodifiable!");
    }

    //Allowed read operation: delegate
    @Override
    T getAt(int i){ 
        return delegate.getAt(i);
    }

    ...
}

这种方法的好处是您只需实现一次业务逻辑,然后可以使用其自身的方法和验证检查来构建对象,然后将其转换为不可变对象。


重要的是要注意对象之间的区别,有些对象提供对可能可变对象的只读视图,而另一些对象则提供对它们自己创建的对象的只读视图,并且从不在任何可能改变它的上下文中公开。在这方面,“不可修改”这个术语有点模糊。我更喜欢使用诸如“ReadableFoo”之类的术语来指代已知可读且不能直接修改但可能通过其他方式发生变化的事物,以及使用“ImmutableFoo”来指代保证永远不会改变的事物。而“不可修改”似乎是一个奇怪的折中方案。 - supercat
是的,你说得对。原始列表的持有者可以更改其内容,并且这将暴露给视图。另一方面,视图在计算和内存方面要高效得多。正确的工具取决于用途,不幸的是,提问者没有提供太多信息... - Bruno Kim

1

不可变类是一种一旦创建,其内容就无法更改的类。不可变对象是其状态无法更改的对象。

Java中不可变类的常见示例是String类。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接