Java的各种内部类

17-各种内部类


  • https://www.cnblogs.com/kxxiaomutou/p/15646878.html
  • 2022-11-23 21:51:14

一、介绍

可以将一个类的定义放在另一个类的定义内部,这就是内部类

内部类可以分为几种具体的类型:

  1. 成员内部类(常说的内部类)
  2. 匿名内部类
  3. 局部内部类
  4. 静态内部类

二、具体四种内部类

1、成员内部类

成员内部类访问外部类的变量

成员内部类又称为普通内部类,它的地位就相当于外部类的一个成员,举个例子:

class animal{private String name;int size = 20;
//成员内部类
class gsize{public void getsize(){System.out.println(size);}public void getname(){System.out.println(name);}
}

}

在这个例子中,gsize​ 类为 animal​ 的内部类,animal​ 为外部类,可以看到 gsize 类可以访问其外部类 animal 的全部成员,包括 private 类型的变量

​#重要 ⭐:#​因为内部类会先创建外部类,然后再创建自己,所以可以直接访问外部类,而外部类不能直接访问内部类(因为有外部类,但是不一定就有内部类),但是外部类可以通过内部类的实例来访问内部类的字段和方法.

由于成员内部类定义在外部类的成员位置上,所以其称为成员内部类,这里有几点要注意:

  1. 成员内部类是可以被修饰的,默认是包的访问权限(default),同样可以添加 protectprivatepublic 修饰,但成员内部类不能用 static 修饰,用 static 修饰就不是成员内部类了,就是后面要介绍的静态内部类了
  2. 成员内部类实质上是一个类的成员,所以它能访问外部类全部成员(包括 static 和 private

外部类访问成员内部类的变量

那外部类如何访问成员内部类的成员呢?看下面的例子:

class animal {private String name;int size = 20;// 声明一个成员内部类的实例gsize x = new gsize();
public void getxx() {x.getname();x.getsize();
}
// 成员内部类
class gsize {public void getsize() {System.out.println(size);}public void getname() {System.out.println(name);}
}

}

外部类想要访问成员内部类的成员必须先创建一个成员内部类的对象(如例子第 5 行),并通过这个得到的对象的引用来访问内部类的元素。

成员内部类访问与外部类重名的变量

既然内部类可以随意访问外部类的成员,但如果内部类的成员与外部类的成员发生重名,那么内部类该如何访问这个两种重名的变量?看下面的的例子:

class animal{private String name;int size = 20;gsize x = new gsize();public void getxx(){x.getname();x.getsize();}class gsize{int size = 1;public void getsize(){System.out.println(animal.this.size);System.out.println(size);}public void getname(){System.out.println(name);}}
}

还是和上面相似的例子,看例子的第 10 行到 13 行,会发现内部类和外部类都有一个 size 的成员变量,而此时如果直接输出 size 就是输出的 gsize 自己内部的 size 成员,如果要访问外部类的成员则需要用

外部类.this.成员变量
外部类.this.成员方法

外部类外****创建成员内部类对象

那在外部类外如何创建一个成员内部类的对象?看下面的例子:

class Test{public static void main(String[] args) {animal a1 = new animal();animal.gsize a2 = a1.new gsize();//法一 直接创建
    animal.gsize a3 = a1.getgsize();//法二 利用方法创建
}

}
class animal{
private String name;
int size = 20;

public gsize getgsize(){return new gsize();
}
class gsize{public void getsize(){}public void getname(){}
}

}

可以看到创建成员内部类的对象依靠外部类的对象来创建,如上述例子中的两种创建方法,前提都是拥有一个 animal 的对象,才创建了成员内部类的对象。​

2、局部内部类

局部内部类就是定义在外部类的方法的局部区域的类(通常是定义在外部类方法里),作用域则是在方法体或者代码块中(代码块中也可以定义局部内部类),地位与局部变量相当,看如下例子:

class animal{private String name;private int size = 20;public void getsize() {int size = 30;// 定义局部内部类class get {public void getsize() {System.out.println(size);System.out.println(animal.this.size);//可以访问外部类的所有成员(包括 private)}}get var = new get();// 使用局部内部类}
}

从上例可以看到外部类 animal​ 的 getsize​ 方法中有一个 get​ 类,这个 get​ 类就是局部内部类。这里有几点要注意:

1、局部内部类相当于一个局部变量,是不能用访问修饰符修饰的(包括 publicprotectedprivate 以及 static),但可以用 final 修饰,这与一个局部变量十分类似

2、可以看上例第 10 行,局部内部类同样可以访问外部类的所有成员(包括 private),但如果出现重名现象,如第 5 行和第 3 行,单独用变量名字访问的是局部内部类自己的 size(就近原则),而访问外部类的 size 就要用 this,在后文会解释为什么可以用 this

3、同样的,外部类想要访问局部内部类的成员,则需要先创建对象,用对象的引用去访问

3、匿名内部类

匿名内部类是内部类中使用最多的一种内部类,之所以称之为匿名内部类,是因为它没有类名,继承自接口或者抽象类,匿名内部类也就是一个接口或者抽象类的子类,只含有基本的方法,匿名内部类同样是定义在外部类的局部位置,我举个内部类非常典型的使用场景:

  • 成员内部类:需要显示使用class​关键字声明一个内部类,而且反编译后内部类的名称是OutterClass$InnerClass
  • 匿名内部类:不需要使用class​关键字来声明一个内部类,而且反编译后内部类的名称是OutterClass$1
interface Animal{public void cry();
}

class Test{
public static void main(String[] args) {
// 定义并使用了一个继承Animal的匿名类
Animal tiger = new Animal() {
@Override
public void cry() {
System.out.println("老虎叫:");
}
};
}
}

这里发生了有意思的事情,可以发现定义的 Animal​ 是一个接口,但我们想创建一个实现接口的对象的时候通常是用一个类去实现,比如:

class tiger implements Animal{@Overridepublic void cry() {System.out.println("老虎叫:");}
}

但有时候这种方法会很繁琐,如果有很多“动物”,那我们使用额外创建类的实现方法就需要创建很多类,而我们用匿名内部类就解决了这个问题,我们直接在用匿名内部实现了一个接口的对象,并且重写了其 cry 方法。

image

我们来看看这样创建对象的底层实现,反编译后的代码:

​​

image

这是上述使用匿名内部类的 Test 方法的 class 文件反编译后的代码,我截取了主要的部分。可以看到对于 Test 类来说,字节码中其实成为了两个类,一个是普通的 Test 类,第二个 Test$1​ 就是我们的匿名内部类,我们惊奇的发现,其实底层主动为我们创建了这样一个类Test$1​​去实现了 Animal 接口并且重写了 cry(),其实与我们自己的方法差不多,但使用内部类,就把这个操作交给了编译器去实现,从而省去了许多繁琐的操作。不过需要注意的是 JVM 帮我们创建的这个类 Test$1 ​只有使用这个接口对象才有效,其余地方是不能使用的

我们继续来看一下这个表达式:

        Animal tiger = new Animal() {@Overridepublic void cry() {System.out.println("老虎叫:");}};

tiger 是一个引用变量,它的编译类型是 Animal,那它的运行类型就是编译器帮我们创建的这个类,也就是匿名内部类

4、静态内部类

静态内部类就是定义在外部类的成员位置,并且有 static​ 修饰的内部类

静态内部类可以访问外部类的所有静态成员,包括私有的、但不能访问非静态成员(因为静态类、方法、变量的初始化都在类加载的时候,所以不能含有非静态的成员),举个简单的例子:

public class Test {public static void main(String[] args)  {Outter.Inner inner = new Outter.Inner();}
}

class Outter {
public Outter() {

}static class Inner {public Inner() {}
}

}

其实和成员内部类表示方式差不多,只不过多了一个 static​ 修饰,主要的区别就是静态内部类只能访问外部类的静态成员

三、深入理解

1、我们可以观察到,成员内部类可以无条件访问外部类的成员,这是为什么呢?

  先列一个例子:

class Animal{String name;int age;class Get{public void getname(){System.out.println(name);}public void getage(){System.out.println(age);}}
}

对于这样一个内部类来说,它可以访问外部类的所有成员,而这样的程序编译后通常会形成两个.class 文件,如图:

我们现在通过反编译来看看内部类的具体实现:

class Animal {java.lang.String name;int age;Animal();
}//Animal.class

ztc@mp ~ % javap /Users/javacode/neibulei/out/production/neibulei/Animal$Get.class
Compiled from "neibul.java"

class Animal$Get {
final Animal this$0;
Animal$Get(Animal);
public void getname();
public void getage();
}//Animal$Get.class

  我截取了主要的代码,反编译代码中的 Animal​ 为外部类,Animal$Get​ 为其内部类,可以仔细观察一下这两行:

final Animal this$0;
Animal$Get(Animal);

这下我们可以明白,为什么内部类能调用外部类的全部成员,原来编译器会自动帮我们创建一个叫 this$0 ​的 Animal​ 类的引用变量这个引用变量就是内部类能够随意使用外部类成员的原因

  继续看第二行,发现这是一个内部类的构造函数,我们并没有去定义内部类的构造器,这本应该是一个无参构造,但编译器默认帮我们加入了一个 Animal​​ 的参数,这个参数就是用来传入 this​​ 引用变量要指向的对象,至此 this​​ 变量的定义和初始化完成。

2、为什么局部内部类和匿名内部类只能访问局部 final 变量

注意只有这两个内部类:局部内部类匿名内部类,这是因为二者都是定义在方法内部的类,静态内部类成员内部类不用使用final​,

先放一个例子:

class Test {public static void main(String[] args)  {
}public void test(final int y) {final int x = 10;new Thread(){public void run() {System.out.println(x);System.out.println(y);};}.start();
}

}

可以看到从第 8 行到第 12 行,这里是一个匿名内部类,将 6、7 两行的 final 去掉都会报错。(实际上并不会,这是因为编译器在编译的时候自动为我们添加了final)

image-20221202155831-fambdz9

test 的生命周期是和 Thread 生命周期是不一致的,当 test方法 执行完毕后,test 所具有的局部变量的生命周期都结束了,也就是 int x 就消失了,而如果此时的内部类的生命周期没有结束(#问题/待解决 :#​为什么生命周期没有结束?可能是生成的匿名对象处于堆中,没有立马被gc回收,逃逸分析也是到后期才有的东西),它需要输出 x 的值,这时问题就出现了,那编译器如何解决这个问题的?我们同样是反编译一下子节码

编译出来的字节码文件有两个,一个是 Test 普通类的,名为 Test.class​,另一个则是其内部类的字节码文件,名为 Test$1.class​​,我们反编译一下:

//外部类文件反编译
class Test {Test();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: return

public static void main(java.lang.String[]);
Code:
0: return

public void test(int);
Code:
0: bipush 10
2: istore_2
3: new #2 // class Test$1
6: dup
7: aload_0
8: iload_1
9: invokespecial #3 // Method Test$1."<init>":(LTest;I)V
12: invokevirtual #4 // Method Test$1.start:()V
15: return
}

//内部类文件反编译
class Test$1 extends java.lang.Thread {
final int val$b;

final Test this$0;

Test$1(Test, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LTest;
5: aload_0
6: iload_2
7: putfield #2 // Field val$b:I
10: aload_0
11: invokespecial #3 // Method java/lang/Thread."<init>"😦)V
14: return

public void run();
Code:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 10
5: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_0
12: getfield #2 // Field val$b:I
15: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
18: return
}

观察内部类反编译文件中,第 49 行:这条指令表示将操作数 10​ 压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开

也就是在编译的时候直接将 `test`​​ 方法的成员变量直接拷贝了一份

那 test 方法传入进来的 y 值呢?

这个我们再单独看一个例子:

class Test {public static void main(String[] args)  {
}
public void test(final int y) {new Thread(){public void run() {System.out.println(y);};}.start();
}

}

其匿名内部类的反编译文件为:

class Test$1 extends java.lang.Thread {final int val$y;final Test this$0;Test$1(Test, int);public void run();
}

看到反编译文件都第 2 行,发现编译器已经为 y 值创建了一个内部类的局部变量val$y​,同样把 y 值拷贝成了内部类本身的属性:

而这个值则通过默认的构造器来进行初始化:

这个构造器将 Test 类型的 this 引用变量和 int 型的传入参数 y 都进行了初始化,变成了匿名内部类的局部变量

那我们现在可以来总结一下:

我们首先分析的是方法里定义好的变量的传入方式,这个变量在编译期间值就确定了,所以编译器直接将确定好的值做一个拷贝,加入到匿名内部类中,作为匿名内部类自己的属性,而对于在编译期间确定不了的值,如上文中 Test 类传入值,需要等待输入,则在匿名内部类里创建一个变量,然后利用构造器对这个变量的值进行初始化,这个变量则是用来接受 Test 类传入的值。

那现在还是没有解释为什么只能是 final?

这里就要思考一个问题,如果不是 final 呢?不是 final 的话就代表这个值可以修改(引用变量的话就代表可以修改指向),但由于匿名内部类这种传值性质,如果你在匿名内部类接收好值后又去再其他地方修改这些值,会造成一个变量在不同地方有不一样的值,也就是数据不统一,会造成错误,所以匿名内部类之所以只能接受 final 变量的原因就在此,为了防止数据的不一致

3.静态内部类有特殊的地方吗?

  从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的。

四、内部类的作用

为什么在 Java 中需要内部类?总结一下主要有以下四点:

  1. 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
  2. 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
  3. 方便编写事件驱动程序
  4. 方便编写线程代码

第一点是最重要的原因之一,内部类的存在使得 Java 的多继承机制变得更加完善。

参考资料:

《Java 编程思想》

https://www.cnblogs.com/dolphin0520/p/3811445.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/893439.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无源晶振测试仪测试参数详解

晶振测试仪 GDS-80 是一款高性价比的晶振测试系统,采用微处理器技术,实现智能化测量,符合 IEC-444 标准。其测量频率范围为 20KHz-100MHz,能够对晶振的多种参数进行精确测量,广泛应用于晶体行业、邮电、通信、广播电视、学校、研究所及工矿企业等生产和科研领域。晶振测试…

FormCreate设计器v5.6发布—AI加持的低代码表单设计器正式上线!

近期DeepSeek可谓是刷遍全网,当然,在DeepSeek等AI技术的推动下,人工智能正以惊人的速度改变着各行各业。AI不仅是一种技术趋势,更是未来生产力的核心驱动力。 如今,FormCreate设计器也正式迈入AI时代🎉🎉,推出v5.6版本,搭载AI智能表单助理,让表单设计从繁琐的手动操…

清华大学第7弹 | 亿万家长福音 -《DeepSeek赋能家庭教育》 | 免费下载

《DeepSeek赋能家庭教育》是由清华大学张诗瑶博士出品的教程,帮助中国家长解放自己的同时,提高孩子的自主学习能力和核心竞争力。下载地址:https://pdfs.top/book/清华大学-DeepSeek赋能家庭教育.html直到听了清华博士后张诗瑶的直播,我才发现: “不是孩子笨,而是我们用错…

揭秘Chrome DevTools:从原理到自定义调试工具

作者:京东科技 杜强强 引言Chrome DevTools 是前端开发者的必备工具,不仅可以用于调试 Chrome 网页,还支持 Android WebView、Roma (跨平台开发框架) 安卓&鸿蒙端等平台的调试。 作为最常用的调试工具之一,DevTools 不仅能快速定位问题,还能让我们深入了解调试的内部机…

升级到Windows 11 24h2版本后惠普打印机报错spl error 11-1114 line 601

问题描述 近日升级到Windows 11 24h2版本后发现使用HP Laser MFP 1136w Printer打印文档皆会打出一页报错页面,内容如下图所示,但是使用HP Smart Assistant打印PDF一切正常,尝试了使用HP SMART进行修复,修改网络连接方式,重新添加打印机等等方式皆无果。解决方案 排查后发…

Outlook邮箱登录失败,0x80190001错误的多种解决方法

一、Outlook邮箱客户端“0x80190001”解决方法起初出现这个问题我以为是电脑有病毒了,无奈之下全盘杀毒仍然无法解决,随后开始搜索“0x80190001”相关问题查阅,试过好多办法依然无效,直到我找到以下方法:“win+s”打开搜索界面,找到“Internet选项”点击进入该项设置。在…

AUTOSAR网络管理自动化测试如何创建测试框架

在AUTOSAR网络管理自动化测试中,需求分析、模块化设计、自动化执行是创建测试框架的核心要点,其中模块化设计尤为关键,通过将复杂的网络管理功能拆分为独立模块,实现灵活扩展与维护;这不仅能提高测试效率,还能在多变的系统环境中保持稳定性。采用标准化的设计模式,并辅以…

jmeter中的while循环控制器中变量提取

${__javaScript(${status}==0)} ${__groovy(vars.get("status") == 0)}

docker compose部署nacos高可用集群,并开启账号密码

架构图 部署 一、准备yml文件、nacos sql初始化文件、nginx.conf文件 nacos sql初始化文件/** Copyright 1999-2018 Alibaba Group Holding Ltd.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in complian…

Flutter QQ聊天项目(1):登录界面实现

下面介绍 Flutter 最基本的通用项目框架搭建,顺便实现一个 QQ 聊天界面,这里只先实现了 QQ 登录界面。先看下效果图:一、运用技术编辑器:VS Code 框架技术:Flutter3.104 + Dart3.105 路由/状态管理:get^4.6.6更具体的:使用ScreenUtilInit自适应界面大小; 使用Stack支持…

PrivHunterAI:越权漏洞检测的工具

免责声明: 工具仅供安全研究与学习之用,若将工具做其他用途,由使用者承担全部法律及连带责任,作者及发布者不承担任何法律及连带责任。信息及工具收集于互联网,真实性及安全性自测!!​ 项目介绍 功能 一款通过被动代理方式调用Kimi、DeepSeek和通义千问AI,实现越权漏洞…

开启科创服务新篇章!八月瓜科技CRM数字化管理系统成功上线

近日,北京八月瓜科技有限公司(以下简称 “八月瓜科技”)与纷享销客达成深度战略合作,成功部署并上线CRM数字化管理系统。此次合作是八月瓜科技在数字化转型进程中的重要里程碑,标志着其在科技创新服务领域的数字化变革进程中迈出了坚实且具有战略意义的一步。 八月瓜科技是…