java 内部类调用外部类_深入理解Java中为什么内部类可以访问外部类的成员

2023-10-11 13:40

本文主要是介绍java 内部类调用外部类_深入理解Java中为什么内部类可以访问外部类的成员,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

内部类简介

虽然Java是一门相对比较简单的编程语言,但是对于初学者, 还是有很多东西感觉云里雾里, 理解的不是很清晰。内部类就是一个经常让初学者感到迷惑的特性。 即使现在我自认为Java学的不错了, 但是依然不是很清楚。其中一个疑惑就是为什么内部类对象可以访问外部类对象中的成员(包括成员变量和成员方法)? 早就想对内部类这个特性一探究竟了,今天终于抽出时间把它研究了一下。

内部类就是定义在一个类内部的类。定义在类内部的类有两种情况:一种是被static关键字修饰的, 叫做静态内部类, 另一种是不被static关键字修饰的, 就是普通内部类。 在下文中所提到的内部类都是指这种不被static关键字修饰的普通内部类。 静态内部类虽然也定义在外部类的里面, 但是它只是在形式上(写法上)和外部类有关系, 其实在逻辑上和外部类并没有直接的关系。而一般的内部类,不仅在形式上和外部类有关系(写在外部类的里面), 在逻辑上也和外部类有联系。 这种逻辑上的关系可以总结为以下两点:

1 内部类对象的创建依赖于外部类对象;

2 内部类对象持有指向外部类对象的引用。

上边的第二条可以解释为什么在内部类中可以访问外部类的成员。就是因为内部类对象持有外部类对象的引用。但是我们不禁要问, 为什么会持有这个引用? 接着向下看, 答案在后面。

通过反编译字节码获得答案

在源代码层面, 我们无法看到原因,因为Java为了语法的简介, 省略了很多该写的东西, 也就是说很多东西本来应该在源代码中写出, 但是为了简介起见, 不必在源码中写出,编译器在编译时会加上一些代码。 现在我们就看看Java的编译器为我们加上了什么?

首先建一个工程TestInnerClass用于测试。 在该工程中为了简单起见, 没有创建包, 所以源代码直接在默认包中。在该工程中, 只有下面一个简单的文件。

该文件很简单, 就不用过多介绍了。 在外部类Outer中定义了内部类Inner, 并且在Inner的方法中访问了Outer的成员变量outerField。

虽然这两个类写在同一个文件中, 但是编译完成后, 还是生成各自的class文件:

a5fa7c1fb9893c7a3d66090bfd60ba06.png 

这里我们的目的是探究内部类的行为, 所以只反编译内部类的class文件Outer$Inner.class 。 在命令行中, 切换到工程的bin目录, 输入以下命令反编译这个类文件:

-classpath . 说明在当前目录下寻找要反编译的class文件 -v 加上这个参数输出的信息比较全面。包括常量池和方法内的局部变量表, 行号, 访问标志等等。

注意, 如果有包名的话, 要写class文件的全限定名, 如:

反编译的输出结果很多, 为了篇幅考虑, 在这里我们省略了常量池。 下面给出除了常量池之外的输出信息。

首先我们会看到, 第一行的信息如下:

这句话的意思是, 在内部类Outer$Inner中, 存在一个名字为this$0 , 类型为Outer的成员变量, 并且这个变量是final的。 其实这个就是所谓的“在内部类对象中存在的指向外部类对象的引用”。但是我们在定义这个内部类的时候, 并没有声明它, 所以这个成员变量是编译器加上的。

虽然编译器在创建内部类时为它加上了一个指向外部类的引用, 但是这个引用是怎样赋值的呢?毕竟必须先给他赋值, 它才能指向外部类对象。 下面我们把注意力转移到构造函数上。 下面这段输出是关于构造函数的信息。

我们知道, 如果在一个类中, 不声明构造方法的话, 编译器会默认添加一个无参数的构造方法。 但是这句话在这里就行不通了, 因为我们明明看到, 这个构造函数有一个构造方法, 并且类型为Outer。 所以说, 编译器会为内部类的构造方法添加一个参数, 参数的类型就是外部类的类型。

下面我们看看在构造参数中如何使用这个默认添加的参数。 我们来分析一下构造方法的字节码。 下面是每行字节码的意义:

aload_0 : 将局部变量表中的第一个引用变量加载到操作数栈。 这里有几点需要说明。 局部变量表中的变量在方法执行前就已经初始化完成;局部变量表中的变量包括方法的参数;成员方法的局部变量表中的第一个变量永远是this;操作数栈就是执行当前代码的栈。所以这句话的意思是: 将this引用从局部变量表加载到操作数栈。

aload_1:

将局部变量表中的第二个引用变量加载到操作数栈。 这里加载的变量就是构造方法中的Outer类型的参数。

putfield #10 // Field this$0:LOuter;

