CORE JAVA 第4章 对象与类
4.1 面向对象程序设计概述
4.1.1 类
由类构造对象的过程称为创建类的实例。
封装是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。
对象中的数据称为实例域,操纵数据的过程称为方法。
实现封装的关键在于绝对不能让类中的其他方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。
继承;
4.1.2 对象
对象的三个主要特性:
- 对象的行为
- 对象的状态
- 对象标识
4.1.3 识别类
对于OOP,首先从设计类开始,然后再往每个类中添加方法。
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
4.1.4 类之间的关系
最常见的关系有:
- 依赖(uses-a)
- 聚合(has-a)
- 继承(is-a)
4.2 使用预定义类
4.2.1 对象与对象变量
要想使用对象,就必须首先构造对象,并指定其初始状态。然后,对对象应用方法。
使用构造器构造新实例。
构造器是一种特殊的方法,用来构造并初始化对象。
构造器的名字与类名相同。要想构造一个对象,需要在构造器前面加上new操作符。
通常希望构造的对象可以多次使用,因此,需要将对象存放在一个变量中。
变量不是对象。
初始化变量,有两个选择:
- 用新构造的对象初始化这个变量。
- 让这个变量引用一个已存在的对象。
一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用。
下列语句:
Date deadline = new Date();
有两个部分。表达式new Date()构造了一个Date类型的对象,并且它的值是对新创建对象的引用。这个引用存储在变量deadline中。
可以显式的将对象变量设置为null。
局部变量不会自动地初始化为null,而必须通过调用new或将它们设置为null进行初始化。
4.2.2 Java类库中的LocalDate类
日历表示法;
不使用构造器来构造LocalDate对象,应使用静态工厂方法代表调用构造器。
LocalDate.now()
会构造一个新对象,表示构造这个对象时的日期。
4.2.3 更改器方法与访问器方法
只访问对象而不修改对象的方法称为访问器方法。
4.3 用户自定义类
一个完整的程序,有若干类组合在一起,其中只有一个类有main方法。
4.3.1 Employee类
源文件名必须与public类的名字相匹配。
在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
4.3.2 多个源文件的使用
许多程序员习惯于将每一个类存放在一个单独的源文件中。这样组织文件,有两种编译源程序的方法:
- 使用通配符:javac Employee*.java
- javac EmployeeTest.java
4.3.3 剖析Employee类
实例域一般标记为private
类通常包括类型属于某个类类型的实例域。
4.3.4 构造器
构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
如:
james.Employee("James Bond",250000,1950,1,1) //ERROR
将产生编译错误。
- 构造器与类同名。
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
4.3.5 隐式参数与显式参数
方法用于操作对象以及存取它们的实例域。
隐式参数是出现在方法名前的类对象。
显式参数位于方法名后面括号中的数值。
在每一个方法中,关键字this表示隐式参数:
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
这样可以将实例域与局部变量明显地区分开来。
4.3.6 封装的优点
在有些时候,需要获得或设置实例域的值。因此,应该提供下面三项内容:
- 一个私有的数据域;
- 一个公有的域访问器方法;
- 一个公有的域更改器方法。
好处:
可以改变内部实现,除了该类的方法之外,不会影响其他代码。
更改器方法可以执行错误检查。
注意不要编写返回引用可变对象的访问器方法。如果需要返回一个可变对象的引用,应该首先对它进行克隆。
4.3.7 基于类的访问权限
一个方法可以访问所属类的所有对象的私有数据。
4.3.8 私有方法
4.3.9 final实例域
可以将实例域定义为final,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
final修饰符大都应用于基本类型域,或不可变类的域。
对于可变的类,使用final修饰符,例如:
private final StringBuilder evaluations;
evaluations = new StringBuilder();
fnal关键字只是表示存储在evaluations变量中的对象引用不会再指示其他StringBuilder对象,不过这个对象可以更改。
4.4 静态域与静态方法
4.4.1 静态域
如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。
静态域属于类,而不属于任何独立的对象。
4.4.2 静态常量
- Math.PI
- System.out
4.4.3 静态方法
静态方法是一种不能向对象实施操作的方法,即没有隐式参数。
静态方法不能访问实例域,因为它不能操作对象。但是,静态方法可以访问自身类中的静态域。
可以通过类名调用静态方法。
在下面两种情况使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显示参数提供(例如:Math.pow)。
- 一个方法只需要访问类的静态域。
4.4.4 工厂方法
类似LocalDate和NumberFormat的类使用静态工厂方法来构造对象。
4.4.5 main方法
静态的main方法将执行并创建程序所需要的对象。
4.5 方法参数
按值调用表示方法接收的是调用者提供的值。
按引用调用表示方法接收的是调用者提供的变量地址。
一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
- JAVA总是采用按值调用,也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
一个方法不能修改一个基本数据类型的参数(数字、布尔值),而对象引用作为参数则不同,可改变一个对象参数的状态。
一个方法不能让对象参数引用一个新的对象。
4.6 对象构造
4.6.1 重载
有些类有多个构造器,这种特征叫做重载。如果多个方法有相同的名字、不同的参数,便产生了重载。
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名。返回类型不是签名的一部分。
4.6.2 默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会自动地被赋为默认值:数值为0、布尔值为false、对象引用为null。
必须明确地初始化方法中的局部变量。
4.6.3 无参数的构造器
很多类都包含一个无参数的构造器,对象由无参数构造函数创建时,其状态会设置为适当的默认值。
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。
4.6.4 显式域初始化
确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值。
可以在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方法特别有用。
初始值不一定是常量,也可以调用方法对域进行初始化。
4.6.5 参数名
有些程序员在每个参数前面加上一个前缀"a":
public Employee(String aName, double aSalary)
{
name = aName;
salary = aSalary;
}
还有一种常用的技巧,它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来。
例如,如果将参数命名为salary,salary将引用这个参数,而不是实例域。但是,可以采用this.salary的形式访问实例域。
public Employee(String name, double salary)
{
this.name = name;
this.salary = salary;
}
4.6.6 调用另一个构造器
如果构造器的第一个语句形如this(……),这个构造器将调用同一个类的另一个构造器。
public Employee(double s)
{
//call Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
这样对公共的构造器代码部分只编写一次即可。
4.6.7 初始化块
前面已经讲过两种初始化数据域的方法:
- 在构造器中设置值
- 在声明中赋值
第三种:初始化块:在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。
这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。
建议将初始化块放在域定义之后。
调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值。
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
可以通过提供一个初始化值,或者使用一个静态的初始化块(将代码放在一个块中,并标记关键字static)来对静态域进行初始化。
在类第一次加载的时候,将会进行静态域的初始化。所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。
4.6.8 对象析构与finalize方法
析构器方法中放置一些当对象不再使用时需要执行的清理代码,最常见的操作是回收分配给对象的存储空间。
由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要。
可以为任何一个类添加finalize方法,将在垃圾回收器清除对象之前调用。
如果某个资源需要在使用完毕之后立刻被关闭,那么就需要由人工来管理。应用close方法。//7.2.5节
4.7 包
Java允许使用包将类组织起来。
4.7.1 类的导入
一个类可以使用所属包中的所有类,以及其他包中的公有类。
可以使用import语句导入一个特定的类或者整个包。语句应位于源文件的顶部(但位于package语句的后面)。
4.7.2 静态导入
import语句也可以导入静态方法和静态域。
4.7.3 将类放入包中
要想讲一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。
如果没有在源文件中放置packege语句,这个源文件中的类就被放置在一个默认包中。
编译器对文件(带有文件分隔符和扩展名.java的文件)进行操作,而Java解释器加载类(带有.分隔符)。
编译器在编译源文件的时候不检查目录结构,如果它不依赖于其他包,则不会出现编译错误。但是,最终的程序将无法运行,除非先将所有类文件移到正确的位置上。如果包与目录不匹配,虚拟机就找不到类。
4.7.4 包作用域
如果没有指定public或private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。
4.8 类路径
在前面已经看到,类存储在文件系统的子目录中。类的路径必须与包名匹配。
类文件也可以存储在JAR文件中。在一个JAR(JAVA归档)文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省又可以改善性能。
为了使类能够被多个程序共享,需要做到以下几点:
- 把类放到一个目录中,例如/home/user/classdir。这个目录是包树状结构的基目录。如果希望将com.horstmann.corejava.Employee类添加到其中,这个Employee.class类文件就必须位于子目录/home/user/classdir/com/horstmann/corejava中。
- 将JAR文件放在一个目录中,例如/home/user/archives。
- 设置类路径。
类路径是所有包含类文件的路径的集合。
在Windows环境中,类路径中的不同项目之间以分号分隔:
c:\classdir;.;c:\archives\archives.jar
类路径包括:
- 基目录/home/user/classdir或c:\classes。
- 当前目录(句点.)。
- JAR文件/home/user/archives/archives.jar或c:\archives\archives.jar。
运行时库文件会被自动地搜索,所以不必将它们显式的列在类路径中。
如果设置了类路径却忘记了包含"."目录,则程序仍然可以通过编译,但不能运行。
类路径所列出的目录和归档文件是搜寻类的起始点。
虚拟机搜寻文件;
编译器定位文件;
4.8.1 设置类路径
java -classpath
4.9 文档注释
javadoc可以由源文件生成一个html文档。
在源代码中添加以专用的定界符/**开始的注释,可以生成文档,将代码与注释保存在一个地方。
4.9.1 注释的插入
javadoc实用程序从下面几个特性中抽取信息:
- 包
- 公有类与接口
- 公有的和受保护的构造器及方法
- 公有的和受保护的域
应该为上面几部分编写注释。注释应该放在所描述特性的前面。注释以/**
开始,并以*/
结束。
每个 /**……*/
文档注释在标记之后紧跟着自由格式文本。标记由@开始,如@author或@param。
自由格式文本的第一句应该是一个概要性的句子。javadoc自动地将这些句子抽取出来形成概要页。
在自由格式文本中,可以使用HTML修饰符。
如果文档中有到其他文件的链接,例如图像文件,就应该将这些文件放到子目录doc-files中。
4.9.2 类注释
类注释必须放在import语句之后,类定义之前。
4.9.3 方法注释
每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:
- @param变量描述
对当前方法的参数部分添加一个条目。这个描述可以占据多行,可以使用HTML标记。一个方法的所有@param标记必须放在一起。 - @return描述
这个描述可以跨越多行,可以使用HTML标记。 - @throws类描述
这个标记将添加一个注释,用于表示这个方法有可能抛出异常。
4.9.4 域注释
只需要对公有域(通常指的是静态常量)建立文档。
4.9.5 通用注释
下面的标记可以用在类文档的注释中。
- @author姓名
- @version文本
下面的标记可以用在所有的文档注释中。
- @since文本
- @deprecated文本
这个标记将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建议。
通过@see和@link标记,可以使用超级链接,链接到javadoc文档的相关部分或外部文档。
- @see引用
这个标记将在“see also”部分增加一个超级链接。它可以用于类中,也可以用于方法中。
- package.class#feature lable
只要提供类、方法或变量的名字,javadoc就在文档中插入一个超链接。
例如:
@see com.horstmann.corejava.Employee#raiseSalary(double)
建立一个链接到com.horstmann.corejava.Employee类的raiseSalary(double)方法的超链接。
一定要使用#而不是.分隔类名与方法名,或类名与变量名。
2.
<a href="……">label</a>
可以超链接到任何URL。
例如:
@see The Core Java home page
在上述两种情况下,都可以指定一个可选的标签label作为链接锚,如果省略了label,用户看到的锚的名称就是目标代码名或URL。
如果@see标记后面有一个双引号(")字符,文本就会显示在"see also"部分。
例如:
@see "Core Java 2 volume 2"
可以为一个特性添加多个@see标记,但必须将它们放在一起。
- 还可以在注释中的任何位置放置指向其他类或方法的超链接,以及插入一个专用的标记,例如:
{@link package.class#feature label}
这里的特性描述规则与@see标记规则一样。
4.9.6 包与概述注释
可以直接将类、方法和变量的注释放置在Java源文件中,只要用```/** …… */文档注释界定就可以了。但是,要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:
- 提供一个以package.html命名的HTML文件。在标记
<body>...</body>
之间的所有文本都会被抽取出来。 - 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以
/**
和*/
界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
还可以为所有的源文件提供一个概述性的注释。这个注释将被放置在一个名为overview.html的文件中,这个文件位于包含所有源文件的父目录中。标记<body>...</body>
之间的所有文本都会被抽取出来。当用户从导航栏中选择“overview”时,就会显示出这些注释内容。
4.9.7 注释的抽取
假设HTML文件将要被存放在目录docDirectory下,执行以下步骤:
- 切换到包含想要生成文档的源文件目录。如果有嵌套的包要生成文档,例如com.horstmann.corejava,就必须切换到包含子目录com的目录。
- 如果是一个包,应该运行命令:
javadoc -d docDirectory nameOfPackage
或对于多个包生成文档,运行:
javadoc -d docDirectory nameOfPackage1,nameOfPackage2
如果文件在默认包中,就应该运行:
javadoc -d docDirectory *.java
还可以使用多种形式的命令行选项对javadoc程序进行调整。……
4.10 类设计技巧
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
就是说,用其他的类代替多个相关的基本类型的使用。 - 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
命名类名的良好习惯是采用一个名词、前面有形容词修饰的名词或动名词修饰名词。
对于方法来说,习惯是访问器方法用get开头,更改器方法用set开头。 - 优先使用不可变的类
如果类是不可变的,就可以安全地在多个线程间共享其对象。