我刚刚完成了对不可变对象及其优点的研究,所以我认为自己应该创建一个。这是我的员工班级,扩展了人员班级。这是不可变的,因为我对可变日期对象有吸气剂。

package objects.objects;

import java.util.Date;

public class Person {

    private final String name;
    private final Date dateOfBirth;

    Person(String name,Date dateOfBirth){
        this.name=name;
        this.dateOfBirth=dateOfBirth;

    }
    public String getName() {
        return name;
    }
    public Date getDateOfBirth() {
        return dateOfBirth;
    }
}


Employee类: >指定枚举:

package objects.objects;

import java.util.Date;

public final class Employee extends Person{

    private final String empolyeeID;
    private final Designation designation;
    private final double salary;
    private final Date dateOfJoining;


    public Employee(String Name,Date dateOfBirth,String employeeID,Designation designation,double salary,Date dateOfJoining)
    {
        super(Name,dateOfBirth);
        this.empolyeeID=employeeID;
        this.designation=designation;
        this.salary=salary;
        this.dateOfJoining=dateOfJoining;

    }


    public String getEmpolyeeID() {
        return empolyeeID;
    }


    public Designation getDesignation() {
        return designation;
    }


    public double getSalary() {
        return salary;
    }


    public Date getDateOfJoining() {
        return dateOfJoining;
    }
}


Oracle Java文档说:


不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用。如有必要,
创建副本,并存储对副本的引用。同样,在必要时创建内部可变对象的副本
,以避免
返回方法中的原始内容

此外,Oracle文档说:

不允许子类覆盖方法。最简单的方法是将类声明为final。一种更复杂的方法是使构造函数私有化,并在factory
方法中构造实例。如果我不将类定型为final或拥有私有构造函数,那会有什么害处? )还是我缺少其他方法?

还可以指出可变对象引用的日期吗?我的代码中是否还有其他任何违反可变政策或任何其他改进的缺陷?

评论

员工成为最终员工有什么原因吗?

是的。几乎忘记了。我想将其包括在问题中。文档说:“不允许子类覆盖方法。最简单的方法是将类声明为final。更复杂的方法是使构造函数私有化并在工厂方法中构造实例”。。。如果我希望类被子类化怎么办?如果我不使自己的课程最终定下来或没有私有的构造函数,会有什么危害?我无法遵循这条线。

我不确定继承在Java中是如何工作的。我认为您应该更新您的问题以询问此问题,显然有人可以正确回答:)

#1 楼

您所有的字段都是私有的且是最终字段,这是不可更改的第一步。这很好。

Date类不是不可变的,因此,例如,您对方法有疑问: br />
因为某人可以做:

public Date getDateOfBirth() {
    return dateOfBirth;
}


突然你的员工已经老了很多。 oracle文档的意思是,如果您具有可变的内容,则应返回该数据的副本。这通常被称为“防御性副本”:对于final)。如果方法不是最终方法,则子类可能会覆盖该方法,并以使getDateOfBirth() / getName()可变的方式来实现。

请注意,在这种情况下,name的构造函数取一个dateOfBirth值。这导致了一个建议,即不可变类应存储Date而不是long。如果是最终版本,则long始终是不可变的。很好,甚至。

您所选择的软件包名称缺少... Date。您不能给包裹起个更好的名字吗?

评论


\ $ \ begingroup \ $
另外,如果您使用的是Java 8,则应使用新的LocalDate类。默认情况下它是不可变的。
\ $ \ endgroup \ $
–黄昏
14-10-14在12:15



\ $ \ begingroup \ $
您忘记了日期也必须被复制才能存储。显然,切换到长时可以完成工作。
\ $ \ endgroup \ $
– maaartinus
14-10-14在12:16

\ $ \ begingroup \ $
绝对,我已经想了很久,所以我假设...。
\ $ \ endgroup \ $
–rolfl
14-10-14在12:19

\ $ \ begingroup \ $
@rolfl文档说:“不允许子类覆盖方法。最简单的方法是将类声明为final。更复杂的方法是将构造函数私有化并在工厂方法中构造实例。 “ ...如果我希望我的班级被子类化怎么办?如果我不使自己的课程最终定下来或没有私有的构造函数,会有什么危害?我无法遵循这条线
\ $ \ endgroup \ $
– Ishan Soni
14-10-14在13:21

\ $ \ begingroup \ $
@IshanSoni-在评估不变性时,需要考虑很多因素。我遍历了您的代码,没有看到很多问题(尽管我错过了一些问题,并且多种/其他答案都很好)。回答“什么使类不可变?”与回答“此类是不可变的?”不同。至于子类,子类可以声明自己的dateOfBirth,覆盖现有方法,然后将其存储为“错误”。您的getName()和getDateOfBirth()方法应该是最终的。会修改我的答案。
\ $ \ endgroup \ $
–rolfl
14-10-14在13:26

#2 楼

除了按@rolfl的说明保护getDateOfBirth
在构造函数中制作防御性副本也同样重要: ,那么有人可以编写以下代码:

Person(String name, Date dateOfBirth) {
    this.name = name;
    this.dateOfBirth = new Date(dateOfBirth.getTime());
}


由于诸如this.dateOfBirth = dateOfBirth;之类的易变对象非常麻烦,
存储Datelong值要安全得多而不是dateOfBirth.getTime()本身。
那样,您根本就不需要记住使用防御性副本,
您的Date值将始终保持不变,与long对象相反。

如果您想要最安全的,不变的类,那么绝对应该使用此选项,并存储代表日期的Date值。 long中的dateOfJoining也是如此。

