本文内容参考《Java核心技术 卷Ⅰ》

用户自定义类

用var声明局部变量

首先是一个正常的变量声明,这是我们常用的声明方式。

1
2
3
4
5
6
7
8
9
class Employee {
int x;
}

public class Main {
public static void main(String[] args) {
Employee a = new Employee();
}
}

在Java 10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,这样可以避免重复写类名。使用var时需要注意,它只能用于方法中的局部变量,参数和字段的类型必须声明。

1
2
3
4
5
6
7
8
9
class Employee {
int x;
}

public class Main {
public static void main(String[] args) {
var a = new Employee();
}
}

使用null引用

如果对null值应用一个方法,会产生一个NullPointerException异常。

1
2
LocalDate birthday = null;
String s = birthday.toString(); // NullPointerException

在定义类时,我们最好清楚的知道哪些字段可能为null。我们有两种解决方法,第一种是“宽容型”方法,把null参数转换为一个适当的非null值,第二种是“严格型”方法,就是直接拒绝null参数,抛出异常。

1
2
3
4
5
6
7
8
// 宽容型
LocalDate birthday = null;
String s;
if (birthday != null) {
s = birthday.toString();
} else {
s = "unknown";
}

在Java 9中,Objects类对此提供了一个便利的方法。

1
2
3
4
5
6
7
8
9
10
11
// 相当于上面那种
LocalDate birthday = null;
String s = Objects.requireNonNullElse(birthday.toString(), "unknown");

// 严格型方法, 直接拒接null参数, 并抛出异常
String a = null;
String b = Objects.requireNonNull(a, "The name cannot be null");

Exception in thread "main" java.lang.NullPointerException: The name cannot be null
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at test.Main.main(Main.java:9)

封装

1
2
3
4
5
6
7
8
9
10
11
public String getName {
return name;
}

public double getSalary {
return salary;
}

public LocalDate getHireDay {
return hireDay;
}

这些是典型的访问器方法。由于它们只返回实例字段值,因此又称为字段访问器。有时候可能想要获得或设置实例字段的值,那么需要提供三项内容:

  • 一个私有的数据字段。
  • 一个公共的字段访问器方法。
  • 一个公共的字段更改器方法。

这样设置的好处是,可以改变内部实现,除了该类的方法之外,这不会影响到其他代码。

注意不要编写返回可变对象引用的访问器方法,例如:

1
2
3
4
5
6
7
class Employee {
private Date hireDay;

public Date getHireDay {
return hireDay;
}
}

Date类有一个更改器方法setTime。由于返回的DateEmployee类里的hireDay引用同一个对象,如果对返回的引用变量调用更改器方法,会将类里的变量一起改变。如果需要返回一个可变对象的引用,首先应该对他进行克隆(指存放在另一个新位置的对象副本)。

1
2
3
4
5
6
7
class Employee {
private Date hireDay;

public Date getHireDay {
return (Date) hireDay.clone();
}
}

基于类的访问权限

一个方法可以访问所属类的所有对象的私有数据。

1
2
3
4
5
6
7
class Employee {
private String name;

public boolean equals(Employee other) {
return name.equals(other.name);
}
}

典型的调用方法是

1
if (harry.equals(boss)) ...

这个方法不仅访问了harry的私有字段,还访问了boss的私有字段,这是合法的。因为bossEmployee类型的对象。而Employee类的方法可以访问任何Employee类型对象的私有字段。

final实例字段

可以将实例字段定义为final,这样的字段必须在构造对象时初始化。也就是说,必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。final修饰符对于类型为基本类型或者不可变类的字段非常有用。

对于可变的类,使用final修饰符可能会造成混乱,例如

1
private final StringBuilder evaluations;

它在构造器中初始化为

1
evaluations = new StringBuilder();

final关键字只是表示存储在evaluations变量中的对象引用不会再指向另一个不同的StringBuilder对象,但这个对象还可以更改

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

静态字段与静态方法

静态字段

1
2
3
4
class Employee {
private static int nextId = 1;
private int id;
}

对于Employee这个类来说,每个Employee对象都有一个自己的id字段,但这个类的所有实例将共享一个nextId字段。静态字段属于类,而不属于任何单个的对象。

静态方法

