第16篇 内部类
写在最前面
网上关于内部类的 blog 很多很乱,甚至写错的也不少。所有我写了一篇笔记,还算齐全,欢迎补充与指正!
1. 内部类的含义
内部类就是在一个类的内部再定义一个类。比如A类中定义了一个类B,那么 B就是A 的内部类,A是B的外部类
class A{//外部类 class B{//内部类 } }
内部类可以分为:成员内部类、静态内部类、局部内部类、匿名内部类。分类逻辑为(这个分类不精准,只是辅助理解)
从内部类是类的角度看:
- 内部类也可以声明属性、方法、构造器;
- 内部类也可以继承父类或者接口;
- 可以用final修饰,让这个内部类不被继承;
- 可以用abstract修饰 ???
- 那内部类是抽象类的话,外部类是抽象类吗?
从内部类是外部类成员的角度看:
- 内部类可以调用外部类的结构,包括 外部类中 private 修饰的内容;
- 内部类可以用 public 、省缺默认、protected、private 修饰;
- 可以用 static 修饰,作为外部类的静态成员,定义时与外部类一起加载(有点像静态变量、方法与类的关系);
- 2.和3.中的重点:内部类可以使用private 和 static 修饰,外部类不可以!
- JDK8不支持在成员内部类定义static成员,JDK16才开始支持!
1.1 关于非静态内部类是否支持 static 的探讨
(主要是成员内部类)
先聊聊类与类的关系
- has-a、 is-a 、like-a 与 use-a
- has-a : 从属关系,如包含。 整体与部分的关系,一个拥有独立存在,但是生命周期应该一致
- is-a : 继承关系,如类继承、接口实现
- like-a : 具体与抽象的关系,如 抽象类继承与接口实现(可以当作是is-a 的特例)
- use-a : 使用关系,一个类和另外一个类的属性或方法之间的关系,依然是其中一个拥有另外一个,但是不负责销毁,也就是声明周期不一样,如方法栈,一个类中 new 另一个对象
再聊聊内部类
- 内部类自己的继承与实现的行为是 is-a 或者 like-a
- 外部类包含成员内部类这一行为像是 has-a,但不像是use-a。因为成员内部类是依附外部类实例而存在的
- 外部类包含静态内部类的这一行为像has-a,且也像是 use-a。因为静态内部类可以独立于外部类实例而存在
回归到static
成员内部类强调的是成员,即外部类实例的一个成员。成员内部类是依附外部类实例而存在的,这一点是由创建内部类的过程决定的,需要先创建外部类实例,再通过外部类实例来new成员内部类。如果成员内部类中的方法或者属性可以是static,那它不就可以独立存在了?从分类与用途的角度来看,这样它就完全可以取代了静态内部类的工作;从创建的角度看,总所周知,静态属性和方法是可以直接通过类名来调用的,这一点与它的实现相违背;从概念的角度看,static就是共享,有驳于实例私有。 所有 成员内部类中不应有static属性或者方法,对于有static属性或者方法的应该声明为静态内部类。这也是(包含 JDK8 在内)JDK15及之前的 JDK 版本的实现逻辑。
上述理解只是在对号入座,看了答案后再填过程。实际上,java长期以来允许在非静态嵌套类中使用静态常量变量,例如字符串文字。
class OuterClass { void foo() { class Inner{ static final int a = 5; // fine static final String s = "hello"; // fine static final Object o = new Object(); // compile error, because cannot be written during compilation } } }
JDK16中引入了新变化:成员内部类中也可以有static属性或者方法!!!
1.2 内部类编译文件解析
2.内部类的作用
- 封装和隐藏实现细节:内部类可以访问外部类所有成员,这样可以将一些实现细节封装在内部类中,从而对外部类隐藏实现细节,提高代码的安全性和可维护性。
- 间接实现多继承:Java 不支持类的多继承,但是一个外部类可以包含多个内部类,这些内部类可以继承不同的类或实现多个接口,从而间接实现了多继承的效果。
- 回调和事件处理:内部类特别是匿名内部类通常用于实现回调和事件处理。
3.内部类的用法
-
如果声明内部类时不使用任何修饰符,那么默认是public static(静态内部类)
-
声明在接口中的内部类,默认是public static。
3.1.成员内部类
成员内部类的用法类似于实例变量。所以创建成员内部类需要先创建了外部类实例才行。
3.1.1加载顺序(此处与静态内部类不同!)
生成一个内部实例的执行顺序为:
- 先加载外部类的静态代码块(按照上下顺序)
- 加载外部类的构造方法
- 先加载内部类的静态代码块(按照上下顺序)
- 加载内部类的构造方法
3.1.2 实例化格式(此处与静态内部类不同!)
先实例化一个外部类,再通过外部类的实例new 一个内部类的实例,该内部类实例的类型为 Outer.Inner。(成员内部类是依附外部类而存在的)
// 格式 外部类实例.new 内部类的构造函数(); public class Outer { public class Inner{} } public static void main(String[] args) { //方式一: 先创建非匿名外部实例,再用外部实例 new 成员内部类实例 //先创建非匿名外部实例 Outer outer = new Outer(); //再通过这个外部类的实例来实例化内部类 Outer.Inner inner1 = outer.new Inner();//注意new的位置,成员内部类与静态内部类不同 // Outer.Inner inner = new outer.Inner();//错误的格式 //方式二:通过匿名外部类实例直接 new 成员内部类实例 Outer.Inner inner2 = new Outer().new Inner(); }
3.1.3 内部类外部类的访问逻辑与范围
public class Outer { private int outerId = 10; public void sameOut(){System.out.println("外部类同名的方法被调用");} public void outerOut(){System.out.println("外部类非同名的方法被调用");} /* **********外部类如何调用内部类的属性与方法*********** */ /*public int getInnerId(){ //错误示范 innerOut();//报错 return innerId;//报错,不能直接通过不到innerId }*/ public int getInnerId(Inner in){ //通过实例来调用 in.innerOut();//成功 return in.innerId;//成功 } /*外部类调用成员内部类的总结: 1. 不能通过属性名或者方法名直接获取内部类的属性。因为成员内部类加载在外部类之后,无法直接找到 2. 只能通过内部类的实例来调用内部类的属性或者方法 3. 也就是说外部类无法访问内部类的属性名或者方法 */ public class Inner{ //内部类也可以是public!!! private int innerId = 20; public void sameOut(){System.out.println("内部类同名的方法被调用");}//与外部类方法同名没报错! public void innerOut(){System.out.println("内部类非同名的方法被调用");} /* **********内部类如何调用外部类的属性与方法*********** */ public int getOuterId(){ //outerOut();//报错 Outer.this.outerOut();//成功 return outerId;//outerId是private,所以内部类可以获取外部类的私有属性 } /*成员内部类调用外部类的总结: 1. 可以通过属性名直接获取外部类的属性,甚至包括访问权限为private的部分。因为内部类也在外部类声明中 2. 不能通过方法名直接获取外部类的方法,这点与继承不同。从成员的角度看应该能访问到才对,所以java提供了以下3.中的是实现方式。 3. 可以通过 `外部类名.this.外部方法()` 来直接调用外部类的方法 4. 内部类中的 this 指内部类自己的引用;外部类名.this 才是内部类对应外部类的引用 */ }; }
public class main { public static void main(String[] args) { /* ***********同名同参方法的调用*************** */ Outer outer = new Outer(); //再通过这个外部类的实例来实例化内部类 Outer.Inner inner = outer.new Inner(); outer.sameOut();//外部类同名的方法被调用 inner.sameOut();//内部类同名的方法被调用 /* ***********不同名方法的调用*************** */ //自己调用 outer.outerOut();//外部类非同名的方法被调用 inner.innerOut();//内部类非同名的方法被调用 //交叉调用 //inner.outerOut();//报错 //outer.innerOut();//报错 inner.getOuterId();//外部类非同名的方法被调用 outer.getInnerId(inner);//内部类非同名的方法被调用 /*成员内部类与外部类的调用的总结: 1. 外部类与成员内部类可以有同名同参的方法,不会报错 2. 用方法名调用同名同参方法,外部类、内部类引用会调用各自的方法 3. 外部类只能通过成员内部类的实例来调用内部类的方法,无论是否同名同参 4. 成员内部类可以通过 `外部类名.this.外部方法()` 来直接调用外部类的方法 */ } }
3.2 静态内部类(编译器源码用的比较多)
-
static可以修饰内部类,非内部类不能被它修饰
-
使用static修饰符修饰的内部类称之为静态内部类,也有人称静态内部类为静态嵌套类(其他三者为非静态嵌套类)
-
创建静态内部类不会加载外部类的静态代码块,不会调用外部类的构造方法,所有内部类无妨访问外部类非静态成员属性
-
内部类中只有静态内部类才可以有静态属性或者方法(JDK16之前的版本)
public class A{//外部类 private int id; public static class B{//静态内部类 public int getOuterId(){//获取外部类的私有属性 return id;//报错,静态内部类无法访问外部类的非静态属性 } }
3.2.1 加载顺序
创建静态内部类不会加载外部类的静态代码块,不会调用外部类的构造方法
- 先加载内部类的静态代码块(按照上下顺序)
- 加载内部类的构造方法
3.2.2 实例化格式
// 格式 new 外部类名.内部类的构造函数(); public class A{//外部类 public static class B{//静态内部类 } } public static void main(String[] args) { //直接通过new 静态内部类的构造函数创建实例,但是需要用`外部类.内部类`为了表明内部类的声明位置,此处的 `外部类.`相当于作用域 Outer.Inner inner2 = new Outer.Inner(); //Warning:通过匿名外部类实例直接 new 成员内部类实例 Outer.Inner inner2 = new Outer().new Inner();//不推荐,通过实例调用静态属性或者方法编译器都会Warning }
3.2.3 调用逻辑
- 静态内部类可以直接访问外部类的静态成员,如果访问外部类的实例成员,必须通过外部类的实例去访问。
- 可以通过完整的类名直接访问静态内部类的静态成员,完整类名指 外部类名.内部类名
3.3. 局部内部类(可以认为是函数内部类)
-
定义在方法内的内部类,我们称之为局部内部类。
-
局部类不能用public或private修饰符进行声明。它的作用域被限定在声明这个局部类的块中。
-
局部类有一个优势, 即对外部世界可以完全地隐藏起来。
3.4.匿名内部类
匿名内部类就是继承了某个接口或者某个类的没有名字的局部内部类。相当于在局部给某个接口或者某个类写了个没有名字的子类。
- 它是局部内部类,一般写在方法体中,临时创建并调用,作用时长就是方法的时长
- 它是一个没有名字的类,通常用重写它的父类来定义
- 它必须是某个已经定义的接口或类的子类。
public interface MyIntertface{//定义了一个接口 public abstract void fun(); } //在测试类的方法中创建一个该接口的匿名实现类,即匿名内部类 public class Test{ public static void main(String[] args){ //创建匿名内部类 MyInterface a = new MyInterface(){ @Override void fun(){System.out.println("success");}//重写接口的方法 } /* 显然 Interface 接口不应该有构造方法,而且接口不能直接创建实现类。那么上述代码怎么解释呢? * 1. 等号左边的 MyInterface a 实际上是在用父类的引用指向子类的对象 * 2. 等号又边的 new MyInterface(){//重写} 实质上创建了一个接口的是实现类。 * 3. 显然等号右边这个类是没有类名的 * 如果有类名的话,相当于如下代码: */ a.fun(); } } //如下代码可以实现上述的等价效果 public interface MyIntertface{//定义了一个接口 public abstract void fun(); } public class MyInterfaceInmpl implements MyInterface{ @Override public void fun(){System.out.println("success");}//重写 } //在测试类的方法中创建一个该接口的匿名实现类,即匿名内部类 public class Test{ public static void main(String[] args){ //创建实现类实例,且用接口的引用指向其实现类 MyInterface a = new MyInterfaceInmpl(); a.fun(); } } /* 在上面的代码中,实现类MyInterfaceInmpl全程只使用一次, * 为了这一次的使用要去创建一个类,未免太过麻烦。那么我们可以使用匿名内部类 */
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!