Java中的结构体对象

198

创建类似结构体的对象是否完全违反了 Java 的方式?

class SomeData1 {
    public int x;
    public int y;
}

我可以看到一个带有访问器和修改器的类更像Java。

class SomeData2 {
    int getX();
    void setX(int x);

    int getY();
    void setY(int y);

    private int x;
    private int y;
}

第一个示例中的类在符号上很方便。

// a function in a class
public int f(SomeData1 d) {
    return (3 * d.x) / d.y;
}

这不太方便。

// a function in a class
public int f(SomeData2 d) {
    return (3 * d.getX()) / d.getY();
}

9
建议使用公共的不可变字段或者包内可变字段,而不是公共的可变字段。在我看来,任何一个都比公共的可变字段更好。 - Peter Lawrey
请记住,虽然getter和setter看起来很丑陋/冗长,但这是Java的核心。它是一种非简洁的语言。另一方面,你永远不应该自己输入任何内容,因为这就是你的IDE为你做的事情。在动态语言中,你需要输入的内容较少,但你仍然需要输入(尽管IDE可能会帮助你)。 - Dan Rosenstark
具有封装性是面向对象编程的优点之一,然而这也意味着需要付出CPU和存储方面的代价。垃圾回收器(几乎完全)消除了担心何时清除对象引用的必要性。当前的趋势是通过使用类似于C的结构体来实现全循环。这非常适合缓存类型解决方案、进程间通信、更快的内存密集型操作、更低的GC开销,甚至可以从数据集的较低存储开销中受益。如果你知道自己在做什么,就不会问这个问题……所以再想一想! - KRK Owner
@user924272:关于“当前趋势是通过使用类似C的结构体来进行堆外内存操作”,那么在Java中应该如何实现呢???我认为,这正是Java显得有些老旧的领域之一…… - ToolmakerSteve
@ToolmakerSteve - 我看到了一个圆圈。我不是唯一一个看到的人。像Azul这样的公司正在热衷于无暂停垃圾回收。Java已经老了。没错。那些发现弱点并采取行动而不是抱怨的工程师们值得尊重!我给Azul加10分 :-) - KRK Owner
20个回答

303

看起来很多Java开发人员不熟悉Sun Java编码规范,规范说明在Java中,如果一个类本质上是“struct”(也就是没有行为),使用公共实例变量是相当合适的。

人们倾向于认为Getter和Setter是Java的标志性特征,好像它们是Java核心的关键所在。但事实并非如此。如果你遵循Sun Java编码规范,在适当的情况下使用公共实例变量,你会写出比混杂着无用的Getter和Setter更好的代码。

Java Code Conventions 1999年发布至今未改变。

10.1 提供对实例和类变量的访问

没有充分的理由不要将任何实例或类变量设置为public。通常,实例变量不需要显式地设定或获取——通常这会随方法调用的副作用而发生。

一个合适的公共实例变量的例子是当类本质上是数据结构时(也就是说,如果Java支持struct,则使用类的实例变量是合适的)

http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

http://en.wikipedia.org/wiki/Plain_old_data_structure

http://docs.oracle.com/javase/1.3/docs/guide/collections/designfaq.html#28


92
谢谢你的称赞。+1 对于确实有权威来源的回答。其他的回答都是人们像事实一样旋转他们的观点。 - ArtOfWarfare
1
有一个 Java Beans 规范,它是使用 get 和 set 方法访问属性的行业标准方式...请参阅 http://en.wikipedia.org/wiki/JavaBeans 了解概述。 - KRK Owner
4
Java Beans规范与这个回答讨论何时适合使用“public instance variables”有什么关系?如果该规范是一种标准的方式,类似于C#,自动将实例变量转换为属性,那么它可能是相关的。但事实并非如此,对吧?它只是指定需要创建样板式getter和setter的命名,以进行这样的映射。 - ToolmakerSteve
@ToolmakerSteve。这是一个Java问题。此外,该问题暗示了存在一个常见的问题,即存在规范。从老派的角度来看,当有一种标准方法可以调试字段变异时,这将更容易 - 在setter处设置断点。现代调试器可能已经使其过时,但我仍然不喜欢直接“打”对象的类...虽然这对于较小的应用程序来说还好,但对于较大的应用程序和大型组织来说,这确实是一个真正的头疼问题。 - KRK Owner
自Java 16(2021年)以来,已经有了记录类修饰符,这是几个版本之前的事情了!https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Record.html也已经完全发布。 - brunoais

224

请理性思考。如果你有以下内容:

public class ScreenCoord2D{
    public int x;
    public int y;
}

那么将它们包装在getter和setter中没有太大的意义。你永远不会以其他方式存储完整像素的x、y坐标。getter和setter只会拖慢你的速度。

