第二章 对象和类

一、面向对象程序设计概念

面向对象程序设计(OOP)是当今主流的程序设计范型,它取代了 20 世纪 70 年代的 “结构化” 或过程式编程技术。而 Java 正是使用了面向对象的编程思想。

二、使用预定义类

在 Java 中,没有类就无法做任何事情。然而,并不是所有的类都表现出面向对象的典型特征。例如,Math 类。可以直接使用 Math 类的方法,如 Math.random,而不必了解它具体是如何实现的,你只需要知道方法名和参数(如果有的话)。这正是封装的关键所在,当然所有类都是这样。但 Math 类之封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必考虑创建对象和初始化它们的实例字段,因为根本没有实例字段!

1、对象与对象变量

要想使用对象,首先必须构造对象,并指定其初始状态。然后对对象应用方法。
在 Java 程序设计语言中,要使用构造器(contructor,或称构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。

构造器的名字应该与类名相同。因此 Date 类的构造器名为 Date。要想构造一个 Date 对象,需要在构造器前面加上 new 操作符,如下所示:

new Date()

这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。
如果需要的话,也可以将这个对象传递给一个方法:

System.out.println(new Date());

或者,也可以对刚刚创建的对象应用一个方法。Date 类中有一个 toString 方法。这个方法将返回日期的字符串描述。下面的语句可以说明如何对新构造的 Date 对象应用在 toString 方法。

String s = new Date().toString();

在这两个例子中,构造的对象仅使用了一次。通常,你会希望构造的对象可以多次使用,因此,需要将对象存放在一个变量中:

Date birthday = new Date();

所有的 Java 对象都存储在堆中。当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。

2、Java 类库中的 LocalDate 类

在前面的例子中,已经使用了 Java 标准类库中的 Date 类。Date 类的实例有一个状态,即特定的时间点。

但是,Date 类对于处理人类记录日期的日历信息并不是很有用,如:“December 31, 1999”。

类库设计者决定将保存时间与时间点命名分开。所以标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类;另一个是用大家熟悉的日历表示法表示日期的 LocalDate 类。

不要使用构造器来构造 LocalDate 类的对象。实际上,应当使用静态工厂方法,它会代表你调用构造器。下面的表达式:LocalDate.now() 会构造一个新对象,表示构造这个对象时的日期。

可以提供年、月和日来构造一个特定日期的对象:

LocalDate.of(1999, 12, 31)

当然,我们通常希望将构造的对象保存在一个对象变量中:

LocalDate newYearEve = LocalDate.of(1999, 12, 31);

一旦有了 LocalDate 对象,可以用方法 getYear、getMonthValue 和 getDayOfMonth 得到年、月和日:

int year = newYearEve.getYear(); // 1999
int month = newYearEve.getMonthValu(); // 12
int day = newYearEve.getDayOfMonth(); // 31

实际上,Date 类也有得到日、月、年的方法,分别是 getDay、getMonth 以及 getYear,不过这些方法已经废弃。

三、用户自定义类

1、Employee 类

我们来看一个非常简单的 Employee 类的使用。

public class EmployeeTest {
	public static void main(String[] args) {
		// 用三个 Employee 对象填充人员数组
		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);

		// 每个人提薪 5%
		for (Employee e: staff)
			e.raiseSalary(5);
		
		// 打印出所有 Employee 对象的信息
		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 hireDate;
	
	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;
	}
}

在这个程序中,我们构造了一个 Employee 数组,并填入了 3 个 Employee 对象:

Employee staff = new Employee[3];

staff[0] = new Employee("Carl Cracker", ...);
staff[1] = new Employee("Harry Hacker", ...);
staff[2] = new Employee("Tony Tester", ...);

接下来,使用 Employee 类的 raiseSalary 方法将每个员工薪水提高 5%:

for (Employee e: staff)
	e.raiseSalary(5);

最后,调用 getName 方法、getSalary 方法和 getHireDay 方法打印各个员工的信息:

for (Employee e: staff)
	System.out.println("name=" + e.getName()
		+ ",salary=" + e.getSalary()
		+ ",hireDay=" + e.getHireDAY());

注意,在这个示例程序中包含两个类:Employee 类和带有 public 类访问修饰符的 EmployeeTest 类。EmployeeTest 类包含了 main 方法。

源文件名是 EmployeeTest.java,这是因为文件名必须与 public 类的名字相匹配。在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。

当编译这段源代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.class 和 Employee.class。

2、多个源文件的使用

