Java继承的多态魅力:向上转型的艺术
Java继承的多态魅力:向上转型的艺术
dong4jJava 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。比如可以先定义一个类叫车,车有以下属性:车体大小,颜色,方向盘,轮胎,而又由车这个类派生出轿车和卡车两个类,为轿车添加一个小后备箱,而为卡车添加一个大货箱。
- 子类拥有父类非 private 的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。(重载)
继承 (类与类的关系)
有相同的属性和相同的行为时, 定义为父类,
其他的 子类 只需要写特有的属性和方法.
在面向对象中, 可以通过扩展一个已有的类, 并继承该类的属性和行为,
来创建一个新的类, 从而达到代码的复用.
继承是所有 OOP 语言不可缺少的部分,
在 java 中使用 extends
关键字来表示继承关系。
当创建一个类时,总是在继承,如果没有明确指出要继承的类,
就总是隐式地从根类 Object 进行继承。
继承不会继承父类的构造方法
因为:
- 语法上如果继承来了父类的构造方法, 但是类名是父类名, 跟子类名不一样
- 在面向对象的思想上说不通
类与类的关系
- has a (组合) - 属性
- is a (继承)
- use a (使用) - 参数或局部变量
关于私有属性 有没有继承 能不能继承 算不算继承?
私有属性在子类的存储空间当中他是切实存在的
子类可以继承父类的所有成员跟方法,继承下来不代表可以访问,要访问得看访问控制规则。私有属性也可以继承,不过根据访问控制规则,私有属性虽继承下来却不可以访问的,只有通过 public 的方法才能访问继承下来的私有属性。
B 继承 A 类,。A 类中的私有属性,到了 B 会怎么样,能继承、访问吗?
答案是:如果 A 中的属性有增加 setter getter 方法,可以访问的:
比如下面这段代码:
1 | class Person { |
类 Man 继承于 Person 类,这样一来的话,
Person 类称为父类(基类),Man 类称为子类(导出类 / 派生类)。
如果两个类存在继承关系,则子类会自动继承父类的方法和变量,
在子类中可以调用父类范围访问修饰符允许的方法和变量。
在上面的例子中, 子类实例可以通过 setter 方法访问父类的私有属性, 所以就证明了子类是继承了父类的全部属性和方法的.
只是这样就破坏了封装性,
在 java 中,只允许单继承,也就是说 一个类最多只能显示地继承于一个父类。
但是一个类却可以被多个类继承,一个类可以拥有多个子类。
子类继承父类的成员变量
当子类继承了某个类之后,便可以使用父类中的成员变量,
但是并不是完全继承父类的所有成员变量。具体的原则如下:
- 能够继承父类的 public 和 protected 成员变量;能够继承父类的 private 成员变量 (用 setter,getter 方法来使用 private 属性);
- 对于父类的包访问权限成员变量,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承;
- 对于子类可以继承的父类成员变量,如果在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。如果要在子类中访问父类中同名成员变量,需要使用
super
关键字来进行引用
子类继承父类的方法
- 对于父类的包访问权限成员方法,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承;
- 对于子类可以继承的父类成员方法,如果在子类中出现了同名称的成员方法,则称为覆盖,即子类的成员方法会覆盖掉父类的同名成员方法。如果要在子类中访问父类中同名成员方法,需要使用 super 关键字来进行引用。
构造器
子类是不能够继承父类的构造器,它只能够被调用.
值得注意的是,如果父类的构造器都是带有参数的,则必须在子类的构造器中显示地通过 super 关键字调用父类的构造器并配以适当的参数列表。如果父类有无参构造器,则在子类的构造器中用 super 关键字调用父类构造器不是必须的,如果没有使用 super 关键字,系统会自动调用父类的无参构造器。
这是为什么呢?:
因为使用子类时候, 实例化一个子类对象时, 然后就可以调用父类的 public 属性和方法;
但是在调用父类属性和方法之前, 我们并没有实例化父类对象, 所以这个时候,
必须在子类构造器中调用父类构造器;
子类 a = new 子类();
- 如果父类没有无参构造器, 则在子类构造器中必须显示的用 super 调用父类构造器.
- 如果父类有无参构造, 则可以不写 super, 系统自动调用父类无参构造;
当 new 一个子类时, 调用子类构造器, 然后子类构造器中调用父类构造器,
看下面这个例子就清楚了:
1 | class Shape { |
这样的代码是没有问题的,如果把父类的无参构造器去掉,则下面的代码必然会出错:
改成下面这样就行了:
1 | class Shape { |
子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法。如果父类没有不带参数的构造方法,那么子类必须用 supper 关键子来调用父类带参数的构造方法,否则编译不能通过
super
super 主要有两种用法:
- super. 成员变量 / super. 成员方法;
- super(parameter1,parameter2….)
第一种用法主要用来在子类中调用父类的同名成员变量或者方法;第二种主要用在子类的构造器中显示地调用父类的构造器,要注意的是,如果是用在子类构造器中,则必须是子类构造器的第一个语句。
可以在我的另一篇博客中看到
向上转型
在上面的继承中我们谈到继承是 is-a 的相互关系,猫继承与动物,所以我们可以说猫是动物,或者说猫是动物的一种。这样将猫看做动物就是向上转型。如下:
1 | public class Person { |
在这我们通过 Person.display(husband)。这句话可以看出 husband 是 person 类型。
将子类转换成父类,在继承关系上面是向上移动的,所以一般称之为向上转型。由于向上转型是从一个叫专用类型向较通用类型转换,所以它总是安全的,唯一发生变化的可能就是属性和方法的丢失。这就是为什么编译器在 “未曾明确表示转型” 或“未曾指定特殊标记”的情况下,仍然允许向上转型的原因。
这也是多态的一种, 关于多态, 将在下一篇博客中总结.
常见的面试笔试题
- 下面这段代码的输出结果是什么?
1 | public class Test { |
输出 1.shape draw construtor
2.shape construtor
3.circle draw constructor
4.circle constructor
这道题目主要考察的是类继承时构造器的调用顺序和初始化顺序。要记住一点:父类的构造器调用以及初始化过程一定在子类的前面。由于 Circle 类的父类是 Shape 类,所以 Shape 类先进行初始化,然后再执行 Shape 类的构造器。接着才是对子类 Circle 进行初始化,最后执行 Circle 的构造器。
类继承时构造器的调用顺序和初始化顺序
在子类中访问一个变量时的查找顺序 (查找方法一样)
重写 Override
发生在继承的时候, 子类方法跟父类方法重名, 发生了覆盖, 重写子类方法的实现;
- 方法名相同
- 参数列表相同
- 返回类型相同
- 访问修饰符必须大于等于父类
- 抛出的异常不能比父类多
隐藏和覆盖的区别
- 隐藏发生在子类属性名跟父类属性名同名的时候, 在子类中调用重名的属性的时候, 只能访问到子类的那个属性; 如果要调用父类的同名属性, 必须用 super. 属性名调用
- 覆盖发生在子类方法名跟父类方法名相同的时候, 也要用 super. 方法名 () 来调用父类中的同名方法;
重载和重写
重写: 是有继承关系的 2 个类发生的
重载: 一个类中有多个相同的方法名
总结:
继承: 把具有相同属性, 相同行为的类抽取出来, 作为一个父类, 然后另一个类去继承这个类, 那么这个类就有了父类的属性和方法
关系:
is a
特点:
内存上, 叠加
效果:
子类拥有父类的属性和行为方法, 但是不继承父类的构造方法;
构造调用顺序:
先父类构造, 再子类构造
修改父类的行为–> 重写
谨慎继承
上面讲了继承所带来的诸多好处,那我们是不是就可以大肆地使用继承呢?送你一句话:慎用继承。
首先我们需要明确,继承存在如下缺陷:
1、父类变,子类就必须变。
2、继承破坏了封装,对于父类而言,它的实现细节对与子类来说都是透明的。
3、继承是一种强耦合关系。
所以说当我们使用继承的时候,我们需要确信使用继承确实是有效可行的办法。那么到底要不要使用继承呢?《Think in java》中提供了解决办法:问一问自己是否需要从子类向父类进行向上转型。如果必须向上转型,则继承是必要的,但是如果不需要,则应当好好考虑自己是否需要继承。
Object 类
Object 类是所有类的直接父类, 如果声明一个类时没有显式的继承另一个类, 那么这个类就会继承 Object 类, 从而拥有了 Object 类中的所有方法, 数组也不例外.
Object 类的中方法
Object 类一共有 11 个方法, 作为祖宗类, 而这 11 个方法还得以保留, 说明这些方法的通用性很高, 所以我们都必须掌握.
protected Object clone()
创建并返回此对象的一个副本boolean equals(Object obj)
指示其他某个对象是否与此对象 “相等”。- 对于 Object 类中, equals 的底层实现就是使用的
==
运算符, 而这并不能满足我们对对象的比较, 所以必须重写, 在 String 类中就重写了此方法 - 基本思路:
- 先判断是否是同一个类的实例, 如果是就返回 true;
- 如果不是, 再分别比较每个属性值, 如果有一个不同, 就返回 flase
- 如果都相同, 就返回 true
- 对于 Object 类中, equals 的底层实现就是使用的
protected void finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。Class<?> getClass()
返回此 Object 的运行时类 (反射中用到)。int hash Code()
返回该对象的哈希码值void notify()
唤醒在此对象监视器上等待的单个线程void notifyAll()
唤醒在此对象监视器上等待的所有线程String toString()
返回该对象的字符串表示- 此方法的实现:
getClass().getName() + '@' + Integer.toHexString(hashCode())
- 如果不重写这个此方法, 返回的是
类名@引用值
, 对于我们来说没用, 所以一般使用 toString 方法时都需要重写
- 此方法的实现:
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。void wait(long timeout)
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待void wait(long timeout, int nanos)
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。