图片出处:The world's biggest drone photo and video sharing platform | SkyPixel.com
封装、继承、多态等是面向对象程序三大特性,也是JavaSe基础语法中较难的部分。在本文之我会结合所学的知识与自己的理解向大家阐明这三种特性的概念及运用。
目录
前言
封装
1,封装的概念
2,封装的实现
继承
1,继承的概念
2,继承的语法
3,父类成员的访问
4,super关键字
5,子类构造方法
6,继承时代码执行顺序
7,继承方式
多态
1,向上转型
2,重写
3,动态绑定
4,多态
5,向下转型
6,多态的优缺点
7,避免在构造方法中调用重写方法
封装,简而言之就是套壳屏蔽细节。将数据的操作、数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在Java语言中主要通过类和访问权限来实现封装:类可以将数据以及封装数据的方法结合在一起,更符合人类对事物的认 知,而访问权限用来控制方法或者字段能否直接在类外使用。
在Java中,主要通过 private 访问修饰限定符来实现封装。被private修饰封装后,可以在类中提供公开的get 或者 set 接口来帮助我们进行获取或者修改的操作。
private:
类权限,允许在同一类中进行访问
示例:
实例引入:
如上,我创建了两个类,分别是猫类和狗类,在这两个类中我们可以发现,这两个小动物之间都有姓名、年龄、吃饭这几个共同特点。
那么在Java语言中我们便可以将代码做出如下优化:
我将这里这两个类中相同的特点抽取出来,单独创建一个新的Animal类,再通过extends关键字将Animal类继承给Dog和Cat这两个类,同样也实现了这两个类的创建。
这就是继承,专门用来进行共性抽取,实现代码的复用。
继承机制:是面对对象程序设计使代码可以复用的最主要最重要的手段,它允许程序员在保持原有类特性的基础上进行拓展,增加新功能这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
在继承中,其被继承的类称之为父类,继承的类称之为子类。如上示例,Animal就是Cat和Dog的父类,Cat和Dog就是Animal的子类。
当然其还有更多叫法,如下图:
在Java语法中,继承由extends关键字实现
关键字extends用于创建一个类的子类(派生类)。通过使用extends关键字,子类可以继承父类的属性和方法,并且可以添加自己的属性和方法。子类可以继承父类的非私有成员变量和方法,但不能继承构造函数和私有成员变量。
继承语法形式:
//修饰符 class 子类 extends 父类 {
//...
}
注意:
子类中访问父类的成员变量:
(1)子类和父类不存在同名成员变量
当不存在同名成员变量时,直接访问从父类继承下来的成员变量。
(2)子类和父类存在同名成员变量
如果子类和父类存在同名的成员变量,则优先访问子类的成员变量。
子类中访问父类的成员方法:
(1)成员方法名字不同
成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,优先访问子类自己的,自己没有时再去父类中找,如果父类中也没有则报错。
(2)成员方法名字相同
相同时,优先调用子类自有的方法。
综上:在子类中,若是存在与父类的成员变量/方法 ,名字、参数相同的成员,优先访问子类中的;若不存在,就直接去父类中找,找不到就报错。
(1)super关键字的概念及使用
由于设计不好或者场景需要,子类和父类中可能会存在相同名称的成员,如果需要在子类方法中访问父类同名成员时,该如何操作?直接访问是做不到的,Java提供了super关键字。
主要作用:在子类方法中访问父类的成员
使用示例:
在上述代码中,我分别在子类与父类中定义了同名的成员变量及成员方法,通过加不加super关键字修饰来查看输出结果,输出如下:
可以看到,当遇到子类、父类成员变量/方法同名时,都是优先使用子类的成员变量/方法。但是一旦使用super关键字修饰,便可指定为父类成员变量/方法。
(2)super关键字的解析(不严谨,仅作理解)
super关键字类似于this引用,只不过super关键字会默认指向父类继承的部分。
如下图:
不论是父类继承的成员还是子类自己的成员,这都是子类对象的,此时this引用指向这整个对象,并且当子类父类成员同名时,this会默认先指向子类。
但是当使用super关键字修饰成员变量/方法时,便会直接指向父类。
(3)super的三个引用
super.成员变量
//引用父类继承来的成员变量
super.成员方法
//引用父类继承来的成员方法
super.();
//调用父类的构造方法 -->在下文会讲到
在继承中创建构造方法时一定要谨记一个原则:先调用父类构造方法,待父类构造完毕后再执行子类构造方法!
(1)当父类中没有定义构造方法时
可以看到,父类未创建构造方法,但是上述代码并未出现报错。这是因为当类中无构造方法时,编译器会自动添加无参构造方法。
并且在子类中,编译器会在子类的构造方法第一行调用父类的构造方法,以实现先完成父类构造,再完成子类构造。实际实现效果如下:
(2)当父类中定义了有参构造方法时
可以看到,当在父类中创建有参的构造方法时,子类出现报错。
原因是,当父类中有了至少一个构造方法时,编译器本着“救急不救穷”的原则,将不再对其添加无参构造方法;而在子类的构造方法中,只会自动引用无参构造方法,此时没有无参的构造方法,编译错误。
可以修改为:
可以选择在父类中添加一个无参构造方法,也可以添加一个新的子类构造方法,在调用父类构造方法时对其传参。当然,最好两种都有,如此一来就可以根据super();括号内的参数选择合适的构造方法。
注意:
(1) 在没有继承时,静态代码块、实例代码块、构造方法的执行顺序如下:
(2)继承代码如下:
public class Main {
public static class Base{
public static int a;
static{
System.out.println("父类静态代码块");
}
{
System.out.println("父类实例代码块");
}
Base(){
System.out.println("父类构造方法");
}
}
public static class Test extends Base{
static{
System.out.println("子类静态代码块");
}
{
System.out.println("子类实例代码块");
}
Test(){
System.out.println("子类构造方法");
}
public static void main(String[] args) {
Test test = new Test();
System.out.println("==========");
Test test1 = new Test();
}
}
}
两次实例化对象输出结果:
可以看到,静态代码块是第一个执行的,并且只会执行一次。由于需要先调用父类的构造方法,故父类执行先于子类。
在Java语言中,仅支持以下三种继承方式:
时刻牢记:
我们写的类是现实事物的抽象。而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加复杂。
但是即使如此, 我们并不希望类之间的继承层次太复杂。 一般我们不希望出现超过三层的继承关系。 如果继承层次太多, 就需要考虑对代码进行重构了
如果想从语法上进行限制继承, 就可以使用 final 关键字
多态的概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
多态实现条件:
- 必须在继承关系上(向上转型)
- 子类和父类有 同名的重写/覆盖 方法
- 通过父类对象的引用 去调用这个重写的方法
先大致总结,再分点讲解:
(1)概念
在继承关系中,向上转型可以理解为:使用父类类型 引用 子类实例对象。
同时注意一点:Object类是所有类的父类
假设:Animal是父类,Dog是子类,那么如下便是向上转型的一种:
Animal animal = new Dog();
//使用父类类型引用子类实例对象
(2)常见的可以发生向上转型的三个时机
1))直接赋值:
Animal animal = new Dog();
//直接用父类类型引用子类示例对象
2))形参为父类,实参为子类:
public static void func(Animal animal) {
//
}
public static void main(String[] args) {
Dog dog = new Dog();
func(dog);
}
//实参传递子类对象,但是方法形参使用父类类型接收
3))返回类型为父类,实际返回值为子类:
public static Animal func() {
Dog dog = new Dog();
return dog;
}
总而言之就是使用父类类型引用子类实例对象。
(1)概念
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
(2)方法重写的规则
- 子类在重写父类方法时,一般必须与父类方法原型一致: 返回值类型、方法名 、(参数列表) 要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的(例如父类中方法返回值为父类类型,子类方法返回值为子类类型),专业术语叫协变类型
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
- 父类被static、private修饰的方法、构造方法都不能被重写
- 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写
(3)重写与重载的区别
最本质的区别就是:方法重写一般要求返回值、参数列表、方法名完全相同;而方法重载会要求参数链表不同。
满足向上转型,并且子类、父类中有相同的重写方法时,当通过父类对象调用这个重写方法时,就会发生动态绑定。
动态绑定即是:当父类对象调用重写方法时,编译器会自动匹配执行 其向上转型的子类的重写方法。通俗来说就是:父类对象引用哪个子类对象,在调用重写方法时就会自动匹配该子类对象的重写方法。
当当前环境具备向上转型、重写方法、动态绑定这几个条件时,就可以实现多态性。如下:
上图是一个基本的多态应用示例,实现逻辑如下:
完成以上步骤即是实现程序的多态性。
那么到现在,我们就可以对多态的概念进行更加深入的总结:
多态概念:
当父类引用,引用的子类对象不一样的时候,调用这个重写的方法,所表现的行为是不一样的,我们把这种思想叫做多态。
将子类对象经过向上转型可以让父类引用调用子类的重写方法,但是却无法调用子类特有的方法,此时:将父类引用再还原成子类对象即可,这就是向下转型。
但是向下转型是不安全的,以上面多态为例:
(1)当Shape引用的对象是Triangle类实例对象时:
编译通过
(2)当Shape引用的对象不是Triangle类实例对象时:
报告异常:类型转换异常
2)))规避方法:可以使用 instanceof 关键字
instanceof关键字:
if(实例对象 instanceof 类名) { } //若前者实例对象为该类的实例对象,则返回true,否则返回false
综上原因:
向上转换是父类类型引用子类实例对象,如:动物类引用狗类实例对象。狗属于动物,故属于一种包含关系;
向下转换是子类类型引用父类实例对象,如:
狗类引用动物类实例对象。动物中包含狗,但不能说动物就是狗。但是当此时动物类实例对象为狗类实例对象时,强转成功,不会报错;
如果此时动物类引用实例对象是猫类、鼠类实例对象时,这些就和狗类无关,强转会失败,并报类型转换异常。
优点:
(1)能够降低代码的 "圈复杂度", 避免使用大量的 if - else
(2)可扩展能力更强
缺点:
(1)代码的运行效率降低。
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
思考一个问题:num打印的是几?
实际运行逻辑:
可以看到,逻辑比较复杂,故应当避免写出这样的代码!