在上面的例子中,一个源文件包含了两个类。在开发过程中,我们习惯于将每个类存放在一个单独的源文件中。例如,将 Employee 类存放在文件 Employee.java 中,将 EmployeeTest 类存放在文件 EmployeeTest.java 中。

这样组织文件,可以有两种编译源程序的方法。一种是使用通配符调用 Java 编译器:javac Employee*.java。这样一来,所有与通配符匹配的源文件都将被编译成类文件。或者键入一下命令:javac EmployeeTest.java

你可能会感到惊讶,使用第二种方式时并没有显式地编译 Employee.java。不过,当 Java 编译器发现 Employee.java 使用了 Employee 类时,它会查找名为 Employee.class 的文件。如果没有找到这个文件,就会自动地搜索 Employee.java,然后,对它进行编译。更重要的是:如果Employee.java 版本较已有地 Employee.class 文件版本更新,Java 编译器就会自动地重新编译这个文件。

3、剖析 Employee 类

下面我们来对 Employee 进行剖析。首先从这个类的方法开始。通过查看源代码会发现,这个类包含一个构造器和 4 个方法:

public Employee(String n, double s, int year, int month, int day)
public String getName()
private double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)

这个类地所有方法都被标记为 public。关键字 public 意味着任何类的任何方法都可以调用这些方法。

接下来,需要注意在 Employee 类的实例中有 3 个实例字段用来存放将要操作的数据:

private String name;
private double salary;
private LocalDate hireDay;

关键字 private 确保只有 Employee 类自身的方法能够访问这些实例字段,而其他类的方法不能够读写这些字段。

4、从构造器开始

下面来看看 Employee 类的构造器:

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

可以看到,构造器与类同名。在构造 Employee 类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态。

例如,当使用下面这条代码创建 Employee 类的实例时:

new Employee("James Bond", 100000, 1950, 1, 1)

将会把实例字段设置为:

name = "James Bond";
salary = 100000;
hireDay = LocalDate.of(1950, 1, 1); // January 1, 1950

构造器与其他方法有一个重要的不同。构造器总是结合 new 运算符来调用。不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的。例如,

James.Employee("James Bond", 1950, 1, 1) // ERROR

将产生编译错误。

目前我们只需要记住:

  • 构造器与类同名。
  • 每个类可以有一个以上的构造器。
  • 构造器可以有 0 个、1 个或多个参数。
  • 构造器没有返回值。
  • 构造器总是伴随着 new 操作符一起调用。

5、用 var 来声明局部变量

在 Java 10 中,如果可以从变量的初始值推导出它们的类型,那么可以用 var 关键字声明局部变量,而无须指定类型。例如,可以不这样声明:

Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);

只需要写以下代码:

var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);

这一点很好,因为这样可以避免重复写类型名 Employee。

注意:var 关键字只能用于方法中的局部变量。参数和字段的类型必须声明。

6、封装的优点

最后再仔细看一下非常简单的 getName 方法、getSalary 方法和 getHireDay 方法。

public String getName() {
	return name;
}

public double getSalary() {
	return salary;
}

public LocalDate getHireDay() {
	return hireDay;
}

这些是典型的访问器方法。由于它们只返回实例字段值,因此又称为字段访问器。

如果将 name、salary 和 hireDay 字段标记为公共,而不是编写单独的访问其方法,难道不是更容易一些吗?

不过,name 是一个只读字段。一旦在构造器中设置,就没有任何办法可以对它进行修改,这样我们可以确保 name 字段不会受到外界的破坏。

虽然 salary 不是只读字段,但是它只能用 raiseSalary 方法修改。特别是一旦这个值出现了错误,只需要调试这个方法就可以了。如果 salary 字段是公共的,破坏这个字段值的捣乱者有可能出没在任何地方(那就很难调试了)。

有些时候,可能想要获得或设置实例字段的值。那么你需要提供下面三项内容:

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

这样做要比提供一个简单的公共数据字段复杂些,但却有着下列明显的好处:

首先,可以改变内部实现,而除了该类的方法之外,这不会影响其他代码。例如,如果将存储名字的字段改为:

String firstName;
String lastName;

那么 getName 方法可以改为返回

firstName + " " + lastName

这个改变对于程序的其他部分是不可见的。

7、私有方法

在实现一个类时,由于公共数据非常危险,所以应该将所有的数据字段都设置为私有的。然而,方法又应该怎么设计呢?尽管绝大多数方法都被设计为公共的,但在某些特殊情况下,将方法设计为私有可能很有用。有时,你可能希望将一个计算代码分解成若干个独立的辅助方法。通常,这些辅助方法不应该成为公共接口的一部分。最好将这些方法设计为私有方法。