另一方面,使用:

public class BankAccount{
    public int balance;
}

你可能希望在将来的某个时间更改余额计算方式。这应该使用getter和setter。

了解为什么要应用良好的实践是始终可取的,这样您就知道何时可以放松规则。


3
我同意这个答案,并进一步表示,只要这些字段粗体相互独立,即一个字段不依赖于另一个字段,你可以创建一个具有公共字段的类。在许多情况下,这非常有用,例如从函数返回多个值或极坐标 {角度、长度} 等一起使用但本质上彼此独立的情况。 - Spacen Jasset
@SpacenJasset:FYI,我不明白你的例子(多个返回值;极坐标)与使用公共字段还是getter/setter有什么关系。在多个返回值的情况下,这甚至可能是适得其反的,因为可以说调用者只应该获取返回的值,这支持使用公共getter和私有setter(不可变)。对于从(x,y)对象返回极坐标的情况,这也可能是正确的-考虑到累积数学误差,因为将极坐标的各个组件的更改转换回(x,y)。 - ToolmakerSteve
@SpacenJasset:但我同意你的原则。 - ToolmakerSteve
1
你说得有道理,但我觉得像像素这样的事情不是一个好的例子,因为确保像素在窗口内(只是一个例子)是某人可能做的事情,而且,让某人将像素设置为“(-5,-5)”可能不是一个好主意。 :-) - Horsey
@Horsey 在哪个窗口内部(是哪一个?)并不是坐标的责任,为什么可能会选择(-5,-5)不是一个好主意呢?这取决于原点在哪里。也许是中心点? - BlackJack

62

这是一个经常讨论的话题。在对象中创建公共字段的缺点是你无法控制设置给它的值。在有多个程序员使用相同代码的群体项目中,避免副作用非常重要。此外,有时最好返回字段对象的副本或以某种方式进行转换等。您可以在测试中模拟这些方法。如果您创建一个新类,则可能看不到所有可能的操作。这就像防御性编程 - 有一天,获取器和设置器可能会有帮助,并且创建/使用它们的成本并不高,因此有时它们非常有用。

实际上,大多数字段都有简单的getter和setter。可能的解决方案如下:

public property String foo;   
a->Foo = b->Foo;

更新:在Java 7或者未来的版本中,加入对属性支持的可能性极小。不过,其他JVM语言像Groovy,Scala等现在已经支持了这一特性。- Alex Miller


28
太糟糕了,我喜欢C#风格的属性(听起来就像你所说的)。 - Jon Onstott
2
因此,使用重载...private int _x; public void x(int value) { _x = value; } public int x() { return _x; } - Gordon
12
我更喜欢使用“=”符号,因为我认为这可以让代码更加简洁。 - Svish
6
@T-Bull:仅仅因为你可以有两个不同的东西叫做“x”,并不意味着这是一个好主意。在我看来,这是一个糟糕的建议,因为它可能会让读者感到困惑。基本原则:不要让读者反复阅读;明确你要表达的内容--使用不同的名称来表示不同的实体。即使是在前面添加下划线的方式也可以区分不同之处。不要依赖周围的标点符号来区分实体。 - ToolmakerSteve
3
“可能会导致混淆”是保卫编码教条(与编码风格相对)时最常见也是最薄弱的论点。总有人会被最简单的事情所困惑。你总能找到一些人抱怨说,在熬夜编码半周之后,他犯了一个错误,然后责怪误导性的编码风格。我不认为这样算数。那种风格是有效的、显而易见的,可以使命名空间保持清洁。此外,在这里没有不同的实体,只有一个实体和一些样板代码围绕着它。 - T-Bull
显示剩余3条评论

54

为解决可变性问题,您可以将x和y声明为final。例如:

class Data {
  public final int x;
  public final int y;
  public Data( int x, int y){
    this.x = x;
    this.y = y;
  }
}

试图写入这些字段的调用代码将在编译时出现"field x is declared final; cannot be assigned"的错误。

客户端代码可以使用您在帖子中描述的"简写"便利性。

public class DataTest {
    public DataTest() {
        Data data1 = new Data(1, 5);
        Data data2 = new Data(2, 4);
        System.out.println(f(data1));
        System.out.println(f(data2));
    }

    public int f(Data d) {
        return (3 * d.x) / d.y;
    }

    public static void main(String[] args) {
        DataTest dataTest = new DataTest();
    }
}