使用操作数栈顶端的引用变量为指定的成员变量赋值。 这里的意思是将外面传入的Outer类型的参数赋给成员变量this$0 。 这一句putfield字节码就揭示了, 指向外部类对象的这个引用变量是如何赋值的。

下面几句字节码和本文讨论的话题无关, 只做简单的介绍。 下面几句字节码的含义是: 使用this引用调用父类(Object)的构造方法然后返回。

用我们比较熟悉的形式翻译过来, 这个内部类和它的构造函数有点像这样: (注意, 这里不符合Java的语法, 只是为了说明问题)

说到这里, 可以推想到, 在调用内部类的构造器初始化内部类对象的时候, 编译器默认也传入外部类的引用。 调用形式有点像这样: (注意, 这里不符合java的语法, 只是为了说明问题)

ba6deb1c3b400f0e0a36e52852790c62.pngvcq9ysfP4M2stcShoyDU2sTasr/A4LXESW5uZXJNZXRob2S3vbeo1tCjrCC3w87KwcvN4rK/wOC1xLPJ1LGx5MG/b3V0ZXJGaWVsZKOsIM/Cw+a1xNfWvdrC673Syr7By7fDzsrKx8jnus69+NDQtcSjugo8YnI+Cgo8cHJlIGNsYXNzPQ=="brush:java;"> void InnerMethod(); flags: Code: stack=1, locals=2, args_size=1 0: aload_0 1: getfield #10 // Field this$0:LOuter; 4: getfield #20 // Field Outer.outerField:I 7: istore_1 8: return

getfield #10 // Field this$0:LOuter;

将成员变量this$0加载到操作数栈上来

getfield #20 // Field Outer.outerField:I

使用上面加载的this$0引用, 将外部类的成员变量outerField加载到操作数栈

istore_1

将操作数栈顶端的int类型的值保存到局部变量表中的第二个变量上(注意, 第一个局部变量被this占用, 第二个局部变量是i)。操作数栈顶端的int型变量就是上一步加载的outerField变量。 所以, 这句字节码的含义就是: 使用outerField为i赋值。

上面三步就是内部类中是如何通过指向外部类对象的引用, 来访问外部类成员的。

总结

文章写到这里, 相信读者对整个原理就会有一个清晰的认识了。 下面做一下总结:

本文通过反编译内部类的字节码, 说明了内部类是如何访问外部类对象的成员的,除此之外, 我们也对编译器的行为有了一些了解, 编译器在编译时会自动加上一些逻辑, 这正是我们感觉困惑的原因。

关于内部类如何访问外部类的成员, 分析之后其实也很简单, 主要是通过以下几步做到的:

1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用;

2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为1中添加的成员变量赋值;

3 在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用。

这篇关于java 内部类调用外部类_深入理解Java中为什么内部类可以访问外部类的成员的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/188449

相关文章

一文深入详解Python的secrets模块

《一文深入详解Python的secrets模块》在构建涉及用户身份认证、权限管理、加密通信等系统时,开发者最不能忽视的一个问题就是“安全性”,Python在3.6版本中引入了专门面向安全用途的secr... 目录引言一、背景与动机:为什么需要 secrets 模块?二、secrets 模块的核心功能1. 基

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志

《SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志》在SpringBoot项目中,使用logback-spring.xml配置屏蔽特定路径的日志有两种常用方式,文中的... 目录方案一:基础配置(直接关闭目标路径日志)方案二:结合 Spring Profile 按环境屏蔽关

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

SpringBoot排查和解决JSON解析错误(400 Bad Request)的方法

《SpringBoot排查和解决JSON解析错误(400BadRequest)的方法》在开发SpringBootRESTfulAPI时,客户端与服务端的数据交互通常使用JSON格式,然而,JSON... 目录问题背景1. 问题描述2. 错误分析解决方案1. 手动重新输入jsON2. 使用工具清理JSON3.

java中long的一些常见用法

《java中long的一些常见用法》在Java中,long是一种基本数据类型,用于表示长整型数值,接下来通过本文给大家介绍java中long的一些常见用法,感兴趣的朋友一起看看吧... 在Java中,long是一种基本数据类型,用于表示长整型数值。它的取值范围比int更大,从-922337203685477

java Long 与long之间的转换流程

《javaLong与long之间的转换流程》Long类提供了一些方法,用于在long和其他数据类型(如String)之间进行转换,本文将详细介绍如何在Java中实现Long和long之间的转换,感... 目录概述流程步骤1:将long转换为Long对象步骤2:将Longhttp://www.cppcns.c

SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程

《SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程》LiteFlow是一款专注于逻辑驱动流程编排的轻量级框架,它以组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑,下面给大... 目录一、基础概念1.1 组件(Component)1.2 规则(Rule)1.3 上下文(Conte