197

是否完全违反 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();
}
4

20 回答 20

298

似乎许多 Java 人不熟悉 Sun Java 编码指南,其中说当类本质上是“结构”时,如果 Java 支持“结构”(当没有行为时),使用公共实例变量是非常合适的。

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

Java 代码约定从 1999 年开始,仍然没有改变。

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

于 2011-12-19T18:29:21.810 回答
224

真正使用常识。如果你有类似的东西:

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

然后将它们包装在 getter 和 setter 中就没有什么意义了。您永远不会以任何其他方式存储整个像素中的 x、y 坐标。getter 和 setter 只会减慢你的速度。

另一方面,与:

public class BankAccount{
    public int balance;
}

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

总是最好知道你为什么要应用良好的做法,这样你就知道什么时候可以改变规则。

于 2008-08-31T14:44:57.457 回答
62

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

在实践中,大多数字段都有简单的 getter 和 setter。一个可能的解决方案如下所示:

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

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

于 2008-08-31T09:50:48.210 回答
53

为了解决可变性问题,您可以将 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 已声明为最终;无法分配”的编译时错误。

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

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();
    }
}
于 2008-09-02T01:51:58.233 回答
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 之外,该类没有任何行为。当类只表示一个数据结构,并且没有,永远不会有行为时,使用公共字段是可以接受的(这里考虑细的 getter 和 setter 行为)。可以这样写更好:

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. 编程实践”

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

所以官方文档也接受这种做法。)


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

public final double x;
public final double y;
于 2014-09-25T06:48:38.170 回答
8

顺便说一句,您作为示例提供的结构已经存在于 Java 基类库中,即java.awt.Point. 它有 x 和 y 作为公共字段,自己检查一下

如果您知道自己在做什么,并且团队中的其他人也知道,那么拥有公共字段是可以的。但是您不应该依赖它,因为它们可能会引起令人头疼的问题,就像与开发人员使用对象相关的错误一样,就好像它们是堆栈分配的结构一样(java 对象总是作为引用而不是作为副本发送到方法)。

于 2008-08-31T14:24:06.710 回答
8

回复:aku、izb、约翰·托普利……

注意可变性问题...

省略 getter/setter 似乎是明智的。在某些情况下实际上可能没问题。此处显示的提议模式的真正问题是可变性。

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

当该对象是另一个对象的内部状态的重要部分时,它会变得很糟糕,您刚刚暴露了内部实现。为了防止这种情况,必须返回对象的副本。这可行,但可能会因创建的大量一次性副本而导致巨大的 GC 压力。

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

请参阅:Joshua Bloch 的“ Effective Java ”——第 13 条:支持不变性。

PS:还要记住,如果可能的话,现在所有的 JVM 都会优化掉 getMethod,从而只产生一个字段读取指令。

于 2008-08-31T16:27:29.707 回答
7

我已经在几个项目中尝试过这个,理论是 getter 和 setter 用语义上无意义的杂物弄乱了代码,而其他语言似乎可以很好地处理基于约定的数据隐藏或职责划分(例如 python)。

正如其他人在上面提到的那样,您遇到了两个问题,并且它们并不是真正可修复的:

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

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

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

于 2008-12-01T20:50:48.187 回答
4

如果 Java 方式是 OO 方式,那么是的,创建一个具有公共字段的类打破了围绕信息隐藏的原则,即对象应该管理自己的内部状态。(所以我不只是对你说行话,信息隐藏的一个好处是一个类的内部工作隐藏在一个接口后面——比如你想改变你的结构类保存它的一个字段的机制,您可能需要返回并更改使用该类的任何类...)

您也不能利用对 JavaBean 命名兼容类的支持,如果您决定在使用表达式语言编写的 JavaServer Page 中使用该类,这将造成伤害。

JavaWorld 文章Why Getter and Setter Methods are Evil文章也可能对您感兴趣,以考虑何时不实现访问器和修改器方法。

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

于 2008-08-31T09:33:52.383 回答
3

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

于 2010-04-15T02:59:08.510 回答
3

一个非常古老的问题,但让我再做一个简短的贡献。Java 8 引入了 lambda 表达式和方法引用。Lambda 表达式可以是简单的方法引用,而不是声明“真实”主体。但是您不能将字段“转换”为方法引用。因此

stream.mapToInt(SomeData1::x)

不合法,但是

stream.mapToInt(SomeData2::getX)

是。

于 2014-07-10T15:03:55.420 回答
2

使用公共字段访问的问题与使用 new 而不是工厂方法的问题相同 - 如果您稍后改变主意,所有现有的调用者都会被破坏。因此,从 API 演进的角度来看,硬着头皮使用 getter/setter 通常是个好主意。

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

顺便说一句,根据 e-bartek 的说法,IMO 极不可能在 Java 7 中添加属性支持。

于 2008-09-15T16:21:20.450 回答
2

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

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

于 2008-10-15T04:28:04.103 回答
1

如果您知道它总是一个简单的结构并且您永远不会想将行为附加到它,我看不出有什么害处。

于 2008-08-31T09:20:18.080 回答
1

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

于 2008-08-31T10:13:01.230 回答
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 中......你知道吗???:)

于 2013-02-19T05:50:04.413 回答
0

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

于 2008-10-15T04:35:09.423 回答
0

有时我会使用这样的类,当我需要从一个方法返回多个值时。当然,这样的对象是短暂的,可见性非常有限,所以应该没问题。

于 2008-11-23T18:43:14.950 回答
0

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

于 2010-04-25T04:13:52.203 回答
0

面向方面的编程让您可以捕获分配或获取并将拦截逻辑附加到它们,我认为这是解决问题的正确方法。(它们是否应该是公共的、受保护的或受包保护的问题是正交的。)

因此,您从具有正确访问限定符的未拦截字段开始。随着您的程序需求的增长,您可能会附加逻辑来验证,制作返回对象的副本等。

getter/setter 哲学在大量不需要它们的简单案例上施加了成本。

方面风格是否更清洁在某种程度上是定性的。我会发现只查看类中的变量并单独查看逻辑很容易。事实上,面向 Apect 的编程存在的理由是,许多关注点是横切的,并且在类主体中划分它们本身并不理想(日志就是一个例子——如果你想记录所有的东西,Java 想要你编写一大堆 getter 并使它们保持同步,但 AspectJ 允许您使用单线)。

IDE 的问题是一个红鲱鱼。与其说是打字,不如说是 get/sets 引起的阅读和视觉污染。

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

我希望对 AspectJ 的认识可以防止人们过早地使用动态语言。

于 2011-12-19T19:02:53.247 回答