在 Java 中,要实现私有方法,只需将关键字 public 改为 private 即可。

8、final 实例字段

可以将实例字段设置为 final。这样的字段必须在构造对象时初始化。也就是说,必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。例如,可以将 Employee 类中的 name 字段声明为 final,因为在对象构造之后,这个值不会再改变,即没有setName 方法。

class Employee {
	private final String name;
	...
}

final 修饰符对于类型为基本类型或者不可变类的字段尤其有用。(如果类中的所有方法都不会改变其对象,这样的类就是不可变的类。例如,String 类就是不可变的。)

对于可变的类,使用 final 修饰符可能会造成混乱。例如,考虑以下字段:

private final StringBuilder evaluations;

它在 Employee 构造器中初始化为:evalustions = new StringBuilder();

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

public void giveGoldStar() {
	evaluations.append(LocalDate.now() + ": Gokd star!\n");
}

四、静态字段与静态方法

1、静态字段

如果将一个字段定义为 static,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。例如,假设需要给每一个员工赋予唯一的标识码。这里给 Employee 类添加一个实例字段 id 和一个非静态字段 nextId:

class Employee {
	private static int nextId = 1;
	private int id;
	...
}

现在,每一个 Employee 对象都有一个自己的 id 字段,但这个类的所有实例将共享一个 nextId 字段。换句话说,如果有 1000 个 Employee 类对象,则有 1000 个实例字段 id,分别对应每一个对象。但是,只有一个静态字段 nextId。即使没有 Employee 对象,静态字段 nextId 也存在。它属于类,而不属于任何单个的对象。

下面实现一个简单的方法:

public void setId() {
	id = nextId;
	nextId++;
]

假定为 harry 设置员工标识码:harry.setId();
harry 的 id 字段将被设置为静态字段 nextId 当前的值,并且静态字段 nextId 的值加 1:

harry.id = Employee.setId;
Employee.nextId++;

2、静态常量

静态变量使用的比较少,但静态常量却很常用。例如,在 Math 类中定义一个静态常量:

public class Math {
	...
	public static final double PI = 3.14159265358979323846;
	...
}

在程序中,可以用 Math.PI 来访问这个常量。

如果省略关键字 static,PI 就变成了 Math 类的一个实例字段。也就是说,需要通过 Math 类的一个对象来访问 PI,并且每一个 Math 对象都有它自己的一个 PI 副本。

我们已经多次使用过的另一个静态常量是 System.out。它在 System 类中声明如下:

public class System {
	...
	public static final PrintStream out = ...;
	...
}

3、静态方法

静态方法是不在对象上执行的方法。例如,Math 类的 pow 方法就是一个静态方法。表达式 Math.pow(x, a) 会计算幂 x²。在完成运算时,它并不使用任何 Math 对象。

Employee 类的静态方法不能访问 id 实例字段,因为它不能在对象上执行操作。但是,静态方法可以访问静态字段。下面是这样一个静态方法的示例:

public static int getNextId() {
	return nextId; // returns static field
}

可以提供类名来调用这个方法:

int n = Employee.getNextId();

五、对象构造

前面已经编写过简单的构造器,可以定义对象的初始状态。但是,由于对象构造器非常重要,所以 Java 提供了多种编写构造器的机制。

1、重载

有些类可以有多个构造器。例如,可以如下构造一个空的 StringBuilder 对象:

var message = new StringBuilder();

或者,可以指定一个初始字符串:

var todoList = new StringBuilder("To do:\n");

这种功能叫做重载(overloading)。如果多个方法(比如,StringBuilder 构造器方法)有相同的名字、不同的参数,便出现了重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器找不到匹配的参数,将会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好(这个查询匹配的过程被称为重载解析)。

Java 允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指定方法名以及参数类型。这叫做方法的签名(signature)。

返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却有不同返回类型的方法。

2、默认字段初始化

如果构造器中没有显式地为字段设置初值,那么就会自动地赋值为默认值:数值为 0、布尔值为 false、对象引用为 null。

这是字段与局部变量的一个重要区别。方法中的局部变量必须明确地初始化。但是在类中,如果没有初始化类中的字段,将会自动初始化为默认值(0、false 或 null)。

3、无参数的构造器

很多类都包含一个无参数的构造器,由无参数构造器创建对象时,对象的状态会设置为适当的默认值。例如,以下是 Employee 类地无参数构造器:

