Java基础3-JVM层面理解Java继承、封装、多态的实现原理(二)

简介: Java基础3-JVM层面理解Java继承、封装、多态的实现原理(二)

Java基础3-JVM层面理解Java继承、封装、多态的实现原理(一):https://developer.aliyun.com/article/1535614

继承的实现原理

Java 的继承机制是一种复用类的技术,从原理上来说,是更好的使用了组合技术,因此要理解继承,首先需要了解类的组合技术是如何实现类的复用的。

使用组合技术复用类 假设现在的需求是要创建一个具有基本类型,String 类型以及一个其他非基本类型的对象。该如何处理呢?

对于基本类型的变量,在新类中成员变量处直接定义即可,但对于非基本类型变量,不仅需要在类中声明其引用,并且还需要手动初始化这个对象。

这里需要注意的是,编译器并不会默认将所有的引用都创建对象,因为这样的话在很多情况下会增加不必要的负担,因此,在合适的时机初始化合适的对象,可以通过以下几个位置做初始化操作:

在定义对象的地方,先于构造方法执行。在构造方法中。在正要使用之前,这个被称为惰性初始化。使用实例初始化。

class Soap {    
    private String s;    
    Soap() {        
        System.out.println("Soap()");        
        s = "Constructed";    
    }    
    public String tiString(){        
        return s;    
    }
}
public class Bath {    
    // s1 初始化先于构造函数    
    private String s1 = "Happy", s2 = "Happy", s3, s4;    
    private Soap soap;    
    private int i;    
    private float f;    
    public Both() {        
        System.out.println("inSide Both");        
        s3 = "Joy";       
         f = 3.14f;        
        soap = new Soap();    
    }    
    {        
        i = 88;    
    }    
    public String toString() {        
        if(s4 == null){            
            s4 = "Joy"        
        }        
        return "s1 = " + s1 +"\n" +               
                "s2 = " + s2 +"\n" +               
                "s3 = " + s3 +"\n" +               
                "s4 = " + s4 +"\n" +               
                "i = " + i +"\n" +               
                "f = " + f +"\n" +               
                "soap = " + soap;    
    }
}

继承 Java 中的继承由 extend 关键字实现,组合的语法比较平实,而继承是一种特殊的语法。当一个类继承自另一个类时,那么这个类就可以拥有另一个类的域和方法。

class Cleanser{    
    private String s = "Cleanser";    
    public void append(String a){        
        s += a;    
    }    
    public void apply(){        
        append("apply");    
    }    
    public void scrub(){        
        append("scrub");    
    }    
    public String toString(){        
        return s;    
    }    
    public static void main(String args){        
        Cleanser c = new Cleanser();        
        c.apply();        
        System.out.println(c);    
    }
}
public class Deter extends Cleanser{    
    public void apply(){       
         append("Deter.apply");       
         super.scrub();    
    }    
    public void foam(){        
        append("foam");   
     }   
     public static void main(String args){        
        Deter d = new Deter();        
        d.apply();        
        d.scrub();        
        d.foam();        
        System.out.println(d);       
         Cleanser.main(args);    
    }
}

上面的代码中,展示了继承语法中的一些特性:

子类可以直接使用父类中公共的方法和成员变量(通常为了保护数据域,成员变量均为私有) 子类中可以覆盖父类中的方法,也就是子类重写了父类的方法,此时若还需要调用被覆盖的父类的方法,则需要用到 super 来指定是调用父类中的方法。子类中可以自定义父类中没有的方法。可以发现上面两个类中均有 main 方法,命令行中调用的哪个类就执行哪个类的 main 方法,例如:java Deter。继承语法的原理 接下来我们将通过创建子类对象来分析继承语法在我们看不到的地方做了什么样的操作。

可以先思考一下,如何理解使用子类创建的对象呢,首先这个对象中包含子类的所有信息,但是也包含父类的所有公共的信息。

下面来看一段代码,观察一下子类在创建对象初始化的时候,会不会用到父类相关的方法。

