大家好,我是呼噜噜,今天我们来深挖面向对象编程三大特性:封装、继承、多态封装 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 通俗的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。但是如果个类没有提供给外界访问的法,那么这个类也没有什么意义了。 我们来看一个常见的类,比如:StudentpublicclassStudentimplementsSerializable{privateLongid;privateStringname;publicLonggetId(){returnid;}publicvoidsetId(Longid){this。idid;}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this。namename;}} 将对象中的成员变量进行私有化,外部程序是无法访问的。对外提供了访问的方式,就是set和get方法。而对于这样一个实体对象,外部程序只有赋值和获取值的权限,是无法对内部进行修改继承 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。在Java中通过extends关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:class父类{}class子类extends父类{} 继承概念的实现方式有二类:实现继承与接口继承。 实现继承是指直接使用基类的属性和方法而无需额外编码的能力接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力一般我们继承基本类和抽象类用extends关键字,实现接口类的继承用implements关键字。 注意点: 通过继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过继承(Inheritance)和组合(Composition)来实现。子类可以拥有父类的属性和方法。子类可以拥有自己的属性和方法,即类可以对类进扩展。子类可以重写覆盖父类的方法。JAVA只支持单继承,即一个子类只允许有一个父类,但是可以实现多级继承,及子类拥有唯一的父类,而父类还可以再继承。 使用implements关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。implements关键字publicinterfaceA{publicvoideat();publicvoidsleep();}publicinterfaceB{publicvoidshow();}publicclassCimplementsA,B{} 值得留意的是:关于父类私有属性和私有方法的继承的讨论这个网上有大量的争论,我这边以Java官方文档为准:Withtheuseoftheextendskeyword,thesubclasseswillbeabletoinheritallthepropertiesofthesuperclassexceptfortheprivatepropertiesofthesuperclass。子类不能继承父类的私有属性(事实),但是如果子类中公有的方法影响到了父类私有属性,那么私有属性是能够被子类使用的。 官方文档明确说明:private和final不被继承,但从内存的角度看的话:父类private属性是会存在于子类对象中的。 通过继承的方法(比如,public方法)可以访问到父类的private属性 如果子类中存在与父类private方法签名相同的方法,其实相当于覆盖 个人觉得文章里的一句话很赞,我们不可能完全继承父母的一切(如性格等),但是父母的一些无法继承的东西却仍会深刻的影响着我们。多态 同一个行为具有多个不同表现形式或形态的能力就是多态。网上的争论很多,笔者个人认同网上的这个观点:重载也是多态的一种表现,不过多态主要指运行时多态Java多态可以分为重载式多态和重写式多态: 重载式多态,也叫编译时多态。编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。也就是说这种多态再编译时已经确定好了。重写式多态,也叫运行时多态。运行时多态是动态的,主要指继承父类和实现接口时,可使用父类引用指向子类对象实现。这个就是大家通常所说的多态性。这种多态通过动态绑定(dynamicbinding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。这种多态可通过函数的重写以及向上转型来实现。 多态存在的三个必要条件:继承 重写 父类引用指向子类对象:ParentpnewChild(); 我们一起来看个例子,仔细品读代码,就明白了:SpringBootTestclassDemo2021ApplicationTests{classAnimal{publicvoideat(){System。out。println(动物吃饭!);}publicvoidwork(){System。out。println(动物可以帮助人类干活!);}}classCatextendsAnimal{publicvoideat(){System。out。println(吃鱼);}publicvoidsleep(){System。out。println(猫会睡懒觉);}}classDogextendsAnimal{publicvoideat(){System。out。println(吃骨头);}}TestvoidcontextLoads(){part1CatcatnewCat();cat。eat();cat。sleep();cat。work();part2AnimalcatnewCat();cat。eat();cat。work();cat。sleep();此处编译会报错。AnimaldognewDog();dog。eat();结果为:吃骨头。此处调用子类的同名方法。part3如果想要调用父类中没有的方法,则要向下转型,这个非常像强转Catcat222(Cat)cat;向下转型(注意,如果是(Cat)dog则会报错)cat222。sleep();结果为:猫会睡懒觉。可以调用Cat的sleep()}} 我们来看上面part1部分:CatcatnewCat();cat。eat();cat。sleep();cat。work(); 结果: 吃鱼猫会睡懒觉。动物可以帮助人类干活! cat。work();这处继承了父类Animal的方法,还是很好理解的我们接着看part2:AnimalcatnewCat();cat。eat();cat。work();cat。sleep();此处编译会报错。AnimaldognewDog();dog。eat();结果为:吃骨头。此处调用子类的同名方法。 这块就比较特殊了,我们一句句来看AnimalcatnewCat();像这种这个父类引用指向子类对象,这种现象叫做:向上转型,也被称为多态的引用。cat。sleep();这句编译器会提示编译报错。表明:当我们当子类的对象作为父类的引用使用时,只能访问子类中和父类中都有的方法,而无法去访问子类中特有的方法值得注意的是:向上转型是安全的。但是缺点是:一旦向上转型,子类会丢失的子类的扩展方法,其实就是子类中原本特有的方法就不能再被调用了。所以cat。sleep()这句会编译报错。 cat。eat();这句的结果打印:吃鱼。程序这块调用我们Cat定义的方法。cat。work();这句的结果打印:动物可以帮助人类干活!我们上面Cat类没有定义work方法,但是却使用了父类的方法,这是不是很神奇。其实此处调的是父类的同名方法AnimaldognewDog();dog。eat();这句的结果打印为:吃骨头。此处调用子类的同名方法。由此我们可以知道当发生向上转型,去调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。如果子类没有同名方法,会再次去调父类中的该方法 我们现在知道了向上转型时会丢失子类的扩展方法,哎,但我们就是想找回来,这可咋办?向下转型可以帮助我们,找回曾经失去的 我们来看part3:Catcatreal(Cat)cat;注意此处的cat对应上面父类Animal,可不是子类catreal。sleep(); Catcat(Cat)cat;cat222。sleep();这个向下转型非常像强转。打印的结果:猫会睡懒觉。此处又能调用了子类Cat的sleep()方法了。一道简单的面试题 我们再来看一道有意思的题,来强化理解publicclassMain{staticclassAnimal{intweight10;publicvoidprint(){System。out。println(thisAnimalPrint:weight);}publicAnimal(){print();}}staticclassDogextendsAnimal{intweight20;Overridepublicvoidprint(){System。out。println(thisDogPrint:weight);}publicDog(){print();}}publicstaticvoidmain(String〔〕args){DogdognewDog();System。out。println();Animaldog222newDog();Dogdog333(Dog)dog222;System。out。println();Dogdog444(Dog)newAnimal();}} 执行结果: thisDogPrint:0thisDogPrint:20 thisDogPrint:0thisDogPrint:20 thisAnimalPrint:10Exceptioninthreadmainjava。lang。ClassCastException:com。zj。MainAnimalcannotbecasttocom。zj。MainDogatcom。zj。Main。main(Main。java:15) 做对了嘛,不对的话,复制代码去idea中debug看看 我们先看第一部分DogdognewDog();程序内部的执行顺序:先初始化父类Animal的属性intweight10 然后调用父类Animal的构造方法,执行print() 实际调用子类Dog的print()方法,打印:thisDogPrint:0,由于此时的子类属性weight并未初始化 初始化子类Dog的属性intweight20 调用子类Dog的构造方法,执行print() 实际调用当前类的print()方法,打印thisDogPrint:20 其中有几处我们需要注意一下:实例化子类dog,程序会去默认优先实例化父类,即子类实例化时会隐式传递Dog的this调用父类构造器进行初始化工作,这个和JVM的双亲委派机制有关,这里就不展开讲了,先挖个坑,以后再来填。当程序调用父类Animal的构造方法时,会隐式传递Dog的this,类似于:DogdognewDog(this);staticclassAnimal{publicAnimal(this){print(this);子类又把print()给重写了}}staticclassDogextendsAnimal{publicDog(this){print(this);此时子类的属intweight被没有初始化默认为0}} 这块其实和JVM的虚方法表有关,这又是一个大坑,以后慢慢填。我们接着看第2部分Animaldog222newDog();这句是向上转型,程序加载顺序和第一部分DogdognewDog();是一样的,都是实例化类的过程Dogdog333(Dog)dog222;这个是向下转型,并没有调用类构造器,这块等会和第3部分结合讲 最后我们来看下第3部分Dogdog444(Dog)newAnimal();这句先实例化Andimal类,它没有父类,就直接实例化当前类,打印thisAnimalPrint:10。然后(Dog)表示向下转型,但是为啥运行会报ClassCastException异常呢?且第2部分Dogdog333(Dog)dog222;却没有问题?我们可以发现,向下转型可以让子类找回其独有的方法但是向下转型是不安全的,实现向下转型前需要先实现向上转型。 本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我! 计算机内功、JAVA源码、职业成长、项目实战、面试相关资料等更多精彩文章在公众号小牛呼噜噜