在Java中像对象一样结构


195

是否完全违反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
代替公共可变字段,请考虑公共不可变字段或程序包本地可变字段。恕我直言,这会更好。
彼得·劳瑞

请记住,尽管getter和setter难看/冗长,但这是Java的核心。这是一种不简洁的语言。但是,另一方面,您绝对不要键入任何内容,因为这正是您的IDE所为。在动态语言中,您必须键入的内容较少,但必须键入(通常,尽管IDE可能会有所帮助)。
丹·罗森斯塔克

具有讽刺意味的是,尽管OO在封装方面具有优势,但要在CPU和存储方面付出一定的代价。垃圾收集器(几乎全部)消除了担心何时应该清除对象引用的麻烦。当前的趋势是通过在堆外使用类似C的结构来全面发展。这非常适合缓存型解决方案,进程间通信,更快的内存密集型操作,更低的GC o / h,甚至可以从数据集的更低存储o / h中受益。如果您知道自己在做什么,那么您就不会问这个问题...因此,请再考虑一下!
user924272 2014年

@ user924272:有关“当前的趋势是通过采用类似C的结构堆进行全面发展”。您将在Java中做什么?恕我直言,这是Java展示其时代的地方...
ToolmakerSteve14年

@ToolmakerSteve-我看到一个圆圈。我不是唯一的一个。像Azul这样的公司非常喜欢无停顿垃圾收集。Java很老了。真正。那些发现弱点并对此有所作为而不是抱怨的工程师?他们值得尊重!+10 to Azul from me :-)
user924272

Answers:


62

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

实际上,大多数字段都有简单的获取器和设置器。可能的解决方案如下所示:

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

更新:极不可能在Java 7中甚至可能永远不会添加属性支持。Groovy,Scala等其他JVM语言现在确实支持此功能。-亚历克斯·米勒


28
太糟糕了,我喜欢C#样式的属性(听起来像您在说什么)
Jon Onstott 2011年

2
因此,请使用重载... private int _x; public void x(int value){_x = value; } public int x(){return _x; }
Gordon

12
我更喜欢能够使用=,我认为这会使代码更整洁。
Svish 2012年

6
@ T-Bull:仅仅因为您可以有两个x是两个不同的东西,但这并不是一个好主意。恕我直言,这是一个糟糕的建议,因为它可能会引起人类读者的困惑。基本原则:不要让读者做双重决定;使您的意思清晰可见-为不同的实体使用不同的名称。即使区别只是在下划线之前。不要依靠周围的标点来区分实体。
制造商史蒂夫(Steve)2014年

2
@ToolmakerSteve:在捍卫编码教条(与编码风格相反)时,“可能导致混乱”是最常见但仍然最弱的论点。总会有人对最简单的事情感到困惑。您总会发现有人抱怨他在保持清醒状态并进行了半个星期左右的编码后犯了一个错误,然后将其归咎于误导性的编码风格。我不认为这一点。这种样式是有效的,显而易见的,并且可以使命名空间保持整洁。另外,这里没有不同的实体,周围有/ one /实体和一些样板代码。
T-Bull

290

似乎许多Java人士都不熟悉Sun Java Coding Guidelines,该声明说,如果Java支持“ struct”(没有行为),则在类本质上是“ Struct”时,使用公共实例变量是非常合适的。

人们倾向于认为getter和setter是Java的方式,就好像它们是Java的核心一样。事实并非如此。如果您遵循Sun Java Coding Guidelines,在适当的情况下使用公共实例变量,则实际上编写的代码要好于不必要的getter和setter使其混乱。

从1999年开始的Java代码约定仍然保持不变。

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

没有充分的理由就不要公开任何实例或类变量。通常,不需要显式设置或获取实例变量,这通常是方法调用的副作用。

适当的公共实例变量的一个例子是,该类实质上是一个数据结构,没有任何行为。换句话说,如果您将使用结构而不是类(如果Java支持的结构),那么将类的实例变量设为public是适当的

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


88
+1实际拥有权威来源。其他所有答案都是人们像事实一样旋转自己的观点。
ArtOfWarfare 2012年

