继承·引出
自我理解
老生常谈的是,Java纯面向 对象,面向对象便存在一个问题:确保代码的精简性,即如何让代码每一步有着精确的作用,简洁的代码长度。而不是代码冗长,些许代码似是复制,让一行甚至多行代码如重复使用般执行相同功能。
此时则不是每一步有自己该有的功能,而是相同几步有着如出一辙的目的,整个项目中,代码的体系庞大,精简性若是缺失,“新四军,过绑带,又臭又长又难挨”便是最好的形象描述那些 大同小异的代码
回归代码,在面向 对象 的思想下,树状图式步骤把功能的实现一步步分开,,这其中一步甚至几步似是别无二致。如何归结为一步? 归结一步之后,要使用便声明一下,则是对代码的极大精简
在这个方面,静态关键字static,即是代码归结为一步的方式之一。那么static归结为一步在代码中的作用范围是什么?
显而易见,static修饰的成员属于类,促成了全局共享的特性。同类中,直接用其名字便可调用,不同类(需看其所修饰的访问限定符是否支持其他类调用),其所在类名.名字也可调用
static在“多步归结为一步”上,同类之中最为方便(不受对访问限定符的限制),其他类也可调用
但有个前提,static每次只能修饰 一个 成员变量或方法
这即是static的缺点:如果调用静态成员较多,在内外类方面,同类还好,不同类就不好了:不止在调用时,需每个要申请一下,便出现了多行代码都是声明的情况 ——> 别致的“声明”风景,而如果所需调用静态成员的分布在多类,光在找其对应类名就会心烦意躁,简称脑大:心想,好像知道在哪创建过你,但我创建了那么多,哪知道每个对应的类是谁啊!还要我去翻着找!烦死了!哼!找的时间,还不如自己再写一遍。 这样就回归原始了(复杂粘贴),static便失去了让代码精简性的效果
上方说,找其对应类名,似乎忽略了一个问题,就是在类中每个的成员 的封装性!!!修饰的访问限定符是哪个, 就是我们所要调用的,其是否支持其他类调用。 倘若耐着性子,憋着脑大,找到了其对应的是哪个类,天塌的发现,其又被private的修饰,(夸张)抡起锤子砸电脑的想法油然而生
这其中,如果依赖调用静态成员,则需过度用static修饰成员,回归本质,static修饰的成员过多,也并不好
①复杂:不言而喻的就是多次调用static修饰的时,代码潜移默化的就会混乱起来,在调试时,每一 步的运行在整个项目中跳来跳去,目不暇接,另外static有着全局共享的特性,任何地方都可修改,在检查或维护代码时,追踪其来源便会眼花缭乱,即变得复杂
②破坏封装性:上面提过,创建的成员要么为了逻辑紧密防止为修改,可以用private封装起来,要么不怕被修改值,公开其他类调用,如果 追求 static修饰的被其他类调用,就得选择减少封装,会过度暴露类的内部细节,会让代码整体设计不佳
③不支持多态
此处,可以总结的是:面对多个类中,有某些(不多,且不会破坏封装性的情况下)成员变量和方法被多次用到,可以用static修饰,这样调用时声明简单,更方便快捷。
而如果,某个类中所需调用其他类的成员过多,如何不用static,不用多次声明,声明一次,就可以访问另一个类的随意成员,且可以不破坏另一个类封装性的情况下(即private修饰的其他类依旧不能访问)——继承
继承定义
继承,通俗而言,就是一个类,把另一个类全部承接过来,随时可选择调用承接过来那个类的任何成员
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。
继承和static有相似的特性:共性的抽取,实现代码复用
如下方演示代码:
// Animal.java ——>父类classAnimal{Stringname;intage;privateintz;publicvoideat(){System.out.println(name+"正在吃饭");}publicvoidsleep(){System.out.println(name+"正在睡觉");}}// Dog.java ——>子类1classDogextendsAnimal{voidbark(){System.out.println(name+"汪汪汪~~~");eat();}z=10;//报错,无法访问}// Cat.Java ——>子类2classCatextendsAnimal{voidmew(){System.out.println(name+"喵喵喵~~~");}z=10;// 同样如此,报错,无法访问}可知,子类和父类要表示继承关系,需在子类中加以关键字 —— extends !!!
即继承语法:
修饰符 class 子 · 类名 extends 父 · 类名 {
…
}
通过以上代码可知,子类继承父类之后,其父类成员就是子类成员,子类调用父类成员的调用区别和同类下调用无区别,但每个类都有自己的封装性,若是父类中有的成员没有被其他类访问的权限,即使是子类也无法访问,即任何类无法访问,不会被随意改变其成员数据,这样会极大程度保护每个类的逻辑安全性, 避免debug,减少使程序运行错误的概率
// TestExtend.java ——>测试类classTestExtend{publicstaticvoidmain(String[]args){Dogdog=newDog();// dog类中并没有定义任何成员变量,name和age属性肯定是从父类Animal中继承下来的System.out.println(dog.name);System.out.println(dog.age);// dog访问的eat()和sleep()方法也是从Animal中继承下来的dog.eat();dog.sleep();dog.bark();}}继承的“合理” 行为
- 继承关系通过extends关键字可确立,即1类的子类是2类,2类的子类是3类,3类的子类是4类,4类的子类是5类……(A ——> B ——> C ——> D ——> E ……) 因而继承关系可 !一直继承!下去,但继承过多就不合理了
- 过深的继承,意味着“代码异味”,那么多过深的继承,就反映了一个问题,即代码设计有问题
- 就相当于一个功能基本可以在三步之内完成,非要在五步之外完成继承层次多,父类如果改动某个地方,子类就会可能出现问题,继承这种依赖关系就很隐蔽,排查难以发现,维护代码,调试debug,调用栈极长,在整个项目代码里跳来跳去,整个逻辑模糊,面对那些“祖宗十八代”,想分清的念头都没有
- 以此,可以规定的是,继承关系尽量控制在 三层及以内
- 第二,如果子类相似于父类,就是没有该子类自己异于父类的成员,那么这种继承关系有何作用,干脆要实现的功能都在父类执行就可,免得节外生枝创建子类,这样代码错误几率也少。就相当于子类什么不做不创建自己的成员,就依赖父类。
- 要知道,父类是对子类共性的抽取,说明子类有对应不是共性的成员,即子类应有自己的“特点”
继承方式
单层继承 —— 所谓的 ”单层“ ,就是一层继承。 有个父类,其子类只有一个 A ——> B
多层继承 —— ”多层“,就是多层继承关系,就是父类有子类,子类还有子类,还有子类等,A ——> B ——> C ——> ……
在前面分析过,多继承适合 三层继承,过多层的继承,虽合法,但不合理,代码维护会出现问题
不同类继承一个父类 —— 表面含义即为,该父类具有多个子类需继承的成员,从而父类出现有多个子类。 A ——> B C ——> B ……
一个类继承多个父类 —— ”多 继承“ ”多“就是,一个子类有多个父类。 这种多继承在 Java中并不合法!
但可以间接多继承 即通过“接口”形式,在后续的“接口”博客会分析到
地址的传递
子类与父类 没有重名的成员 变量classBase1{inta;intb;}classDerivedextendsBase1{intc;publicvoidmethod(){a=10;// 访问从父类中继承下来的ab=20;// 访问从父类中继承下来的bc=30;// 访问子类自己的c}}//子类与父类 没有重名的成员 方法classBase2{publicvoidmethodA(){System.out.println("Base中的methodA()");}}classDerived2extendsBase2{publicvoidmethodB(){System.out.println("Derived中的methodB()方法");}publicvoidmethodC(){methodB();// 访问子类自己的methodB()methodA();// 访问父类继承的methodA()// methodD(); // 编译失败,在整个继承体系中没有发现方法methodD()}}//成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的//自己没有时再到父类中找,如果父类中也没有则报错//相同变量 —— 子类和父类都创建有相同成员变量classBase3{inta;intb;intc;}classDerived3extendsBase3{inta;// 与父类中成员a同名,且类型相同charb;// 与父类中成员b同名,但类型不同publicvoidmethod(){a=100;// 访问子类自己新增的ab=101;// 访问子类自己新增的bc=102;// 子类没有c,访问从父类继承下来的c}}//相同方法 —— 即 子类和父类都创建有相同成员方法classBase4{publicvoidmethodA(){System.out.println("Base中的methodA()");}publicvoidmethodB(){System.out.println("Base中的methodB()");}}classDerived4extendsBase4{publicvoidmethodA(inta){//方法的重载System.out.println("Derived中的method(int)方法");}publicvoidmethodB(){System.out.println("Derived中的methodB()方法");}publicvoidmethodC(){methodA();// 没有传参,访问父类中的methodA()methodA(20);// 传递int参数,访问子类中的methodA(int)methodB();// 直接访问,则永远访问到的都是子类中的methodB(),父类的无法访问到}}在子类方法中 或者 通过子类对象访问成员时:
- 如果访问的成员变量子类中有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找
通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错
通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,没有则报错
关键字 —— super
子类中存在与父类中相同的成员时,会自动调用子类中的成员,那如何在子类中访问父类相同名称的成员呢?
不一般调用,如何把 调用的类 确定为父类? 由关键字确定 ——super
“super”字面意思,即是在子类方法中访问父类的成员。在要调用的成员前,加上“super.”此时调用的便是父类与子类重名的成员
classBase5{inta;intb;publicvoidmethodA(){System.out.println("Base5中的methodA()");}publicvoidmethodB(){System.out.println("Base5中的methodB()");}}classDerived5extendsBase5{inta;// 与父类中成员变量同名且类型相同charb;// 与父类中成员变量同名但类型不同publicvoidmethodA(inta){// 与父类中methodA()构成重载System.out.println("Derived5中的method()方法");}// 与父类中methodB()构成重写publicvoidmethodB(){System.out.println("Derived5中的methodB()方法");}publicvoidmethodC(){a=100;// 等价于: this.a = 100;b=101;// 等价于: this.b = 101;super.a=200;super.b=201;// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法methodA();// 没有传参,访问父类中的methodA()methodA(20);// 传递int参数,访问子类中的methodA(int)methodB();// 直接访问,则永远访问到的都是子类中的methodA(),父类的无法访问到super.methodB();// 访问父类的methodB()}}分析:
子类和父类出现相同成员,默认调子类,本质就是没有针对性的传递父类地址。创建针对子类的对象时,通过对象引用相同成员时,在子类中若有就会直接把子类的地址传递过去,若没,编译器才会去父类查找,即是 就近原则
区别 —— super && this
super和this有相同地方,也有不同
- 在地址传递方面:就都是给相同名字的变量传递地址,而super不同的就多了。super变量和方法的地址都可传递,且只能在继承关系时,类与类之间,为子类传递父类相同成员的地址!
- 在格式或特性上基本相同:
① 都是传递地址,super同样不能在静态成员中直接传递地址。
② 在构造方法方面,this()必须在第一条语句,super也是一样,由于只能在第一条语句,因而super和this犹如一山不能容二虎,即是
不能同时存在
总结
【相同点】
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
【不同点】
- this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
- 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
- 在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现
- 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加,但是this(…)用户不写则没有
不同点 —— 初始化
在【代码块】博客时,总结了所有初始化方式,那在继承关系上初始化是否有不同? 由于没什么新增的,只是关系上更复杂了,即 初始化方式不变
既然继承在关系上更复杂了,【代码块】模块时总结了各种方法的执行顺序,关系变复杂了,自然各种方法执行顺序也变了
在“this引用”内容模块时,知晓:在本类中,创建无参数构造方法(如果没创建,编译器会自动创建 空 的构造方法),在主类的main方法中,针对某类创建对象时,如果不主动调用其他构造方法,编译器会自动调用无参数的构造方法,如果手动调用构造方法,则是在创建该类对象时 同时在括号里输入 想要某调用构造方法的参数
对构造方法而言,基本是整个代码最初执行的(静态代码块也是如此,虽是比构造方法更早执行),即是对该类的数据的一种规划(数据的初始化,更好利用数据)。面对这些数据而创建的一些初始化方式就是为了更好管理,利用,调用数据,换句话说,类中数据的如何初始化就是创造方法,静态代码块等规定的,没有这些规定,就相当于在main中创建对象时,只是调用数据,不能在调用的同时进行初始化,需单独初始化,代码利用率较低,且数据之间是有逻辑关系的,单独初始化会破坏逻辑性
有人说,在创建对象,调用类的成员时,某些数据就是单独初始化的呀。对此,我的理解是,创造方法和静态代码块是对一些数据 逻辑性之间的整合,然后逻辑编写好之后,提供接口 让在调用时进行初始化。而那些可以单独初始化的,则是对那些有逻辑性的数据 影响不大的。
就相当于一些数据的初始化为了逻辑性,必须在调用这些数据之前进行初始化。 并不是这些初始化的值后续不能改,而是这些具有逻辑性的数据提供接口初始化之后,而是尽量不要改,这些接口已经就数据逻辑之间针对性的规划如何初始化。 这些数据身具逻辑性,可能在类中创建这些数据时 就进行封装性设计了,即使想要单独初始化(修改),也是不能直接初始化,如何间接? ——set && get
因此,静态代码块,实例化代码块,构造方法,这些代码中只要有,就相当于官方定义,每次创建对象时都是 !必须调用的!,只是调用方式 调用次数不一样!
静态代码块和实例化代码块,是每次创建对象自动在编译器自动调用的,但静态代码块 只能在最开始创建 该类 第一个对象时 调用一次, 另外,类中如果有多个静态和实例化代码块,都会把所有的自动调用
构造方法就不一样了,需手动调用,如有多个,可选择性的调用哪个
在继承关系上,静态和实例化代码块,都是自动调用,因此如果父类有此类代码块,在子类中也都是自动调用的,无关调用格式的区别,而构造方法也是对某些类成员的初始化,所以在子类中是需强制性调用父类构造方法,由于是两个类之间,且是继承关系,调用父类的构造方法的方式也有所不同
这也是为什么在继承关系上,初始化单独分析构造方法,多变!
上方可知,父类有构造方法时,子类必须调用
子类调用时,必须在子类的构造方法中 调用父类的构造方法,即使子类构造方法中没有想要初始化的成员,也必须在子类构造方法里声明一行代码调用父类构造方法
构造方法也是父类的成员,所以通过super在子类中调用
publicclassBase{publicBase(){System.out.println("Base()");}}publicclassDerivedextendsBase{publicDerived(){//super();// 注意子类构造方法中默认会调用基类的无参构造方法:super(),// 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,// 并且只能出现一次//如果调用父类有参数构造方法,在括号里输入对应参数即可System.out.println("Derived()");}}publicclassTest{publicstaticvoidmain(String[]args){Derivedd=newDerived();}}结果打印:Base()Derived()//上方代码剖析可知,调用父类构造方法的 子类构造方法中,声明调用的那一行代码必须在 第一行中。 而在this内容模块时,一个类中可以多个构造方法,在一个构造方法中可通过this调用另一个构造方法,其this()也必须在第一行。 因而,super和this不能同时出现!!!
运行结果可可看出,先运行父类的构造方法,再运行子类的构造方法
是继承关系和不是继承关系(在一个类中,创建另一个类的对象) 调用·构造方法,仍有相同 —— 性质方面
如果父类有构造方法,子类同时必须调用父类的一个构造方法,否则报错。
而非继承关系,换个说法,也是如此,在创建某类的对象时,必须在括号里输入参数,调用该类的一个构造方法,不然同样报错
非继承关系中,所创建对象的类中如果没有构造方法,那么在该对象创建时 在编译器会自动生成一个 空(无参数) 的构造方法。 如果有构造方法(有参数),创建对象时没有输入参数调用构造方法(就相当于调用无参数构造方法),编译器此时不会自动生成无参数构造方法,便会报错
继承关系里,就是——换言之! 父类没有构造方法,子类有构造方法时,编译器运行时 会自动生成一行代码 —— super() ,即自动为父类生成 空 的构造方法 并 为子类自动调用父类的空构造方法! 如果父类有构造方法(有参数 没有无参数),而在子类的构造方法敲有super(),编译器此时同样 不会自动为父类 生成无参数构造方法,也会报错
回归这块“初始化”内容开始,继承关系上,各种初始化方式的执行顺序是什么?
classPerson{publicStringname;publicintage;publicPerson(Stringname,intage){this.name=name;this.age=age;System.out.println("Person:构造方法执行");}{System.out.println("Person:实例代码块执行");}static{System.out.println("Person:静态代码块执行");}}classStudentextendsPerson{publicStudent(Stringname,intage){super(name,age);System.out.println("Student:构造方法执行");}{System.out.println("Student:实例代码块执行");}static{System.out.println("Student:静态代码块执行");}}/* 运行结果: Person:静态代码块执行 Student:静态代码块执行 Person:实例代码块执行 Person:构造方法执行 Student:实例代码块执行 Student:构造方法执行 =========================== Person:实例代码块执行 Person:构造方法执行 Student:实例代码块执行 Student:构造方法执行 **/可看出:总体执行顺序还是 静态代码块 ——> 实例化代码块 ——> 创造方法 ,且静态代码块只在 该类的第一个对象运行
细节上:父类先执行,子类后执行
特殊上:实例化代码块和构造方法(先实例化再构造) 父类和子类一同执行, 且虽是两个一起,但还是先父类再子类
一致上:和是否为继承关系 没影响,即静态代码(块)只执行一次
执行顺序为:
访问限定符 —— protected
protected:同一家族和不同家族的亲戚能看的到 —— 最大访问范围:不同包子类
//extend01包中publicclassB{privateinta;protectedintb;publicintc;intd;}// extend01包中// 同一个包中的子类publicclassDextendsB{publicvoidmethod(){// super.a = 10; 编译报错,父类private成员在相同包子类中不可见super.b=20;super.c=30;super.d=40;}}// 父类中protected成员在相同包子类中可以直接访问// 父类中public成员在相同包子类中可以直接访问// 父类中默认访问权限修饰(即default)的成员在相同包子类中可以直接访问// extend02包中// 不同包中的 子类publicclassCextendsB{publicvoidmethod(){// super.a = 10; // 编译报错,父类中private成员在不同包子类中不可见super.b=20;// 父类中protected修饰的成员在不同包子类中可以直接访问super.c=30;// 父类中public修饰的成员在不同包子类中可以直接访问//super.d = 40; // 父类中默认访问权限修饰(default)的成员在不同包子类中不能直接访问}}publicclassTestC{publicstaticvoidmain(String[]args){Cc=newC();c.method();// System.out.println(c.a); // 编译报错,父类中private成员在不同包其他类中不可见// System.out.println(c.b); // 父类中protected成员在不同包其他类中不能直接访问System.out.println(c.c);// 父类中public成员在不同包其他类中可以直接访问// System.out.println(c.d); // 父类中默认访问权限修饰的成员在不同包其他类中不能直接访问}}总结:
private ——> default ——> protected ——> public 访问权限逐步增大
即使private,default修饰的在不同包子类中无法直接访问,但还是继承到子类中了,可通过创建public修饰的方法间接访问(与set && get 方法 同理
子类与父类的 权限问题 —— 即是在多态中,向上转型,子类权限必须大于父类权限(在后续 多态 中分析)
如何挑选访问限定符?
创建类时,要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者。 因而在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就应尽量不要用 public
此外,有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 但 这种方式属于是对访问权限的滥用 !
即应仔细斟酌,该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用),再选择合适的访问限定符修饰
protected 的双胞胎 且反目成仇的 兄弟 ——— final
访问限定符是为了隐藏 类的内部 细节而达成 封装性。
有多个类,并存在多个继承关系,在创建继承关系时,在第三层时,有意识的规定着不能再创建第四层继承关系了,但往后敲其他代码时,在根据实际要求时,创建一个类,并且想要继承另一个类,如果继承的那另一个类 再继承一次就刚好是第四次继承了,面对庞大的代码体系,可能此时有几层继承关系并不清楚,那么如何避免出现四层甚至五层的继承关系?
即是,觉得该继承关系达到最高层数(大多为三层,根据情况可有四层的出现)时,通过某种声明,表示改类拒绝再次被其他类继承
拒绝其他类继承,不仅在 避免多层继承 方面上,另一个要避免的是 封装性 的破坏! 即使是有两层继承,但在第二层继承的类上,此类的细节并不合适公开再被继承,就像被private修饰的成员一样,防止破坏类 里面的封装性,此时上升到了整个类的封装性,此时为此类通过声明 拒绝继承(即使是A ——> B,这种单层继承,B这个类不想再被继承,可声明 拒绝继承;甚至本身没有继承,即没有创建继承关系,此时不想被继承,也可通过声明 拒绝继承).
这种情况就是 可极大程度保护整个类的封装性
可以分析的是,需要一个声明,来拒绝被继承,避免上方两种情况的发生。 即在创建类时,加上 关键字 ——final
访问限定符是为了隐藏 类的内部 细节而达成 封装性。 而final虽然不属于 访问限定符 的定义,但作用上相差无几,即final是为了隐藏 整个类 细节而达成 封装性
finalpublicclassAnimal{...}publicclassBirdextendsAnimal{...}// 编译出错// Error:(3, 27) java: 无法从最终com.bit.Animal进行继承上面 分析的是,final对 类的作用,但不是指final只能修饰类,也可修饰类里面的成员变量,作用有异曲同工, 甚至修饰成员方法,作用不同(但我觉得有千丝万缕的联系),是防止该方法被重写
final修饰类,是不被其他类继承,主要是不被其他类修改里面的成员数据,就是封装性的破坏。而final修饰成员变量,也是为了改成员变量后续不被修改!!! 即把变量 变成常量无法修改其数据
finalinta=10;// 修饰变量或字段,表示常量(即不能修改)a=20;// 编译出错可总结的是,final就像是个拒绝命令,被修饰的类,成员变量,成员方法,就是为了防止 被其他东西 给修改其数据,保护封装性
结语
以上结论带有我自己对该内容理解而总结得出,可能分析不全,用词不适,总结不当,接受批评,欢迎指出