1. 定义类
下面是最简单的类的定义形式:
class ClassName {
field1
field2
...
constructor1
constructor2
...
method1
method2
...
}
下面是一个简单的 Employee(员工)类:
import java.time.LocalDate;
import java.util.Objects;
public class Employee {
// 姓名
private String name;
// 薪资
private double salary;
// 认知日期
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day) {
Objects.requireNonNull(name, "The name cannot be null");
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
/**
* 增加工资
* @param byPercent 百分比
*/
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
public static void main(String[] args) {
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);
for (Employee e : staff) {
e.raiseSalary(5);
}
for (Employee e : staff) {
System.out.println("name=" + e.getName() +
",salary=" + e.getSalary() +
",hireDay=" + e.getHireDay());
}
}
}
这个程序中,构造了一个 Employee 数组,并填入了 3 个 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);
接下来调用 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());
}
2. 剖析 Employee 类
Employee 类包含一个构造器和 4 个方法:
public Employee(String name, double salary, int year, int month, int day)
public String getName()
public 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 类自身的方法能够访问这些实例字段,而其他方法不能够读写这些字段。
3. 构造器
Java 对象都是在堆中构造的。
先看看 Employee 类的构造器:
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
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); // 1950-01-01
构造器与其他方法有一个重要的不同:构造器总是结合 new 运算符来调用。不能对一个已经存在的对象调用构造器来达到重新设置的字段目的。
有关构造方法,需要记住:
- 构造器与类同名
- 每个类可以有一个以上的构造器。
- 构造器可以有 0 个、1 个或多个参数。
- 构造器没有返回值。
- 构造器总是伴随着 new 操作符一起调用。
注意,不要在构造器中定义与实例字段同名的局部变量。例如:
public Employee(String n, double s, int y, int m, int d) {
String name = n; // 错误
double salary = s; // 错误
...
}
这个构造器声明了局部变量 name 和 salary。这些变量只能在构造器内部方法。这些变量会遮蔽同名的实例字段。这些程序员偶尔会不假思索地写出这类代码,因为它们的手指会不自觉地增加数据类型。这种错误很难检查出来,因此,必须注意在所有方法中都不要使用与实例字段同名的变量。
4. 用 var 声明局部变量
在 Java 10 中,如果可以从变量的初始值推导出它们的类型,那么可以用 var 关键字声明局部变量,而无需指定类型。例如:
Employee harry = new Employee("Harry Hacker", 500000, 1989, 10, 1);
可以改写为:
var harry = new Employee("Harry Hacker", 500000, 1989, 10, 1);
这样可以避免重复写类型为 Employee。
如果无须了解任何 Java API 就能从等号右边明显看出类型,在这种情况下可以使用 var 表示法。不要对数值类型使用 var,如 int、long 或 double,可以不用当心 0、0L 和 0.0 之间的区别。对 Java API 有了更多使用经验后,你可能会希望更多地使用 var 关键字。
注意: var 关键字只能用于方法中的局部变量。参数和字段的类型必须声明。
5. 使用 null 引用
一个对象变量可以包含一个对象的引用,或者包含一个特殊的 null,null 表示没有引用任何对象。
如果对 null 值应用一个方法,会产生一个 NullPointException 异常。
LocalDate birthday = null;
String s = birthday.toString(); // NullPointException
这是一个很严重的错误,类似于“索引越界”异常。如果你的程序没有“捕获”异常,程序就会终止。正常情况下,程序并不捕获这类异常,而是依赖于程序员从一开始就不要带来异常。
定义一个类时,最好清楚地知道哪些字段可能为 null。
针对可能为 null 的字段有两种解决方式:
- “宽容性”的方法,就是把 null 参数转换为一个适当的非 null 值:
在 Java 9 中,Objects 类对此提供了一个便利方法:public Employee(String name, double salary, int year, int month, int day) { if (name == null) { this.name = "unknown"; } else { this.name = name; } }
public Employee(String name, double salary, int year, int month, int day) { this.name = Objects.requireNonNullElse(name, "unknown"); }
- “严格型”方法则是干脆拒绝 null 参数:
如果 name 为 null 时,会抛出 NullPointerException 异常。使用这种方法有两个好处:public Employee(String name, double salary, int year, int month, int day) { Objects.requireNonNull(name, "The name cannot be null"); name = name; ... }
- 异常报告会提供这个问题的描述。
- 异常报告会准确地指出问题所在的问题所在位置,否则 NullPointException 异常可能在其他地放出现,而很难追踪到真正导致问题的这个构造方法。
如果要接受一个对象引用作为构造参数,就要认真思考是不是真的希望接受可有可无的值。如果不是,“严格型”方法更为合适。
6. 隐式参数与显式参数
方法用于操作对象以及存取它们的实例字段。
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
raiseSalary 方将会将调用这个方法的对象的 salary 实例字段设置为一个新值。
number007.raiseSalary(5);
它的结果是将 number007.salary 字段的值增加 5%。这个调用将执行下列指令:
double raise = number007.salary * 5 / 100;
number007.salary += raise;
raiseSalary 方法有两个参数。
- 第一个参数称为隐式(implicit)参数,是出现在方法名前的 Employee 对象。
- 第二个参数位 方法名后面括号中的数值,这是一个显式(explicit)参数。(有些人把隐式参数称为方法调用的目标或接受者。)
显式参数是明显地列在方法声明中的,隐式参数没有出现在方法声明中。在每一个方法中,关键字 this 表示隐式参数。
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
有些程序员更偏爱这样的风格,因为这样可以将实例域与局部变量明显地区分开来。
在 Java 中,所有的方法都必须在类的内部定义,但并不表示它们是内联方法。是否将某个方法设置为内联方法是 Java 虚拟机的任务。即时编译器会监事调用那些简洁、经常被调用、没有被重载以及可优化的方法。