继承
继承关键字extends
英文含义是延伸、扩展的意思。继承的本质就是在复用父类代码的基础上进行功能的扩展、延伸。需要指出的是,子类不会继承父类的私有属性、方法以及构造函数。
创建子类对象的时候会创建父类对象吗?
不会,这一点可以通过在父类和子类的构造方法中输出 this 的哈希地址证明。
public Person(){
System.out.println(this);
}
public student(){//继承自Person类
System.out.println(this);
}
//输出结果都是子类对象的哈希地址
子类对象是怎么创建的?
public class Person
{
private String name;
private int age;
String sex;
public Person(){
this.name="123";
this.age=20;
this.sex="man";
//this.department="教务部";
}
public String showName(){
return this.name;
}
//三个属性的 get/set 方法,节约篇幅,省略
}
public class Teacher extends Person
{
//扩充父类属性
private String department;
//扩充父类方法
//新增属性的 get/set 方法
public Teacher()
{ //this.name="123";
//this.age=20;
this.sex="woman";
this.department="教务部";
}
//方法覆盖
public String showName()
{ //return this.name;
return getName()+"Teacher 老师";
}
//测试
public static void main(String[] args)
{
Person t = new Teacher();
t.setName("Alex");
System.out.println(t.showName());
// System.out.println(tje());
System.out.println(t.sex);
}
}
通过上述代码为例讲解具体过程:
- 在堆里面为父类中的私有属性开辟空间,并在父类中构造一个动态绑定表[子类对象地址,父类私有属性空间地址,父类字节码文件],将这块私有属性空间和父类做动态绑定。
- 为子类新增属性和从父类继承而来的非私有属性开辟空间
- 为父类的方法开辟空间
- 为子类重写了父类的方法开辟空间
- 为子类新增的方法开辟空间
- 子类对象内存空间开辟完成,将此 this 地址给动态绑定表。通过查询父类维护的动态绑定表调用父类的构造函数,并将子类对象的地址 this 作为参数传递过去。这就是动态绑定机制,没有通过对象使用父类的属性和方法,而是通过类维护的动态绑定表找到其所在地址。
- 在调用父类构造函数给 name 和 age 赋值时,首先会检查子类是否有相同属性,如果有就给子类赋值。如果没有,则通过动态绑定表找到这些父类的私有属性进行赋值的。
- 给子类继承过来的和新增加的属性赋值,返回 this 地址给该对象的引用。至此,子类对象构造完成。
理解了调用父类构造函数时给父类私有属性赋值的动态绑定机制,就完全能够理解调用继承而来的父类方法中是如何使用父类私有属性的了,二者完全一样。t.setName("Alex");
上转型和下转型
向上向下转型都是对对象内容的强制转换,并没有改变对象的数据类型,仍然是子类对象啊。
显示上转型
Person t = new Teacher()
中把 Teacher 类型对象的地址给了一个 Person 类型的变量 t ,这种将父类引用指向子类对象的写法就是向上转型,转型后的引用变量不能看见子类所特有的属性和方法。但是,对于子类重写的父类的方法,父类引用所看到的是重写后的方法。所以,方法重写也叫作覆盖!
向上转型不仅是多态的一个基础,还能够给程序增加灵活性,面向接口编程就是一个典型应用。
隐式上转型
被老师称之为隐式上转型的想法是出自于,一个父类引用指向子类对象的时候,拿到的是子类的 this 指针,以为这样在调用父类构造函数时可以直接找到子类的属性,好像也符合逻辑哦?但是跳出这个逻辑,这个属性至少在编译阶段就不可能通过啊,因为这个属性都不存在。
所以,感觉是老师故意塞进来的一个概念,目的是为了引出重写。通过重写就可以让父类的引用通过重写后的方法使用到子类的属性。
下转型
只有经历过上转型的对象,才可以向下转型回到最初的对象。在实际编码中最好使用 instance of 进行判断,以免出现类型转换异常。
方法重载
方法重载是指在一个类中存在多个方法名相同,但是参数签名不同的方法。他们在实际功能上含义相同,只是因为不同情况要达到相同功能时,需要采用不同处理方式。比如Arrays.sort()
和out.println()
方法都重载了很多次,还有类的构造方法。
方法重载对返回值的类型和访问权限没有要求,调用哪个重载方法在编译阶段就会被识别出来,这叫做静态联编。抽象方法、main 方法和静态方法都是方法啊,方法有什么不可以被重载的呢?
重写
实例方法重写
当子类继承父类时,重新实现了父类中的非私有属性的方法,就叫做重写。实例方法重写还可以叫做覆盖,因为父类引用指向子类对象时,看到的是子类重写过后的方法,在子类对象的内存空间根本不存在父类继承下来的方法。
方法重写要求是方法名、参数签名、返回值类型必须完全相同,可以使用 @override
注解来保证一定是重写了父类的方法。父类的私有方法不存在重写的概念,因为根本就不会被子类继承。
方法访问权限必须大于或等于父类,子类在重写父类中的抽象方法时处理的异常一定要比父类中处理的异常多,也就是说子类 throws 的异常一定只能比父类 throws 的异常少。说的更直白点,就是儿子一定不能比老子懒,再不济也要一样勤快。只要这样世界才会不断进步呢!。
静态方法和属性重写
当子类中有和父类相同的属性以及静态方法时,这种称之为隐藏。在子类的内存空间中,会同时存在父类和子类的这种属性和方法的内存空间。父类引用看到的是父类的属性和方法,子类引用看到的是子类的属性和方法。以下可以证明,但在实际开发中用的不多,规范化命名不就好了,吃多了没事做。
class Person{
public int age;
public Person(){
this.age = 20;
}
public static void print(){
System.out.println("我是父类方法");
}
}
class Student extends Person{
public int age;
public Student(){
this.age = 30;
}
public static void print(){
System.out.println("我是子类方法");
}
}
public class Main{
public static void main(String[] args){
Person p = new Student();
System.out.println("p: "+p.age);//输出20
System.out.println(p);//输出Student@15db9742
p.print();//输出"我是父类方法"
Student s = (Student)p;
System.out.println("s: "+s.age);//输出30
System.out.println(s);//输出Student@15db9742
s.print();//输出"我是子类方法"
}
}
super关键字
在子类的构造函数中通过 super 关键字显式的调用父类的构造方法 。如果子类的构造方法中没有显式指定调用哪个父类构造函数,那么就会有一个 默认的无参的super()
。
在分析构造函数调用时可以使用构造函数方法栈来辅助思考,不就是方法调用栈吗?不是什么新概念。
如果子类重写了父类的方法,但又想调用父类原有的方法,怎么办呢?可以使用 super 关键字显示调用父类的方法。本质是去找和父类动态绑定的实例方法并将 this 作为参数传递过去。还可以调用父类的已经被子类隐藏了的属性!
但是这样做,完全破坏了封装性,实际写代码中应该完美避开这些坑。所以,这里只要知道有这些东西就可以了。
this关键字
代表对象的地址,在类的实例方法中用来指明对象调用的实例方法。在 IDE 工具中还可以用这个关键字来作为代码提示的工具。
在构造函数中使用 this 显式调用本类中某个重载的构造函数,同 super 关键字一样都只能位于构造函数的第一条语句,所以 super 和 this 不可能同时在一个构造函数中显式出现。但是,一个只有 this 出现的子类的构造函数中,会有一个默认的无参的super()
出现。
多态
多态是面向对象的一个非常重要的特性,本以为老师会大讲特讲,竟然几句话就给说完了,我还以为自己听错了呢。
这里只讲方法多态。首先要向上转型,父类引用指向子类对象,调用的是父类的被子类重写的方法,这样在编译阶段无法确定具体调用哪个对象的方法,只能在程序运行的时候最终确定。这叫做动态绑定,也叫动态多态。