为什么每个实例只允许一次Java中的构造函数调用?如果在一次调用中设置多个实例变量而不是调用多个setter是有用的。
答案 0 :(得分:12)
如果要一次设置多个变量,只需定义一个函数:
public class Person {
public Person(String firstName, String lastName, Date dateOfBirth) {
set(firstName, lastName, dateOfBirth);
}
public void set(String firstName, String lastName, String dateOfBirth) {
...
}
}
根据定义,对象只构造一次,因此构造函数只被调用一次。
值得注意的一点是:支持不变性更为常见,所以:
public class Person {
private final String firstName;
private final String lastName;
private final Date dateOfBirth;
public Person(String firstName, String lastName, Date dateOfBirth) {
this.firstName = firstName;
this.lastName = lastName;
this.dateOfBirth = dateOfBirth == null ? null new Date(dateOfBirth.getTime());
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public Date getDateOfBirth() { return dateOfBirth == null ? null : new Date(dateOfBirth.getTime()); }
public Person withFirstName(String firstName) {
return new Person(firstName, lastName, dateOfBirth);
}
public Person withLastName(String lastName) {
return new Person(firstName, lastName, dateOfBirth);
}
public Person withDateOfBirth(Date dateOfBirth) {
return new Person(firstName, lastName, dateOfBirth);
}
}
因为当你这样做时,许多并发问题就会消失。不仅仅是并发问题。 String
,BigDecimal
,BigInteger
和其他一些标准类是不可变的(不可变的意思是一旦实例化,其状态永远不会改变)。 Date
不是。你在上面的代码中看到我必须不断地防御性地复制出生日期?那是因为Date
不是不可变的。如果您没有呼叫者可以这样做:
public class Person {
private final Date dateOfBirth;
...
public Date getDateOfBirth() { return dateOfBirth; }
}
Person person = new Person(...);
Date date1 = person.getDateOfBirth();
date1.setTime(1000000000L);
System.out.println(person.getDateOfBirth());
出生日期将发生变化。该问题仅由Date
可变引起。这是支持不变性的另一个原因。
答案 1 :(得分:6)
您的错误是假设您应该编写默认构造函数并使用setter设置所有值。一个对象在创建时应该100%准备就绪,所以你应该编写一个构造函数来初始化它的整个状态。
错误的不是Java“众神”,而是你。
答案 2 :(得分:3)
这里有一个更深层的观点:每个对象可以运行任意次数,零到多次。构造函数只能运行一次这一事实是一个优点,因为它允许您编写每个对象只执行一次的代码。例如:注册,获取外部资源(文件,套接字,数据库连接),数据操作(如果您希望对输入进行排序,则在构造函数中进行排序,语言确保将进行排序)。
答案 3 :(得分:2)
此处描述的构造函数和其他构造函数模式的另一种替代方法是Builder模式。有很多页面描述它是如何工作的,但可以用以下简洁的例子来总结:
import java.awt.Color; import java.util.LinkedList; import java.util.List;
public class Badger {
private int legs;
private boolean nose;
private List<Color> colors;
private Badger() {
colors = new LinkedList<Color>();
}
public static class BadgerBuilder {
private Badger badger;
private BadgerBuilder(){}
public static BadgerBuilder buildBadger() {
BadgerBuilder b = new BadgerBuilder();
b.badger = new Badger();
return b;
}
public BadgerBuilder legs(int legs) {
badger.legs = legs;
return this;
}
public BadgerBuilder hasNose(boolean nose) {
badger.nose = nose;
return this;
}
public BadgerBuilder addColor(Color c) {
badger.colors.add(c);
return this;
}
public Badger build() {
return badger;
}
}
public static void main(String [] args) {
Badger b = BadgerBuilder.buildBadger()
.legs(4)
.hasNose(true)
.addColor(Color.black)
.addColor(Color.white)
.build();
}
答案 4 :(得分:1)
没有什么可以阻止您编写另一个一次设置所有所需字段的方法。它甚至可以从构造函数中调用,以防止代码重复。
答案 5 :(得分:0)
您可以从另一个构建函数中调用它。
例如:
class MyClass {
public MyClass() {
// initialize what you want
}
public MyClass(String custom) {
this();
// custom initialization
}
}
答案 6 :(得分:0)
正如许多人所提到的,构造函数实际上并不构造,而是在对象上设置初始值。这很重要,因为它是设置最终字段的唯一地方,否则这些字段是未初始化的。这应该是构造函数与任何其他函数不同的线索。此外,拥有一个完善的构造函数可以帮助定义不变量。
然而,这里有一些权衡和潜在的陷阱。当构造函数完成时,对象“完全完成”并不总是正确的。构造函数是自下而上的继承调用,但是自上而下完成。因此,即使构造函数完成,子类仍可能有工作要做。因此,如果在构造期间传递对自己的引用(转义)并且其他类期望调用者处于完整/稳定状态,则可能发生问题,因为子类没有完成其机会。这在高度并发的代码中尤其危险。在这种情况下,使用构造/初始化模式变得更安全甚至是强制性的。因此,可能无法将对象置于足够安全的状态以传递给构造函数中的其他对象。在很多情况下,对象具有循环依赖关系,并且必须对所有对象进行基本构造,然后将它们全部初始化。
IRC Josh Bloch(Effective Java)和Brian Goetz(Java Concurrency in Practice)都对这些主题进行了很好的讨论。工厂建设与私人建筑师,如果往往是一个很好的选择,并有很多好处,并由布洛赫提供。