像Java中的对象一样结构化

时间:2008-08-31 09:17:02

标签: java oop struct

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

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

我可以看到一个带有访问器和mutator的类更像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();
}

20 个答案:

答案 0 :(得分:281)

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

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

1999年的Java代码约定,但仍未改变。

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

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

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

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

答案 1 :(得分:223)

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

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

然后将它们包裹在吸气剂和制定者中是没有意义的。你永远不会以任何其他方式在整个像素中存储x,y坐标。吸气剂和制定者只会减慢你的速度。

另一方面,用:

public class BankAccount{
    public int balance;
}

您可能希望更改将来某个时间点的平衡计算方式。这应该真的使用getter和setter。

最好知道为什么你正在应用良好的练习,以便你知道什么时候可以弯曲规则。

答案 2 :(得分:63)

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

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

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

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

答案 3 :(得分: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();
    }
}

答案 4 :(得分: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和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));

清洁!

但请记住:不仅您的班级必须缺少行为,而且还应该没有理由在将来也有行为。


(这正是this answer所描述的内容。引用"Code Conventions for the Java Programming Language: 10. Programming Practices"

  

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

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


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

public final double x;
public final double y;

答案 5 :(得分:8)

顺便说一句,您作为示例提供的结构已经作为java.awt.Point存在于Java基类库中。它将x和y作为公共字段check it out for yourself

如果您知道自己在做什么,而团队中的其他人知道这一点,那么可以使用公共字段。但是你不应该依赖它,因为它们会引起头痛,就像开发人员使用对象的bug一样,就好像它们是堆栈分配的结构一样(java对象总是作为引用而不是副本发送给方法)。

答案 6 :(得分:8)

Re:aku,izb,John Topley ......

注意可变性问题......

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

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

当该对象是另一个对象的内部状态的重要部分时,它会变坏,您刚刚暴露了内部实现。为防止这种情况,必须返回对象的副本。这样做有效,但可能会造成大量的一次性副本创建的GC压力。

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

见Joshua Bloch的“Effective Java” - 项目#13:偏爱不变性。

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

答案 7 :(得分:7)

我已经在一些项目中尝试了这个,理论上getter和setter在语义上毫无意义地混淆了代码,并且其他语言似乎可以很好地使用基于约定的数据隐藏或职责划分(例如蟒蛇)。

正如其他人已经提到的那样,您遇到了两个问题,而且它们无法解决问题:

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

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

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

答案 8 :(得分:4)

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

您也无法利用对JavaBean命名兼容类的支持,如果您决定在使用表达式语言编写的JavaServer Page中使用该类,这将会受到影响。

JavaWorld文章Why Getter and Setter Methods are Evil文章也可能在您考虑何时不实现访问器和增变器方法时感兴趣。

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

答案 9 :(得分:3)

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

答案 10 :(得分:2)

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

顺便说一句,即使我将这个对象写成私有内部类,我仍然会提供一个构造函数来简化代码来初始化对象。

需要有3行代码才能获得可用的对象

答案 11 :(得分:2)

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

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

顺便说一下,在e-bartek的断言中,IMO极不可能在Java 7中添加属性支持。

答案 12 :(得分:2)

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

stream.mapToInt(SomeData1::x)

不合法,但

stream.mapToInt(SomeData2::getX)

答案 13 :(得分:1)

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

答案 14 :(得分: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中......您知道吗? :)

答案 15 :(得分:1)

如果您知道它总是一个简单的结构并且您永远不会想要将行为附加到它上面,我不会看到这种伤害。

答案 16 :(得分:0)

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

答案 17 :(得分:0)

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

答案 18 :(得分:0)

与大多数事情一样,这是一般规则,然后是具体情况。 如果您正在执行已关闭的捕获应用程序,以便了解将如何使用给定对象,那么您可以更自由地支持可见性和/或效率。 如果您正在开发一个将由您无法控制的其他人公开使用的课程,那么请倾向于getter / setter模型。 与所有事情一样,只需使用常识。 通常可以与公众进行初步轮次,然后将其更改为getter / setter。

答案 19 :(得分:0)

面向方面的编程允许您捕获分配或提取并将拦截逻辑附加到它们,我建议这是解决问题的正确方法。 (它们是公开的还是受保护的或受包裹保护的问题是正交的。)

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

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

方面风格是否更清晰在某种程度上是定性的。我会发现很容易看到类中的变量并分别查看逻辑。事实上,面向Apect的编程存在的理由是许多问题是交叉的,并且在类体中划分它们本身并不理想(日志记录就是一个例子 - 如果你想记录所有Java,你想要你写一大堆getter并让它们保持同步但是AspectJ允许你单行。)

IDE的问题是一个红鲱鱼。打字并不是因为读取和设置引起的阅读和视觉污染。

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

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