3.2 类的组合

3.2 类的组合

程序是用来解决现实问题的,将现实中的概念映射为程序中的概念,是初学编程过程中的一步跨越。本节通过一些例子来演示如何将一些现实概念和问题通过类以及类的组合来表示和处理,涉及的概念包括图形处理、电商、人之间的血缘关系以及计算机中的文件和目录。

我们先介绍两个基础类String和Date,它们都是Java API中的类,分别表示文本字符串和日期。

3.2.1 String和Date

String是Java API中的一个类,表示多个字符,即一段文本或字符串,它内部是一个char的数组,提供了若干方法用于操作字符串。

String可以用一个字符串常量初始化,字符串常量用双引号括起来(注意与字符常量区别,字符常量是用单引号)。例如,如下语句声明了一个String变量name,并赋值为“老马说编程”。

1
String name = "老马说编程";

String类提供了很多方法,用于操作字符串。在Java中,由于String用得非常普遍, Java对它有一些特殊的处理,本节暂不介绍这些内容,只是把它当作一个表示字符串的类型来看待。

Date也是Java API中的一个类,表示日期和时间,它内部是一个long类型的值,也提供了若干方法用于操作日期和时间。

用无参的构造方法新建一个Date对象,这个对象就表示当前时间。

1
Date now = new Date();

日期和时间处理是一个比较大的话题,我们留待第7章详解,本节我们只是把它当作表示日期和时间的类型来看待。

3.2.2 图形类

我们先扩展一下Point类,在其中增加一个方法,计算到另一个点的距离,代码如下:

1
2
3
public double distance(Point p){
return Math.sqrt(Math.pow(x-p.getX(), 2)+Math.pow(y-p.getY(), 2));
}

在类Point中,属性x、y都是基本类型,但类的属性也可以是类。我们考虑一个表示线的类,它由两个点组成,有一个实例方法计算线的长度,如代码清单3-2所示。

代码清单3-2 表示线的类Line
1
2
3
4
5
6
7
8
9
10
11
public class Line {
private Point start;
private Point end;
public Line(Point start, Point end){
this.start= start;
this.end = end;
}
public double length(){
return start.distance(end);
}
}

Line由两个Point组成,在创建Line时这两个Point是必需的,所以只有一个构造方法,且需传递这两个点,length方法计算线的长度,它调用了Point计算距离的方法获取线的长度。可以看出,在设计线时,我们考虑的层次是点,而不考虑点的内部细节。每个类封装其内部细节,对外提供高层次的功能,使其他类在更高层次上考虑和解决问题,是程序设计的一种基本思维方式

使用这个类的代码如下所示:

1
2
3
4
5
6
public static void main(String[] args) {
Point start = new Point(2,3);
Point end = new Point(3,4);
Line line = new Line(start, end);
System.out.println(line.length());
}

这也很简单。我们再说明一下内存布局,line的两个实例成员都是引用类型,引用实际的point,整体内存布局如图3-1所示。

epub_923038_36

图3-1 图形类Point和Line对象的内存布局

start、end、line三个引用型变量分配在栈中,保存的是实际内容的地址,实际内容保存在堆中,line的两个实例变量line.start和line.end还是引用,同样保存的是实际内容的地址。

3.2.3 用类描述电商概念

接下来,我们用类来描述一下电商系统中的一些基本概念,电商系统中最基本的有产品、用户和订单。

1)产品:有产品唯一id、名称、描述、图片、价格等属性。
2)用户:有用户名、密码等属性。
3)订单:有订单号、下单用户、选购产品列表及数量、下单时间、收货人、收货地址、联系电话、订单状态等属性。

当然,实际情况可能非常复杂,这是一个非常简化的描述。

产品类Product如代码清单3-3所示。

代码清单3-3 表示产品的类Product
1
2
3
4
5
6
7
8
9
10
11
12
public class Product {
//唯一id
private String id;
//产品名称
private String name;
//产品图片链接
private String pictureUrl;
//产品描述
private String description;
//产品价格
private double price;
}

我们省略了类的构造方法,以及属性的getter/setter方法,下面大部分示例代码也都会省略。

这是用户类User的代码:

1
2
3
4
public class User {
private String name;
private String password;
}

一个订单可能会有多个产品,每个产品可能有不同的数量,我们用订单条目OrderItem这个类来描述单个产品及选购的数量,如代码清单3-4所示。

代码清单3-4 表示订单条目的类OrderItem
1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderItem {
//购买产品
private Product product;
//购买数量
private int quantity;
public OrderItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public double computePrice(){
return product.getPrice()*quantity;
}
}

OrderItem引用了产品类Product,我们定义了一个构造方法,以及计算该订单条目价格的方法。

订单类Order如代码清单3-5所示。

代码清单3-5 表示订单的类Order
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
public class Order {
//订单号
private String id;
//购买用户
private User user;
//购买产品列表及数量
private OrderItem[] items;
//下单时间
private Date createtime;
//收货人
private String receiver;
//收货地址
private String address;
//联系电话
private String phone;
//订单状态
private String status;
public double computeTotalPrice(){
double totalPrice = 0;
if(items! =null){
for(OrderItem item : items){
totalPrice+=item.computePrice();
}
}
return totalPrice;
}
}