3
谢谢 - 一个有用而简洁的回答。显示了如何利用字段语法来获得收益,当不需要可变性时。 - ToolmakerSteve
@ToolmakerSteve - 感谢您的反馈 - 非常感谢。 - Brian
我尝试使用带有final字段的final类的final实例作为结构“实例”,但在引用此类实例字段的case表达式中,我得到了“Case expressions must be constant expressions”的错误。如何解决这个问题?这里的惯用概念是什么? - n611x007
1
最终字段仍然是对对象的引用,这些对象不是常量,因为它们将在类第一次使用时初始化。编译器无法知道对象引用的“值”。常量必须在编译时已知。 - Johan Tidén
1
+1 非常重要的答案。在我看来,不可变类的实用性不容小觑。它们的“写一次,永不改变”的语义使得对代码的推理通常更简单,特别是在多线程环境中,它们可以在线程之间任意共享,而无需同步。 - TheOperator

12

不要使用 public 属性

当您确实需要包装类的内部行为时,请勿使用 public 属性。以 java.io.BufferedReader 为例,它具有以下字段:

private boolean skipLF = false; // If the next character is a line feed, skip it

skipLF 在所有读取方法中都会被读取和写入。如果一个在另一个线程中运行的外部类在读取过程中恶意修改了 skipLF 的状态,BufferedReader 将肯定出现故障。

使用 public 字段

以这个 Point 类为例:

class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return this.x;
    }

    public double getY() {
        return this.y;
    }

    public void setX(double x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }
}
这将使得计算两点之间的距离非常繁琐。
Point a = new Point(5.0, 4.0);
Point b = new Point(4.0, 9.0);
double distance = Math.sqrt(Math.pow(b.getX() - a.getX(), 2) + Math.pow(b.getY() - a.getY(), 2));

这个类除了简单的getter和setter之外没有任何行为。当类仅表示数据结构,并且没有以及永远不会有任何行为时,使用公共字段是可以接受的(这里认为仅存在thin getters和setters不是行为)。可以用以下方式更好地编写:

class Point {
    public double x;
    public double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

Point a = new Point(5.0, 4.0);
Point b = new Point(4.0, 9.0);
double distance = Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));

清理!

但请记住:不仅您的类必须没有行为,而且它将来也不应该有任何行为。


这正是此答案所描述的。引用《Java编程语言代码规范:10. 编程实践》的话:

公共实例变量的一个适当用法示例是,如果类本质上是一个数据结构,没有行为。换句话说,如果Java支持struct,则使用struct而不是类是适当的,并且可以将类的实例变量设置为public。

因此,官方文档也接受这种做法。


此外,如果您非常确定上述Point类的成员应该是不可变的,那么您可以添加final关键字来强制执行:

public final double x;
public final double y;

自Java 16(2021年)以来,已经有了记录类修饰符,这是几个版本之前的事情了!https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Record.html也已经完全发布。 - brunoais

8
顺便提一下,你作为示例给出的结构在Java基类库中已经存在,名为java.awt.Point。它有公共字段x和y,可以自行查看
如果你知道自己在做什么,并且团队中的其他人也了解情况,那么使用公共字段是可以的。但是不应该依赖它们,因为这可能会导致开发人员将对象错误地用作堆栈分配的结构体而引起问题(Java对象始终作为引用而不是副本发送到方法中)。

+1 对一个问题的有益提及--结果仍不像 C 结构体一样。但是,你提到的关于Java对象总是按引用传递的问题,通过创建setter而不是具有公共可写字段(这是OP问题的本质--使用哪种表示)并没有得到改善。相反,这是支持两者都不做的论据。这是支持不变性的论点。可以将其作为public final字段完成,就像在Brian的答案中所述,或者通过具有公共getter但没有公共setter来完成。也就是说,无论是使用字段还是访问器都是无关紧要的。 - ToolmakerSteve

8

关于 aku, izb, John Topley 的回复...

注意可变性问题...

省略getter/setter可能看起来是明智的。在某些情况下,这实际上可能是可以接受的。这里所展示的提议模式的真正问题是可变性。

问题在于一旦你传递一个包含非final、公共字段的对象引用。任何其他使用该引用的东西都可以自由地修改这些字段。你不再对该对象的状态有任何控制。(想象一下如果字符串是可变的会发生什么。)

当该对象是另一个对象的重要内部状态时,情况就变得很糟糕了,你刚刚暴露了内部实现。为了防止这种情况,必须返回对象的副本。这样做是有效的,但可能会导致大量单次使用的副本产生巨大的GC压力。

如果你有公共字段,请考虑使类只读。将这些字段添加为构造函数的参数,并将它们标记为final。否则,请确保不要暴露内部状态,并且如果需要为返回值构造新实例,请确保不会被过度调用。

参见:Joshua Bloch的 "Effective Java" - 条款#13:支持不可变性。

提示:同时请记住,现今所有的JVM都会尽可能地优化getMethod方法,从而只产生一条字段读取指令。


