Java 核心技术卷一基础知识第 10 版

#对象与类

封装(encapsulation)是与对象有关的一个重要概念。 从形式上看,封装不过是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域(instance field),操纵数据的过程称为方法( method)。对于每个特定的类实例(对象)都有一组特定的实例域值。

继承(inheritance):通过扩展一个类来建立另外一个类的过程。

一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。
局部变量不会自动地初始化为 null,而必须通过调用 new 或将它们设置为 null 进行初始化。

  • 更改器方法(mutator method):改变对象的状态。
  • 访问器方法(accessor method):只访问对象而不修改对象的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// MyCalendar.java
import java.time.LocalDate;

public class MyCalendar {
public static void main(String[] args) {
LocalDate nowDate = LocalDate.now();
int day = nowDate.getDayOfMonth();
System.out.println("Sun Mon Tue Wed Thu Fri Sat");
int week = nowDate.getDayOfWeek().getValue();
int n = (7 - (day - week) % 7) + 1;
for (int i = 0; i < n; i++) {
System.out.print(" ");
}
for (int i = 1; i <= nowDate.lengthOfMonth(); i++) {
n++;
System.out.printf("%3d", i);
if (n % 7 == 0) {
System.out.println();
} else {
if (i == day) {
System.out.print("*");
} else {
System.out.print(" ");
}
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.time.*;

/**
* This program tests the Employee class.
*
* @version 1.12 2015-05-08
* @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 LocalDate hireDay;

public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}

public String getName() {
return name;
}

public double getSalary() {
return salary;
}

public LocalDate getHireDay() {
return hireDay;
}

public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
  • 在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
  • 类通常包括类型属于某个类类型的实例域。
  • 构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
  • 构造器
    • 构造器与类同名
    • 每个类可以有一个以上的构造器
    • 构造器可以有 0 个、1 个或多个参数
    • 构造器没有返回值
    • 构造器总是伴随着 new 操作一起调用
  • 不要在构造器中定义与实例域重名的局部变量。
  • 一个方法可以访问所属类的所有对象的私有数据。

可以将实例域定义为 final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。

对于可变的类, 使用 final 修饰符可能会对读者造成混乱。

1
private final StringBuilder evaluations;

在 Employee 构造器中会初始化为

1
evaluations = new StringBuilder();

final 关键字只是表示存储在 evaluations 变量中的对象引用不会再指示其他 StringBuilder 对象。不过这个对象可以更改:

1
2
3
4
public void giveGoldStar()
{
evaluations.append(LocalDate.now() + ": Gold star!\n");
}

  • 如果将域定义为 static,每个类中只有一个这样的域。
  • 每一个对象对于所有的实例域却都有自己的一份拷贝。
  • 静态方法是一种不能向对象实施操作的方法。[1]
  • 静态方法可以访问自身类中的静态域。
  • 使用静态方法的情况:
    1. 方法不需要访问对象状态,其所需参数都是通过显式参数提供。
    2. 一个方法只需要访问类的静态域。

  • Java 程序设计语言总是采用 按值调用。也就是说, 方法得到的是所有参数值的一个拷贝,方法不能修改传递给它的任何参数变量的内容。
  • Java 的对象引用也是按值传递的。
  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

重载(overloading):多个方法有相同的名字、不同的参数。
重载解析(overloading resolution):编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好。

  • Java 允许重载任何方法,而不只是构造器方法。

  • 如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。

  • 如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法

  • 可以在类定义中, 直接将一个值赋给任何域。

    1
    2
    3
    4
    5
    class Employee
    {
    private String name = "";
    ...
    }
  • 在执行构造器之前,先执行赋值操作。

  • 如果构造器的第一个语句形如 this(…),这个构造器将调用同一个类的另一个构造器。

    1
    2
    3
    4
    5
    6
    public Employee(double s)
    {
    // calls the Employee(String, double) constructor
    this("Employee #" + nextId, s);
    // 对公共的构造器代码部分只编写一次即可。
    }
  • 初始化数据域的方法:

    1. 在构造器中设置值
    2. 在声明中赋值
    3. 初始化块(initialization block)
  • new 操作时,首先运行初始化块,然后才运行构造器的主体部分。通常会直接将初始化代码放在构造器中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Employee
    {
    private static int nextld;
    private int id;
    private String name;
    private double salary;

    // object initialization block
    {
    id = nextld;
    nextld++;
    }

    public Employee(String n, double s)
    {
    name = n;
    salary = s;
    }

    public Employee()
    {
    name = "";
    salary = 0;
    }
    ...
    }
  • 调用构造器的具体处理步骤:

    1. 所有数据域被初始化为默认值(0、false 或 null)。
    2. 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
    3. 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
    4. 执行这个构造器的主体。
  • 如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。

    1
    2
    3
    4
    5
    6
    7
    // static initialization block
    static
    {
    Random generator = new Random();
    // set nextId to a random number between 0 and 9999
    nextId = generator.nextInt(10000);
    }
  • 由于 Java 有自动的垃圾回收器,不需要人工回收内存, 所以 Java 不支持析构器。

  • 可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。


  • import语句应该位于源文件的顶部,但位于package语句的后面。

  • 如果能够明确地指出所导人的类,将会使代码的读者更加准确地知道加载了哪些类。

  • 在不同的包具有相同的类名冲突时,可以增加一个特定的import语句。

    1
    2
    3
    import java.util.*;
    import java.sql.*;
    import java.util.Date;
  • 如果都需要使用,就在每个类名的前面加上完整的包名。

    1
    2
    java.util.Date deadline = new java.util.Date();
    java.sql.Date today = new java.sql.Date(...);
  • 在包中定位类是编译器(compiler)的工作。类文件中的字节码肯定使用完整的包名来引用其他类。

  • import 语句不仅可以导人类,还增加了导人静态方法和静态域的功能。

    1
    2
    3
    4
    5
    6
    7
    import static java.lang.System.*;
    // 就可以使用 System 类的静态方法和静态域,而不必加类名前缀:
    out.println("Goodbye, World!"); // i.e., System.out
    exit(9); // i.e., System.exit

    // 另外,还可以导入特定的方法或域:
    import static java.lang.System.out;
  • 要想将一个类放人包中, 就必须将包的名字放在源文件的开头。

    1
    2
    3
    4
    5
    package com.horstmann.corejava;
    public class Employee
    {
    ...
    }
  • 如果没有在源文件中放置package语句,这个源文件中的类就被放置在一个默认包(default package)中。


  • 类设计技巧:
    1. 一定要保证数据私有
    2. 一定要对数据初始化
    3. 不要在类中使用过多的基本类型
    4. 不是所有的域都需要独立的域访问器和域更改器
    5. 将职责过多的类进行分解
    6. 类名和方法名要能够体现它们的职责
    7. 优先使用不可变的类

  1. 我自己的理解是,因为静态方法可以在不 new 的前提下使用,所以不能对需要 new 之后的对象操作。 ↩︎