读后笔记 -- 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
2. 静态常量
// 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. 为了使类能够被多个程序共享,需要做到下面几点:
  • 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 指令,确定其中是否包含了被引用的类。

      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.优先使用不可变的类