原创 | 正确区分属性和字段
很多开发人员搞不清属性和字段的区别,本文试图对其作出澄清。
在进行Java软件开发的时候,很多人都没有搞清Java对象中属性(Property)和字段(Field)的区别,以为属性就是字段。本文试图对这两个概念作一个澄清。
范例
首先上一个例子:平面直角坐标系中的矩形。
一个矩形的形状可以用宽(width)和高(height)来表示,而它的位置可以用左下角坐标lowerLeftCoordinate(例如(10, 5))来表示。平面直角坐标系中任何一个点(Point)可以用这个点的坐标(x, y)来表示。
点Point的定义是这样的:
而矩形Rectangle的定义是这样的:
为了方便,矩形宽度、高度和点坐标都用整数int而不是浮点数double来表示。
属性和字段的定义
在很多情况下,在一个类中,字段和属性往往一一对应,例如Point类中,我们有两个字段x和y,也有两个属性x和y与之一一对应:
下面一行:
在Point类中定义了一个字段(Field),名为x。
根据JavaBean规范,下面的代码:
定义了一个可读(因为有getX()方法)、可写(因为有setX()方法)的属性(Property)x。属性的名字来自getX()或setX()。根据JavaBean规范,属性的名字是方法名getX/setX去掉get/set前缀之后,剩下的部分首字母改为小写来定义。因此,getX去掉get之后剩下X,再将它的首字母(在这里是唯一的字母)改为小写就是x。这就是属性名。同样地,在Rectangle类中,getWidth方法的存在意味着Rectangle类中存在一个名为width的属性。
属性和字段之间没有必然的一一对应关系
请牢记:
具体表现在:
属性和字段的名称可以是不同的
例如在Rectangle类中,字段_width对应的属性名是width:
有的字段没有对应的属性,有的属性没有对应的字段
在Rectangle类中,我们有一个area属性(由getArea()方法定义):
却没有一个相应的area字段存在。这个属性是通过计算(width * height)得来的。
在Rectangle类中,只有左下角的坐标点,字段和属性一一对应:
而右下角、左上角、右上角的坐标点,只有相应的属性(loweRightCoordinate、upperLeftCoordinate和upperRightCoordinate),没有相应的字段。它们都是计算属性:
有的属性是多个字段综合表现的结果
例如判断矩形是不是正方形的属性(通过isSquare()方法定义),是比较_width和_height两个字段的结果:
面积属性也是这样:
属性可以是只读、只写或读写的,而字段都是可读写的
如果一个类中同时存在getAbc()(或isAbc(),如果属性类型是boolean类型)和setAbc()方法,那么属性abc就是可读可写的。如果没有相应的setAbc()方法,该属性就是只读的。如果没有相应的getAbc()(或isAbc(),如果属性类型是boolean类型)方法,该属性就是只写的。
下面的Rectangle类代码中,width和height属性都是只读的,它们通过Rectangle类的构造函数设置,然后就永远不可以改变了,因为类中没有定义setWidth()和setHeight()方法:
属性是类外部接口的一部分,字段是类内部实现的一部分
其实,上面那些区别都是浮云,真正重要的是要记住:
因此:
无论是在设计一个系统还是一个类,区分其的外部接口和内部实现都是非常有意义的。在系统的层级,外部接口主要包含接口和抽象基类,而内部实现包含上述接口的实现类和和基类的具体子类。在类的层级,外部接口包含属性和公有(public)方法签名,而内部实现包括字段、私有方法和公有方法的方法体。接口和实现的区分比起字段和方法的区分具有更重大的意义。
字段代表内部实现的静态部分,属性代表外部接口的静态部分。方法属于动态部分。
重要推论
在软件开发领域,有一条基本原则:
因此我们可以得到这样的推论:
先定义属性而不是字段
例如我在设计Rectangle类的时候,是根据外界对矩形的期待设计出它的长度、宽度、四个角的坐标、面积、是否正方形等属性,为了支撑这几个属性,我定义长度、宽度和左下角坐标三个字段;而不是先定义那几个字段,再通过IDE自动创建对应的属性(getter和setter)。
不要为每个字段默认生成属性
我发现大多数程序员都是不假思索地先定义字段,然后利用IDE为所有的字段自动生成对应的读写属性。这是一种非常有害的习惯,会生成一个臃肿的、充满无用代码的类。直接将所有的字段设置为public的,或者封装为public属性,都违反了封装性原则,将大量的内部实现细节暴露给外界,加重了你的代码用户的认知负担,同时限制了你未来重构代码的自由度。
在很多时候,设计一个不可变类(或部分属性不可变)是非常有益的。也就是说,只有getter,没有setter。字段初始值通过构造函数参数传入。
尽量通过方法而不是setter来修改字段值
本质上,getter和setter不是真正的方法,而是属性访问器。Java不像C#那样拥有专门的属性访问器语法,只得用getter和setter方法来代替。在面向对象的设计中,方法和字段是相互作用的:字段可以影响方法调用的结果,而方法可以修改字段的取值。
在Rectangle类中,我是通过三个move方法来修改lowerLeftCoordinate字段的值的:
而不是通过属性修改器来重新设置左下角坐标:
这样做的原因是:
隐藏内部实现。不必将“通过修改左下角坐标来实现矩形的移动”这样的内部实现细节呈现给代码的用户。
面向意图编程。因为用户的意图就是将矩形移动到一个新的位置,而不是重新设置它的左下角坐标。
JPA/Hibernate中区分属性和字段持久化
在JPA和Hibernate这样的ORM软件中,我们可以根据属性/字段对实体进行查询。这时候必须区分是按属性还是按字段进行持久化。
JPA/Hibernate中,如果@Id注解如果定义在属性上,就是根据属性进行持久化的;如果是定义在字段上,就是根据字段来进行持久化的。
有什么不同?
我们先看字段持久化(@Id定义在id字段上):
在进行jpql或hql查询的时候,应该根据字段名_width而不是属性名width:
或者
下面是属性持久化(@Id定义在getId()方法上):
在进行jpql或hql查询的时候,应该根据属性名width而不是字段名_width:
或者
@Id字段定义在字段还是属性上决定了整个类的所有属性/字段的持久化策略。
深入思考
现实世界中的事物,其实也有这种二分法:外部可见的属性和行为和内部隐藏的结构和机制。正是不可见的内部的结构和机制决定了它的可见的外部属性和行为,即所谓“动于内,形诸外”。面向对象编程模仿的就是现实世界的对象。对于一个类而言,属性和public
方法签名是属于外部可见的接口,而私有字段、私有方法和public
方法的方法体,都属于隐藏起来的内部结构和机制。在对象设计(以及模块设计)中正确划分内外是非常重要的。
范例代码可以从https://git.oschina.net/yyang/properties-and-fields下载。
详细内容请戳这里↓↓↓
如果觉得有收获,点个【赞】鼓励一下呗!
版权声明: 本文为 InfoQ 作者【编程道与术】的原创文章。
原文链接:【http://xie.infoq.cn/article/53aece5efd1dff358bcdfb6eb】。文章转载请联系作者。
评论