1 面向对象程序设计概述
- 面向对象程序设计(简称OOP)是当今主流的程序设计范型
- Java是完全面向对象的,必须熟悉OOP才能编写Java程序
1.1 类
- 类(class)是构造对象的模板和蓝图
- 由类构造(construct)对象的过程称为创建类的实例
- 封装(encapsulation,有时称为数据隐藏)是与对象有关的一个重要概念;对象中的数据称为实例域(instance field),操纵数据的过程称为方法(method)
- 实现封装的关键在于绝对不能让类中的方法直接访问其他类的实例域。
- 通过扩展一个类来建立另外一个类的过程称为继承
1.2 对象
- OOP的三个主要特性:
1) 对象的行为(behavior) —— 可以对对象施加那些操作,或可以对对象施加哪些方法
2) 对象的状态(state) —— 当施加那些方法时,对象应该如何响应
3) 对象标识(identity) —— 如何辨别具有相同行为与状态的不同对象
1.4 类之间的关系
- 在类之间,最常见的关系有:依赖("uses-a"),聚合("has-a"),继承("is-a")
- 依赖,是一种最明显的,最常见的关系;如果一个类的方法操纵着另一个类的对象,我们就说一个类依赖于另一个类
- 聚合,是一种具体而且易于理解的关系;聚合关系意味着类A的对象包含类B的对象
- 继承,用于表示特殊与一般关系的;继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
2 使用预定义类
2.1 对象与对象变量
- 要想使用对象,就必须首先构造对象,并且指定其初始状态,然后,对对象应用方法
- 在Java程序设计语言中,使用构造器构造新实例,构造器是一种特殊的方法,用来构造并初始化对象
- 构造器的名字应该与类名相同
- 如果希望构造的对象可以多次使用,需要将对象存放于变量中
- 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象
- 在Java中,任何对象变量的值都是对储存在另一个方法的一个对象的应用,new操作符的返回值也是一个引用
2.2 Java类库中的LocalDate类
- 类库设计者决定将保存时间与时间点命名分开。所以标准的Java类库分别包含了两个类:一个是用来表示时间点的Date类;另一个是用来表示大家熟悉的日历表示法的LocalDate类
- 不要使用构造器来构造LocalDate类的对象,应该使用静态工厂方法代表你调用的构造器
LocalDate.now()
会构造一个新对象,表示构造这个对象时的日期
2.3 更改器方法与访问器方法
- 对对象(或者实例域)做出修改的方法被称为更改器方法(mutator method)
- 只访问对象(或者实例域)而不修改对象的方法有时称为访问器方法(accessor mothed)
- 在Java语言中,访问器方法与更改器方法在语法上没有明显的区别
- 通常的习惯是在访问器方法名前面加上前缀 get,在更改器方法前面加上前缀 set(例如getName和setName)
3 用户自定义类
- 想要创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法
- 源文件名必须与public类的名字相匹配。
- 在一个源文件中,只能拥有一个公有类,但可以有任意数目的非公有类。
3.2 多个源文件的使用
- 两种编译源文件的方法:
一种是用通配符*调用Java编译器
例:
javac Employee*.java
一种是我们常用的编译方法
例:
javac EmployeeTest.java
3.3 剖析Employee 类
public Employee(String n,double s,int year,int mouth,int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)
private String name;
private double salary;
private LocalDate hireDay;
- 关键字public意味着任何类的任何方法都可以调用这些方法
- public数据域允许程序中的任何方法对其进行读取和修改,这就完全破坏了封装,是一种不推荐的做法。
- 关键字private确保只有Employee类自身的方法能够访问这些实例域,而其他类的方法不能够读写这些域
- 类通常包括类型属于某个类类型的实例域
3.4 从构造器开始
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作符一起调用
- 不要在构造器中定义与实例域重名的局部变量
3.5 隐式参数和显示参数
- 显式参数时明显地列在方法声明中的,隐式方法没有出现在方法声明中
- 在每一个方法中,关键字this表示隐式参数
3.6 封装的优点
一个只读域一旦在构造器中设置完毕,就没有任何一个办法可以对它进行修改,这样确保只读域不会受到外界的破坏
有些时候,需要获得或设置实例域的值,因此,应该提供下面三项内容:
1) 一个私有的数据域 (private)
2) 一个公有的域访问器方法 (getter)
3) 一个共有的域更改器方法 (setter)
以上的好处是:
①可以改变内部实现,除了该类的方法之外,不会影响其他代码
②更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理
3.7 基于类的访问权限
- 方法可以访问所调用对象的私有数据
- 方法可以访问所属类的私有特性(feature),而不仅限于访问隐式参数的私有特性
3.8 私有方法
- 公有数据是非常危险的,因此应该将所有的数据域都设置为私有的
- 在Java中,为了实现一个私有的方法,只需将关键字public改为private即可
- 当方法时私有的时候,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删除,如果方法时公有的,就不能将其删除,因为其他的代码很可能依赖它。
3.9 Final实例域
- 可以将实例域定义为final,但是必须在构件对象时初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改
- final修饰符大都应用于基本(primitive)类型域,或者不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)
4 静态域与静态方法
4.1 静态域
- 如果将域定义为static,,每个类中只有一个这样的域
- 静态域属于类,而不属于任何独立的对象
- 在绝大多数的面向对象程序设计语言中,静态域被称为类域,术语static只是沿用了C++的叫法,并无实际意义
4.2 静态常量
- 静态变量使用得比较少,但静态常量却使用得比较多。
- 例如在Math类中定义了一个静态常量:
Math类中的PI
public class Math
{
...
public static final double PI = 3.14159265358979323846;
...
}
如果关键字static被省略,PI就变成了Math类的一个实例域
4.3 静态方法
- 静态方法是一种不能向对象实施操作的方法
- 可以认为静态方法时没有this参数的方法
- 在下面两种情况下使用静态方法:
1) 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)
2) 一个方法只需要访问类的静态域(例如Employee.getNextId)
例子:
public static int getNextId(){
return nextId; //return static field
}
可以通过类名调用这个方法:
int n = Employee.getNextId();
4.4 工厂方法
- 静态方法还有另外一种常见的用途。类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象
4.5 main方法
- main方法是一个静态方法
- main方法不对任何对象进行操作。
- 如果某个类时一个更大型应用程序的一部分,执行此类的话main方法永远不会执行
5 方法参数
- 按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址
- 一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的的变量值
- Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容
- Java程序设计语言对对象采用(引用)的是按值传递的
- 方法参数共有两种类型:
1)基本数据类型(数字,布尔值)
2) 对象引用 - Java中方法参数的使用情况
1) 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
2) 一个方法可以改变一个对象参数的状态
3) 一个方法不能让对象参数引用一个新的对象 - 例子:
假定这个方法试图将一个参数值增加至三倍
public static void tripleValue(double x){
x = 3 * x;
}
然后调用以下方法:
double percent = 10;
tripleValue(percent);
执行之后,percent的值依旧为10,而且这个方法结束后,参数变量x不再使用
而当对象作为参数就不同了:
public static void tripleSalary(Employee x){
x.raiseSalary(200);
}
当调用
harry = new Employee(...);
tripSalary(harry);
后x和harry同时引用的那个Employee对象的Salary提高了200%
6 对象构造
6.1 重载(overloading)
- 如果多个方法(比如,StringBuilder构造器方法)有相同的名字,不同的参数,便产生了重载。
- 编译器通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。
- Java允许重载任何方法,但不能有两个名字相同,参数类型也相同却返回不同类型值的方法(即与返回值无关)
6.2 默认域初始化
- 如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null
6.3 无参数的构造器
- 如果在编写一个类时没有编写构造器,那么系统就会提供一个无参的构造器,这个构造器将所有的实例域设置为*默认值。于是,实例域中的数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null。
- 如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会视为不合法
6.4 显式域初始化
- 通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎么调用构造器,每个实例域都可以被设置为一个有意义的初值,这是一种很好的设计习惯
- 在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用
6.5 参数名
- 在编写很小的构造器时(这是十分常见的),常常在参数命名上出现错误。通常,参数用单个字符命名:
public Employee(String n,double s)
{
name = n;
salary = s
}
但这种方法有一个缺陷:只有阅读了源代码才能了解n与s的含义。
有一些程序员在每个参数面前添加一个前缀,如“aName”和 "aSalary":
public Employee(String aName,double aSalary)
{
name = aName;
salary = aSalary;
}
这样很清晰,能让读者一眼能够知道参数的含义。
- 还有一种常用的技巧,它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来:
public Employee(String name,double salary)
{
this.name = name;
this.salary = salary;
}
6.6 调用另一个构造器
- 如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。
- 例子:
public Employee(double s)
{
//calls Employee(String,double)
this("Employee #" + nextId,s)
nextId++;
}
6.7 初始化块
- 两种初始化数据域的方法:在构造器中设置值,在声明中赋值
- 还有一种机制,称为初始化块(initialization block)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行
- 调用处理器的具体处理步骤:
1) 所有数据域被初始化为默认值(0、false、true)
2) 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块
3) 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体
4) 执行这个构造器主体
6.8 对象解析与finalize方法
- 可以为任何一个类添加finalize方法。
- finalize方法将在垃圾回收期清除对象之前调用
- 不能依赖于finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能调用
- 如果某个资源需要使用完毕后立即关闭,可以使用一个close方法来完成相应的清理操作
7 包
- Java允许使用包(package)将类组织起来
- 使用包的只要原因是确保类名的唯一性
7.1 类的导入
- 一个类可以使用所属包中所有类,以及其他包中的公有类(public class)
- 我们可以采用两种方式访问另一个包中的公有类:第一种方式是在每个类名之前添加完整的包名,第二种是使用import语句导入一个特定的类和整个包:
第一种方法的例子:
java.time.LocalDate today = java.time.LocalDate;
第二种方法的例子:
import java.time.*;
或者
import java.time.LocalDate
- 需要注意的是,只能用星号(*)导入一个包,而不能使用import java. * 或import java. * . * 导入以java为前缀的所有包
7.2 静态导入
- import语句不仅可以导入类,还增加了导入静态方法和静态域的功能
例如:
import static java.lang.System.*;
导入上述包后,就可以直接调用System类的静态方法和静态域,而不用加类名前缀:
out.println("Hello World!");
exit(0);
7.3 将类放入包中
- 要想将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前
- 如果没有源文件中放置package语句,这个源文件的类就被放置在一个默认包(defualt package)
- 如果要从基(根)目录编译和运行类,需要包含com目录:
javac com/mycompany/Employee.java
java com.mycompany.Employee
8 类路径
- 类储存在文件系统的子目录中,类的路径必须与包名匹配
- 另外,类文件也可以存储在JAR(Java归档)文件中,在一个JAR文件中,可以包含多个压缩形式的类文件和子目录
- 为了使类能够被多个程序共享,需要做到下面几点:
1) 把类放到一个目录中,例如/home/user/classdir,注意,这个目录是包树状结构的基目录
2) 将JAR文件放在一个目录中,例如:/home/user/jar
3) 设置类路径(class path)。类路径是所有包含类文件的路径的集合 - 类路径包括:
1) 基目录 /home/user/classdir 或 c:\classes
2) 当前目录(.)
3) JAR文件 /home/user/jar/archive.jar 或 c:\jar\archive.jar
9 文档注释
9.1 注释的插入
- javadoc实用程序(utility)从下面几个特性中抽取信息:
1) 包
2) 公有类和接口
3) 公有的和受保护的构造器和方法
4) 公有的和受保护的域 - 注释应该由 / ** 开始,并以 * /结束
- 每个文档注释在标记之后紧跟着自由格式文本。标记有@开始,如@author或@param
- 自由格式文本的第一句应该是概要性额橘子
- 在自由格式文本中,可以使用HTML注释符
9.2 类注释
- 类注释必须放在import语句之后,类定义之前。
9.3 方法注释
- @param 变量描述:
这个标记将对当前方法的“param”(参数)部分添加一个条目。这个描述可以占据多行,并可以使用HTML标记。一个方法的所有@param标记必须放在一起 - @return 描述:
这个标记将对当前方法添加"return"(放回部分)。这个描述可以跨越多行,并可以使用HTML标记 - throws 类描述:
这个标记将添加一个注释,用于表示这个方法有可能抛出异常
9.4域注释
- 只需要对公有域(通常指的是静态常量)建立文档。例如:
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;
9.5 通用注释
- @author 姓名:
这个标记将产生一个author条目。可以使用多个@author标记,每个@author标记对应一个作者 - @version 文本:
这个标记将产生一个version条目。这里的文本可以是对当前版本的任何秒速 - @since 文本
这个标记将产生一个since(始于)条目。这里的text可以是引入特性的版本秒速 - @deprecated 文本
这个标记将对类、方法和变量添加一个不再使用的注释 - @see 引用:
这个标记在“see also”部分添加一个超级链接。它可以用于类中,也可以拥有方法中
9.6 包与概述注释
- 要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有以下两个选择:
1) 提供一个以package.html命名的HTML文件。在标记<body>...</body>之间的所有文本都会被抽取出来
2) 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/** 和 */ 界定的Javadoc注释,跟在一个包语句之后,它不应该包含更多的代码和注释
9.7 注释的抽取
- 假设HTML文件被存放在docDirectory下。执行以下步骤:
1)切换到包含想要生成文档的源文件目录。如果有嵌套的包要生成文档,就必须切换到包含子目录com的目录
2)如果是一个包,应该运行以下命令:
javadoc -a docDirectory nameOfPackage
或对于多个包生成文档:
javadoc -d docDirectory nameOfpackage1 nameOfPackage2...
如果文件在默认包中,应该运行:
javadoc -d docDirectory *.java
10 类设计技巧
- 一定要保证数据私有:
这是最重要的;绝对不要破坏封装性 - 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解:
如果明显地可以将一个复杂的类分解成两个更简单的类,就应该将其分解 - 类名和方法名要能够体现它们的职责:
命名类名的良好习惯是采用一个名词(order)、前面有形容词修饰的名词(RushOrder)或动名词(有“-ing”后缀)修饰词(例如:BillingAddress)。对于方法来说,习惯是访问器方法用小写get开头,更改器方法用小写set开头 - 优先使用不可变的类