读后笔记 -- Java核心技术(第10版 卷I ) Chapter4 对象与类
Chapter 4. 对象与类
4.1 OOP 概述
1. 几个重要概念
- 类 class:构造对象的模板或蓝图
- 实例 instance:类构造(construct)对象的过程
- 封装 encapsulation:将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式
- 实例域 instance field:对象中的数据
- 方法 method:操纵数据的过程
2. 对象的三个主要特征
- 对象的行为(behavior) — 可以对对象施加哪些操作,或可以对对象施加哪些方法?
- 对象的状态(state ) — 当施加那些方法时,对象如何响应?
- 对象标识(identity ) — 如何辨别具有相同行为与状态的不同对象?
3. 类之间的关系
- 依赖("uses-a"):类A 操纵 类B 的对象
- 聚合("has-a"):类A 包含 类B 的对象
- 继承("is-a"):类B 继承 类A,则 类B 包含了A的所有的方法,还有一些额外的功能
4.2/4.3 使用预定义类/自定义类
1. 对象与对象变量
- 可以显式地将对象设置为null,表明这个对象变量没有引用任何对象。
- 局部变量不会自动地初始化为 null,而必须通过调用 new 或将他们设置为 null 进行初始化
Date deadline = null;
2. Date 类和 LocalDate 类
- Date 类:表示时间点
- LocalDate:日历表示法
3. 访问器方法 和 构造器方法
// 访问器方法(只访问对象而不修改对象), curDate 在 plusDay 后还是原值 LocalDate curDate = LocalDate.now(); LocalDate laterDate = curDate.plusDays(100); int year = laterDate.getYear(); int month = laterDate.getMonthValue(); int date = laterDate.getDayOfMonth(); // 更改器方法,someDay 的值随即被更改 GregorianCalendar someDay = new GregorianCalendar(2021, 12, 23); someDay.add(Calendar.DAY_OF_MONTH, 100); int year2 = someDay.get(Calendar.YEAR); int month2 = someDay.get(Calendar.MONTH); int day2 = someDay.get(Calendar.DAY_OF_MONTH);
4. 构造器
- 与类同名
- 每个类可以有一个以上的构造器
- 可以有0、1 或 多个参数
- 没有返回值
- 总是伴随着 new 操作一起调用
- ! 不要再构造器中定义与实例域重名的局部变量
5. 隐式参数与显式参数
// case1: 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); for (Employee e : staff) { // e:类的对象 -- 隐式参数; 5:数值 -- 显式参数 e.raiseSalary(5); } // case2: public void raiseSalary (double byPercent){ // 这里的 salary 完整的是 this.salary -- 隐式参数 double raise = salary * byPercent / 100; salary += raise; }
6. 封装的优点
一般情况下,需要提供三项内容:
- 一个私有的数据域
- 一个公有的域访问器方法
- 一个公有的域更改器方法
7. 基于类的访问权限
- 一个方法可以访问所属类的所有对象的私有数据
8. 私有方法
Java 设计时,基本上将所有的数据域设置成私有的。
方法:
- 1)大部分方法都是 公有 public
- 2)私有方法 private:
- (1)辅助方法,与当前的实现机制非常紧密,或需要一个特别的协议及一个特别的调用次序;
- (2)如改用其他的方法来实现时,私有方法即可删除,而公有方法可能被其他代码依赖;
9. final 实例域
可以将实例域定义为 final。 构建对象时必须初始化这样的域。在后面的操作中, 不能够再对它进行修改。声明为 final 后,这个值就没有对应的 set 方法 final 修饰符大都应用于基本 (primitive ) 类型域,或不可变(immutable) 类【类中的每个方法都不改变其对象】的域。private final String user = "root"; private final String password = "jun19931129"; private final String driver = "com.mysql.jdbc.Driver"; private final String url = "jdbc:mysql://localhost:3306/ant";
4.4 静态域与静态方法
1. 静态域
class Employee {
// 静态域(大多数OOP程序中,也称类域) nextId private static int nextld = 1;
// 实例域 private int id; .... } public void setld() { id = nextld; nextld++; } // 1) 假定为 harry 设定雇员标识码: Empolyee harry = new Employee(); harry.setld(); // => 实际的操作是: harry.id = Employee.nextld; Employee.nextId++; // 2) 假定为 james 设定雇员标识码: Empolyee james = new Employee(); james.setld(); // => 实际的操作是: james.id = Employee.nextld; Employee.nextId++;
- static 定义的域 , 每个类中只有一个,即这个类的所有实例将共享一个 nextId 域
- 每一个对象对于所有的实例域,却都有自己的一份拷贝。即如果有100个Employee类的对象,则有 100个实例域 id
// 1) 静态常量 public static final double PI = 3.14; // 2)另一个静态常量 System.out,它在 System 类中声明 public final class System { ... public final static PrintStream out = null; ... }
3. 静态方法
- 不能向对象实施操作的方法,如 Math.pow(x, a),不能使用任何 Math 对象,级没有隐式的参数
- 静态方法是没有 this 参数的方法
- 静态方法不能访问 id 实例域,因为不能操作对象。但可以访问自身类中的静态域 nextId
public class Employee { private static int nextId =1; private int id; public Employee(int id) { this.id = id; } public static int getNextId() { return nextId; } public static void setNextId(int nextId) { Employee.nextId = nextId; } public int getId() { return id; } public void setId(int id) { this.id = id; } }
两种情况下使用静态方法:
- 一 方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)
- 一个方法只需要访问类的静态域(例如:Employee.getNextld)
4. 工厂方法
如 LocalDate,NumberFormat的类 使用静态工厂方法来构造对象
main 方法是个静态方法,不需要使用对象调用。
public class EmployeeFS { private static int nextId = 1; // 静态域 nextId private String name; private double salary; private int id; public EmployeeFS(String name, double salary) { this.name = name; this.salary = salary; id = 0; } public static int getNextId() { return nextId; } public static void setNextId(int nextId) { EmployeeFS.nextId = nextId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } public int getId() { return id; } public void setId() { this.id = nextId; nextId++; } /** * add main method in class file to do unit test * @param args */ public static void main(String[] args) { EmployeeFS e = new EmployeeFS("Harry", 40000); System.out.println(e.getName() + ", " + e.getSalary()); } } public class StaticTest { public static void main(String[] args) { EmployeeFS[] staff = new EmployeeFS[3]; staff[0] = new EmployeeFS("Tom", 40000); staff[1] = new EmployeeFS("Dick", 50000); staff[2] = new EmployeeFS("Harry", 60000); for (EmployeeFS e : staff){ e.setId(); System.out.println("name: " + e.getName() + ", id = " + e.getId() + ", salary = " + e.getSalary()); } // call static method int n = EmployeeFS.getNextId(); System.out.println("next available id : " + n); } }
4.5 方法参数
1. 常见的参数传递给方法的方式:
- 1)按值调用(Java 采用的方式):方法接收的是调用者提供的值。方法不能修改传递给它的任何参数变量的内容。如 double percent =10; tripleValue(percent); // 执行完后,percent 还是 10,完成3倍计算的止损方法里面的变量
- 2)按引用调用:方法接收的是调用者提供的地址 。(C++,Pascal)。Java 的对象引用也是按照值传递的
2. 方法参数有两种类型:
- 基本数据类型(数字、布尔值):方法不能修改传递给它的任何参数变量的内容
- 对象引用:对象引用作为参数。 方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象
3. Java 中方法参数的使用情况
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
4.6 对象构造
1. 重载
- 重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
- 每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
- 最常用的地方就是构造器的重载。
** 重写与重载的区别,更多详细解释参看:https://www.runoob.com/java/java-override-overload.html
2. 默认域初始化
一般,构造器需要显式地赋予初值,否则被自动初始为默认值
3. 无参数的构造器
如果在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。 4. 显式域初始化 当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,在执行构造器之前,先执行赋值操作。 单字母的参数一般很难理解其意思,下面的2种方式较好的处理了这种情况:// case 1:参数变量同 构造器的变量名相同,构造器中使用 this(隐式参数)访问实例域 public Employee(String name, double salary) { this.name = name; this.salary= salary; } // case 2:构造器参数加一前缀,如“a” public Employee(String aName, double aSalary) { name = aName; salary= aSalary; }
5. 调用另一个构造器
如果构造器的第一个语句形如 this(...), 这个构造器将调用同一个类的另一个构造器
public Employee(double s) { // calls Employee(String, double) this("Employee #" + nextld, s); nextld++; }当调用 new Employee(60000) 时, Employee(double) 构造器将调用 Employee(String,double) 构造器。
采用这种方式使用 this 关键字非常有用, 这样对公共的构造器代码部分只编写一次即可。
7. 初始化块
1)初始化数据域的方法:
- 1)在构造器中设置值
- 2)在声明中赋值
- 3)初始化块 --- 不常见,通常将初始化代码放在构造器中
2)调用构造器的具体处理步骤:
- 1 ) 所有数据域被初始化为默认值(0、false 或 null。)
- 2 ) 按照在类声明中出现的次序, 依次执行所有域初始化语句和初始化块。
- 3 ) 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体
- 4 ) 执行这个构造器的主体
3)如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。
----- Employee.java -------- package constructtest; import java.util.Random; public class Employee { private static int nextId; private int id; /** * instance field initialization */ private String name = ""; private double salary; // static initialization block { Random generator = new Random(); nextId = generator.nextInt(1000); } // object initialization block { id = nextId; nextId++; } /** * three overloaded constructors * @param n * @param s */ public Employee(String n, double s) { name = n; salary = s; } public Employee(double s) { // calls the Employee(String, double) constructor this("Empolyee #" + nextId, s); } /** * the default constructor */ public Employee(){ // name initialized to "" -- see above // salary not explicitly set -- initialized to 0 // id initialized in initialization block } public int getId() { return id; } public String getName() { return name; } public double getSalary() { return salary; } }
----- ContructorTest.java --------- package constructortest; public class ConstructorTest { public static void main(String[] args) { Employee[] staff = new Employee[3]; staff[0] = new Employee("Harry", 40000); staff[1] = new Employee(60000); staff[2] = new Employee(); for (Employee e : staff) { System.out.println("name = " + e.getName() + ", id = " + e.getId() + ", salary = " + e.getSalary()); } } }
----- output ----- name = Harry, id = 494, salary = 40000.0 name = Empolyee #495, id = 707, salary = 60000.0 name = , id = 900, salary = 0.0
8. 对象析构与 finalize 方法
- Java有自动的垃圾回收器,所以不支持析构器
- 当使用了内存以外的资源,可为任何类 添加 finalize 方法,但不建议使用,因为不知道什么时候才能够调用
- 对象用完后,可以通过 close 方法,具体看 7.2.5节
4.7 包
1. 类的导入
一个类可以使用所属包中的所有类, 以及其他包中的公有类( public class)。两种方法方式: 1)完整包名,如 java.time.LocalDate today = java.time.LocalDate.now(); 2)import java.util.*; LocalDate today = LocalDate.now();* 如果包中都还有类 或 包中相同的类都要被使用时,
import java.time.LocalDate; import java.util.*; import java.sql.*; // 1) add special import import java.util.Date; public class PackageTest { public static void main(String[] args) { LocalDate today = LocalDate.now(); System.out.println(today); Date deadline = new Date(); System.out.println(deadline); // 2) add full package name before the class java.util.Date deadline2 = new java.util.Date(); java.sql.Date today2 = new java.sql.Date(2021, 12, 25); System.out.println("deadline2: " + deadline2 + ", today2: " + today2); } }
2. 静态导入
// 在源文件的顶部引用,就可以导入 静态方法和静态域,而不必加类名前缀。但实际上不是很清晰 import static java.lang.System.*; out.println("Hello, World!") // i.e, System.out exit(0); //i.e, System.out
3. 将类放在包中
4. 包作用域
- 标记为 public 的部分可以被任意的类使用;
- 标记为 private 的部分只能被定义它们的类使用。
- 如果没有指定 public 或 private ,如示例 4-2 中的 Employee 类,仅定义为“class Employee”, 这个部分(类、方法或变量)可以被同一个包中的所有方法访问
- 类可以这样定义,但是变量必须显式定义
4.8 类路径
- 类存储在文件系统的子目录中,且类的目录必须与包名匹配
- 类文件也可以存储在 JAR(Java 归档)文件中。在一个 JAR 文件中, 可以包含多个压缩形式的类文件和子目录【JDK的 /jre/lib/rt.jar】
- 1 ) 把类放到一个目录中, 如 /home/user/classdir。注意, 这个目录是包树状结构的基目录。如希望将 com.horstmann.corejava.Employee 类添加其中,Employee.class 必须位于子目录 /home/user/classdir/com/horstmann/corejava 中
- 2 ) 将 JAR 文件放在一个目录中,例如:/home/user/archives
- 3 ) 设置类路径(classpath)。类路径是所有包含类文件的路径的集合
- UNIX:类路径采用冒号 (:) 分隔:/home/user/classdir:.:/home/use r/archives/archive.jar
- Windows:分号(;)分割: c:\classdir;.;c:\archives\archive.jar
=>
- 基目录:/home/user/classdir 或 c:\classdir
- 当前目录: .
- JAR 文件 /home/user/achives.jar 或 c:\achives\achive.jar
2)另外,可以在 JAR 文件目录中指定通配符, 如 /home/user/dassdir:.:/home/aser/archives/'*' 或 c:\classdir;.;c:\archives\*
这样,archives 目录的所有 JAR 文件都包含在类路径中 3) javac 编译器总是在当前的目录中查找文件, 但 Java 虚拟机仅在类路径中有目录的时候才查看当前目录,如没有设置类路径, 也不会产生什么问题, 默认的类路径包含目录 . 然而如果设置了类路径却忘了包含目录, 则程序仍然可以通过编译, 但不能运行。4) 类路径示例:/home/user/classdir:.:/home/use r/archives/archive.jar
虚拟机查找的 com.horstmann.corejava.Employee 类文件的顺序:
- 1)jre/lib 和 jre/lib/ext 目录下的归档文件中所存放的系统类文件
- 2)查看类路径
- 3)查找如下文件:
- /home/user/classdir/com/horstmann/corejava/Employee.class
- com/horstmann/corejava/Employee.class 从当前目录开始
- com/horstmann/corejava/Employee.class inside /home/user/archives/archive.jar
import java.util.*;
import com.horstmann.corejava.*; 并且源代码引用了 Employee 类。 编译器将试图查找 java.lang.Employee (因为java.lang 包被默认导入)、java.util.Employee、 com.horstmann.corejava.Employee 和当前包中的 Employee。2. 设置类路径
1)使用 -classpath:
java -classpath /home/user/dassdir:.:/home/user/archives/archive.jar HyProg (Linux)
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg (Windows)
2)通过配置 ClASSPATH 环境变量
3)shell 脚本
export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar (Linux)
set CLASSPATH=c:\classdir;.;c:\archives\archive.jar (Windows)
4.9 文档注释
1. javadoc
文档注释工具 javadoc,从下面几处抽取信息
- 包
- 公有类与接口
- 公有的和受保护的构造器及方法
- 公有的和受保护的域
2. 各种注释
| 注释类型 | 标识 | 说明 |
| 类注释 |
/** */ |
类注释必须在 import 之后,类定义之前 |
| 方法注释 |
1. @param 变量描述 |
一个方法的所有 @param 标记必须放在一起 |
|
2. @return 返回描述 |
||
|
3. @throws 异常描述 |
||
| 域注释 |
/** */ |
只需要对公有域(通常指的是静态常量)建立文档 |
| 通用注释 |
1. @author 姓名 |
|
| 2. @version 当前版本 | ||
| 3. @since 引入特性的版本描述 | ||
| 4. @deprecated 对类、 方法或变量添加一个不再使用的注释 |
@deprecated Use setVIsible(true) instead
通过 @see 和@link标记,可以使用超级链接, 链接到 javadoc 文档的相关部分或外
部文档。
|
|
|
5. @see 引用 1)@see com.horstraann.corejava.Employee#raiseSalary(double) 2)@see The Core ]ava home page 3)@see "Core Java 2 volume 2" |
将在“ see also” 部分增加一个超级链接。它可以用于类中,也可以用于方法中 1)建立一个链接到 com.horstmann.corejava.Employee 类的 raiseSalary(double) 方法的超 链接。一定要用“#”分隔 2)@see 后面是 “<” 时,必须是 超链接 3)@see 后面是 “”” 时,文本就会显示在 “see also”部分 可以为一个特性添加多个 @see,但必须放在一起 | |
| 包与概述注释 |
1. 包注释:在每一个包目录中添加一个单独的文件 Option1: package.html 文件,在标记 ... 之间的所有文本都会被抽取出来Option2: package-info.java 文件, 必须包含一个初始的以 /** 和 */ 界定的 javadoc 注释,跟随在一个包语句之后。 不包含更多的代码或注释 2. 概述注释:overview.html 文件,放在包含所有源文件的父目录中, 在标记 ... 之间的所有文本都会被抽取出来 |
3. 生成 Javadoc
1)命令
- 多个包:javadoc -d docDirectory nameOfPackage1 nameOfPackage2
- 文件在默认包:javadoc -d docDirectory *.java
其他选项:
- -author / -version:添加 @author / @version 标记
- -link:为标准类添加超链接,如 javadoc -link http://docs.oracle.com/javase/8/docs/api *.java
2)IdeaJ 生成 javadoc : https://blog.csdn.net/qq_42498502/article/details/90078972
4.10 类设计技巧
- 1. 数据私有
- 2. 显式地初始化所有的数据:提供默认值或通过构造器设置默认值
- 3. 不要在类中使用过多的基本类型
- 4. 不是所有的域都需要独立的域访问器和域更改器
- 5. 将职责过多的类进行分解
- 6. 类名和方法名要能够体现它们的职责:类名的定义可以清楚地理解其含义;访问器用 get 开头(getSalary),更改器用 set 开头(setSalary)
- 7.优先使用不可变的类