关于oop:使用Java结构类似对象

关于oop:使用Java结构类似对象

Struct like objects in Java

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

1
2
3
4
class SomeData1 {
    public int x;
    public int y;
}

我可以看到一个带有访问器和增变器的类,它们更像Java。

1
2
3
4
5
6
7
8
9
10
class SomeData2 {
    int getX();
    void setX(int x);

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

    private int x;
    private int y;
}

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

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

这不是那么方便。

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

似乎许多Java人都不熟悉Sun Java Coding Guidelines
这说当类是
如果Java支持"结构"(没有任何行为),则本质上是"结构"。

人们倾向于认为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
2
3
4
public class ScreenCoord2D{
    public int x;
    public int y;
}

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

另一方面,使用:

1
2
3
public class BankAccount{
    public int balance;
}

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

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


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

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

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

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


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

1
2
3
4
5
6
7
8
class Data {
  public final int x;
  public final int y;
  public Data( int x, int y){
    this.x = x;
    this.y = y;
  }
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
    }
}

不要使用public字段

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

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

skipLF以所有读取方法读取和写入。如果在单独线程中运行的外部类在读取中间恶意修改了skipLF的状态怎么办? BufferedReader肯定会成为麻烦。

请使用public字段

以这个Point类为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
    }
}

这将使编写两点之间的距离非常困难。

1
2
3
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不视为行为)。这样可以写得更好:

1
2
3
4
5
6
7
8
9
10
11
12
13
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.编程实践":

One example of appropriate public instance variables is the case where the class is essentially a data structure, with no behavior. In other words, if you would have used a struct instead of a class (if Java supported struct), then it's appropriate to make the class's instance variables public.

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


另外,如果您特别确定上述Point类的成员应该是不可变的,则可以添加final关键字来实施它:

1
2
public final double x;
public final double y;

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

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


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

当心可变性问题...

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

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

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

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

请参阅:Joshua Bloch撰写的" Effective Java"(有效Java)-项目#13:有利不变性。

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


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

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

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

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

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


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

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

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

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


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


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

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


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

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

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


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

1
stream.mapToInt(SomeData1::x)

不合法,但是

1
stream.mapToInt(SomeData2::getX)

是。


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


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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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中即可。 :)


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


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


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

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

getter / setter方法的原理是在许多不需要的简单情况下增加成本。

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

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

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

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


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


推荐阅读