跳转至

第4章 对象与类

对象变量是引用

java 中的对象变量类似于 C++ 中的对象指针,对象变量实质是引用某个对象,所以新建一个对象变量必须要用 new 构造函数()java 中不能得到一个对象本身,所有的对象都是在堆中构造的。

构造函数总是与 new 配合使用,不能对一个已经存在的对象调用构造器来打到重新设置实例字段的目的。如果要重设实例字段,还是需要配合 new 使用,这会引用一个新的对象。

某些类的对象并不使用构造函数来构造,而是使用 静态工厂方法 , 例如LocalDate类(用来表示某个日期)使用 LocalDate.now()LocalDate.of(),调用这些方法直接就能获得一个对象的引用(不需要 new )。

构造函数

默认字段初始化

  • 数值型的默认值为 0,布尔型默认值为 false,对象引用默认值为 null
  • 如果一个类没有构造函数,则默认会提供一个无参数的构造函数将所有实例字段设置为默认值。
  • 如果一个类已经写了至少一个构造函数,则不会提供默认的无参数构造函数。
  • 在构造函数中没有被赋值的字段会被赋为默认值。

重载

java 支持重载语法,包括构造函数和其他函数都可以重载。

null 值处理

直接定义一个对象变量(而不调用构造函数)会得到一个 null 。对 null 使用方法会产生异常。

调用构造函数时,可以对传入的参数检查,假设有这样的类:

class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;
}

其中 namehireDay 都有可能传入 null 值。对于可能传入的 null 值,第一种处理方法是赋一个默认对象变量,在 java 9 中, Objects 类提供了一个 .requireNonNullElse() 方法(所有类都扩展自 Objects 类):

public Employee(String n, double s, int year, int month, int day) {
    name = Objects.requireNonNullElse(n, "unknown");
    ...
}

使用这个方法,当 n == null 时会赋默认值 name = "unknown"

第二种处理是借助.requireNonNull() 方法,可以在 n == null 时抛出异常:

public Employee(String n, double s, int year, int month, int day) {
    Objects.requireNonNull(n, "The name cannot be null");
}

调用另一个构造函数

使用 this 关键字, 可以在一个构造函数中调用另一个构造函数:

public Employee(double s) {
    // 假设之前定义了 Employee(String n, double s)
    this("Employee #" + nextId, s);
    nextId++;
}

final

类似于 C++ 中的 const,用来表示常量,定义后不可修改。

final int maxn = 1010; // 不能更改的常量

实例字段如果设置为 final ,则必须在构造函数中设置 final 字段的值,并且设置之后不能更改。

初始化块

一共有三种初始化数据字段的方式,按照顺序为:

  1. 在声明时直接赋值。
  2. 在初始化块中赋值。
  3. 在构造函数中赋值。

如果构造函数的第一行调用了另一个构造函数,则会先执行第二个构造函数。否则,先执行初始化块。初始化块可以有多个,会按照顺序执行。

静态字段与静态方法

static 表示 静态字段,同一个类的所有对象都共享同一个字段。静态字段 通过类名调用。

例如要给每个员工分配一个递增的 id,则 nextID 变量用于记录下一个分配的 id 值。

class Employee {
    private static int nextID = 1;
    private int id;
    public void setId() {
        id = nextID++;
    }
}

static final 表示 静态常量,例如 Math.PI

静态方法 是指可以通过类名直接调用的方法,例如 Math.pow()

静态工厂方法 是指可以通过类名直接调用的构造函数,例如 LocalDate.now()LocalDate.of()

main() 函数的缩写是 psvm ,即 public static void main(String[] args)。每个类可以有一个 main 函数,用于对类进行单元测试。单元测试只会运行单独的一个 main 函数,例如 java Employee 就是运行 Employee.classmain 函数。

java 的参数传递是按值引用的

java 只有按值传递参数的方式,因此:

  • 方法不能修改基本数据类型的参数(即数值型或布尔型)。
  • 方法可以改变对象参数的状态。
  • 方法不能让一个对象参数引用一个新的对象。

类的导入

一个类可以使用所属包中的所有类,以及其他包中的公共类。

有两种方法访问其他包中的公共类:

  1. 使用完全限定名,例如:
    java.time.LocalDate today = java.time.LocalDate.now();
    
  2. 使用 import 导入整个包或者特定的类,例如:
    import java.time.LocalDate; //导入特定的类
    
    或者:
    import java.time.* //导入整个包
    
    注意只能使用使用 * 导入一个包,例如不能使用 import java.* ,因为 java.* 不止一个包。

java 中的 import 类似于 C++ 中的 using namespace ,因为 java 编译器可以查看其他文件的内容,而 C++ 编译器不行,所以在 C++ 中需要使用 #include

静态导入

使用 import static 可以导入静态方法和静态字段,例如:

import static java.lang.Math.sqrt;
public class Main {
    public static void main(String[] args) {
        double ans = sqrt(2.0);
        System.out.println(ans);
    }    
}

包路径

包和目录要匹配,虽然编译器编译时不检查目录结构,但是虚拟机运行时会按照包的结构去寻找类。所以推荐的做法就是按照包的结构设置文件的结构。

JAR文件

可以将 .class 文件打包在一个 .jar 文件中, .jar 文件实质就是 .zip 文件,这样可以节省空间、改善性能。

在使用第三方的库文件时,通常都会得到一个或者多个 .jar 文件。在以下这种项目结构中,通常将.jar 文件放在 lib 文件夹中。

.
├── bin
├── lib
└── src

在运行时,需要用 :lib 文件夹添加到 CLASSPATH 中,这样虚拟机

类设计技巧

  1. 一定要保证数据私有。
  2. 一定要对数据进行初始化。
  3. 不要在类中使用过多的基本类型(当基本类型过多时,考虑用一个新的类型替换相关的字段)。
  4. 不是所有的字段都需要单独的访问器和更改器。
  5. 分解有过多职责的类。
  6. 类名和方法名要能够体现它们的功能。
  7. 优先使用不可变的类(例如 LocalDate 类就是不可变的,这样可以保证线程安全)。

最后更新: 2023-12-17
创建日期: 2023-10-05