1
有一个Java Beans规范,它是使用get和set方法访问属性的行业标准方法。有关概述,请参见en.wikipedia.org/wiki/JavaBeans
user924272 2014年

4
@ user924272:Java Beans规范与此答案有什么关联,该规范讨论了何时适合使用“公共实例变量”?如果规范是将实例变量自动转换为属性(例如C#)的标准方法,则可能是相关的。但是不是吗?它仅指定了需要创建的样板吸气剂和设置器的名称,以进行这种映射。
制造商史蒂夫(Steve)2014年

@ToolmakerSteve。这是一个Java问题。同样,问题是存在规范的常见问题。从过去的角度来看,如果有一种标准的方法可以调试字段突变,那就很容易了-在设置器上设置一个断点。现代调试器可能已经淘汰了这种方法,但是我不得不对直接“冲压”对象的类皱眉……尽管对于较小的应用程序来说这是可以的,但对于较大的应用程序和大型组织而言,这确实是令人头疼的问题
user924272

223

确实使用常识。如果您有类似的东西:

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

然后,将它们包装在吸气剂和吸气剂中毫无意义。您将永远不会以其他任何方式在整个像素中存储x,y坐标。Getter和Setter只会让您放慢速度。

另一方面,具有:

public class BankAccount{
    public int balance;
}

您可能需要更改将来某个时候的余额计算方式。这实际上应该使用getter和setter。

总是最好知道为什么要应用良好实践,以便知道何时可以改变规则。


3
我会回答这个问题,并进一步说,您可以使用公共字段创建一个类,只要这些字段的粗体彼此独立即可。即一个领域不依赖于另一个领域。在许多情况下,对于一个函数的多个返回值,或者对于极性共正值,这可能非常有用。{角度,长度}在一起但彼此之间不存在任何内在联系。
Spacen Jasset 2012年

@SpacenJasset:仅供参考,我看不出您的示例(多个返回值;极坐标)如何影响是否使用公共字段与getter / setter。在有多个返回值的情况下,它甚至可能适得其反,因为可以说调用方只能获取返回的值,这主张使用公共获取者和私有设定者(不可变)。对于从(x,y)对象返回极坐标的情况也可能是正确的-考虑累积数学误差,因为极坐标的各个分量的更改都转换回(x,y)。
制造商史蒂夫(Steve)2014年

@SpacenJasset:但是我同意你的原则。
制造商史蒂夫(Steve)2014年

1
您有一个有效的点,但是我觉得像素是一个不好的例子,因为确保像素在窗口内(只是一个例子)某人可能要做的事情,此外,让某人将像素设置(-5, -5)为一个好主意。:-)
霍西

50

为了解决可变性问题,您可以将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;
  }
}

尝试写入这些字段的调用代码将获得编译时错误“字段x被声明为final;无法分配”。

然后,客户端代码可以具有您在帖子中描述的“捷径”便利

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-感谢您的反馈-非常感谢。
布赖恩

我尝试将带有最终字段的最终类的最终实例用作结构“实例”,但是在case引用此类实例字段的表达式中,我得到了Case expressions must be constant expressions。这是怎么回事?这里的惯用概念是什么?
n611x007 2014年

1
final字段仍然是对对象的引用,它们不是常量,因为它们将在首次使用该类时被初始化。编译器无法知道对象引用的“值”。编译时必须知道常量。
JohanTidén2014年

1
+1非常重要的答案。在我看来,不可变类的有用性不能低估。它们的“一劳永逸”语义使代码推理通常更简单,尤其是在多线程环境中,在多线程环境中可以在线程之间任意共享它们而无需同步。
TheOperator'Mar 3''16

11