静态方法是不在对象上执行的方法,可以认为静态方法是没有this参数的方法(在一个非静态的方法中,this参数指示这个方法的隐式参数)。

静态方法不能访问非静态的实例字段,因为它不能在对象上执行操作。不过静态方法可以访问静态字段,例如

1
2
3
public static int getNextId() {
return nextId;
}

当然,这个方法也可以省略static关键字,但是省略就需要用过Employee类对象的引用来调用这个方法。

使用对象调用静态方法是合法的,假如harry是一个Employee对象,可以用harry.getNextId()代替Employee.getNextId()。但这种写法很容易造成混淆,所以还是建议用对象调用非静态方法,类名调用静态方法。

以下两种情况可以使用静态方法:

  • 方法不需要访问对象状态,因为它需要的所有参数都是通过显示参数提供(例如:Math.pow)。

  • 方法只需要访问类的静态字段(例如:Employee.getNextId)。

工厂方法

静态方法还有另一种常见的用途。类似LocalDateNumberFormat的类使用静态工厂方法来构造对象。NumberFormat类如下生成不同风格的格式化对象

1
2
NumberFormat currentFormatter = NumberFormat.getCompactNumberInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();

NumberFormat不利用构造器的原因是:

  • 无法命名构造器。构造器的名字必须与类名相同,但这里希望有两个不同的名字。

  • 使用构造器时,无法改变所构造对象的类型。而工厂方法实际上将返回DecimalFormat类的对象(NumberFormat的一个子类)。

方法参数

Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个副本。具体来讲,方法不能修改传递给它的任何参数变量内容。然而,有两种类型的方法参数:

  • 基本数据类型(数字、布尔值)。
  • 对象引用。

一个方法不能修改基本数据类型,但可以修改对象引用数据。因为方法得到的是对象引用的副本,原来的对象和这个副本都指向同一个对象。

有些人会误认为这是按引用传递,有一个反例,下面是一个交换Employee对象的方法

1
2
3
4
5
public static void swap(Employee x, Employee y) {
Employee temp = x;
x = y;
y = temp;
}

如果Java是按引用调用,那么这个方法就能实现交换

1
2
3
var a = new Employee();
var b = new Employee();
swap(a, b);

但是,这个方法并没有改变存储在变量ab中的对象引用。也就是说,swap方法其实是对xy这两个副本的交换。

总结:

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

对象构造

重载

如果有多个方法有相同的名字、不同的参数,就出现了重载,例如

1
2
3
public int foo();
public int foo(int x);
public int foo(int x, int y);

调用另一个构造器

关键字this指示一个方法的隐式参数,它还有另一个含义。如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器

1
2
3
4
public Employee(double s) {
this("Employee #" + nextId, s);
nextId++;
}

当调用new Employee(100)时,Employee(double)将调用Employee(String, double)构造。

初始化块

在一个类声明中,可以包含任意多个代码块。只要构造这个对象,这些块就会被执行。

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

{
id = nextId;
nextId++;
}

public Employee() {
name = "";
salary = 0;
}

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

无论使用哪个构造器,id字段都会在对象初始化块中初始化。首先运行初始化块,然后才运行构造器的主体部分。

如果静态字段需要很复杂的初始化代码,可以使用静态初始化块来初始化静态字段。

1
2
3
4
static {
var generator = new Random();
nextId = generator.nextInt(10000);
}

在类第一次加载的时候,将会进行静态字段的初始化。

类设计技巧

  1. 一定要保证数据私有。

    绝对不要破坏封装性。有时候可能需要编写一个访问器方法或更改器方法,但是最好还是保持实例字段的私有性。

  2. 一定要对数据进行初始化。

    Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖系统的默认值,而是应该显式地初始化所有数据。

  3. 不要在类中使用过多的基本类型。

    用其它类来替代使用多个相关的基本类型,例如

    1
    2
    3
    4
    5
    6
    7
    class Customer {
    ...
    private String street;
    private String city;
    private String state;
    private int zip;
    ...

    可以用一个名为Address的新类替换Customer类中的这几个实例字段。这样更容易处理地址的变化。

  4. 不是所有的字段都需要单独的字段访问器和字段更改器。

  5. 分解有过多职责的类。

  6. 类名和方法名要能够体现它们的职责。

  7. 优先使用不可变的类。