1. 从“完成哪些步骤”到“让哪个对象做什么”
面向过程关注完成一件事需要哪些步骤;面向对象关注由哪些对象参与、每个对象负责什么,以及对象之间如何配合。
例如洗衣服:
- 面向过程:依次考虑加水、浸泡、搓洗、漂洗、拧干等步骤。
- 面向对象:把洗衣机看作一个对象,给它放入衣物、设置时间并启动,由洗衣机完成内部的洗涤过程。
面向对象不是没有步骤,而是把相关数据和操作组织到对象中。两者都是解决问题的思路,没有绝对的优劣。
Java 支持面向对象编程,简称 OOP(Object-Oriented Programming)。
2. 类、对象与成员
2.1 类描述“有什么”和“能做什么”
类是一种自定义类型,用来描述一类对象共同具有的属性和行为。
例如描述宠物狗:
| 要描述的内容 | Java 中的表示 | 例子 |
|---|---|---|
| 属性:有什么数据 | 成员变量,也叫字段 | 名字、颜色 |
| 行为:能做什么 | 成员方法 | 叫、摇尾巴 |
使用 class 定义类:
public class PetDog { public String name; public String color; public void bark() { System.out.println(name + ":汪汪汪!"); } public void wag() { System.out.println(name + ":摇尾巴!"); } } - 成员变量定义在类中、方法外;方法内部声明的变量是局部变量。
- 类名使用大驼峰,例如
PetDog、WashMachine;变量名和方法名通常使用小驼峰。 - 本章的成员统一使用
public,表示允许外部访问;访问权限的完整规则后续再学。 - 上面的成员方法不加
static,需要通过具体对象调用。
通常一个源文件放一个类。上面的 public 类必须保存在 PetDog.java 中;如果修改类名,文件名也要同步修改,使用开发工具的重命名功能更稳妥。
2.2 类是模板,对象是具体实例
PetDog 描述宠物狗有哪些数据、能做哪些事;“阿黄”和“赛虎”则是根据这个类创建出的两个具体对象。
可以把类理解成房屋设计图,把对象理解成按图建造的房屋:一张设计图能建多套房屋,一个类也能创建多个对象。 每个对象都有自己的实例成员变量。
定义类只是定义类型,不等于已经创建了这个类的对象。
3. 创建对象并访问成员
3.1 用 new 实例化对象
通过类创建对象的过程叫实例化。
PetDog dog = new PetDog(); 这条语句可以拆开理解:
| 部分 | 含义 |
|---|---|
PetDog | 变量的类型 |
dog | 引用变量,用来引用对象 |
new PetDog() | 创建并初始化一个 PetDog 对象 |
= | 将新对象的引用赋给 dog |
引用变量不是对象本身。 可以把引用暂时理解成“找到对象的线索”。
PetDog dog; // 只声明局部引用变量,没有创建对象,也还不能读取它 dog = new PetDog(); // 创建对象,把引用赋给 dog 3.2 用 . 访问成员
访问格式:
引用.成员变量 引用.成员方法(实参) 将下面的代码保存为 Main.java,与前面的 PetDog.java 放在同一目录:
public class Main { public static void main(String[] args) { PetDog dog1 = new PetDog(); dog1.name = "阿黄"; dog1.color = "黄色"; PetDog dog2 = new PetDog(); dog2.name = "赛虎"; dog2.color = "黑色"; dog1.bark(); dog2.wag(); dog1.color = "白色"; System.out.println(dog2.color); } } 输出:
阿黄:汪汪汪! 赛虎:摇尾巴! 黑色 两次 new 创建了两个对象,修改 dog1 所引用对象的颜色,不会改变另一个对象的颜色。
3.3 null 不是“空对象”
PetDog dog = null; 这表示 dog 没有引用任何对象,不是创建了一个内容为空的对象。此时不能通过 dog 访问实例成员,否则会出现空指针异常。
一个引用变量在同一时刻只能引用一个对象,或者为 null;它不能同时指向多个对象。
4. this:当前对象的引用
4.1 为什么 year = year 没有修改成员变量?
假设日期类有成员变量 year,方法也有同名参数:
public void setDate(int year, int month, int day) { year = year; month = month; day = day; } 这里的参数遮蔽了同名成员变量,year = year 的左右两边都指向参数,相当于把参数赋给它自己,对象中的成员变量没有变化。
解决办法是用 this.成员变量 明确指出对象中的字段:
public class Date { public int year; public int month; public int day; public void setDate(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate() { System.out.println(this.year + "-" + this.month + "-" + this.day); } public static void main(String[] args) { Date d1 = new Date(); Date d2 = new Date(); d1.setDate(2021, 6, 9); d2.setDate(2022, 7, 10); d1.printDate(); d2.printDate(); } } 输出:
2021-6-9 2022-7-10 以 this.year = year 为例:
- 左边
this.year:当前对象的成员变量。 - 右边
year:本次调用传入的参数。
4.2 谁调用方法,this 就引用谁
- 执行
d1.setDate(...)时,方法里的this引用d1所引用的对象。 - 执行
d2.setDate(...)时,方法里的this引用d2所引用的对象。
所以,同一个成员方法能够处理不同对象的数据。调用时不需要额外传入 this,Java 会隐式提供当前对象的引用;也不要在普通参数列表里手动添加所谓的 this 参数。
在没有同名局部变量或参数遮蔽时,访问当前对象的成员可以省略 this。例如上面 printDate() 中的 this.year 可以简写成 year。
记住以下用法与限制:
| 写法或规则 | 含义 |
|---|---|
this.成员变量 | 访问当前对象的成员变量 |
this.成员方法(实参) | 调用当前对象的成员方法 |
this 的类型 | 在本例中是 Date 类型 |
不能写 this = ... | 不能让 this 改为引用另一个对象 |
static 方法中不能使用 this | 静态方法不依附于某一个对象,没有当前对象 |
main 是 static 方法,因此需要先创建对象,再通过引用访问实例成员。this 也能用于构造方法,下面会用到。
5. 对象中的数据如何初始化?
5.1 成员变量有默认值,局部变量不能照搬
局部变量必须在读取前被赋值:
int a; // System.out.println(a); // 编译错误:变量 a 可能尚未初始化 但对象的成员变量会自动获得默认值:
| 成员变量类型 | 默认值 |
|---|---|
byte、short、int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | '\u0000',不是字符 '0',也不是空格 |
boolean | false |
引用类型,如 String、数组、自定义类 | null |
使用上一节中尚未定义构造方法的 Date 类:
Date d = new Date(); d.printDate(); // 0-0-0 d 这个局部变量已被赋予对象引用,而对象中的三个 int 字段获得了默认值 0。
注意:String 字段的默认值是 null,不是空字符串 "";日期字段默认为 0,也不代表这个日期有实际意义。
5.2 就地初始化:声明字段时直接赋值
public class Date { public int year = 1900; public int month = 1; public int day = 1; } 此时执行 new Date(),得到的日期数据是 1900、1、1。
这种方式适合为对象设置统一的初始数据。如果希望创建不同日期的对象时直接传入不同的年月日,就需要构造方法。
5.3 构造方法:创建对象时完成初始化
构造方法也叫构造器,用来在创建对象时执行初始化操作。
public class Date { public int year; public int month; public int day; public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d = new Date(2021, 6, 9); d.printDate(); // 2021-6-9 } } 执行 new Date(2021, 6, 9) 时,会调用参数匹配的构造方法。这样,对象创建完成时就已有需要的数据,不必再单独调用 setDate()。
构造方法的两个关键语法要求:
- 名字必须与类名完全相同。
- 没有返回值类型,连
void也不能写。
public Date() { } // 构造方法 public void Date() { } // 普通方法,不是构造方法 构造方法随对象创建而调用,不能写成 d.Date(...),也不能在对象创建完成后再次调用它来重新初始化对象。后续若要修改日期,可以调用普通的 setDate() 方法。
本章使用 public 构造方法;private 构造方法属于后续特殊用法,此处不展开。
5.4 默认构造方法:什么情况下会自动生成?
只有一个类没有声明任何构造方法时,编译器才会提供默认的无参构造方法。
| 类中自己写了什么 | 是否自动提供默认无参构造方法 | 能否使用 new Date() |
|---|---|---|
| 没写任何构造方法 | 是 | 可以 |
| 只写了有参构造方法 | 否 | 不可以 |
| 自己写了无参构造方法 | 否 | 可以,调用自己写的那个 |
例如,5.3 节的 Date 只有三个参数的构造方法:
Date d1 = new Date(2021, 6, 9); // 正确 // Date d2 = new Date(); // 编译错误:没有匹配的无参构造方法 解决办法是传入所需参数,或者自己补写无参构造方法。这是编译错误,不是运行后才发现的问题。
另外,成员变量的默认值不依赖编译器生成的默认构造方法。即使自己写了构造方法,字段也会先获得默认值。
5.5 构造方法重载与 this(...)
一个类可以提供多个参数列表不同的构造方法,让对象有不同的创建方式:
public class Date { public int year; public int month; public int day; public Date() { this(1900, 1, 1); } public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d1 = new Date(); Date d2 = new Date(2021, 6, 9); d1.printDate(); // 1900-1-1 d2.printDate(); // 2021-6-9 } } 这两个构造方法构成重载。new Date() 进入无参构造方法后,this(1900, 1, 1) 会调用本类的三个参数构造方法,复用赋值代码。
执行顺序:
new Date() → 进入无参构造方法 → this(1900, 1, 1) 调用三参数构造方法 → 给当前对象的 year、month、day 赋值 → 返回无参构造方法,继续执行剩余语句 → 对象初始化完成 整个过程只创建了一个对象。所以,不能简单理解成“创建一个对象时只可能执行一个构造方法”;多个构造方法可以通过 this(...) 配合完成同一个对象的初始化。
沿用本章课件的语法规则,this(...):
- 只能在构造方法中使用,并放在第一条语句。
- 调用的是本类其他构造方法,不会新建对象。
- 不能直接或间接形成循环调用。
下面的片段会编译失败,因为两个构造方法互相调用:
public Date() { this(1900, 1, 1); } public Date(int year, int month, int day) { this(); } 不要混淆:this.year 是访问字段,this.printDate() 是调用普通成员方法,this(...) 是调用本类构造方法。
5.6 把初始化顺序串起来
对本章这种普通类,只看当前对象的实例字段,顺序是:
默认初始化 → 就地初始化 → 构造方法中的赋值。
public class Date { public int year = 1900; public int month = 1; public int day = 1; public Date(int year, int month, int day) { System.out.println(this.year + "-" + this.month + "-" + this.day); this.year = year; this.month = month; this.day = day; } public static void main(String[] args) { Date d = new Date(2021, 6, 9); System.out.println(d.year + "-" + d.month + "-" + d.day); } } 输出:
1900-1-1 2021-6-9 字段的变化过程:
| 阶段 | year | month | day |
|---|---|---|---|
| 默认初始化 | 0 | 0 | 0 |
| 就地初始化后 | 1900 | 1 | 1 |
| 构造方法赋值后 | 2021 | 6 | 9 |
传入参数不等于自动给字段赋值。 如果把构造方法体留空,即使写了 new Date(2021, 6, 9),字段仍然是就地初始化得到的 1900、1、1。必须通过 this.year = year 等语句把参数保存到对象中。
6. 打印对象:toString()
直接打印自定义对象的引用:
System.out.println(person); 如果没有自定义 toString(),通常会看到类似内容:
Person@1b6d3586 默认格式是 类的全限定名 + @ + 哈希码的十六进制表示。全限定名包含类所属的包名;没有包名时就是类名。后面的数字不应直接当作对象的内存地址,也不需要背下来。
如果希望打印对象的属性,就在类中重写 toString():
public class Person { public String name; public String gender; public int age; public Person(String name, String gender, int age) { this.name = name; this.gender = gender; this.age = age; } @Override public String toString() { return "[" + name + "," + gender + "," + age + "]"; } public static void main(String[] args) { Person person = new Person("Jim", "男", 18); System.out.println(person); } } 输出:
[Jim,男,18] 这里先掌握三点即可:
- Java 的
Object类提供了默认的toString();这里的“重写”可以先理解为,为当前类重新提供对象转成字符串的实现,继承与重写的完整规则后续再学。 -
@Override是一个标记,让编译器检查是否正确重写了已有方法。 -
toString()要返回字符串,不能改成void,也不要只在里面打印而不返回。打印非空对象引用时,println会使用它返回的字符串。
7. 用本章练习检查易错点
7.1 判断下面的说法
| 说法 | 判断与原因 |
|---|---|
| 一个类只能实例化一个对象 | 错。一个类可以创建多个对象 |
| 一个引用可以同时指向多个对象 | 错。一个引用变量在同一时刻只能引用一个对象,或为 null |
Person p = null; 表示引用指向一个空对象 | 错。null 表示没有引用任何对象 |
| 类中没写构造方法,就不会有构造方法 | 错。此时编译器会提供默认无参构造方法 |
7.2 创建对象时,参数必须匹配构造方法
如果 Person 类只提供了下面这个构造方法:
public Person(String name, int age) { this.name = name; this.age = age; } 那么:
// Person p1 = new Person(); // 错:没有无参构造方法 // Person p2 = new Person("zhangfei"); // 错:没有只接收 String 的构造方法 Person p3 = new Person("lisi", 9); // 正确 最直接的修改方式是按已有构造方法传参:
Person p1 = new Person("zhangsan", 10); Person p2 = new Person("zhangfei", 10); Person p3 = new Person("lisi", 9); 如果确实需要无参或单参数的创建方式,则应补写对应的构造方法。不能指望先用不存在的构造方法创建对象,再通过赋值补救。