系列文章目录
第一部分——编程基础与二进制 1
第一部分——编程基础与二进制 2
第二部分——面向对象.类的基础
文章目录
第二部分——面向对象
4.类的继承
4.1继承语法
class Animal {
public int id;
public String name;
public int age;
public int weight;
public Animal(int id, String name, int age, int weight) {
this.id = id;
this.name = name;
this.age = age;
this.weight = weight;
}
//这里省略get set方法
public void sayHello() {
System.out.println("hello");
}
public void eat() {
System.out.println("I'm eating");
}
public void sing() {
System.out.println("sing");
}
}
class Dog extends Animal {
public Dog(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
@Override
public void sayHello() {
System.out.println("dog says hello");
}
}
class Cat extends Animal {
public Cat(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
@Override
public void sayHello() {
System.out.println("cat says hello");
}
}
class Chicken extends Animal{
public Chicken(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
@Override
public void sayHello() {
System.out.println("chicken say hello");
}
//鸡下蛋
public void layEggs() {
System.out.println("我是老母鸡下蛋啦,咯哒咯!咯哒咯!");
}
}
- 通过关键字extends在声明类的时候表达类的继承
- 子类的构造方默认调用父类空构造方法,但是如果父类没有空构造方法,那么必须使用super() 显示的调用父类构造方法
- 子类默认会继承父类中所有的属性和方法
4.2基本概念
类的继承的核心目的就是代码复用,继承作为面向对象三大特性之一,是多态特性的基础,一方面可以将公共的属性和行为放到父类中,儿子类只需要关注子类特有的就可以了;另一方面,不同子类对象可以更为方便的被统一化处理
4.2.1Object类
Object类是java所有类的父类,即使没有声明父类,也会默认继承这个类,这个类没有定义属性,定义了几个基础的方法
其中红框中标注的方法,是我们经常使用的一些方法
- getClass方法我们可以获得类对应的Class类,这个类可以帮助我们友好的访问类信息
- hashCode方法可以让我们获得一个对象的hash值
- equals方法可以用来判定两个对象是否相等,一般当我们需要覆写equals方法时也需要覆写hashCode方法,这个原因等到容器那一个章节我们会进行叙述
- clone方法用于获取这个对象对应类型的一个克隆实例,这个实例与该对象有同一个Class类,但却不是同一个对象
- toString方法能够返回一个这个对象的字符串表述,默认情况下为类名@hashCode的实现
- wait、notify、notifyAll这三个方法可以归为一类,都是对当前线程的操作,具体介绍会在后续的juc系列文章中介绍
- finalize方法在被覆写后,会在对象被垃圾回收器检测可以回收时调用,然后加入F-queue队列等待回收,有利用此方法逃避gc回收的可能但不推荐
由于这是公共的父类,因此所有类都可以根据自身需要覆写Object类中的方法
4.2.2Override and Overload
Override
- 参数列表与被重写方法的参数列表必须完全相同
- 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected
- 父类的成员方法只能被它的子类重写
- 声明为 final 的方法不能被重写
- 声明为 static 的方法不能被重写,但是能够被再次声明
- 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法
- 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法
- 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以
- 构造方法不能被重写
- 如果不能继承一个类,则不能重写该类的方法
Overload
- 被重载的方法必须改变参数列表(参数个数或类型不一样)
- 被重载的方法可以改变返回类型
- 被重载的方法可以改变访问修饰符
- 被重载的方法可以声明新的或更广的检查异常
- 方法能够在同一个类中或者在一个子类中被重载
- 无法以返回值类型作为重载函数的区分标准
这里建议不要记中文的重写和重载,因为中文书籍中对这两个词有很多不同的翻译,直接记override和overload即可
@Override注解
当你试图覆写某一个方法时,在覆写的方法上标注一个@Override注解是一个好习惯,这个注解没有任何功能上的提升,但是可以帮你在编译阶段检查出覆写的方法符不符合规范
4.2.3多继承与单继承
- 单继承
单继承指的是有且仅有一个父类的继承,这也是java和c++中继承的模式,优点是继承体系结构很清晰,缺点是表达力不够,没有办法实现一些复杂的功能
- 多继承
多继承指的是允许有多个父类的继承,这种继承方式优点是单继承所缺少的表达力,可以很轻松的设计出具有多种组合功能或者特性的子类,但是缺点十分明显,整个的继承结构会很复杂混乱,多个父类的相同属性或方法的继承难以确定
在java中我们允许的是单继承的方式,但是我们仍然有解决单继承缺陷的三种方式,间接的实现了多继承:
- 内部类:内部类可以多继承其他的类,让类的功能更丰富,表达力更强,但是语义不通顺,结构仍然很复杂混乱
- 多层继承:形成一条继承链,A继承B继承C的模式让A相当于继承了B与C,但是问题也很明显,增加了无效的系统复杂度,且对于一些场景下,我们无法使得B继承C
- 实现接口:这里就提到了另一层概念,接口;接口是一个相较于抽象类更抽象的个体,虽然不能实现类的多继承,但是实现的接口可以有多个,我们完全可以使用实现接口的方式来达到多继承的目的
4.3继承的细节
4.3.1构造方法
之前一直没有谈构造方法,是因为构造方法一直到继承这个部分才有讲的必要
- 构造方法是一种特殊的方法,这个方法不用写返回值且方法名与类名相同,构造方法也分无参构造和有参构造,可以根据要求自定义传入参数,在类被创建的时候会默认调用参数匹配的那个构造方法初始化类
- 当一个类没有声明任何构造方法时,会默认存在一个无参的空构造方法,这也就是为什么不写构造方法仍然可以通过new Xxx()的方式新建一个实例,但是当写了任何构造方法之后,这个默认的无参空构造方法就没有了
- 可以存在多个构造方法,在new关键字新建实例时会像调用方法重载时一样调用参数匹配的那一个方法
- 构造方法一般设置为public,但是也可以设置为private修饰的私有构造方法,而一个私有构造方法无法被外界访问,因此需要由类自己创建好自己的实例并返回给外部使用,常用于单例模式,详细可以参考我的设计模式栏目下的单例模式
构造方法在继承中的特殊点:
- 父类的构造方法不能被子类继承
- jvm会在调用子类构造方法之前默认调用父类的无参构造方法,如果父类以及存在构造方法且不存在无参构造方法,需要显示的通过super()的方式自己调用合适的父类构造方法,且这个语句只能出现在子类构造方法的第一行,这里尤其需要主要一点,很多人都会犯错,构建子类的过程中没有创建父类的对象只是初始化了父类的这些方法与属性,并存储在子类对应的内存空间中
- 父类构造方法中尽量不要调用可以被子类覆写的方法,容易产生意想不到的错误,如
class Base {
public Base() {
test();
}
public void test() {
}
}
class Child extends Base {
private int a = 123;
public Child() {
}
public void test() {
System.out.println(a);
}
}
当我们构建子类对象的时候
public static void main(String[] args) {
Child c = new Child();
c.test();
}
结果如下:
0
123
第一次输出0,第二次输出123,123很好理解是调用c的test方法打印的a,那么0是怎么来的?
当我们准备初始化子类的时候,会默认调用了父类的空构造,而父类的空构造方法中又调用了test方法,对于public修饰的实例方法,jvm使用动态绑定,此时test()方法以及被子类覆写,因此调用的是子类的test方法,而此时子类并没有初始化成员变量,因此a为默认值0,这才打印了一个0
这个例子说明了,在父类构造方法中嗲用可以被子类覆写的方法,容易在继承的过程中出现意外的结果
4.3.2this and super
首先明确一点,这两个关键字都是属于子类的,只是分别指向不同的引用
this.属性;//访问自己的某个成员变量
this.方法名();//调用自己的某个方法
this();//自己的空构造方法,可以带参数
super.属性;//访问父类的某个成员变量
super.方法名();//调用父类的某个方法
super();//父类的空构造方法,可以带参数
4.3.3静态绑定与动态绑定
对于静态绑定与动态绑定,我们使用以下这个案例引入,思考以下这个案例的输出
父类Base定义了一个静态成员变量,一个实例成员变量,一个静态方法,一个实例方法,一个final修饰的方法
class Base {
public static String s1 = "static_base";
public String s2 = "base";
public static void staticTest() {
System.out.println("base static method");
}
public void test() {
System.out.println("base method:");
}
public final void finalTest() {
System.out.println("base final method");
}
}
子类Child继承父类,定义了和父类重名的一个静态成员变量,一个实例成员变量,一个静态方法,一个实例方法
class Child extends Base {
public static String s1 = "static_child";
public String s2 = "child";
public static void staticTest() {
System.out.println("child static method");
}
public void test() {
System.out.println("child method");
}
}
main方法创建一个子类对象赋值给父类引用(这其实就是多态的体现),并依次打印执行
public static void main(String[] args) {
Base b = new Child();
System.out.println(b.s1);
System.out.println(b.s2);
b.staticTest();
b.test();
b.finalTest();
}
我们先看结果:
static_base
base
base static method
child method
base final method
为什么结果是这样的,我们先给出结论,因为实例变量、静态变量、静态方法、private方法、final修饰的方法,都是通过静态绑定的,而上述案例中的test方法则是动态绑定的
- 绑定
绑定指将方法和属性同它们的主体也就是类做关联的过程,在java中绑定分两种,静态绑定与动态绑定,又称前期绑定、编译时绑定和后期绑定、运行时绑定
- 静态绑定
静态绑定是指在运行前,也就是编译的时候jvm虚拟机就已经决定了方法由谁调用,此时属性和方法就已经和类做挂钩
- 动态绑定
动态绑定指的是,在运行时根据具体对象的类型进行绑定。提供了一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体
我们再来分析上述结果,对于静态绑定的部分,此时b是Base类的引用,因此通过b访问调用到的也都是早就绑定好给Base类的属性和方法,故而即使本质上是一个Child的实例对象,但是显示的都是Base类的东西;而对于test方法,由于是动态绑定,所以我们会在运行时判断怎么调用,因此在运行时我们通过某种机制确定了这是一个Child的实例,因此调用的也就是Child的test方法
4.3.4再谈static和final
在之前类的基础的一章我们提过了static和final这两个关键字,这里我们再从继承的角度谈一谈这两个关键字的注意事项
static
- 静态方法中不存在当前对象,因而不能使用 this,当然也不能使用 super。
- 静态方法不能被非静态方法覆写
- 静态方法能被静态方法覆写
class Base {
public static void test1() {
this.Xxx;//不合法的
super.Xxx;//不合法的
}
public static void test2(){
//do something
}
public static void test3(){
//do something
}
}
class Child extends Base {
public void test2(){} //不合法的
public static void test3(){}//允许的
}
final
- 父类中的 final 方法可以被子类继承,但是不能被子类重写,声明 final 方法的主要目的是防止该方法的内容被修改
- final修饰的类无法被继承
4.3.5转型
转型分为两种即向上转型与向下转型,举一个简单的例子
//Child 继承自 Base
Base base = new Child();//向上转型
上面这个过程就是一个向上转型的过程,对于一个向上转型来讲,由于我们通过子类实例化父类,小范围转大范围,这是一个自动转换的过程,很好理解,但是在这种情况下,base只能调用Base类已经声明的方法与属性,而当子类覆写了父类方法时,b调用的方法到底是哪一个就参考4.3.3的内容;此时如果我们的确需要base调用Child类特有的方法时,就需要强制转换,也就是一个向下转型
Child child = (Child)base;//向上转型
这个过程就是向上转型,而向上转型是一个大范围变小范围的过程,因此需要我们特殊的使用()强制声明,不过这里存在一个问题
Base base = new Base();
Child child = (Child) base;//不合法
Child child = new Base();//不合法
以上这两种过程都是不合法的,子类的引用无法指向父类的对象,即使通过强制转换也不行,因此当外界传给了我一个Base型的引用的时候,我怎么知道这个引用到底是指向Base对象还是Base子类的对象呢?
if(base instanceof Child) {
Child c = (Child) base;
}
instanceof关键字可以帮我们解决这个问题,上述的逻辑就是如果base指向的确实是一块Child类型的内存空间就会返回true,也就会执行下面转型的这一步,并且在JDK14中对instanceof关键字做了加强,JDK14之后我们可以这么写:
if(base instanceof Child c) {
//可以直接使用c
}
这个语法糖可以直接帮助我们省去转型的那一步,同样的在jdk17(这个是一个LTS版本)中还加入了这样的可能性:
switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
(小声说一句,这貌似是scala的模式匹配)
4.3.6子类父类的初始化顺序
这里只用一段代码就可以知道
class Base {
static {
System.out.println("父类static代码块");
}
{
System.out.println("父类普通代码块");
}
public Base() {
System.out.println("父类构造方法");
}
}
class Child extends Base {
static {
System.out.println("子类static代码块");
}
{
System.out.println("子类类普通代码块");
}
public Child() {
System.out.println("子类构造方法");
}
}
public class Test02 {
public static void main(String[] args) {
Child child = new Child();
}
}
结果如下:
父类static代码块
子类static代码块
父类普通代码块
父类构造方法
子类类普通代码块
子类构造方法
从结果中我们可以总结执行顺序如下:
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造函数
- 子类中普通成员变量和代码块,子类的构造函数
总的来说就是,静态优先于非静态,父类优先于子类,代码块优先于构造函数
4.4面向对象三大特性
在这一章的最后,我们整体的梳理一遍面向对象的三大特性,在这之前我们都穿插在了知识点中间,这里只是梳理
-
封装:是对类的封装,封装是对类的属性和方法进行封装,只对外暴露方法而不暴露具体使用细节,所以我们一般设计类成员变量时候大多设为私有而通过一些get、set方法去读写
-
继承:子类继承父类,即“子承父业”,子类拥有父类除私有的所有属性和方法,自己还能在此基础上拓展自己新的属性和方法。主要目的是复用代码
-
多态:多态是同一个行为具有多个不同表现形式或形态的能力。即一个父类可能有若干子类,各子类实现父类方法有多种多样,调用父类方法时,父类引用变量指向不同子类实例而执行不同方法,这就是所谓父类方法是多态的(多态出现在了大多数设计模式里,设计模式的初衷就是解决面向对象的缺陷)