#3 楼

这个答案与不变性无关,因为@rolfl已将其覆盖。

=的吸气剂之间有很多空格,一个应该足够,否则代码将占用很多空间。 of camelCased,难道是java特有的某种东西使它无法命名为=吗?否则,应命名它。.Employee

#4 楼

如不同答案中所述,Date是可变结构,因此每次共享时都需要复制它。我的建议根本不使用,而是使用像Joda Data这样的不可变结构。使用正确的构造函数,该构造函数将double作为参数

BigDecimal salary =  new BigDecimal("502.34");


BigDecimal本身是不可变的,因此您无需复制它。

#5 楼


如果我希望我的班级被子类化怎么办?如果我不将我的类定型为final或拥有私有构造函数,那会有什么害处?根据您所依赖的不变性的属性,这可能会抛出本来可以工作的代码。吸气剂?)还是我缺少其他方法?


不可变性与您的班级是否有吸气剂无关;特别是,没有setter的类不会成为不可变的类。在其最纯粹的定义中,不变性是缺乏可变性。当且仅当没有办法改变其状态时,该对象才是不可变的。

在Java中,这暗含了文档中所说的: br />不要共享对可变对象的引用。因为那将意味着其他代码可以更改您的内部状态。

不允许子类覆盖方法。因为子类可以更改行为,因此可以更改对象的可观察状态。考虑:

public class ImmutablePoint {
    // mutable class; share no references!
    private final Point p;

    public ImmutablePoint(Point p) {
        // copy values, do not store reference
        this.p = new Point(p.x, p.y);
    }

    public double getX() { return p.getX(); }
    public double getY() { return y.getY(); }
}

// no longer immutable
public class ExtensionPoint extends ImmutablePoint {
    private Point offset = new Point();

    public ExtensionPoint(Point origin) {
        super(origin);
    }

    public void setOffset(int x, int y) {
        offset.x = x;
        offset.y = y;
    }

    public double getX() { return super.getX() + offset.getX(); }
    public double getY() { return super.getY() + offset.getY(); }
}

public class JitteryPoint extends ImmutablePoint {
    public JitteryPoint(Point origin) {
        super(origin);
    }

    // Here there be shenanigans
    public double getX() { return super.getX() + Math.random(); }
    public double getY() { return super.getY() + Math.random(); }
}




#6 楼

我想提一下其他大多数人都没有提到的方面,那就是您的构造函数

public Employee(String, Date, String, Designation, double, Date)


向构造函数添加的参数越多,难度就越大之所以开始使用,是因为参数的顺序不再明确。当您有多个相同类型的参数时(例如本例中的两个String和两个Date),将它们混合起来非常容易,这会导致在阅读代码时很难发现错误。

处理不可变对象的一个​​好方法是将构造函数设为私有,然后从Builder类调用它,该类是在其构建的类内部声明的公共类(它必须位于内部才能调用私有构造函数)。

Employee.Builder builder = new Employee.Builder();
builder.setName(name);
builder.setDateOfBirth(dateOfJoining);
builder.setEmployeeID(id);
builder.setDesignation(designation);
builder.setSalary(salary);
builder.setDateOfJoining(birthdate);

Person p = builder.build();


构建器将收集您在私有变量中设置的所有属性。调用build()时,它将调用Employee的构造函数,该构造函数具有之前设置的属性,并返回结果。如果尚未设置一个属性,则可以使用合理的默认值,也可以在没有默认值的情况下引发异常。

通过这种方式,您是否看到上面代码中的错误?如果我直接调用了构造函数,是否可以发现它? br />
 Person p = new Employee.Builder().setName(name)
                                  .setDateOfBirth(birthdate)
                                  .setEmployeeID(id)
                                  .setDesignation(designation)
                                  .setSalary(salary)
                                  .setDateOfJoining(dateOfJoining)
                                  .build();


#7 楼

根据其他一些很好的答案,这有点像尼特,但您始终将employee误拼为empolyee

#8 楼

与某些人所说的相反,拥有一个可继承的不可变类从根本上来说并没有错,只要继承契约规定如果两个或多个实例被视为等效,则它们必须永远永远被视为等效。此外,如果将某个对象的部分或全部引用替换为被认为“等效”的对象引用,则该类必须正确工作,而不会抱怨或明显改变其行为。如果未声明类的方法,则该类将无法阻止违反其继承协定的非法派生类的定义,但由于即使该类的方法被禁止,它也无法阻止非法派生类的定义。 final,因此制作方法final并不特别重要。重要的是,继承协定规定所有派生类不仅必须在其继承字段中而且在所有可观察的特性(无论是继承的还是其他特性)上都是完全不变的。如果派生类不遵守该合同,则使用该派生类的代码可能会发生故障,但故障将完全出在违反合同的派生类上。

如果不希望确切指定任何派生类型将需要什么并且可能期望不可变类型,那么最好使它成为不可变类型。但是,在许多情况下,使用抽象基类或接口来指定所有合法的派生类或实现将是不可变的可能会有所帮助。例如,可以定义一个final类或与成员的接口,以获取尺寸或在任何(行,列)坐标处读取单元格。尽管最常见的实现可能使用2d数组作为后备存储,但是派生类可以通过多种不同方式存储信息。如果final是使用数组作为后备存储的Matrix2d类,那么即使99%[或就此而言100%]为空白,所有可用作Matrix2d类型的对象都必须为每个单元格都具有一个后备存储元素。将其设置为非最终类可以定义诸如final之类的派生类,这些派生类可以使用更简单的后备存储。