[01] 继承
1、继承的声明
继承,是指一个类的定义可以基于另一个已经存在的类,即子类基于父类,从而实现父类代码的重用,子类能吸收父类的属性和行为,并扩展新的能力。Java中的继承是单继承,即最多只能有一个父类。
所谓 “龙生龙,凤生凤,老鼠的儿子会打洞”,这句话简单明白地阐述了继承:
- 子类基于父类,也意味着是 “is-a” 的关系
- 子类拥有父类的能力,也就是代码得到了复用
继承的声明也很简单,直接使用extends关键字即可:
【访问权限修饰符】【修饰符】子类名 extends 父类名 { 子类类体 }
1
1
【访问权限修饰符】【修饰符】子类名 extends 父类名 { 子类类体 }
2、构造方法
子类能复用的是父类的属性和方法,但是这里并不包括构造方法,因为构造方法是不能被继承的。但是可以在子类构造方法中通过super关键字调用父类构造方法,super只能在构造方法的首行。
//父类
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
}
//子类
public class Cat extends Animal{
public Cat(String name) {
super(name);
}
}
15
1
//父类
2
public class Animal {
3
private String name;
4
5
public Animal(String name) {
6
this.name = name;
7
}
8
}
9
10
//子类
11
public class Cat extends Animal{
12
public Cat(String name) {
13
super(name);
14
}
15
}
如果你不想调用父类的构造函数,那么也必须保证父类有一个无参的构造函数,子类在调用自身构造函数时会默认调用父类无参构造函数(相当于默认在构造方法首行插入super( )方法)。
也即使是说,无论如何,要保证父类的构造函数,因为子类的构造方法总是先调用父类的构造方法,再调用自己的构造方法。毕竟,没有老子哪来的儿子?
3、this和super
this关键字代表自身,主要用在:
- 在构造方法中引用自身类的其他构造方法
- 用this代表自身类的对象(例如用this引用成员变量或方法)
public class Animal {
private String name;
private int age;
public Animal(String name) {
this.name = name;
}
public Animal(String name, int age) {
this(name);
this.age = age;
}
public String run() {
return "run";
}
public String runQuickly() {
return this.age + " " + this.name + " " + this.run() + " quickly";
}
public static void main(String[] args) {
Animal animal = new Animal("diudiu", 12);
System.out.println(animal.run()); //输出 run
System.out.println(animal.runQuickly()); //输出 12 diudiu run quickly
}
}
29
1
public class Animal {
2
3
private String name;
4
private int age;
5
6
public Animal(String name) {
7
this.name = name;
8
}
9
10
public Animal(String name, int age) {
11
this(name);
12
this.age = age;
13
}
14
15
public String run() {
16
return "run";
17
}
18
19
public String runQuickly() {
20
return this.age + " " + this.name + " " + this.run() + " quickly";
21
}
22
23
public static void main(String[] args) {
24
Animal animal = new Animal("diudiu", 12);
25
System.out.println(animal.run()); //输出 run
26
System.out.println(animal.runQuickly()); //输出 12 diudiu run quickly
27
}
28
29
}
super和this类似,但是super是只有在继承关系里才会使用到的关键字,this是在本类中调用本类,而super则是在本类中调用父类:
- 调用父类的构造方法(super语句只能在子类构造函数的第一行)
- 调用父类的属性或方法
public class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);
}
public String catRun() {
return "cat " + super.runQuickly();
}
public static void main(String[] args) {
Cat cat = new Cat("huahua", 9);
System.out.println(cat.catRun()); //输出 cat 9 huahua run quickly
}
}
16
1
public class Cat extends Animal{
2
3
public Cat(String name, int age) {
4
super(name, age);
5
}
6
7
public String catRun() {
8
return "cat " + super.runQuickly();
9
}
10
11
public static void main(String[] args) {
12
Cat cat = new Cat("huahua", 9);
13
System.out.println(cat.catRun()); //输出 cat 9 huahua run quickly
14
}
15
16
}
4、private和继承
假如父类的某个属性权限设置为private,子类继承父类以后,却是无法直接调用该属性的,所以private的属性无法继承吗?
但是假如该属性有一个public的get方法,子类却可以通过get获取到当初无法直接访问的属性的值,所以private的属性实际上得到了继承吗?
实际上,继承的这个关系有点微妙,在我的理解看来,每次我们获取到一个子类对象时,实际上它包含了 “子类对象和其父类对象”,也就是说,每次我们实例化一个子类对象时,表面上看只有一个对象,实际上背后调用涉及的是一个子类加父类的组合。
我们看看子类实例化的过程和顺序是这样的:
- 初始化父类的静态代码
- 初始化子类的静态代码
- 初始化父类的非静态代码
- 初始化父类构造函数
- 初始化子类非静态代码
- 初始化子类构造函数
但是两者并不是相互交融的,域还是各自的域,但相互又有些关联,所以所谓继承下来的东西,只是让我们看起来像子类的了。而实际上是,当调用子类对象的方法或属性时,先看子类是否有,如果没有,就到父类对象(且权限允许的情况下)去调用。这也是为何子类与父类同名的属性和方法,可以将父类的覆盖掉的原因。
所以开始的private的那个问题也就可以理解了,private属性实际上也是有的,在对应的父类对象里,但是因为没有权限,所以无法直接访问。
最后一个示例,配合食用更佳:
//父类
public class Animal {
public String name = "animal";
public String getName() {
return this.name;
}
}
//子类
public class Cat extends Animal{
public String name = "cat";
}
//测试类
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
System.out.println(cat.name); //输出cat
System.out.println(cat.getName()); //输出animal
}
}
26
1
//父类
2
public class Animal {
3
4
public String name = "animal";
5
6
public String getName() {
7
return this.name;
8
}
9
10
}
11
12
//子类
13
public class Cat extends Animal{
14
15
public String name = "cat";
16
17
}
18
19
//测试类
20
public class Test {
21
public static void main(String[] args) {
22
Cat cat = new Cat();
23
System.out.println(cat.name); //输出cat
24
System.out.println(cat.getName()); //输出animal
25
}
26
}
5、protected引发的权限修饰符认知纠偏
先来回顾一下权限访问修饰符的控制范围:
权限访问修饰符 | 定义 | 权限 | 针对范围 |
public | 公共权限 | 可以被任意类访问 | 属性、方法、类 |
protected | 受保护的权限 | 同包类可以访问,或者非同包的该类子类可访问 | 属性、方法 |
default(即默认不写) | 同包权限 | 只能被同包的类访问 | 属性、方法、类 |
private | 私有权限 | 只能在本类中访问使用 | 属性、方法 |
一直以来我个人都错误地理解了权限修饰符,应该说,把如上表中的权限和针对范围部分混淆了。
在上个标题《4、private和继承》中已经从权限的一部分去说明了和继承的关系,当有一次用到protected权限时(注意看包位置):
//父类
package temp.animal;
public class Animal {
public void eat() {
System.out.println("Animal eat!");
}
protected void run() {
System.out.println("Animal run!");
}
}
//子类
package temp.animal;
public class Cat extends Animal{
}
//测试类
package temp.test;
import temp.animal.Cat;
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat(); // compile ok
cat.run(); // compile error
}
}
x
1
//父类
2
package temp.animal;
3
4
public class Animal {
5
public void eat() {
6
System.out.println("Animal eat!");
7
}
8
protected void run() {
9
System.out.println("Animal run!");
10
}
11
}
12
13
//子类
14
package temp.animal;
15
16
public class Cat extends Animal{
17
}
18
19
//测试类
20
package temp.test;
21
import temp.animal.Cat;
22
23
public class Test {
24
public static void main(String[] args) {
25
Cat cat = new Cat();
26
cat.eat(); // compile ok
27
cat.run(); // compile error
28
}
29
}
Cat是Animal的子类,是继承关系。当我在一个测试类中,新建了一个Cat类对象,试图调用Cat类继承下来的protected修饰的方法run()时,编译不通过。
这里的变量cat,是Cat类吗?是的。Cat类是Animal的子类吗?是的。Cat类继承了Animal的protected方法run()吗?继承了。那为什么cat不能调用run()?
我说了,我的认知把权限和针对范围混淆了,这里的权限修饰符表示的权限始终是针对类的,也就是说,变量cat能否调用protected的run()方法,主要在于它在哪个类里,cat是在Test类中调用的,但是Test显然和Animal不同包,Test也不是Animal子类,所以是没有权限的,也就无法调用。相反,如果在Cat类中新建变量cat,就可以调用run(),或者Test和Animal同包,也是可以调用run()的。
好了,大概就是这样,记录于此,纠偏认知。
6、方法覆盖
在private和继承的关系中我们已经提到,当调用子类对象的方法或属性时,先看子类是否有,如果没有,就到父类对象(且权限允许的情况下)去调用。
这意味着,如果出现和父类同名的方法,调用时会执行子类的方法,而非父类的方法,这个叫方法覆盖,发生在继承关系中。当然,这要求同名、同参、同返回值,且访问权限不能缩小。
另外在方法覆盖和异常抛出上,也有一定的限制:
- 不可以增加新的异常,即使这个新的异常是父类方法声明中的任何一个异常的子类也不行
- 不可以抛出 "被覆盖方法抛出异常" 的父类异常
就像一个修理家电的人,他能够修理冰箱,电脑,洗衣机,电视机。 一个年轻人从他这里学的技术,就只能修理这些家电,或者更少。你不能要求他教出来的徒弟用从他这里学的技术去修理直升飞机。
假如我有Cat子类继承Animal父类,父类有run方法,为何子类要覆盖run方法,而不是另外写catRun方法呢?很简单,如果不使用方法覆盖,那么在调用子类时实际其父类的方法也存在,即也可以调用,这不符合面向对象的封装性,这也是方法覆盖的意义所在。
7、Object类
Object类是所有类的父类,位于java.lang包中,任何类的对象,都可以调用Object类中的方法,包括数组对象。
7.1 toString
toString方法可以将任何一个对象转换为字符串返回,返回值的算法为:
getClass().getName() + '@' + Integer.toHexString(hashCode())
1
getClass().getName() + '@' + Integer.toHexString(hashCode())
即“包名+类名+@+16进制数”,实际上System.out.print( Object obj )则默认调用了toString方法。另外,通常我们某些类的toString需要重写为我们需要的样式。
7.2 equals
equals其实本质上和“==”是一样的,都是比较的对象的虚地址,但是“==”是不能修改的,所以为了按照特定的方式进行两个对象的对比,比如我们希望两个对象的某个属性相同就视为相同的话,就可以采用重写equals方法的方式。
实际上Java中很多提供的类也重写了equals,比如String的equals就是比较两个字符串的内容,而非虚地址。
7.3 hashCode
hashCode方法是获取对象的哈希码,结果是16进制。
(哈希码:哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同)
我们在重写equals,往往要连同把hashCode方法进行重写,要求:
- 如果两个对象用equals返回true,则它们的hashCode必须相同
- 如果两个对象用equals返回false,那么它们的hashCode值不一定不同
为什么要重写hashCode?假如我们有Book类,两本同名书,但不同的出版社,现在我们希望只要名字相同就视为同一本书,我们重写equals,同名书返回了true,但是如果不重写hashCode,在涉及到Set集合时,比如存入Set集合中,会因为hashCode不同,而无法实现去重。