系列文章目录
欢迎各位读者订阅《计算机底层原理》系列专栏,能够帮助到各位就是对我最大的鼓励,我会持续为大家输出、关于Java语法和计算机底层原理的相关内容。
前言
这篇文章将为大家讲解Java内部类的四种形式,从语法到底层带大家全方位地了解和学习Java的内部类,相信您看完这篇文章一定会有很大的收获。
一、为什么需要内部类?
1.内部类的作用
Java当中允许在一个类当中定义另外一个类,它们允许在一个类的内部声明另外一个类、这样就可以更轻松容易地组织和封装代码、内部类可以分为四种类型、成员内部类也叫做实例内部类、静态内部类、局部内部类和实例内部类。
那么面向对象编程语言当中为什么需要内部类呢?机器语言也是为了解决和实现现实生活当中的问题,需要内部类有这么几个原因。
1、封装和组织代码:内部类运行将相关的类组织在一起、是的代码更加模块化和可读、如果一个类只在另外一个类的内部使用、将其定义为内部类可以减少对外部世界的暴露、提高封装性。
2、访问外部类的成员:内部类可以访问外部类的私有成员、这有助于实现封装和隐藏、这样外部类的实现细节可以使用private隐藏起来,需要对外部类进行什么操作只需要在内部类当中进行实现即可,只有内部类可以访问他们。
3、实现“多重继承”:我们都知道Java当中不像C++那样可以实现多继承,一个类只允许继承一个直接父类,但是当我们使用内部类的时候,可以在一个类当中声明多个类,从而达到类似于多重继承的效果。
针对这个实现多继承的问题我多说几句,如果一个类当中定义了内部类的话子类继承了这个类,那么子类也同时会继承这个类当中的所有内部类,那么内部类的访问权限也是会按照他们在父类当中的定义来实现继承,举个例子秒如果父类当中的内部类是public的子类可以直接访问、如果是protected的话子类也可以直接访问他们、如果是默认的(default)包内可见,子类只能在同一包内访问,如果是私有的(private)子类无法直接访问内部类。
4.回调函数的实现:内部类常常用于实现回调函数、通过在内部类中实现某个接口或者继承某个类,可以轻松实现回调机制(这里的回调函数不是我们今天要讲的重点,所有这里就一笔带过了,后面的文章我会讲到)
2.内部类的使用场景
1.事件处理:当我们使用图形开发界面的时候也就是我们所说的(GUI)应用程序的时候,内部类通常被应用于处理各种事件,例如按钮点击、鼠标移动等等操作,因为内部类更容易地访问外部类的组件和数据。
2.迭代器设计模式:内部类可以用于实现迭代器,帮助遍历集合类当中的元素、这样的内部类可以访问外部类的私有成员。
3.还有回调函数、工厂模式、状态机、并发编程等等场景:当然这些都涉及到了Java当中复杂的应用场景,这篇文章我们主要介绍内部类,所以这些复杂的应用后续有时间为大家讲解,这里就不做赘述了。
二、内部类的分类
1.成员内部类(实例内部类)
1、语法层面:成员内部类也叫做实例内部类,是定义在另一个类(外部类)当中内部的一个类,外面的叫做外部类、里面的叫做内部类、成员内部类又访问外部类的所有成员(包括私有成员)的权限。代码如下
package Yangon; public class OuterClass { private int outerVariable; public void outerMethod(){ System.out.println("Outer Method!"); } public class InnerClass{ public void innerMethod(){ outerVariable = 10; outerMethod(); System.out.println("Inner method!"); } } public static void main(String[] args) { //内部类的实例化方法 OuterClass outerClassObj = new OuterClass(); OuterClass.InnerClass innerClassObj = outerClassObj.new InnerClass(); } }
上面这段代码我为大家演示了成员内部类最基本的实现方法和使用场景,着这个例子当中成员内部类可以直接访问外部类的私有成员、并且可以通过创建外部类的实例来创建内部类的对象。
大家可以看我写的主函数、这里需要注意的是成员内部类的实例化必须关联到外部类的实例、这是因为内部类实例与外部类实例之间存在一种特殊的关联关系、内部类实例可以直接访问外部类的任何成员。成员内部类通常用于与外部类有密切关联的情况,一会我会细说、例如需要标识外部类的重要组成部分、或者实现某个接口、用于实现特定功能的辅助类。
什么情况下必须使用成员内部类而不能使用其他内部类来替代呢?我总结了这么四点。
紧密关联的场景:当内部类需要直接访问外部类的时候包括私有成员、并且与外部类有着密切的关联的时候、成员内部类是一个非常合适的选择、这种关系通常体现了一种包含或者拥有的关系,等我为大家讲解完所有的内部类的时候,相信大家经过对比就会明白了。
代码组织和封装:如果内部类仅在外部类内部使用,并且不需要再外部类之外被访问、成员内部类提供给了更好的封装性,它用于组织代码、将相关的类放在一起,从而使代码更易读和维护。
实现接口或者继承:如果内部类需要实现某个接口或者继承某个类、并且这种实现与外部类相关、成员内部类可以更清晰地表达这种关系。
访问外部类的实例:如果内部类需要访问外部类的实例(不仅仅是静态成员)成员内部类是唯一的选择、静态内部类无法访问外部类的实例。
这些必须使用成员内部类的场景大家理解即可、不需要专门背诵。以后各位再写代码做项目的时候就会感受到。
2.编译层面:接下来我们就从
编译的层面来分析一下内部类和外部类之间的关系。这当中主要涉及到编译后生成的字节码文件和访问规则1.命名规则:编译后内部类的字节码文件通常会使用外部类的类名作为前缀,并在类名后添加内部类的名称,例如如果外部类是OuterClass内部类是InnerClass那么编译后的文件就是OuterClass$InnerClass.class这就是内部类生成的字节码文件,同样也会被纳入符号表,编译器会通过这样的方法来识别外部类与内部类之间的关系。
2.访问规则:编译后的字节码文件当中、内部类会维护一个对外部类的引用、以便能够访问外部类的成员、这个引用通常是通过生成一个额外的字段来实现的。内部类之所以能够为所欲为的访问外部类的任何字段,就是因为内部类当中维护了一个外部类的引用。
3.实例化:在编译后的字节码当中、要创建内部类的实现、通常需要提供外部类的实例作为构造函数的参数,这是因为内部类的实例需要与外部类实例关联。
注意了实例化这一点非常重要大家一定要掌握,代码如下:
package Yangon; public class OuterClass { public int OuterVariable; class InnerClass{ private int InnerVariable; public InnerClass(int InnerVariable){ this.InnerVariable = InnerVariable; } public void DisplayValue(){ System.out.println("Outer Variable" + OuterVariable); System.out.println("Inner Variable" + InnerVariable); } } public static void main(String[] args) { OuterClass outerClassObj = new OuterClass(); OuterClass.InnerClass innerClassObj = outerClassObj.new InnerClass(22); } }
大家请看这段代码,内外部类当中各有一个成员,内部类当中定义了一个构造函数,这个构造函数需要接受一个参数,并且在构造函数内部将这个参数赋值给为内部类的成员变量。但是这仅仅只是我们所看到的参数,实际上构造函数其实还会多出一个隐形的参数,也就是我们所看不到的参数,他的作用类似于this引用,接下来我为大家演示一下,内部类的构造函数所生成的字节码内容。
// 编译后的 InnerClass 字节码中的构造函数 public InnerClass(OuterClass outer, int innerVariable) { this.this$0 = outer; // 将外部类的实例传递给内部类 this.innerVariable = innerVariable; }
到了这里想必大家也已经明白了,内部类构造函数的调用需要接受外部类的实例也就是对象引用作为第一个隐式的参数,从而访问内部类,在实际创建对象的内部类实例时,需要提供外部类的实例作为构造函数的参数,这种关联是内部类能够访问外部类成员的基础。每次想要实例化内部类对象的时候,必须先实例化外部类对象。因为成员内部类也相当于是一个外部类的成员,想要访问一个类的成员必须先对这个类进行实例化,只不过这个类的成员它也是一个类而已。
OuterClass outerObject = new OuterClass(); OuterClass.InnerClass innerObject = outerObject.new InnerClass(42);
拓展:
我在这里还想再为大家介绍一个知识点,就是成员内部类的访问限定符有什么意义,例如这样。
public class OuterClass{ public int OuterValue; public(protected\private\default) class InnerClass{ public int InnerValue; } }
接下来我为大家一一介绍一下,这些访问限定符都有什么意义。首先提到public我们首先会想到Java当中的一个文件当中只有一个public类,而且这个类名于文件名是相同的,注意了在内部类当中,这里是一个特例,内部类出现的时候,一个文件是可以允许出现多个public类的,成员内部类的访问限定符决定了它的可见性、即在其他类当中如何访问这个内部类,这个可见性对于程序的设计和封装有一定的影响。接下来我为大家一一介绍。
1.public成员内部类:如果成员内部类声明为public,那么它对于所有的类都是可见的,其他的类都可以通过外部类的实例来访问内部类,这通常用于希望将内部类作为独立的组件对外开放的情况。
2.protected成员内部类:这意味着只有外部类的子类和同一个包里面的其他类可以访问这个内部类,这种限定符通常用于希望内部类在继承关系当中被子类使用的场景。
3.default成员内部类:如果没有指定访问限定符,那么只有同一个包当中的类可以对他进行访问,其他情况下都不行。
4.private成员内部类:这种情况下,只有包含这个内部类的外部类可以去访问它,其他类都不可以访问它,换言之,被private修饰的内部类就意味着这个内部类是这个外部类的私有属性,只有外部类自己可以访问,其它谁都不可以!
2.静态内部类
当我们说一个类是静态内部类的时候,这意味这这个类被声明为另一个类的静态成员、静态内部类于成员内部类不同、它不依赖于外部类的实例、而是于外部类的类级别相关。静态内部类相当于是外部类的静态成员、因此可以直接通过外部类的类名引用、而不需要创建外部类的实例。
这里需要注意一点,静态内部类是不可以访问外部类的非静态成员,包括实例变量和实例方法,因此它不依赖于外部类的实例,但是可以访问外部类的静态成员。创建静态类的实例不需要外部类的实例、可以直接使用外部类的类名来进行初始化,例如:
package Yangon; public class OuterClass { public int OuterVariable; static class InnerClass{ private int InnerVariable; public InnerClass(int InnerVariable){ this.InnerVariable = InnerVariable; System.out.println("Hello World!"); } } public static void main(String[] args) { OuterClass.InnerClass innerClassObj2 = new InnerClass(22); } }
为什么需要静态内部类?
静态内部类在Java当中有着独特的优势和用途、它与成员内部类有着很大的区别。
独立性:静态内部类是与外部类的实例无关的,它可以独立存在,静态内部类并不持有外部类实例的引用,因此它不依赖于外部类的实例。
命名空间:静态内部类有其自己的命名空间,不受外部类的影响,这使得它可以具有与外部类的名称而不会引起冲突,同时不会与外部类的实例变量产生歧义。
访问控制:静态内部类可以拥有与外部类相同的访问修饰符,但不受外部类的访问控制影响,这也就意味着可以在外部类之外的地方随意访问静态内部类,而不受外部类的可见性限制。
独立创建:由于静态内部类不依赖于外部类的实例,因此可以独立地创建静态内部类的实例,这样的特性导致了一个类可以在没有外部类实例的情况下直接使用静态内部类的实例。
降低耦合度:静态内部类有助于降低外部类和内部类之间的耦合度,如果一个内部类不需要访问外部类的实例或者方法,或者只需要在特定的情况下需要访问,那么可以使用静态内部类来减少对外部类的依赖。
什么场景下需要使用静态内部类?我在上面的文章当中也已经提到过了,如果一个内部类不需要访问外部类的实例变量、方法或者只需要在特定的情况下去访问,那么这个时候我们就不需要使用成员内部类而直接使用静态内部类即可。
1.帮助类:当一个类仅在外部类的内部使用、并且不依赖于外部类的实例时、可以考虑将其设计为静态内部类、这种情况下静态内部类充当外部类的帮助类,提供一些辅助的功能。
public class OuterClass2 { public static class InnerClass{ public void Help(){ System.out.println("Doing something with outer static field"); } } }
2.工厂模式:静态内部类可以用于实现工厂模式、其中静态内部类负责创建外部类的实例,这种方式将创建实例的逻辑封装在内部类当中,提高了代码的模块化和可读性。
public class OuterClass2 { private int value; public static class Factory{ public OuterClass2 createInstance(int value){ OuterClass2 instance = new OuterClass2(); instance.value = value; return instance; } } }
3.迭代器模式、线程池:这里的内容距离这部分内容有些遥远、就不为大家做出详细的介绍了。
从编译器的角度看静态内部类:1.类文件生成:当我们编写包含静态内部类的Java源代码的时候、编译器会生成多个类文件、每个类文件对应一个类,对于静态内部类、会生成两个类文件、一个是外部类文件一个是内部类文件。
2.命名规则:编译器生成类文件的时候会遵循一定的命名规则、静态内部类的类文件通常采用以下命名规则,外部类名$内部类名.class例如,如果外部类是如果外部类是
OuterClass
,静态内部类是StaticInnerClass
,那么生成的类文件名就是OuterClass$StaticInnerClass.class
。3.访问外部类的成员:静态内部类可以直接访问外部类的静态成员、但是不能直接访问外部类的实例成员。这是因为静态内部类不持有对外部类实例的引用。
4.独立性:编译器会确保静态内部类是相对独立的,它不依赖于外部类的实例,这意味着我们可以在没有外部类实例的情况下使用静态内部类。
3.局部内部类
什么是局部内部类?
局部内部类是定义在方法或者代码块当中的内部类,其中作用域仅限于包含它的方法或者代码块。局部内部类与成员内部类和静态内部类相比,有一些特定的特点和使用场景。
1.作用域:局部内部类的作用域仅限于包含他的方法或者代码块,这意味着你无法在定义局部内部类的方法或者代码块之外的地方使用它。
2.可访问外方法的局部变量:这里要注意一下在Java8之前的版本局部内部类只能访问类外方法体内的final修饰的成员。这一点在Java8之后做出了改进可以允许局部内部类访问外部类事实上的final成员。(关于事实上的final我一会会在拓展内容里面去讲)。
public class OuterClass2 { private int outerValue; private int number; public void Print(){ final int value = 10; double num = 0; class InnerClass{ public void Print(){ System.out.println(value); System.out.println(num); } } }
3.不能有访问修饰符和static修饰符:局部内部类不能有访问修饰符和static修饰符,它的可见性和生命周期受限于定义它的方法或者代码块,不需要额外的修饰符。其实这一点很好理解,static修饰的成员生命周期伴随着整个程序,局部内部类的生命周期由定义它的方法的方法体来决定,只要一出方法体,这个局部内部类的生命周期就会结束,所以不管是static修饰还是访问限定符来修饰,都会造成生命周期的混淆。
4.使用场景:局部内部类通常用于解决一些特定的方法或者问题,避免污染外部类的命名空间,它可以在方法内部提供一些封装的功能,同时又不需要将这个类的定义放到整个类的范围内。
为什么要使用局部内部类?局部内部类通常要在一个方法内部封装一些逻辑、实现某个特定的功能、而又不希望该类在方法外部可见,也不想让这个逻辑在方法外被复用所以就会使用局部内部类这些场景。
1.封装辅助功能:当一个方法需要一些辅助功能或者实现某种特殊的逻辑、而这些功能对于整个类来说是独立的且不需要暴露给外部时、可以使用局部内部类、这有助于保持代码的整洁性和可读性。
例如下面这段代码:
class Calculator{ public int addWithLogging(final int a,final int b){ class Logger{ public void logOperation(){ System.out.println("Adding " + a + " and " + b); } } Logger logger = new Logger(); logger.logOperation(); return a + b; } }
2.解决特定的问题:局部内部类可用于解决特定的方法内的问题,提高代码的内聚性。例如,某个方法需要访问特定的局部变量或者实现一些与该方法密切相关的功能。这里涉及到了线程的相关内容,这段代码大家理解即可。
class DataProcessor{ public void process(int[] data){ final int threadHold = 5; class DataHandler{ public void handleData(){ for(int value : data){ if (value > threadHold){ System.out.println("Processing high value" + value); } } } } DataHandler dataHandler = new DataHandler(); dataHandler.handleData(); } }
3.减少作用域:局部内部类可以将类的作用域限制在一个方法内,防止外部类的其他方法或者外部类之外的类访问它,从而降低了可见度,有助于保持类的简洁性。
class OuterClass{ private int outerField = 42; public void outerMethod(){ final int localVar = 10; class LocalInnerClass{ public void innerMethod(){ System.out.println("Outer field in innerMethod: " + outerField); } } LocalInnerClass localInnerClassObj = new LocalInnerClass(); localInnerClassObj.innerMethod(); } public static void main(String[] args) { OuterClass outerClassObj = new OuterClass(); outerClassObj.outerMethod(); } }
局部内部类的实例化只能放在方法体内部,不能再方法体外实例化,局部内部类的用法大概就这么多。
4.匿名内部类
什么是匿名内部类?
匿名内部类是一种在使用过程中定义、实例化并使用的内部类、它没有显示的类名、通常用于创建只需要使用一次的小型类或者接口。以下是匿名内部类的一些关键特点:
1.没有显式类名:匿名内部类没有在代码中显示地声明一个类名、它在使用的同时定义和实例化。
2.通常用于接口和抽象类:匿名内部类通常用于实现接口或者继承抽象类,因为这样可以在创建实例的同时提供必要的实现。
3.可以扩展类或者实现接口:匿名内部类可以扩展一个类(普通类或者抽象类)或者实现一个接口,但是不能同时做两者。
4.可以访问外部类的成员:匿名内部类可以访问外部类的成员变量或者方法,但是在访问的时候需要将外部类的成员变量和方法声明为final或者是事实上的final(至于什么是事实上的final一会我的拓展里面会将)
interface MyInterFace{ void doSomething(); } class OuterClass{ private int outerField = 42; public void outerMethod(){ MyInterFace myInterFace = new MyInterFace() { @Override public void doSomething() { System.out.println("Hello World"); } }; myInterFace.doSomething(); } public static void main(String[] args) { OuterClass outerClassObj = new OuterClass(); outerClassObj.outerMethod(); } }
拓展:
当局部内部类或者匿名内部类访问外部类的变量的时候,Java8 要求这些局部变量要么是final的要么是事实上final的,那么什么是事实上final的呢?就是没有被修改过,就这么简单,一个变量只要没有被修改过,那么它事实上就是final,Java8之前这些类要想访问外部类的变量的话,这些变量必须被final修饰过、Java8之后,只要它没有被修改过就可以访问。
那么匿名内部类和局部内部类为什么一定只能访问事实上的final成员或者被final修饰的成员呢?从生命周期的角度来看当这些类引用一个外部类的成员变量的时候、它可能会在外部类方法执行完毕后继续存在。如果这个成员变量是一个局部变量、而且在外部方法执行后发生了变化,内部类可能引用了一个已经失效或者不符合预期的值,通过要求这些变量是final或者事实final就可以确保内部类引用的是正确的,不会改变的值。
总结
Java的内部类(成员内部类、静态内部类、局部内部类、匿名内部类)就为大家讲解到这里,这里面涉及到了很多计算机底层的内容和知识,如果大家有兴趣可以去看我之前写的文章。能够帮助到大家就是对我最大的鼓励!