Order类引用了用户类User,以及一个订单条目的数组OrderItem,它定义了一个计算总价的方法。这里用一个String类表示状态status,更合适的应该是枚举类型,枚举我们第5章再介绍。

以上类定义是非常简化的,但是大致演示了将现实概念映射为类以及类组合的过程,这个过程大概就是,想想现实问题有哪些概念,这些概念有哪些属性、哪些行为,概念之间有什么关系,然后定义类、定义属性、定义方法、定义类之间的关系。概念的属性和行为可能是非常多的,但定义的类只需要包括那些与现实问题相关的就行了

3.2.4 用类描述人之间的血缘关系

上面介绍的图形类和电商类只会引用别的类,但一个类定义中还可以引用它自己,比如我们要描述人以及人之间的血缘关系。我们用类Person表示一个人,它的实例成员包括其父亲、母亲、和孩子,这些成员也都是Person类型,如代码清单3-6所示。

代码清单3-6 表示人的类Person
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person {
//姓名
private String name;
//父亲
private Person father;
//母亲
private Person mother;
//孩子数组
private Person[] children;
public Person(String name) {
this.name = name;
}
}

这里同样省略了setter/getter方法。对初学者,初看起来这是比较难以理解的,有点类似于函数调用中的递归调用,这里面的关键点是,实例变量不需要一开始就有值。我们来看下如何使用:

1
2
3
4
5
6
7
public static void main(String[] args){
Person laoma = new Person("老马");
Person xiaoma = new Person("小马");
xiaoma.setFather(laoma);
laoma.setChildren(new Person[]{xiaoma});
System.out.println(xiaoma.getFather().getName());
}

这段代码先创建了老马(laoma),然后创建了小马(xiaoma),接着调用xiaoma的set-Father方法和laoma的setChildren方法设置了父子关系,Person类对象的内存布局如图3-2所示。

epub_923038_37

图3-2 Person类对象的内存布局

3.2.5 目录和文件

接下来,我们介绍两个类MyFile和MyFolder,分别表示文件管理中的两个概念:文件和文件夹。文件和文件夹都有名称、创建时间、父文件夹,根文件夹没有父文件夹,文件夹还有子文件列表和子文件夹列表。文件类MyFile如代码清单3-7所示。代码清单3-7 文件类MyFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyFile {
//文件名称
private String name;
//创建时间
private Date createtime;
//文件大小
private int size;
//上级目录
private MyFolder parent;
//其他方法……
public int getSize() {
return size;
}
}

文件夹类MyFolder如代码清单3-8所示。

代码清单3-8 文件夹类MyFolder
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
public class MyFolder {
//文件夹名称
private String name;
//创建时间
private Date createtime;
//上级文件夹
private MyFolder parent;
//包含的文件
private MyFile[] files;
//包含的子文件夹
private MyFolder[] subFolders;
public int totalSize(){
int totalSize = 0;
if(files! =null){
for(MyFile file : files){
totalSize+=file.getSize();
}
}
if(subFolders! =null){
for(MyFolder folder : subFolders){
totalSize+=folder.totalSize();
}
}
return totalSize;
}
//其他方法……
}

MyFile和MyFolder都省略了构造方法、settter/getter方法,以及关于父子关系维护的代码,主要演示实例变量间的组合关系。两个类之间可以互相引用,MyFile引用了MyFolder,而MyFolder也引用了MyFile,这是没有问题的。因为正如之前所说,这些属性不需要一开始就设置,也不是必须设置的。另外,演示了一个递归方法totalSize(),返回当前文件夹下所有文件的大小,这是使用递归函数的一个很好的场景。

3.2.6 一些说明

类中应该定义哪些变量和方法,这是与要解决的问题密切相关的,本节中并没有特别强调问题是什么,定义的属性和方法主要用于演示基本概念,实际应用中应该根据具体问题进行调整。

类中实例变量的类型可以是当前定义的类型,两个类之间可以互相引用,这些初听起来可能难以理解,但现实世界就是这样的,创建对象的时候这些值不需要一开始就有,也可以没有,所以是没有问题的。

类之间的组合关系在Java中实现的都是引用,但在逻辑关系上,有两种明显不同的关系,一种是包含,另一种是单纯引用。比如,在订单类Order中,Order与User的关系就是单纯引用,User是独立存在的;而Order与OrderItem的关系就是包含,OrderItem总是从属于某一个Order。

3.2.7 小结

对初学编程的人来说,不清楚如何用程序概念表示现实问题,本节通过一些简化的例子来解释如何将现实中的概念映射为程序中的类。

分解现实问题中涉及的概念以及概念间的关系,将概念表示为多个类,通过类之间的组合来表达更为复杂的概念以及概念间的关系,是计算机程序的一种基本思维方式。

正所谓,道生一,一生二,二生三,三生万物,如果将二进制表示和运算看作一,将基本数据类型看作二,基本数据类型形成的类看作三,那么,类的组合以及下章介绍的继承则使得三生万物。