Java中是否有C#风格的对象初始化器?

71
7个回答

86

其实是可以的!

Person p = new Person()
{{
    setFirstName("John");
    setLastName("Doe");
    setAddress(new Address()
    {{
        setStreet("1234 St.");
        setCity("Phoenix");
    }});
}};

甚至更多:

Person p = new Person()
{{
    firstName = "John";
    lastName = "Doe";
    address = new Address()
    {{
        street = "1234 St.";
        city = "Phoenix";
    }});
}};

这被称为双括号初始化。然而,我建议避免使用这种习惯用法,因为它会产生一些意外的副作用,例如这种语法实际上会创建一个匿名内部类Person$1Address$

另请参阅


12
当然,这也要求你的领域受到保护。虽然这两种形式在表面上看起来相似,但它们实际上非常不同。 - Jon Skeet
@JonSkeet 您是什么意思? - Hassan Faghihi
1
@deadManN:我的意思是,如果这些字段是私有的(通常都是这样),第二个形式肯定不起作用,因为它试图从子类设置字段。 - Jon Skeet
副作用?匿名的?能被Hibernate读取吗?我设置了我的对象并传递了它的变量,但对于其他关系链接,我传递了一个新创建的类,它只有该对象的ID……我不确定Hibernate是否接受它,就像我在C#中使用EF(实体框架)时生成真正的类一样,但这就是我所做的。 - Hassan Faghihi
那就是为什么如果我想要说new work(),然后设置所有属性,而在此之前我创建了person并执行了相同的操作,以及其他可能包含的类,这将变成一堆难以理解的代码。但也许你有更好的想法。顺便说一句,我钦佩的Java社区知道很多设计模式,并且能够想到我们C#社区无法做到的许多事情,因为我们自然不需要那么多,它的名称默认更加锐利地指向Java。 - Hassan Faghihi
显示剩余3条评论

33

其他人已经展示了“双括号”初始化器,我认为应该避免使用——这不是继承的目的,而且只有在字段直接可见于子类时才能起作用,我也反对这种做法。它并不像 C# 的初始化器块那样。它是一种利用设计用于其他目的的语言特性的 hack。

如果你有比构造函数更多的值需要传递,你可以考虑使用建造者模式:

Person person = Person.newBuilder()
    .setFirstName("John")
    .setLastName("Doe")
    .setAddress(Address.newBuilder()
        .setStreet("...")
        .setCity("Phoenix")
        .build())
    .build();

这也允许您使Person成为不可变的。另一方面,这样做需要为此目的设计Person类。对于自动生成的类很好(这是Protocol Buffers遵循的模式),但对于手动编写的代码来说则是烦人的样板。


3
这听起来很奇怪,但为什么不通过返回“this”来链接“set”函数呢?然后你就可以写成“new Person().setFirstName("John").setLastName("Skeet")”。 - ashes999
2
@ashes999:你可以这样做,但那会相当奇怪。(当然,这也不适用于不可变性。) - Jon Skeet
这就像C#中所谓的“Fluent接口”一样,我认为。我同意这会很奇怪,使用构建器会更有意义。感谢您的反馈。 - ashes999
1
“return this” 技术还有另一个缺点——基类的 setter 返回基类型的对象,导致子类的 setter 无法访问,除非进行强制转换。你可以先在子类上设置属性来解决这个问题,但有时候顺序很重要。虽然这在编写良好的代码中不应该成为问题,但显然并非总是如此。 - Flynn1179
好奇你的 PersonBuilder 会是什么样子。它的属性是否与 Person 的镜像相同?有没有办法在不违反 DRY 原则的情况下完成这个任务? - wrschneider
显示剩余3条评论

5

通常在Java中,我们使用构造函数来处理这种情况。

通过在要创建对象的类中使用构造函数,您可以在创建对象时使用它来传递参数,例如:MyData obj1 = new MyData("name",24);

对于此案例,您必须使用与从主方法传递的参数匹配的参数化构造函数。

例如:

MyData(String name, int age){
    this.name=name;
    this.age=age;
    }

完整代码如下:
class MyData{
public String name;
public int age;

 MyData(String name, int age){
    this.name=name;
    this.age=age;
    }
     public static void main(String args[]){
        MyData obj1 = new MyData("name",24);

    }
}

他们在C#中使用它们,但是想象一下我们有10个以上的字段... 现在比较旧式风格和C#的可读性:new MyData(new BigDecimal(24), new..., ...); new MyData{tax=new BigDecimal(24), amount=..., discount=...} - epox

