对象与类
使用预定义类
对象与类对象
要想使用对象,必须先构造对象,并指定其初始状态,然后对对象应用方法
用构造器构造新实例
1 2 3 |
Date birthday = new Date(); // 构造新对象 Date deadline = birthday; // 设置变量deadline使其引用birthday |
Java类库中的LocalDate类
Data类:表示时间点 LocalDate类: 用日历表示法表示日期
谁不要使用构造器来构造LocalDate类的对象,应当使用静态工厂方法
1 2 3 4 5 6 7 8 9 10 11 |
LocalDate.now() //构造一个新对象,表示构造这个对象的时间 LocalDate newYearsEve = LocalDate.of(1999, 12, 31); int year = newYearsEve.getYear(); // 1999 int month = newYearsEva.getMonthValue(); // 12 int day = newYearsEve.getDayOfMonth(); // 31 LocalDate aThousandDaysLater = newYearsEve.plusDays(1000); // 1000天后的新日期 int year = newYearsEve.getYear(); // 2002 int month = newYearsEva.getMonthValue(); // 09 int day = newYearsEve.getDayOfMonth(); // 26 |
用户自定义类
最常用的类定义为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class ClassName { field1 field2 ... constructor1 constructor2 ... method1 method2 ... } |
Employee类
以下是一个简单的Employee类及其使用
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 56 57 58 59 60 61 62 63 64 65 |
import java.time.LocalDate; public class EmployeeTest { 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()); } } class Employee { // fields private String name; private Double salary; private LocalDate hireDay; // constructor public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } // method 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; } } // output: // name=Carl Cracker,salary=78750.0,hireDay=1987-12-15 // name=Harry Hacker,salary=52500.0,hireDay=1989-10-01 // name=Tony Tester,salary=42000.0,hireDay=1990-03-15 |
构造器
1 2 3 4 5 6 |
public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } |
在构造类对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态
总是结合new运算符来调用,不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个,一个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作符一起调用
注意:
不要在构造器中定义与实例字段同名的局部变量,如,下面的构造器将不会设置salary
1 2 3 4 5 6 |
public Employee(String n, double s, ...) { String name = n; // ERROR double salary = s; // ERROR ... } |
用var声明局部变量
在Java10中,如果可以从变量的初始值推导出它们的类型,可以使用var关键字声明局部变量,无需指定类型
可以不这么声明:
1 |
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1); |
只需要写以下代码:
1 |
var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1); |
只能用于方法中的局部变量!参数和字段的类型必须声明
使用null引用
如果对null值应用一个方法,会产生一个NullPointerException异常。
1 2 |
LocalDate birthday = null; String s = birthday.toString(); // NullPointerException |
这是一个非常严重的错误
定义一个类时,最好要清楚哪些字段可能为null
1 |
if (n == null) name = "unknown"; else name = n; |
在java9中,objects类对此提供了一个便利方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// "宽容型" public Employee(String n, double s, int year, int month, int day) { name = Objects.requireNonNullElse(n, "unknown"); ... } // "严格型" public Employee(String n, double s, int year, int month, int day) { Objects.requireNonNull(n, "The name cannot be null"); name = n; ... } |
隐式参数与显式参数
对于raiseSalary方法,
1 2 3 4 5 |
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } |
有两个参数,第一个为隐式参数,是出现在方法名前的Employee的对象,没有出现在方法声明中 第二个参数是显示参数,是位于方法名后面括号中的数值
在每一个方法中,关键字this指示隐式参数
可以如下改写raiseSalary方法
1 2 3 4 5 |
public void raiseSalary(double byPercent) { double raise = this.salary * byPercent / 100; this.salary += raise; } |
final实例字段
final字段必须在构造对象时初始化。要确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字的
1 2 3 4 5 6 7 |
private final StringBuilder evaluaions; public void giveGoldstar() { evaluation.append(LocalDate.data() + ":Gold star!\n"); } |
final关键字表示存储在evaluations变量中的对象不会再指示另一个不同的StringBuilder对象。但这个对象可更改。
静态字段与动态字段
静态字符
如果将一个字段定义为static,每个类只有一个这样的字段。而对于非静态的实例字段每个对象都有自己的一个副本。
1 2 3 4 5 6 |
class Employee { private static int nextId = 1; private int id; } |
每一个Employee 对象都有一个自己的id字段,但这个类的所有实例将共享一个nextId 即使没有Employee对象,静态字段nextId也存在
1 2 3 4 5 |
public void setId() { id = nextId; nextId++; } |
静态常量
静态方法
静态方法是不在对象上执行的方法,可以认为是没有this参数的方法。
如Math类的pow方法:Math.pow(x, a)。在完成运算时,不使用任何Math对象。
在下面两种情况下可以使用静态方法:
- 方法不需要访问对象状态,(因为它需要的所有参数都通过显式参数提供)
- 方法只需要访问类的静态字段
main方法
main方法也是一个静态方法
1 2 3 4 5 6 7 8 |
public class Application { public static void main(String[] args) { // construct objects here // ... } } |
main方法不对任何对象进行操作,它将执行并构造程序所需要的对象
每一个类可以有一个main方法,这是常用于对类进行单元测试的一个技巧
若Employee类有一个静态的main方法,可用java Employee
执行main方法
方法参数
按值调用 — 按引用调用
java总是采用按值调用
交换两数a, b的错误方法:
1 2 3 4 5 6 7 8 9 10 11 |
// 交换两数a, b的错误方法 public static void swap(Employee x, Employee, y) { Employ temp = x; x = y; y = temp; } var a = Employee("Alice",...); var b = Employee("Bob"); swap(a, b); |
在java中方法参数能做什么、不能做什么
- 不能修改基本数据类型的参数(数值型/布尔型),如下tripleValue()
- 可以改变对象参数的状态,如下 tripleSalary()
- 不能让一个对象参数引用一个新的对象
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 |
public class ParamTest { public static void main(String[] args) { System.out.println("Testing tripleValue:"); double percent = 10; System.out.println("Before:percent=" + percent); // 10 tripleValue(percent); System.out.println("After:percent=" + percent); // 10 System.out.println("Testing tripleSalary:"); var harry = new Employee("Harry", 50000); System.out.println("Before:salary=" + harry.getSalary()); // 50000 tripleSalary(harry); System.out.println("After:salary=" + harry.getSalary()); // 50000 } public static void tripleValue(double x) { x = 3 * x; System.out.println("End of method:x=" + x); // 30 } public static void tripleSalary(Employee x) { x.raiseSalary(200); System.out.println("End of method:salary="+x.getSalary()); //150000 } } |
对象构造
重载
类可以有多个构造器,
1 2 3 4 5 |
// 构造一个空的StringBuilder对象: var messages = new StringBuilder(); // 指定一个初始字符串: var todoList = new StringBuilder("To do:\n"); |
编译器必须选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。 如果编译器找不到匹配的参数,就会产生编译错误
默认字段初始化
类中没有初始化的字段会自动初始化为默认值(不建议!)
无参数的构造器
由无参数构造器创建对象时,对象的状态会设置为适当的默认值。
1 2 3 4 5 6 |
public Employee() { name = ""; salary = 0; hireDay = LocalDate.now(); } |
如果写一个类时没有其他编写构造器,就会提供一个无参数构造器,将实例字段设置为初始值
如果类中提供了至少一个构造器,但没有提供无参数的构造器,那么构造对象时如果不提供参数是不合法的
显式字段初始化
可以在类定义里面直接为任何字段赋值
1 2 3 4 5 |
class Employee { private String name = ""; ... } |
初始值不一定是常量值,也可以使用下列方式进行初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Employee { private static int nextId; private int id = assignId(); ... private static int assignId() { int r = nextId; nextId++; return r; } ... } |
调用另一个构造器
如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器
1 2 3 4 5 6 |
public Employee(double s) { // calls Employee(String, double) this("Employee #" + nextId, s); nextId++; } |
初始化块
第三种初始化数据字段的方法
在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行
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 |
class Employee { private static int nextId; private int id; private String name; private double salary; // object initialization block { id = nextId; nextId++; } public Employee(String n, double s) { name = n; salary = s; } public Employee() { name = ""; salary = 0; } ... } |
在这个示例中,无论使用哪个构造器构造对象,id字段都会在对象初始化块中初始化,并且先运行初始化块,再运行构造器的主体部分
构造器的具体处理步骤
- 如果构造器的第一行调用了另一个构造器,则基于所提供参数执行第二个构造器
- 否则 a) 所有数据字段初始化为其默认值(0、false 或 null) b) 按照在类声明中出现的顺序,执行所有字段初始化方法和初始块
- 执行构造器主体代码
包
包名
使用包的主要原因是确保类名的唯一性
类的导入
一个类可以使用所属包中的所有类,以及其他包的公共类
1 2 3 4 |
// 支持的导入类的方法 import java.time.*; import java.time.LocalDate; |
静态导入
有一种import语句允许导入静态方法和静态字段
例如,如果在源文件顶部添加一条指令:
1 |
import static java.lang.System.*; |
就可以使用System类的静态方法和静态字段,不必加类名前缀
1 2 |
out.println("Hello world!"); exit(0); |
另外,还可以导入特定的方法或字段
1 |
import static java.lang.System.out; |