public Employee {
	name = "";
	salary = 0;
	hireDay = LocalDate().now();
}

如果写一个类时没有编写构造器,将会为你提供一个为u参数构造器。这个构造器将所有的实例字段都设置为默认值。于是,实例中的数值型数据设置为 0,布尔型数据设置为 false,所有对象变量都设置为 null。

如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。例如,Employee 类提供了一个简单的构造器:

public Employee(String n, double s, int year, int month, int day)

对于这个类,构造默认地员工就是不合法的。也就是说,调用 e = new Employee(); 将会产生错误。

仅当类没有任何其他构造器的时候,你才会得到一个默认的无参构造器,编写类的时候,如果写了一个你自己的构造器,要想让这个类的用户能够通过以下调用构造一个实例:new ClassName()。你就必须提供一个无参数的构造器。当然,如果希望所有字段被赋予默认值,只需要提供以下代码:public ClassName() {}

4、参数名

在编写很小的构造器时(这十分常见),常常在参数命名时感到困惑。

我们通常喜欢用单个字母作为参数名:

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

但这样做有一个缺点:只有阅读代码才能够了解参数 n 和参数 s 的含义。

有些时候我们喜欢在每个参数前面加上一个前缀 “a”:

public Employee(String aName, double aSalary) {
	name = aName;
	salary = aSalary;
}

这样很清晰。一眼就可以看懂参数的含义。

这是一种常用的技巧,它基于这样的事实:参数变量会遮蔽同名的实例字段。例如,如果将参数命名为 salary,salary 将指示这个参数,而不是实例字段。但是,还是可以用 this.salary 访问实例字段。回想一下,this 指示隐式参数,也就是所构造的对象。下面是一个例子:

public Employee(String name, double salary) {
	this.name = name;
	this.salary = salary;
}

5、调用另一个构造器

关键字 this 指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。

如果构造器的第一个语句形如 this (…),这个构造器将调用同一个类的另一个构造器。下面是一个典型的例子:

public Employee(double s) {
	// calls Employee(String, double)
	this("Employee #" + nextId, s);
	nextId++;
}

当调用 new Employee(60000) 时,Employee(double) 的构造器将调用 Employee(String, double) 构造器。

采用这种方式使用 this 关键字非常有用,这样对公共的构造器代码只需要编写一次即可。

6、初始化块

前面已经用过两种初始化数据字段的方法:

  • 在构造器中设置值;
  • 在声明中赋值。

实际上,Java 还有第三种机制,称为初始化块。在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。例如,

class Employee {
	private static int nextId;
	
	private int id;
	private String name;
	private double salary;
	
	// object initiazation block
	{
		id = nextId;
		nextId++;
	}

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

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

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

这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。

由于初始化数据有多种途径,所以列出构造过程的所有路径可能让人很困惑。下面是调用构造器的具体处理步骤:

  1. 如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。
  2. 否则,
    a)所有数据字段初始化为其默认值(0、false 或 null)。
    b)按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块。
  3. 执行构造器主体代码。

可以提供一个初始值,或者使用一个静态的初始化块来初始化静态字段。前面已经介绍过的一种机制:

private static int nextId = 1;

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

将代码放在一个块中,并标记关键字 static。下面是一个示例。其功能是将员工 ID 的起始值赋予一个小于 10000 的随机整数。

// static initialization block
static {
	var generator = new Random();
	nextId = generator.nextInt(10000);
}

在类第一次加载的时候,将会进行静态字段的初始化。与实例字段一样,除非将静态字段显式地设置成其他值,否则默认的初始值为 0、false 或 null。所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行。

java.util.Random

  • Random()
    构造一个新的随机数生成器。
  • int nextInt(int n)
    返回一个 0 ~ n-1 之间的随机数。

六、包

Java 允许使用包(package)将类组织在一个集合中。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。

1、包名

使用包的主要原因是确保类名的唯一性。假如创建了两个 Employee 类,只要将这些类放置在不同的包中,就不会产生冲突。

2、类的导入

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

我们可以采用两种方式访问另一个包中的公共类。第一种方式就是使用完全限定名;就是包名后面跟着类名。例如:

java.time.LocalDate today = java.time.LocalDate.now();

这显然很繁琐。更简单且更常用的方式是使用 import 语句。import 语句是一种引用包中各个类的简捷方式。一旦使用了 import 语句,在使用类时,就不必写出类的全名了。

可以使用 import 语句导入一个特定的类或者整个包。import 语句应该位于源文件的顶部(但位于 package 语句的后面)。例如,可以使用下面这条语句导入 java.util 包中的所有类。