不要使用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之外,该类没有其他行为。当类仅表示数据结构而没有并且永远不会有行为时,可以使用公共字段(在这里,瘦的getter和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.编程实践”

适当的公共实例变量的一个例子是,该类实质上是一个数据结构,没有任何行为。换句话说,如果您使用a struct而不是类(如果支持Java struct),则应该公开该类的实例变量。

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


另外,如果您进一步确定上述Point类的成员应该是不可变的,则可以添加final关键字来强制执行:

public final double x;
public final double y;

8

顺便说一下,您作为示例给出的结构在Java基类库中已经存在java.awt.Point。它具有x和y作为公共字段,自行检查

如果您知道自己在做什么,而团队中的其他人都知道,那么可以使用公共场所。但是您不应该依赖它,因为它们会引起头痛,就像与使用对象的开发人员相关的错误一样,就像它们是堆栈分配的结构一样(Java对象始终作为引用而不是副本发送给方法)。


+1很好地提到了问题-结果仍然不像C结构那样。但是,您提出的有关Java对象始终是通过引用的问题,并不能通过创建一个setter而不是拥有一个公共可写字段来解决(这是OP问题的本质-使用哪种表示形式)。更确切地说,这是赞成“精打细算”的观点。这是IMMUTABILITY的一个参数。既可以像public finalBrian的回答那样作为一个领域来完成,也可以通过一个公共获取者来实现,但是没有公共制定者。也就是说,使用字段还是访问者是无关紧要的。
制造商史蒂夫(Steve)2014年

8

回复:阿库,伊兹布,约翰·托普利...

当心可变性问题...

忽略getter / setter似乎是明智的。在某些情况下,实际上可能没问题。这里显示的建议模式的真正问题是可变性。

问题是,一旦您将包含非最终公共字段的对象引用传递出去。带有该参考的其他任何内容都可以自由修改这些字段。您不再可以控制该对象的状态。(想想如果字符串可变,会发生什么。)

当该对象是另一个对象的内部状态的重要组成部分时,情况就变得很糟,您刚刚公开了内部实现。为避免这种情况,必须返回该对象的副本。这可行,但可能会产生大量的一次性使用副本,从而给GC造成巨大压力。

如果您有公共字段,请考虑将该类设置为只读。将字段作为参数添加到构造函数中,并将字段标记为final。否则,请确保您没有暴露内部状态,并且如果您需要为返回值构造新实例,请确保不会过度调用它。

请参阅:Joshua Bloch撰写的“ Effective Java ”(有效Java) -第13项:偏好不变。

PS:还请记住,如今,所有JVM都将尽可能优化getMethod,从而仅产生一条字段读取指令。


12
吸气剂/吸气剂如何解决此问题。您仍然有参考,您与操作没有任何同步。获取者/设置者本身并不能提供保护。
he_the_great

1
如果需要,getter和setter可以提供同步。您还期望getter和setter的工作比指定的要多得多。无论如何,同步问题仍然存在。
user924272 2014年

7

我已经在一些项目中尝试过这种方法,其理论是,getter和setter会在语义上毫无意义的混乱中使代码混乱,并且其他语言似乎对基于约定的数据隐藏或职责划分(例如python)也很好。

就像其他人在上面指出的那样,您遇到了2个问题,它们并不是真正可以解决的:

  • Java世界中几乎所有自动化工具都依赖于getter / setter约定。同上,其他人也提到过,jsp标记,spring配置,eclipse工具等。。。与您的工具期望看到的东西作斗争是长时间讨论Google的秘诀,试图寻找这种非标准的启动方式春豆。真的不值得麻烦。
  • 一旦您的应用程序具有数百个公共变量的精美编码,您可能至少会发现它们不足的一种情况-您绝对需要不变性,或者需要在设置变量时触发一些事件,或者您想抛出变量更改的异常,因为它将对象状态设置为令人不愉快的状态。然后,您会陷入无法避免的选择之间,即在直接引用变量的任何地方都使用一些特殊的方法使代码混乱,在应用程序的1000个变量中有3种具有特殊的访问形式。

最好的情况是完全在一个独立的私人项目中工作。一旦将整个内容导出到可公开访问的库中,这些问题就会变得更大。

Java非常冗长,这是一件很诱人的事情。不要这样


很好地讨论了走在公共领域道路上的问题。Java的明显缺点,当我不得不从C#切换回Java时(这一点是从Java的不便中学到的),这使我很烦。
ToolmakerSteve

4

如果Java方法是OO方法,那么可以,用公共字段创建一个类会破坏围绕信息隐藏的原则,即信息隐藏,即对象应该管理自己的内部状态。(因此,我不只是对您大喊大叫,所以信息隐藏的好处在于,类的内部工作隐藏在接口的后面-假设您想更改struct类保存其字段之一的机制,您可能需要返回并更改使用该类的任何类...)

您也不能利用对JavaBean命名兼容类的支持,如果您决定在使用表达式语言编写的JavaServer Page中使用该类,那么这会很麻烦。

您可能还会对JavaWorld的文章“ 为什么Getter和Setter方法很邪恶”的文章感兴趣,以考虑何时不实现访问器和mutator方法。

如果您正在编写一个小型解决方案,并且希望尽量减少所涉及的代码量,那么Java方法可能不是正确的方法-我想它总是取决于您以及您要解决的问题。


1
+1链接到文章“为什么Getter和Setter方法很邪恶”。但是,如果您指出公共字段和公共getter / setters均不是Java方式,您的答案将会更加清楚:正如该文章中所解释的那样-如果可能,请勿执行任何操作。而是为客户提供特定于他们需要做什么的方法,而不是镜像实例的内部表示。但是,这并不能真正解决所问的问题(对于简单的“ struct”案例,可以使用该问题),developer.g,izb和Brian可以更好地回答。
制造商

3

只要作者知道它们是结构(或数据穿梭)而不是对象,该类型的代码就没有问题。许多Java开发人员无法分辨格式正确的对象(不仅仅是java.lang.Object的子类,而是特定领域中的真实对象)和菠萝之间的区别。因此,他们最终在需要对象时编写结构,反之亦然。


菠萝让我发笑:)
纪尧姆

