很多开发人员搞不清属性和字段的区别,本文试图对其作出澄清。
在进行Java软件开发的时候,很多人都没有搞清Java对象中属性(Property)和字段(Field)的区别,以为属性就是字段。本文试图对这两个概念作一个澄清。
范例
首先上一个例子:平面直角坐标系中的矩形。
一个矩形的形状可以用宽(width)和高(height)来表示,而它的位置可以用左下角坐标lowerLeftCoordinate(例如(10, 5))来表示。平面直角坐标系中任何一个点(Point)可以用这个点的坐标(x, y)来表示。
点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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package yang.yu.training;
import javax.persistence.Embeddable; import java.io.Serializable; import java.util.Objects;
@Embeddable public class Point implements Serializable { private int x; private int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
@Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof Point)) { return false; } Point that = (Point) other; return this.x == that.x && this.y == that.y; }
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "Point(" + x + ", " + y + ")"; }
public static Point of(int x, int y) { return new Point(x, y); } }
|
而矩形Rectangle的定义是这样的:
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| package yang.yu.training;
import javax.persistence.Entity; import javax.persistence.Id; import java.io.Serializable; import java.util.Objects; import java.util.UUID;
@Entity public class Rectangle implements Serializable {
@Id private String id = UUID.randomUUID().toString();
private Point lowerLeftCoordinate;
private int _width;
private int _height;
public Rectangle(int width, int height) { this._width = width; this._height = height; lowerLeftCoordinate = new Point(0, 0); }
public String getId() { return id; }
public int getWidth() { return _width; }
public int getHeight() { return _height; }
public int getArea() { return _width * _height; }
public boolean isSquare() { return _width == _height; }
public Point getLowerLeftCoordinate() { return lowerLeftCoordinate; }
public Point getLoweRightCoordinate() { return new Point(lowerLeftCoordinate.getX() + _width, lowerLeftCoordinate.getY()); }
public Point getUpperLeftCoordinate() { return new Point(lowerLeftCoordinate.getX(), lowerLeftCoordinate.getY() + _height); }
public Point getUpperRightCoordinate() { return new Point(lowerLeftCoordinate.getX() + _width, lowerLeftCoordinate.getY() + _height); }
public void moveHorizontally(int delta) { lowerLeftCoordinate = new Point(lowerLeftCoordinate.getX() + delta, lowerLeftCoordinate.getY()); }
public void moveVertically(int delta) { lowerLeftCoordinate = new Point(lowerLeftCoordinate.getX(), lowerLeftCoordinate.getY() + delta); }
public void moveTo(Point point) { lowerLeftCoordinate = new Point(lowerLeftCoordinate.getX() + point.getX(), lowerLeftCoordinate.getY() + point.getY()); }
@Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof Rectangle)) { return false; } Rectangle that = (Rectangle) other; return this._width == that._width && this._height == that._height; }
@Override public int hashCode() { return Objects.hash(_width, _height); }
@Override public String toString() { return "Rectangle{" + "_width=" + _width + ", _height=" + _height + '}'; } }
|
为了方便,矩形宽度、高度和点坐标都用整数int而不是浮点数double来表示。
属性和字段的定义
在很多情况下,在一个类中,字段和属性往往一一对应,例如Point类中,我们有两个字段x和y,也有两个属性x和y与之一一对应:
1 2 3 4 5 6 7 8 9 10
| private int x;
public int getX() { return x; }
public void setX(int x) { this.x = x; }
|
1 2 3 4 5 6 7 8 9 10
| private int y;
public int getY() { return y; }
public void setY(int y) { this.y = y; }
|
下面一行:
在Point类中定义了一个字段(Field),名为x。
根据JavaBean规范,下面的代码:
1 2 3 4 5 6 7
| public int getX() { return x; }
public void setX(int x) { this.x = x; }
|
定义了一个可读(因为有getX()方法)、可写(因为有setX()方法)的属性(Property)x。属性的名字来自getX()或setX()。根据JavaBean规范,属性的名字是方法名getX/setX去掉get/set前缀之后,剩下的部分首字母改为小写来定义。因此,getX去掉get之后剩下X,再将它的首字母(在这里是唯一的字母)改为小写就是x。这就是属性名。同样地,在Rectangle类中,getWidth方法的存在意味着Rectangle类中存在一个名为width的属性。
属性和字段之间没有必然的一一对应关系
请牢记:
在一个类之中,属性和字段没有必然的一一对应关系。
具体表现在:
属性和字段的名称可以是不同的
例如在Rectangle类中,字段**_width对应的属性名是width**:
1 2 3 4 5
| private int _width;
public int getWidth() { return _width; }
|
有的字段没有对应的属性,有的属性没有对应的字段
在Rectangle类中,我们有一个area属性(由getArea()方法定义):
1 2 3
| public int getArea() { return _width * _height; }
|
却没有一个相应的area字段存在。这个属性是通过计算(width * height)得来的。
在Rectangle类中,只有左下角的坐标点,字段和属性一一对应:
1 2 3 4 5
| private Point lowerLeftCoordinate;
public Point getLowerLeftCoordinate() { return lowerLeftCoordinate; }
|
而右下角、左上角、右上角的坐标点,只有相应的属性(loweRightCoordinate、upperLeftCoordinate和upperRightCoordinate),没有相应的字段。它们都是计算属性:
1 2 3 4 5 6 7 8 9 10 11
| public Point getLoweRightCoordinate() { return new Point(lowerLeftCoordinate.getX() + _width, lowerLeftCoordinate.getY()); }
public Point getUpperLeftCoordinate() { return new Point(lowerLeftCoordinate.getX(), lowerLeftCoordinate.getY() + _height); }
public Point getUpperRightCoordinate() { return new Point(lowerLeftCoordinate.getX() + _width, lowerLeftCoordinate.getY() + _height); }
|
有的属性是多个字段综合表现的结果
例如判断矩形是不是正方形的属性(通过isSquare()方法定义),是比较_width和_height两个字段的结果:
1 2 3
| public boolean isSquare() { return _width == _height; }
|
面积属性也是这样:
1 2 3
| public int getArea() { return _width * _height; }
|
属性可以是只读、只写或读写的,而字段都是可读写的
如果一个类中同时存在getAbc()(或isAbc(),如果属性类型是boolean类型)和setAbc()方法,那么属性abc就是可读可写的。如果没有相应的setAbc()方法,该属性就是只读的。如果没有相应的getAbc()(或isAbc(),如果属性类型是boolean类型)方法,该属性就是只写的。
下面的Rectangle类代码中,width和height属性都是只读的,它们通过Rectangle类的构造函数设置,然后就永远不可以改变了,因为类中没有定义setWidth()和setHeight()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private int _width;
private int _height;
public Rectangle(int width, int height) { this._width = width; this._height = height; lowerLeftCoordinate = new Point(0, 0); }
public int getWidth() { return _width; }
public int getHeight() { return _height; }
|
属性是类外部接口的一部分,字段是类内部实现的一部分
其实,上面那些区别都是浮云,真正重要的是要记住:
属性是类外部接口的一部分;
字段是类内部实现的一部分。
因此:
属性是你的代码和你代码的客户的协议的一部分,必须精心设计,不可随意改动;
而字段属于你的自由王国,你可以随意添加、删除和修改,只要不影响类的外部接口。
无论是在设计一个系统还是一个类,区分其的外部接口和内部实现都是非常有意义的。在系统的层级,外部接口主要包含接口和抽象基类,而内部实现包含上述接口的实现类和和基类的具体子类。在类的层级,外部接口包含属性和公有(public)方法签名,而内部实现包括字段、私有方法和公有方法的方法体。接口和实现的区分比起字段和方法的区分具有更重大的意义。
外部接口的“形诸外”是内部实现的“动于内”的结果。
字段代表内部实现的静态部分,属性代表外部接口的静态部分。方法属于动态部分。
重要推论
在软件开发领域,有一条基本原则:
面向接口编程,不要面向实现编程。
因此我们可以得到这样的推论:
先定义属性而不是字段
先定义属性(因为它是类接口的一部分),然后根据需要定义字段,而不是相反。
例如我在设计Rectangle类的时候,是根据外界对矩形的期待设计出它的长度、宽度、四个角的坐标、面积、是否正方形等属性,为了支撑这几个属性,我定义长度、宽度和左下角坐标三个字段;而不是先定义那几个字段,再通过IDE自动创建对应的属性(getter和setter)。
不要为每个字段默认生成属性
千万不要、不要、不要不假思索地为每个字段添加getter和setter方法生成属性。
我发现大多数程序员都是不假思索地先定义字段,然后利用IDE为所有的字段自动生成对应的读写属性。这是一种非常有害的习惯,会生成一个臃肿的、充满无用代码的类。直接将所有的字段设置为public的,或者封装为public属性,都违反了封装性原则,将大量的内部实现细节暴露给外界,加重了你的代码用户的认知负担,同时限制了你未来重构代码的自由度。
在很多时候,设计一个不可变类(或部分属性不可变)是非常有益的。也就是说,只有getter,没有setter。字段初始值通过构造函数参数传入。
尽量通过方法而不是setter来修改字段值
尽量通过真正的方法而不是通过setter修改字段值。
本质上,getter和setter不是真正的方法,而是属性访问器。Java不像C#那样拥有专门的属性访问器语法,只得用getter和setter方法来代替。在面向对象的设计中,方法和字段是相互作用的:字段可以影响方法调用的结果,而方法可以修改字段的取值。
在Rectangle类中,我是通过三个move方法来修改lowerLeftCoordinate字段的值的:
1 2 3 4 5 6 7 8 9 10 11 12
| public void moveHorizontally(int delta) { lowerLeftCoordinate = new Point(lowerLeftCoordinate.getX() + delta, lowerLeftCoordinate.getY()); }
public void moveVertically(int delta) { lowerLeftCoordinate = new Point(lowerLeftCoordinate.getX(), lowerLeftCoordinate.getY() + delta); }
public void moveTo(Point point) { lowerLeftCoordinate = new Point(lowerLeftCoordinate.getX() + point.getX(), lowerLeftCoordinate.getY() + point.getY()); }
|
而不是通过属性修改器来重新设置左下角坐标:
1 2 3
| public void setLowerLeftCoordinate(Point newCoordinate) { this.lowerLeftCoordinate = lowerLeftCoordinate; }
|
这样做的原因是:
- 隐藏内部实现。不必将“通过修改左下角坐标来实现矩形的移动”这样的内部实现细节呈现给代码的用户。
- 面向意图编程。因为用户的意图就是将矩形移动到一个新的位置,而不是重新设置它的左下角坐标。
JPA/Hibernate中区分属性和字段持久化
在JPA和Hibernate这样的ORM软件中,我们可以根据属性/字段对实体进行查询。这时候必须区分是按属性还是按字段进行持久化。
JPA/Hibernate中,如果@Id注解如果定义在属性上,就是根据属性进行持久化的;如果是定义在字段上,就是根据字段来进行持久化的。
有什么不同?
我们先看字段持久化(@Id定义在id字段上):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Entity public class Rectangle {
@Id private String id;
private int _width;
public String getId() { return id; }
public int getWidth() { return _width; }
|
在进行jpql或hql查询的时候,应该根据字段名**_width而不是属性名width**:
1
| SELECT o FROM Rectangle o WHERE o._width > 100
|
或者
1
| SELECT o._width, o._height WHERE o._width > 100
|
下面是属性持久化(@Id定义在getId()方法上):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Entity public class Rectangle {
private String id;
private int _width;
@Id public String getId() { return id; }
public int getWidth() { return _width; }
|
在进行jpql或hql查询的时候,应该根据属性名width而不是字段名**_width**:
1
| SELECT o FROM Rectangle o WHERE o.width > 100
|
或者
1
| SELECT o.width, o.height WHERE o.width > 100
|
@Id字段定义在字段还是属性上决定了整个类的所有属性/字段的持久化策略。
深入思考
现实世界中的事物,其实也有这种二分法:外部可见的属性和行为和内部隐藏的结构和机制。正是不可见的内部的结构和机制决定了它的可见的外部属性和行为,即所谓“动于内,形诸外”。面向对象编程模仿的就是现实世界的对象。对于一个类而言,属性和public
方法签名是属于外部可见的接口,而私有字段、私有方法和public
方法的方法体,都属于隐藏起来的内部结构和机制。在对象设计(以及模块设计)中正确划分内外是非常重要的。
范例代码可以从https://git.oschina.net/yyang/properties-and-fields下载。