5

由于双花括号通常应避免使用,因此您可以创建一个非常简单和通用的“builder”类,以某种惯用方式设置属性。

注意:我称该类为"Bean"或POJO,以遵循JavaBean标准:什么是JavaBean?。不管怎样,我主要会使用这个类来初始化Javabeans。

Bean.java

public class Bean<T> {
    private T object;
    public Bean(Supplier<T> supplier) { object = supplier.get(); }
    public Bean(T object) { this.object = object; }
    public T set(Consumer<T> setter) {
        setter.accept(object);
        return object;
    }
}

这个Bean类的实例可以从现有对象创建或使用Supplier生成。该对象存储在字段object中。set方法是一个高阶函数,接受另一个函数--Consumer<T>。消费者接受一个参数并返回void。这将在新范围内创建setter副作用。

Bean .set(...)方法返回object,可以直接在赋值中使用。

我喜欢这种方法,因为对象的赋值包含在封闭块中,感觉就像在创建对象之前设置属性,而不是创建对象并对其进行变异。

最终结果是创建新Java对象的一种不错的方式,但与C#对象初始化程序相比仍然有点冗长。


以下是使用该类的示例:

    // '{}' creates another scope so this function's scope is not "polluted"
    // '$' is used as the identifier simply because it's short
    Rectangle rectangle = new Bean<>(Rectangle::new).set($ -> {
        $.setLocation(0, 0);
        $.setBounds(0, 0, 0, 0);
        // set other properties
    });

如果您有嵌套的项目,最好根据名称命名变量。Java不允许重用$,因为它存在于外部作用域中,没有阴影。

    // this time we pass in new Rectangle() instead of a Supplier
    Rectangle rectangle3 = new Bean<>(new Rectangle()).set(rect-> {
        rect.setLocation(-50, -20);
        // setBounds overloads to take in another Rectangle
        rect.setBounds(new Bean<>(Rectangle::new).set(innerRect -> {
            innerRect.setLocation(0, 0);
            innerRect.setSize(new Bean<>(Dimension::new).set(dim -> {
                dim.setSize(640, 480);
            }));
        }));
    });

现在比较普通代码。
    // innerRect and dimension are part of the outer block's scope (bad)
    Rectangle rectangle4 = new Rectangle();
    rectangle4.setLocation(-50, -20);
    Rectangle innerRect = new Rectangle();
    innerRect.setLocation(0, 0);
    Dimension dimension = new Dimension();
    dimension.setSize(640, 480);
    innerRect.setSize(dimension);
    rectangle4.setBounds(innerRect);

另外,您可以编写一个接受 void 并返回所需对象的 lambda,并将其强制转换为 Supplier<DesiredType>,然后立即调用 .get()。这不需要单独的类,但您必须自己创建 bean。

    Rectangle rectangle5 = ((Supplier<Rectangle>)() -> {
        Rectangle rect = new Rectangle();
        rect.setLocation(0, 0);
        return rect;
    }).get();

实用性说明:由于在嵌套元素时无法重复使用$,因此这种方法仍然有点冗长。变量名称开始变得很长,任何语法吸引力都消失了。

还有一点需要注意的是,滥用set()方法可以在闭包内创建对象实例。正确使用时,唯一的副作用应该是在你正在创建的对象上。

还有一个注意事项:这只是为了好玩。不要在生产中使用它。


2
如果您的类有为成员变量赋值的构造函数,可以按照以下方式创建实例:
```html

如果您的类有为成员变量赋值的构造函数,可以按照以下方式创建实例:

```
Person p = new Person("John", "Doe", new Address("1234 St.", "Phoenix"));

如果没有,那么您需要在对象创建后使用setter方法。
Person p = new Person();
p.setFirstName("John");
// and so on

请查看官方Java教程


1

在Java中,您可以使用双括号初始化块来执行类似的操作:

Person p = new Person() {{
    firstName = "John";
    lastName = "Doe";
    address = new Address() {{
        street = "1234 St.";
        city = "Phoenix";
    }};
}};

然而,这只是在匿名内部类中使用初始化块,因此比以正常方式构造对象效率低。


1

不,Java没有对象初始化器。虽然有一些黑科技和变通方法,但基本原则是:使用双大括号初始化,语法上看起来相似但实际上是不同的东西;或者创建构造函数来初始化对象(老式方法);或者像C#内置的那样使用设计模式(比如建造者模式)。


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