但是,此答案未说明该区别是什么。强加于不熟练的开发人员并不能帮助那些开发人员知道何时创建结构以及何时创建类。
ToolmakerSteve

它没有对差异做任何说明,因为他们的作者意识到了差异(他知道结构类实体是什么)。作者问使用OO语言是否适合使用结构,我说“是”(取决于问题域。)如果问题是关于什么使对象成为真正的域对象,那么您将有自己的立场。论据。
luis.espinal 2014年

此外,唯一不知道差异的不熟练开发人员将是仍在学校的那种。这是极其基础的,就像知道链表和哈希表之间的区别一样。如果某人获得了4年软件学位的学位而又不知道真正的目标是什么,那么该人就不会为此事业而工作,或者应该回到学校要求退款。我是认真的。
luis.espinal 2014年

但是为了满足您的抱怨,我将提供一个答案(该答案与原始问题没有任何关系,并且应该有自己的主题)。对象具有行为并封装状态。结构没有。对象是状态机。结构仅用于汇总数据。如果人们想要更详尽的答案,他们可以自由地提出一个新的问题,我们可以根据自己的内心对其进行详细阐述。
luis.espinal 2014年

2

使用公共领域访问的问题与使用new而不是使用工厂方法的问题相同-如果以后改变主意,则所有现有的呼叫者都将断开。因此,从API演变的角度来看,通常最好是硬着头皮使用getters / setter方法。

我走另一条路的地方是,当您强烈控制对类的访问时,例如,在用作内部数据结构的内部静态类中。在这种情况下,使用字段访问可能会更加清晰。

顺便说一下,按照e-bartek的主张,IMO在Java 7中添加属性支持的可能性很小。


1
的确如此,但是如果您使用Java,则不必担心,因为您可以一键重构整个代码库(Eclipse,Netbeans,可能还有VIM和Emacs)。如果您选择了它的动态朋友之一,例如Groovy,那么即使是简单的封装也可能需要数小时或数天的时间来修复。幸运的是,您将拥有可以告诉您的测试用例……您还需要修复多少代码。
丹·罗森斯塔克

2
您当然假设所有用户都在您的代码库中,但是当然,通常这不是事实。通常,出于各种非技术原因,甚至不可能更改您自己的代码库中的代码。
亚历克斯·米勒

2

在构建私有内部类以简化代码时,我经常使用这种模式,但是我不建议在公共API中公开此类对象。通常,使公共API中的对象不可变的频率越高越好,并且不可能以不可变的方式构造“类似于结构的”对象。

顺便说一句,即使我将这个对象编写为私有内部类,我仍然会提供一个构造函数来简化初始化对象的代码。只需三行代码就能获得一个可用的对象,这简直是一团糟。


2

