本文将对Java程序设计的对象和类进行深入详细介绍,主要涉及以下内容:
- 面向对象程序设计
- 如何创建标准Java类库中的类对象
- 如何编写自己的类
1、OOP概述
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。在OOP中,不必关心对象的具体实现,只要能够满足用户的需求即可。
OOP中,数据是第一位的,然后再考虑操作数据的算法。
1.1 类
类是构造对象的模板或蓝图,可以将类想象成制作小甜饼的切割机,将对象想象成小甜饼。由类构造对象的过程称为创建类的实例。
(1)封装(有时称为数据隐藏),从形式上看,是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。
(2)对象中的数据称为实例域,操纵数据的过程称为方法。对于每个特定的类实例都有一组特定的实例域值,这些值的集合就是这个对象的当前状态。
(3)实现封装的关键在于绝对不能让类中的方法直接访问其他类的实例域,程序仅通过对象的方法与对象数据进行交互。
1.2 对象
使用OOP必须搞清楚对象的三个主要特性
- 对象的行为 —— 可以对对象施加哪些操作,即方法
- 对象的状态 —— 当施加那些方法时,对象如何响应
- 对象的表示 —— 如何辨别具有相同行为与状态的不同对象
1.3 类之间的关系
在类之间最常见的关系有
- 依赖(uses-a) —— 如果一个类的方法操作另一个类的对象,就说一个类依赖于另一个类
- 聚合(has-a) —— 实际即包含关系
- 继承(is-a) —— 用来表示特殊与一般关系
2、使用预定义类
2.1 对象与对象变量
要想使用对象,就必须首先构造对象,并制定初始状态,然后再对对象应用方法。
(1)在Java中,使用构造器来构造新的实例,并在构造器前面加new操作符。
构造器实际是一个特殊的方法。构造器的名字应该与类的名字相同。
如构造一个Data类的对象并保存在一个变量中的例子
Data birthday = new Date();
(2)由上面例子可以看到单独new Data()是构造一个对象,单独Data birthday是定义一个对象变量,对象与对象变量之间区别很大。
对象变量不是一个对象,实际上也没有引用对象,因而,是不能将任何类Date的方法应用于这个变量的,但对象就可以。
因此
Data birthday;
s = birthday.toString();
是错的,
而
System.out.println(new Data());
和
String s = new Data().toString();
以及
date birthday;
birthday = new Date();
String s = birthday.toString();
都是对的。
(3)因此,需要认识到,Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用,即
Data birth = new Data();
中等号右侧的表达式构造一个Date类型的对象,且它的值是对新创建对象的引用,这个引用存储在变量birth中。
2.2 GregorianCalendar类
标准Java类库分别包含了两个类:前面使用过的表示时间点的Date类,还有用来表示日历表示法的GregorianCalendar类。虽然后者还扩展了以更加通用的Calendar类,描述了日历的一般特性。
虽然Date类除了前面使用的方法还有一些其他方法,但这些方法在未来类库版本中有可能删去,已被标记为不鼓励使用,仍然使用有可能在编译时出现警告。
GregorianCalendar类包含的方法要比Date类多得多。
2.3 更改器方法与访问器方法
对实例域作出修改的方法称为更改器方法,仅访问实例域而不进行修改的方法称为访问器方法。
如上述日历的作用是提供某个时间点的年、月、日等信息,要想查询这些设置信息,应该使用GregorianCalendar类中的get方法,要想修改,则需用set和add等方法。
通常的习惯是在访问器方法名前面加上前缀get,在更改方法名前面加上前缀set。
3、用户自定义类
下面我们来学习设计复杂应用程序所需的各种主力类,通常,这些类没有main方法,却有自己的实例域和实例方法。
要想创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法。
Java中,最简单的类定义形式为
下面,我们编写一个简单的Employee类,再进行详细讲解
import java.util.*;
/**
* This program tests the Employee class.
* @version 1.11 2004-02-19
* @author Cay Horstmann
*/
public class EmployeeTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// raise everyone‘s salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
}
}
class Employee
{
private String name;
private double salary;
private Date hireDay;
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
// GregorianCalendar uses 0 for January
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
(1)注意一个源文件中只能有一个public类,但可以有任意数目的非公有类,其中该源文件的文件名必须与该公有类的类名相同。
(2)上述源文件中包含了两个类,习惯上我们可以将每一个类放到一个单独的源文件中。编译时,最方便的方式是只键入
javac EmployeeTest.java
当编译器发现EmployeeTest.java使用了Employee类时就会自动查找名为Employee,class的文件,如果没有找到,再自动搜索Employee.java,在对其进行编译。而且如果Employee.java的版本较已有的Employee.class版本新,编译器就会自动重新编译这个文件。
(3)Employee类中包含的一个构造器四个方法均标记为public,表示任何类的任何方法均可以调用这些方法。
关键字private标记的三个实例域表示只有Employee类自身的方法可以访问这些实例域。
因此,非常不提倡使用public标记实例域,这将破坏封装。
3.1 关于构造器
从上述程序可以看到
(1)构造器与类同名
(2)每个类可以有一个以上的构造器
(3)构造器可以有0、1或多个参数
(3)构造器没有返回值
(4)构造器总是随着new操作仪器调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的
(5)注意,不要在构造器钟定义与实例域重名的局部变量,如
public Employee(String n, double s, int year, int month, int day)
{
String name = n; //Error!
Double salary = s; //Error!
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
// GregorianCalendar uses 0 for January
hireDay = calendar.getTime();
}
上述错误的程序在构造器中又声明了局部变量name和salary,这些变量只能在构造器内部访问,从而对同名的实例域形成了屏蔽,形成错误,注意这种错误很难检查出来,要注意!
3.2 隐式参数和显式参数
例如调用number.raiseSalary(5);中可以看到方法raiseSalary有两个参数:
隐式参数,即出现在方法名前的Employee类对象,number
显式参数,即位于方法名后面括号中的数值,5
在每一个方法中,关键在this表示隐式参数,如果需要,以这种方式来编写raiseSalary方法
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
可以培养这种编写风格,可以将实例域与局部变量明显区分开来。
3.3 封装的优点
上面程序中出现的前三个方法都是典型的访问器方法,由于其只返回实例域值,也称为域访问器。
但有些时候,需要获得或设置实例域的值,因此,应该提供以下三项内容
- 一个私有的数据域
- 一个公有的域访问器方法
- 一个公有的域更改器方法
这样显然比提供一个简单的公有数据域复杂,但也有显著优点
(1)可以改变内部实现,除了该类的方法之外,不会影响其他代码
(2)更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理
注意不要编写返回引用可变对象的访问器方法。
3.4 基于类的访问权限
(1)一个方法可以访问所调用对象的私有数据
(2)一个方法也可以访问所属类的所有对象的私有数据
如
显然调用
if (harry.equals(boss)) …
中方法equals同时访问了harry和boss的私有域。
3.5 私有方法
有时可能希望讲一个计算代码划分为若干个独立的辅助方法,而这些辅助方法不应成为公有接口的一部分,因为它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。
此时,只需将关键字public改为private即可。
3.6 final实例域
使用关键字final定义实例域,那么构建对象时必须初始化这样的域。即必须确保在每一个构造器执行之后,这个域的值被设置,且在后面的操作中,不能再对它进行修改。
final修饰符一般应用于基本类型域,或者不可变类的域。其中,如果类中的每个方法都不会改变其对象,这种类就是不可变的类。