放弃该放弃的是无奈,放弃不该放弃的是无能,不放弃该放弃的是无知,不放弃不该放弃的是执着。
愿自己能在自己所热爱的道路上越走越远。
从最初的学Java的时候我们知道Java的三大特性是:封装、继承、多态. 可是当初在学的时候,有谁把他们之间的关系真正搞明白了吗? 由于本篇文章是对重载与重写的思考, 那么着重讲Java中的多态. 虽然说着重讲多态,但是对于Java面向对象开发的四种特性也先说明下:
Java面向对象四种基本特性:抽象、封装、继承、多态,可以这么理解,多态体现在抽象、封装以及继承上,而抽象、继承、封装体现在Java中的多态特性中. 也可以这么理解:多态是抽象、封装、继承的体现,抽象、封装、继承是多态的基础. Java中的多态就是对同一类消做出不同响应就叫做多态,Java中23中设计模式很大一部分就就是利用Java中多态来完成的. 其目的就是为了解耦.
我们从Java虚拟机角度来理解下多态
- 编译时期的多态既方法重载,这是静态分派(也就是对象的静态类型): 就是在编译时期对象找方法是根据静态类型来查找的. 如果不知道这一点的应该去仔细复习下深入理解Java虚拟机中相关章节.
- 运行时多态既方法的重写:这是一种动态分配,既会根据对象的实体类型进行分配.
众所周知Java中的多态分为两种,第一种:方法重载的多态,第二种:方法重写的多态。 或许我在表达上可能语言不是很准确, 但是对于我目前自己的理解上来看,Java中的多态目前就是这两种性质的多态. 或许我的理解在教科书上没有体现出来, 但是我觉的那也是多态的一种体现.
首先我们从Java源码中来看下多态的应用: 运行时多态
我在阅读ReentrantLock
的源码的时候,注意到了一个有趣的现象:
我们在使用ReentrantLock
加锁的时候使用lock.lock()
方法对其加锁, 当我们点进去看其源码的将会发现其实ReentrantLock
是在内部维护了一个Sync
的内部类, 该内部类继承了AQS
,并且在其分为公平锁和非公平锁, 如果我们随便点进去看看加锁过程,其实不难发现(这里我们举例公平锁), 公平锁也隐式的继承了AQS
, 并且其重写了方法tryAcquire
方法, 但是我们在FairSync
的加锁方法lock()
方法中查看acquire()
方法时发现acquire()
方法时AQS
提供的, 也就是说FairSync
中的lock方法其实是调用了父类的acquire
方法, 但是在该方法中, 其又调用了tryAcquire
方法,但是这个方法其调用的不是AQS
提供的默认方法,而是FairSync
重写了的方法。 如果不懂多态,那么这段源码很有可能就会看不懂.
下面来看看其具体的代码.
// ReentrantLock加锁方法
public void lock() {
// 其调用了内部类sync的lock方法,而该内部类继承了AQS.
sync.lock();
}
// 但是当我们点击去lock犯法查看的时候,发现其Sync内部类只是提供了一个抽象的加锁方法,具体的加锁方法由其子类来来实现而其子类分别有公平锁和非公平锁, 下文就以介绍公平锁为例来阐述Java中的多态.
abstract void lock();
// 下面lock方法是公平锁中的lock方法.
final void lock() {
acquire(1);
}
// 我们来看看acquire方法, 到那时当我们点击去之后我们会发现该方法是AQS的一个默认方法.
// 从这段代码中我们也能看出AQS必须要求让我们去实现以以方法,否则将会报出错误,这是因为tryAcquire方法在AQS中的默认实现就是跑出一个运行时异常,但是如果说我们不去实现这个方法,那么就会直接抛出异常.
// 所以说这里的tryAcquire调用的是公平锁重写AQS类的该方法.
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 而下面这段代码是ReentrantLock内部类对继承的AQS方法的重写,但是在对其使用的时候,是多态使用的.
// 因为这段代码是在父类AQS中被调用的,但是如果不是多态的作用,那么上面代码调用tryAcquire方法的时候应该调用的是AQS的方法,但是明显不是代用的AQS的方法,所以说当JVM执行到上面的方法的时候,就则会执行其被重写的方法. 这也就体现了Java中的多态。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
我们以牛客上的一道Java面试题来更细致的说明多态的作用.
package com.mzx.j2ee;
/**
* @author ZhenXinMa.
* @slogan 浮生若梦, 若梦非梦, 浮生何梦? 如梦之梦.
* @date 2020/9/12 0:25.
*/
public class User {
public void method1() {
System.out.println("这是父类中的方法一");
}
}
class ZhangSan extends User {
@Override
public void method1() {
System.out.println("这是子类中的方法一");
}
public void method2() {
System.out.println("这是子类中的方法二");
}
}
/**
* 模拟测试.
*/
class MainTest {
public static void main(String[] args) {
// 这里面模拟编译时期的方法重载.
// user引用的静态类型是User, 实体类型是ZhangSan
// 在编译时期的多态也就是方法重载是根据对象引用的静态类型进行分配.
User user = new ZhangSan();
// 在编译期间user对象引用调用这个方法是根据静态类型User来调用的.
user.method1();
// 虽然我们知道user这个对象的实体类型是ZhangSan,也知道该类型中有方法二这个方法.
// 但是在编译期间发生的多态是根据静态类型来查找方法并且通过栈帧来执行的.
// 而这里的user对象的静态类型里面么以偶method2这个方法,所以说在编译期间这顿啊代码是不会被执行的.
// 其实我们做开发的,如果不去探讨这些基础的东西,这些东西很可能是不会的,因为在编译期间如果代码不能通过编译,那么就不会进行提示,
// 或者代码直接爆红显示不给予运行,该例中就是这样,在调用方法2的时候,IDE将会给出详细的提示.
user.method2();
}
}
牛客上还有一道面试题,也是关于重载和重写的.
/**
* 重载和重写.
*
* @author ZhenXinMa.
* @slogan 脚踏实地向前看.
* @create 2020-09-29 20:51 周二.
*/
public class Overload {
public static void main(String[] args) {
// TODO: 2020/9/29 重载和重写实现未完成.
// 看看sun最终会输出什么.
Sun sun = new Sun();
}
}
class Father {
public Father() {
// 这个this是对重写的考验.
m2();
}
public void m1() {
System.out.println("父类中的方法1.");
}
public void m2() {
System.out.println("父类中的方法2.");
}
}
/**
* 子类只重写方法一.
*/
class Sun extends Father {
public Sun() {
// 先调用父类的构造函数.
m1();
}
@Override
public void m1() {
System.out.println("这是子类中的方法一.");
}
@Override
public void m2() {
System.out.println("这是子类中的方法二.");
}
}
你们能想到最终的结果会输出什么吗?
其实最终的输出结果可能会出人意料:
但是这里为什么呢? 为什么第一个输出语句是这是子类中的方法二
?
下面来一步一步的分析下.
public static void main(String[] args) {
// 1. 首先会调用sun的空的构造函数.
// 2. 由于Sun继承了Father,所以在空的构造函数中, 会先默认调用super()父类的构造函数.
// 3. 此时JVM会去执行父类的构造函数, 当执行到父类的构造函数时,其调用了m2();
// 3. 但是此时Father的空的构造函数是由于子类触发的, 并且Father中的this指向的实体是Sun.
// 其实最重要的就是理解为什么Father中的this是指向的Sun对象.
// 4. 说明下为什么this指向了Sun实例对象: 首先我们没有去创造出一个Father对象, 在父类构造函数中调用m2()方法的时候, 由于没有一个Father对象实例化, 所以说Father中的m2是没有初始化的, 而其子类却已经实现了m2(), 所以说猜想: 在这里JVM是将m2的方法句柄应该和Sun实例有关.
// 5. 所以说第一句输出应该是: 子类中的方法二.
// 6. 第二句输出应该是: 子类中的方法一.
Sun sun = new Sun();
}