这是一个非常老的问题,但让我再作简短说明。Java 8引入了lambda表达式和方法引用。Lambda表达式可以是简单的方法引用,而不能声明“ true”主体。但是您不能将字段“转换”为方法引用。从而

stream.mapToInt(SomeData1::x)

不合法,但是

stream.mapToInt(SomeData2::getX)

是。


在这种情况下,你可以使用data -> data.x,这仍然是合理的
詹姆斯·克劳斯

1

如果您知道它始终是一个简单的结构,并且您永远也不想对其附加行为,那么我看不出有什么害处。


1

这是关于面向对象设计的问题,而不是Java语言。通常好的做法是将数据类型隐藏在类中,并仅公开属于类API的方法。如果公开内部数据类型,则以后将永远无法更改它们。如果隐藏它们,则对用户的唯一义务是方法的返回值和参数类型。


1

在这里,我创建了一个程序来输入5个不同人的姓名和年龄,并执行选择排序(明智的选择)。我使用了充当结构的类(如C编程语言)和主类来执行完整的操作。以下是我提供的代码...

import java.io.*;

class NameList {
    String name;
    int age;
}

class StructNameAge {
    public static void main(String [] args) throws IOException {

        NameList nl[]=new NameList[5]; // Create new radix of the structure NameList into 'nl' object
        NameList temp=new NameList(); // Create a temporary object of the structure

        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));

        /* Enter data into each radix of 'nl' object */

        for(int i=0; i<5; i++) {
            nl[i]=new NameList(); // Assign the structure into each radix

            System.out.print("Name: ");
            nl[i].name=br.readLine();

            System.out.print("Age: ");
            nl[i].age=Integer.parseInt(br.readLine());

            System.out.println();
        }

        /* Perform the sort (Selection Sort Method) */

        for(int i=0; i<4; i++) {
            for(int j=i+1; j<5; j++) {
                if(nl[i].age>nl[j].age) {
                    temp=nl[i];
                    nl[i]=nl[j];
                    nl[j]=temp;
                }
            }
        }

        /* Print each radix stored in 'nl' object */

        for(int i=0; i<5; i++)
            System.out.println(nl[i].name+" ("+nl[i].age+")");
    }
}

上面的代码没有错误并且经过了测试。。。只需将其复制并粘贴到您的IDE中即可。:)


0

您可以用Java创建没有公共字段的简单类,但没有方法,但是它仍然是一个类,并且在语法和内存分配方面仍然像类一样进行处理。无法用Java真正复制结构。


0

当我需要从一个方法返回多个值时,有时会使用此类。当然,这种物体寿命短,可见性非常有限,因此应该可以。


0

与大多数事情一样,有通用规则,然后有特定情况。如果您正在做一个封闭的,捕获的应用程序,以便知道如何使用给定的对象,那么您可以行使更大的自由度来提高可见性和/或效率。如果您正在开发一个将由您无法控制的其他人公开使用的类,则倾向于使用getter / setter模型。与所有事物一样,请使用常识。通常可以与公众进行一轮初赛,然后稍后再将其更改为吸气剂。


0

面向方面的编程使您可以捕获分配或访存并将侦听逻辑附加到它们上,我认为这是解决问题的正确方法。(它们是公共的还是受保护的或受包装保护的问题是正交的。)

因此,您将从具有正确访问限定符的不受拦截的字段开始。随着程序需求的增长,您可能会附加逻辑以进行验证,复制要返回的对象等。

getter / setter的原理将成本强加于许多不需要的简单情况下。

外观样式是否更简洁在质上。我会发现仅查看类中的变量并分别查看逻辑很容易。实际上,面向方面的编程的理由是,许多问题都是相互交叉的,并且在类主体中将它们分隔开并不理想(记录为例,如果您想记录所有内容,Java希望您这样做)。编写大量的吸气剂并使它们保持同步,但是AspectJ允许您使用单行代码。

IDE的问题是一团糟。与其说是打字,不如说是获取/设置引起的阅读和视觉污染。

注释看起来乍一看类似于面向方面的编程,但是它们要求您通过附加注释来详尽枚举切入点,而不是AspectJ中简洁的类似于通配符的切入点规范。

我希望对AspectJ的了解可以防止人们过早适应动态语言。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.