class Art{    
    Art() {        
        System.out.println("Art Construct");    
    }
}
class Drawing extends Art {    
    Drawing() {        
       System.out.println("Drawing Construct");    
    }
}
public class Cartoon extends Drawing {    
    public Cartoon() {        
    System.out.println("Cartoon construct");    
}    
public void static main(String args) {        
    Cartoon c = new Cartoon();    
    }
}
/*output:Art ConstructDrawing ConstructCartoon construct*/

通过观察代码可以发现,在实例化Cartoon时,事实上是从最顶层的父类开始向下逐个实例化,也就是最终实例化了三个对象。编译器会默认在子类的构造方法中增加调用父类默认构造方法的代码。

因此,继承可以理解为编译器帮我们完成了类的特殊组合技术,即在子类中存在一个父类的对象,使得我们可以用子类对象调用父类的方法。而在开发者看来只不过是使用了一个关键字。

注意:虽然继承很接近组合技术,但是继承拥有其他更多的区别于组合的特性,例如父类的对象我们是不可见的,对于父类中的方法也做了相应的权限校验等。

那么,如果类中的构造方法是带参的,该如何操作呢?(使用super关键字显示调用)

见代码:

class Game {    
    Game(int i){        
        System.out.println("Game Construct");    
    }
}
class BoardGame extends Game {    
    BoardGame(int j){        
        super(j);        
        System.out.println("BoardGame Construct");    
    }
}
public class Chess extends BoardGame{    
    Chess(){        
        super(99);        
        System.out.println("Chess construct");    
    }    
    public static void main(String args) {        
        Chess c = new Chess();    
    }
}

/*output:Game ConstructBoardGame ConstructChess construc*/

重载和重写的实现原理

   刚开始学习Java的时候,就了解了Java这个比较有意思的特性:重写 和 重载。开始的有时候从名字上还总是容易弄混。我相信熟悉Java这门语言的同学都应该了解这两个特性,可能只是从语言层面上了解这种写法,但是jvm是如何实现他们的呢 ?

重载官方给出的介绍:

一.  overload: The Java programming language supports overloading methods, and Java can distinguish between methods with different method signatures. This means that methods within a class can have the same name if they have different parameter lists .

Overloaded methods are differentiated by the number and the type of the arguments passed into the method.

You cannot declare more than one method with the same name and the same number and type of arguments, because the compiler cannot tell them apart.

The compiler does not consider return type when differentiating methods, so you cannot declare two methods with the same signature even if they have a different return type.

首先看一段代码,来看看代码的执行结果:

public class OverrideTest {
    class Father{}    
    class Sun extends Father {}    
    public void doSomething(Father father){        
        System.out.println("Father do something");    
    }    
    public void doSomething(Sun father){        
        System.out.println("Sun do something");    
    }    
    public static void main(String [] args){        
        OverrideTest overrideTest = new OverrideTest();        
        Father sun = overrideTest.new Sun();        
        Father father = overrideTest.new Father();        
        overrideTest.doSomething(father);        
        overrideTest.doSomething(sun);    
    }
}

看下这段代码的执行结果,最后会打印:

Father do something Father do something

为什么会打印出这样的结果呢?首先要介绍两个概念:静态分派和动态分派

静态分派:依赖静态类型来定位方法执行版本的分派动作称为静态分派

动态分派:运行期根据实际类型确定方法执行版本的分派过程。

他们的区别是:

  1. 静态分派发生在编译期,动态分派发生在运行期;
  2. private,static,final 方法发生在编译期,并且不能被重写,一旦发生了重写,将会在运行期处理。
  3. 重载是静态分派,重写是动态分派

回到上面的问题,因为重载是发生在编译期,所以在编译期已经确定两次 doSomething 方法的参数都是Father类型,在class文件中已经指向了Father类的符号引用,所以最后会打印两次Father do something。

二. override: An instance method in a subclass with the same signature (name, plus the number and the type of its parameters) and return type as an instance method in the superclass overrides the superclass's method.

The ability of a subclass to override a method allows a class to inherit from a superclass whose behavior is "close enough" and then to modify behavior as needed. The overriding method has the same name, number and type of parameters, and return type as the method that it overrides. An overriding method can also return a subtype of the type returned by the overridden method. This subtype is called a covariant return type.

