面向对象
- 例子
1
2
3
4class Person {
public String name;
public int age;
}一个class可以包含多个字段(field),字段用来描述一个类的特征。上面的Person类,我们定义了两个字段,一个是String类型的字段,命名为name,一个是int类型的字段,命名为age。因此,通过class,把一组数据汇集到一个对象上,实现了数据封装。
字段就是C++中的成员属性
- 创建实例:
Person ming = new Person();
- 小结:
- 在OOP中,class和instance是“模版”和“实例”的关系;
- 类就是对象的模板(蓝图)
- 定义class就是定义了一种数据类型,对应的instance是这种数据类型的实例;
- class定义的field,在每个instance都会拥有各自的field,且互不干扰;
- 通过new操作符创建新的instance,然后用变量指向它,即可通过变量来引用这个instance;
- 访问实例字段的方法是变量名.字段名;
类和对象的关系
- 类是一个抽象的概念,仅仅是模板,比如说:演员、总统
- 对象是一个你能够看得到、摸得着的具体实体
- 类的定义者和类的使用者是不一样的。越往下越具体,越往上越抽象。
基本步骤
- 发现类
- 找出属性(名词)
- 找出行为(动词)
- 数据抽象:是数据和处理方法的结合
定义类
- 代码
1
2
3
4
5
6
7
8
9
10public class Actor {
public String name;
public char sex;
public String job;
public int age;
public void eat(){
……
}
}
- 代码
类图
- 直观、容易理解
- 参考工具:
- StarUML
- Astah
- +号代表public,-号代表private
- 属性名在前,后面跟冒号和类型名
- 方法名在前,后面跟冒号和返回值类型
- 如果有参数,参数的类型写法同上
- Astah
实例化
1
2
3Role role1 = new Role(); // 在堆空间里面分配了一个空间,把空间的地址赋给了role1,用哈希码来表示在虚拟机里面的内存地址
Role role2; // 声明了一个Role类型的变量,叫role2(可以把Role看成自己定义的数据类型),但是还没有空间
role2 = new Role(); // role2 一定要初始化,对象在运行时,一定要分配空间方法
把field从public改成private,外部代码不能访问这些field,以我们需要使用方法(method)来让外部代码可以间接修改field:
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
32public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setName("Xiao Ming"); // 设置name
ming.setAge(12); // 设置age
System.out.println(ming.getName() + ", " + ming.getAge());
}
}
class Person {
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
}虽然外部代码不能直接修改private字段,但是,外部代码可以调用方法setName()和setAge()来间接修改private字段。在方法内部,我们就有机会检查参数对不对。比如,setAge()就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age设置成不合理的值。
同样,外部代码不能直接读取private字段,但可以通过getName()和getAge()间接获取private字段的值。
所以,一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
调用方法的语法是实例变量.方法名(参数);。一个方法调用就是一个语句,所以不要忘了在末尾加;。例如:ming.setName(“Xiao Ming”);。
定义方法
1
2
3
4修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}private方法
有public方法,自然就有private方法。和private字段一样,private方法不允许外部调用,定义private方法的理由是内部方法是可以调用private方法的。
this变量
在方法内部,可以使用一个隐含的变量this,它始终指向当前实例。因此,通过this.field就可以访问当前实例的字段。如果没有命名冲突,可以省略this,但是如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
方法参数
方法可以包含0个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。(同C++)
可变参数
可变参数用
类型...
定义,可变参数相当于数组类型:1
2
3
4
5
6
7class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}上面的setNames()就定义了一个可变参数。调用时,可以这么写:
1
2
3
4
5Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String完全可以把可变参数改写为String[]类型:
1
2
3
4
5
6
7class Group {
private String[] names;
public void setNames(String[] names) {
this.names = names;
}
}但是,调用方需要自己先构造String[],比较麻烦。例如:
1
2Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]另一个问题是,调用方可以传入null:
1
2Group g = new Group();
g.setNames(null);而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null。
参数绑定: 传参的问题
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型的传递:将地址进行复制,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Main {
public static void main(String[] args) {
Person p = new Person();
String[] fullname = new String[] { "Homer", "Simpson" };
p.setName(fullname); // 传入fullname数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
System.out.println(p.getName()); // "Bart Simpson"
}
}
class Person {
private String[] name;
public String getName() {
return this.name[0] + " " + this.name[1];
}
public void setName(String[] name) {
this.name = name;
}
}Note: 因为传入的是字符串数组,因此将
fullname
数组的地址复制,传给了p.name
,因此p.name
和fullname
指向的是同一个字符串数组,所以这两个同时变化,但是变化的时候还是遵循引用的原则:Homer
仍然存在,只是无法通过fullname[0]
进行访问罢了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Main {
public static void main(String[] args) {
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入bob变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob改名为Alice
System.out.println(p.getName()); // "Bob"
}
}
class Person {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}Note: 和上一个传入引用参数一样,复制的是地址,传入之后
bob
和p.name
指向了同一块内存,只不过bob
改变之后会重新指向新的内存,所以这两个变量指向的内存就不一样了。注意:字符串和数组都是引用类型。
构造方法
- 由于构造方法是如此特殊,所以构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。(具体意义和C++一样)
- 没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false:
- 可以对字段直接进行初始化:
1
2
3
4
5
6
7
8
9class Person {
private String name = "Unamed";
private int age = 10;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}构造方法的代码由于后运行,最终由构造方法的代码确定,即便已经直接将字段初始化了。
方法重载
同C++
继承
Student类包含了Person类已有的字段和方法,只是多出了一个score字段和相应的getScore()、setScore()方法。
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让Student从Person继承时,Student就获得了Person的所有功能,我们只需要为Student编写新增的功能。实现
Java使用extends关键字来实现继承:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Person {
private String name;
private int age;
public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}
class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;
public int getScore() { … }
public void setScore(int score) { … }
}通过继承,Student只需要编写额外的功能,不再需要重复代码。
Note: 子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
在OOP的术语中,我们把
Person
称为超类(super class)
,父类(parent class)
,基类(base class)
,把Student
称为子类(subclass)
,扩展类(extended class)
。继承树
在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
例如:
protected
继承有个特点,就是子类无法访问父类的private字段或者private方法。
为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问: 因此,protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问。
super
super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName。例如:
1
2
3
4
5class Student extends Person {
public String hello() {
return "Hello, " + super.name;
}
}实际上,这里使用super.name,或者this.name,或者name,效果都是一样的。编译器会自动定位到父类的name字段。
但是,在某些时候,就必须使用super。我们来看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class Main {
public static void main(String[] args) {
Student s = new Student("Xiao Ming", 12, 89);
}
}
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
this.score = score;
}
}运行上面的代码,会得到一个 编译错误,大意是在Student的构造方法中,无法调用Person的构造方法。
这是因为在Java中,任何class的构造方法,**第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();**,所以,Student类的构造方法实际上是这样:
1
2
3
4
5
6
7
8class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法
this.score = score;
}
}但是,Person类并没有无参数的构造方法,因此,编译失败。
解决方法是调用Person类存在的某个构造方法。例如:
1
2
3
4
5
6
7
8class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。
这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
阻止继承
只要某个class没有final修饰符,那么任何类都可以从该class继承。
从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。
1
2
3public sealed class Shape permits Rect, Circle, Triangle {
...
}上述Shape类就是一个sealed类,它只允许指定的3个类(Rect, Circle, Triangle)继承它,否则就会报错。
这种sealed类主要用于一些框架,防止继承被滥用。sealed类在Java 15中目前是预览状态,要启用它,必须使用参数–enable-preview和–source 15。
向上转型:一个子类类型安全地变为父类类型的赋值
向上转型实际上是把一个子类型安全地变为更加抽象的父类型
1
2
3
4Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, okp也只能使用 Person类中有的字段和方法,不能使用 Student 添加的字段和方法
向下转型:把一个父类类型强制转型为子类类型
不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
instanceof
实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof
的判断都为false
。
1
2
3
4
5Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。
1
2
3
4
5
6
7
8
9public class Main {
public static void main(String[] args) {
Object obj = "hello";
if (obj instanceof String s) {
// 可以直接使用变量s:
System.out.println(s.toUpperCase());
}
}
}等价于
1
2
3
4
5Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}组合和继承
1
2
3
4
5class Book {
protected String name;
public String getName() {...}
public void setName(String name) {...}
}这个Book类也有name字段,那么,我们能不能让Student继承自Book呢?不可以
从逻辑上讲,这是不合理的,Student不应该从Book继承,而应该从Person继承。
究其原因,是因为Student是Person的一种,它们是is关系,而Student并不是Book。实际上Student和Book的关系是has关系。
具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例:1
2
3
4class Student extends Person {
protected Book book;
protected int score;
}继承是is关系,组合是has关系。
多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。