12
getter/setter如何解决这个问题?你仍然拥有一个引用,但是没有与操作同步。getter/setter本身并不提供保护。 - he_the_great
1
如果需要的话,getter和setter可以提供同步。您可能期望getter和setter做比它们规定要做的事情还要多。尽管如此,同步问题仍然存在。 - KRK Owner

7
我在几个项目中尝试过这种方法,理论上讲,getter和setter会使代码充斥着语义上无意义的冗余代码,其他语言似乎通过基于约定的数据隐藏或责任分区来解决了这个问题(例如Python)。
正如其他人所指出的,你会遇到两个问题,而且这些问题是无法真正解决的:
1. 几乎Java世界中所有的自动化工具都依赖于getter/setter约定。同样地,JSP标签、Spring配置、Eclipse工具等等也是如此。与你的工具期望看到的内容对抗只会导致长时间的谷歌搜索,试图找到那些非标准的初始化Spring bean的方式。这不值得麻烦。
2. 一旦你有了几百个公共变量的优雅编码应用程序,你可能会发现至少有一种情况它们是不够用的 - 当你需要绝对的不可变性时,或者当你需要在变量被设置时触发某些事件时,或者当你想在变量改变时抛出异常,因为它将对象状态设置为某些不愉快的东西。那么你就陷入了一个难以取舍的境地,要么在直接引用变量的每个地方都充满一些特殊的方法,要么在你应用程序中的1000个变量中有3个需要特殊访问形式。
而且这还是在完全在一个独立私有项目中工作的最好情况下。一旦你将整个项目导出到公开可访问的库中,这些问题就会变得更加棘手。
Java非常冗长,这种做法很诱人。但不要这样做。

1
优秀的讨论关于使用公共字段的问题。这是Java明显的缺陷,每当我不得不从C#切换回Java时,这个问题总是让我感到烦恼(因为C#已经从Java的不便中吸取了教训)。 - ToolmakerSteve

4
如果Java方式是面向对象的方式,那么创建一个具有公共字段的类会违反信息隐藏原则,该原则指出对象应该管理其自己的内部状态。(因此,我不只是对你说术语,信息隐藏的好处是类的内部工作被隐藏在接口后面 - 假设你想改变你的结构体类保存其中一个字段的机制,你可能需要回到任何使用该类的类并进行更改...)
您还无法利用支持JavaBean命名规范的类的支持,如果您决定在使用表达式语言编写的JavaServer页面中使用该类,则会受到影响。
JavaWorld文章Why Getter and Setter Methods are Evil也可能对您有所帮助,在思考何时不要实现访问器和变异器方法时,请参阅该文章。
如果您正在编写一个小型解决方案并希望最小化涉及的代码量,则Java方式可能不是正确的方式 - 我想这始终取决于您和您要解决的问题。

1
虽然你提供了一篇名为“为什么Getter和Setter方法是邪恶的”的文章链接,但你的回答如果指出公共字段和公共getter/setter都不符合Java的方式会更清晰:正如该文章所解释的那样,尽可能不要使用它们。相反,为客户端提供特定于其需要执行的操作的方法,而不是镜像实例的内部表示。然而,这并没有真正回答被问到的问题(在简单的“struct”情况下使用哪个更好),这个问题最好由developer.g、izb和Brian来回答。 - ToolmakerSteve

3

只要作者知道它们是structs(或数据传输器)而不是对象,那种类型的代码就没有问题。许多Java开发人员无法区分良好构造的对象(不仅仅是java.lang.Object的子类,而是特定领域中的真正对象)和菠萝之间的区别。因此,当他们需要对象时却编写了structs,反之亦然。


菠萝让我笑了 :) - Guillaume
然而,这个答案并没有说明这种差异是什么。对于那些不熟练的开发人员进行指责并不能帮助他们知道何时应该创建结构体,何时应该创建类。 - ToolmakerSteve
它并没有说明差异,因为作者意识到了差异(他知道结构体类似实体是什么)。作者在询问在面向对象的语言中是否适合使用结构体,我认为是可以的(取决于问题域)。如果问题是关于什么使一个对象成为真正的领域对象,那么你就有立场来支持你的论点。 - luis.espinal
此外,唯一不应该知道区别的无技能开发人员是仍在学校的人。这非常基础,就像知道链表和哈希表之间的区别一样。如果一个人拥有4年软件学位却不知道真正对象是什么,那么要么这个人不适合从事这个职业,要么他/她应该回到学校并要求退款。我是认真的。 - luis.espinal
但为了满足您的抱怨,我会提供一个答案(与原问题几乎没有任何关系,并且值得拥有自己的主题)。对象具有行为并封装状态。结构体则不然。对象是状态机。结构体仅用于聚合数据。如果人们想要更详细的答案,他们可以自由地创建一个新问题,在那里我们可以尽情阐述。 - luis.espinal

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