import java.time.*;

然后,就可以使用

LocalDate today = LocalDate.now();

而无须在前面加上包前缀。还可以导入一个包中的特定类:

import java.time.LocalDate;

java.time.* 的语法比较简单,对代码的规模也没有任何负面影响。不过,如果能够明确地指出所导入的类,代码的读者就能更加准确的知道你使用了哪些类。

注意:只能使用星号(*)导入一个包,而不能使用 import java.* 或 import java.*.* 导入以 java 为前缀的所有包。

大多数情况下,可以只导入你需要的包,并不必过多地考虑它们。但在发生命名冲突的时候,就要注意包了。例如,java.util 和 java.sql 包都有 Date 类。如果程序中导入了这两个包:

import java.util.*;
import java.sql.*;

在程序中使用 Date 类的时候,就会出现一个编译错误:

Date today; // ERROR--java.util.Date or java.sql.Date?

此时编译器无法确定你想使用的是哪一个 Date 类。可以增加一个特定的个 Import 语句来解决这个问题:

import java.util.*;
import java.sql.*;
import java.util.Date;

如果这两个类都需要使用,又该怎么办呢?答案是,在每个类名的前面加上完整的包名:

var deadLine = new java.util.Date();
var today = new java.sql.Date(...);

七、类设计技巧

对于类的设计,以下这些技巧可以使你设计的类得到专业 OOP 圈子的认可。

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

这是最重要的;绝对不要破坏封装性。有时候,可能需要编写一个访问器方法或更改器方法,但是最好还是保持实例字段的私有性。很多惨痛的教训告诉我们,数据的表示形式很可能会改变,但它们的使用方式却不会经变化。当数据保持私有时,表示形式的变化不会对类的使用者产生影响,而且也更容易检测bug。

  1. 一定要对数据惊醒初始化。

Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,可以提供默认值,也可以在所有构造器中设置默认值。

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

这个想法是要用其他的类替换使用多个相关的基本类型。这样会使类更易于理解,也更易于修改。例如,用一个名为Address的新类替换一个Customer类中以下的实例字段:

private String street;
private String city;
private String state;
private int zip;

这样一来,可以很容易地处理地址的变化,例如,可能需要处理国际地址。

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

你可能需要获得或设置员工的工资。而一旦构造了员工对象,肯定不需要更改雇用日期。另外,在对象中,常常包含一些不希望别人获得或设置的实例字段,例如,Address类中的州缩写数组。

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

这样说似乎有点含糊,究竟多少算是“过多”?每个人的看法都不同。但是,如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解(但另一方面,也不要走极端。如果设计10个类,每个类只有一个方法,显然就有些矫枉过正了)。

下面是一个反面的设计示例。

public class CardDeck // bad design
{
    private int[] value;
    private int[] suit;
    
    public CardDeck() { . . . }
    public void shuffle() { . . . }
    public int getTopValue() { . . . }
    public int getTopSuit() { . . . }
    public void draw() { . . . }
}

实际上,这个类实现了两个独立的概念:一副牌(包含shuffle方法和draw方法)和一张牌(包含查看面值和花色的方法)。最好引入一个表示一张牌的Card类。现在有两个类,每个类完成自己的职责:

public class CardDeck
{ 
    private Card[] cards;
    public CardDeck() { . . . }
    public void shuffle() { . . . }
    public Card getTop() { . . . }
    public void draw() { . . . }
}

public class Card
{
    private int value;
    private int suit;
    public Card(int aValue, int aSuit) { . . . }
    public int getValue() { . . . }
    public int getSuit() { . . . }
}
  1. 类名和方法名要能够体现它们的职责。

与变量应该有一个能够反映其含义的名字一样,类也应该如此(在标准类库中,也存在着一些含义不明确的例子,如Date类实际上是一个用于描述时间的类)。对于方法来说,要遵循标准惯例:访问器方法用小写get开头(getSalary),更改器方法用小写的set开头(setSalary)。

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

LocalDate类以及java.time包中的其他类是不可变的——没有方法能修改对象的状态。类似plusDays的方法并不是更改对象,而是返回状态已修改的新对象。

更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。

因此,要尽可能让类是不可变的,这是一个很好的想法。对于表示值的类,如一个字符串或一个时间点,这尤其容易。计算会生成新值,而不是更新原来的值。
当然,并不是所有类都应当是不可变的。如果员工加薪时让raiseSalary方法返回一个新的Employee对象,这会很奇怪。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一支帆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值