还是上面那个代码,稍微改动下

public class OverrideTest {    
    class Father{
        public void doSomething(){                System.out.println("Father do something");    
    }    }   
    class Sun extends Father {
        public void doSomething(){                System.out.println("Sun do something");    
    }    }            
    public static void main(String [] args){        
        OverrideTest overrideTest = new OverrideTest();        
        Father sun = overrideTest.new Sun();        
        Father father = overrideTest.new Father();        
        overrideTest.doSomething();        
        overrideTest.doSomething();    
    }
}

最后会打印:

Father do something

Sun do something

相信大家都会知道这个结果,那么这个结果jvm是怎么实现的呢?

在编译期,只会识别到是调用Father类的doSomething方法,到运行期才会真正找到对象的实际类型。

首先该方法的执行,jvm会调用invokevirtual指令,该指令会找栈顶第一个元素所指向的对象的实际类型,如果该类型存在调用的方法,则会走验证流程,否则继续找其父类。这也是为什么子类可以直接调用父类具有访问权限的方法的原因。简而言之,就是在运行期才会去确定对象的实际类型,根据这个实际类型确定方法执行版本,这个过程称为动态分派。override 的实现依赖jvm的动态分派


目录
打赏
0
0
0
0
93
分享
相关文章
|
13天前
|
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
35 0
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
130 29
JVM简介—1.Java内存区域
JVM实战—1.Java代码的运行原理
本文介绍了Java代码的运行机制、JVM类加载机制、JVM内存区域及其作用、垃圾回收机制,并汇总了一些常见问题。
JVM实战—1.Java代码的运行原理
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
68 17
Java HashMap详解及实现原理
重学Java基础篇—Java对象创建的7种核心方式详解
本文全面解析了Java中对象的创建方式,涵盖基础到高级技术。包括`new关键字`直接实例化、反射机制动态创建、克隆与反序列化复用对象,以及工厂方法和建造者模式等设计模式的应用。同时探讨了Spring IOC容器等框架级创建方式,并对比各类方法的适用场景与优缺点。此外,还深入分析了动态代理、Unsafe类等扩展知识及注意事项。最后总结最佳实践,建议根据业务需求选择合适方式,在灵活性与性能间取得平衡。
50 3
|
13天前
|
重学Java基础篇—Java泛型深度使用指南
本内容系统介绍了Java泛型的核心价值、用法及高级技巧。首先阐述了泛型在**类型安全**与**代码复用**中的平衡作用,解决强制类型转换错误等问题。接着详细讲解了泛型类定义、方法实现、类型参数约束(如边界限定和多重边界)、通配符应用(PECS原则)以及类型擦除的应对策略。此外,还展示了泛型在通用DAO接口、事件总线等实际场景的应用,并总结了命名规范、边界控制等最佳实践。最后探讨了扩展知识,如通过反射获取泛型参数类型。合理运用泛型可大幅提升代码健壮性和可维护性,建议结合IDE工具和单元测试优化使用。
17 1
|
13天前
|
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
23 1
列表结构与树结构转换分析与工具类封装(java版)
本文介绍了将线性列表转换为树形结构的实现方法及工具类封装。核心思路是先获取所有根节点,将其余节点作为子节点,通过递归构建每个根节点的子节点。关键在于节点需包含 `id`、`parentId` 和 `children` 三个属性。文中提供了两种封装方式:一是基于基类 `BaseTree` 的通用工具类,二是使用函数式接口实现更灵活的方式。推荐使用后者,因其避免了继承限制,更具扩展性。代码示例中使用了 Jackson 库进行 JSON 格式化输出,便于结果展示。最后总结指出,理解原理是进一步优化和封装的基础。
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
Java网络编程封装
Java网络编程封装原理旨在隐藏底层通信细节,提供简洁、安全的高层接口。通过简化开发、提高安全性和增强可维护性,封装使开发者能更高效地进行网络应用开发。常见的封装层次包括套接字层(如Socket和ServerSocket类),以及更高层次的HTTP请求封装(如RestTemplate)。示例代码展示了如何使用RestTemplate简化HTTP请求的发送与处理,确保代码清晰易维护。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等