Java内部类
Java内部类
Java中可以将一个类定义在另一个类中或一个方法中,这样的类称为内部类
内部类一般来说分为下面几种:
- 成员内部类(可以分为静态成员内部类、非静态成员内部类)
- 局部内部类
- 匿名内部类
一、成员内部类
成员内部类看起来像是外部类的一个成员,可以使用private、public等访问限制符修饰。也可以使用static修饰。根据是否使用static,成员内部类分为:
- 静态成员内部类:使用了static进行修饰
- 非静态成员内部类:未使用static进行修饰
除了静态内部类以外,所有内部类在编译完成后隐含地保存着一个外围类的对象的引用--《Java核心技术卷一》
1 静态内部类
使用static修饰的内部类称为静态内部类。并且只有内部类才能被声明为static,而外部类不可以。
由于静态内部类编译完成后没有外围类的对象的引用,意味着:
- 静态内部类不依赖于外围类的对象
- 静态内部类不能使用外围类的非static成员和方法
- 静态内部类允许有static属性、方法
2 非静态内部类
由于非静态内部类在编译完成后隐含地保存着一个外围类的对象的引用,意味着:
- 非静态内部类可以访问外部类的所有信息(如果有重名,使用外部类.this.变量/方法的方式来调用外部类的变量或方法)
- 创建内部类对象时,必须先使用外围类的对象来创建
- 外围类可以访问成员内部类信息,但必须先创建一个内部类的对象,再使用这个对象来访问
- 非静态内部类不能存在static的变量和方法,可以存在某一些static final类型的常量(具体指的是编译期常量)
3 内部类语法
对于静态内部类之外的所有内部类:
- 保存了外部类的引用,在内部类使用外围类的语法为:
OutClass.this
表示外围类的引用,如果我们在外围类和内部类定义了相同名称的变量,如num,在内部类中就可以使用他来区分
OutClass.this.num; //表示外围类中定义的num
this.num; //表示内部类定义的num
- 在外围类的作用域之外使用内部类:
OutClass.InnerClass
- 内部类的构造需要用到外部类对象
OutClass.InnerClass InnerObject = OutObject.new InnerClass();
4 为什么非静态内部类不能存在static的变量和方法
这是Java的一个语法规则,为什么会制定这样的规则呢,分析可能的原因
4.1 常量池
常量池用来存储一些数据,这些数据在编译时期就已经被确定,并且被保存在已编译好的.class类文件中。
常量池中的数据主要有两大类:
- 字面量:Java语言层面的常量,如程序中定义的各种基本类型数据、对象型数据(如String类对象和数组)
- 符号引用:编译原理层面的常量,包含:类和接口的全限定名、字段名称和描述符、方法名称和描述符
4.2 关于final
- final可以修饰:属性、方法、类、局部变量
- 用final修饰的变量表示常量,一旦初始化就不可更改,对于基本类型不可更改其数值,对于引用类型不可指向其他的对象
- final可以修饰的变量有三种:静态变量、实例变量、局部变量
- final修饰的属性的初始化有的发生在编译期(编译期常量),有的发生在运行期(运行期常量)
- final修饰的方法表示该方法在子类中不可重写
- final修饰的类不可被继承
-
编译期常量
如果在编译时,final变量是基本类型或String类型,且jvm可以确定它的确切值,那么编译器会把它当做编译期常量使用,使用字面量替换并存入class常量池,在需要它的时候,直接访问这个常量
-
运行时常量
即并不直接用字面量为final常量赋值,中间经过引用或获取的过程(可以是对局部变量的获取或某些处理过程结果的获取),在程序的编译期并不关注其本身的值,只是知道类型即可,在运行阶段才对其值进行确定,而编译期常量在编译期中直接被替换为字面量,写入class常量池中
4.3 对类的依赖
如果在程序中分别调用编译期常量/运行时常量,则
- 编译期常量在编译阶段就存放进了class常量池,使用它不会引起类的加载
- 运行时常量依赖类,会引起类的初始化(类加载)
4.4 问题分析
public class OutClass {
//非静态内部类
public class InnerClass {
public static int i = 1; //错误
public static final int j = 1; //正确
public static final Date d = new Date(); //错误
}
public static void main(String[] args)
{
System.out.println(OutClass.InnerClass.i);
System.out.println(OutClass.InnerClass.j);
System.out.println(OutClass.InnerClass.d);
}
}
Java中变量的初始化顺序为:
(静态变量、静态初始化块)-->(变量、初始化块)--> 构造器
在执行上面的代码时,JVM先加载外部类OutClass,然后执行静态变量、静态初始块的初始化,再加载非静态代码块。此时内部类InnerClass好像是要被加载了,但是实际上并没有。因为非静态内部类需要有外部类的对象的引用,所以非静态内部类的加载必须要等到外部类实例化之后,只有创建了一个外部类对象,JVM才能加载其内部类的字节码。又因为static变量的初始化需要加载字节码,所以此时 i 并未被初始化,d 为运行时常量,所以此时 d 也未被初始化。因此此时这样使用内部类的变量是错误的,为了避免这种错误,Java才规定不能在内部类使用静态变量和运行时常量。
5 应用
- 使用内部类定义复杂的数据结构
- 定义常量
- 静态内部内实现单例
public class SingleTon{
private SingleTon(){}
private static class SingleTonHoler{
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHoler.INSTANCE;
}
}
外部类加载时不需要立即加载内部类,内部类不被加载则不会初始化INSTANCE,这就实现了线程安全的懒汉式单例模式
- 成员内部类可以实现多继承
二、局部内部类
局部类不能使用public或private访问说明符来进行声明。它的作用域被限定在声明这个局部类的块中。局部类可以外部完全隐藏,只能在被声明的作用域中使用,即使是声明代码块所在类的其他方法也不能使用这个内部类。
访问局部变量
局部内部类不仅可以访问包含他的外部类,还可以访问局部变量,但是必须确保局部变量必须事实上是不可变的(不是必须用final修饰),这一点和lambda类似。
局部内部类使用局部变量时,编译器会检测对局部变量的访问,为每个变量建立相应的数据域,并将局部变量拷贝到构造器中,以便这些数据与初始化为局部变量的副本。
为什么要保存副本以及为什么不可变?原因和lambda表达式一样。
局部变量的生存期很短,而局部内部类创建的对象生存期却可能很长,比如作为参数被传递到计时器中等。不论各种情况,如果不保存一个副本当局部内部类中使用到局部变量时,这个局部变量很有可能已经被回收掉了。由于一个局部内部类的对象就会生成这样的一个副本,如果这个变量是可变的,就无法保存数据同步。因此局部内部类只能访问不可变的局部变量
三、匿名内部类
很多时候我们编写局部内部类也只是会它来创建一个对象,并不会重复使用,所以我们连类名都不需要起了。例如
//我们声明了一个类实现了ActionListener接口,并直接创建了一个对象返回给ActionListener类型的引用
ActionListener lister = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("hello");
}
};
众所周知,类构造器需要和类名一致,但是匿名内部类连名字都没有,所以匿名内部类没有构造器。但是可以将参数传递给超类的构造器。但是在实现接口时就不能有任何构造参数了。
//定义了匿名内部类,使用父类的构造器
Person p = new Person("Jack"){...};
匿名内部类的使用技巧
-
双括号初始化
如果想要构造一个数组列表作为函数的参数,可以使用如下方式:
//有一个名为fun的函数,需要一个String类型的数组作为参数 void fun(List<String> strs){...} //第一层大括号表示正在定义匿名内部类,第二层大括号为初始代码块 fun(new ArrayList<String>(){{add("Jack"); add("Tom");}});
-
打印静态方法所在的类名
在生成日志或者调试时,通常需要打印当前的类名,如:
System.out.println("current class is: " + getClass());
但是,这种方式对于静态方法是无效的,因为静态方法没有this,所以应该使用如下的方法获取当前类:
//通过创建Object一个匿名子类的对象,再调用getEnclosingClass()获取到外围类 new Object(){}.getClass().getEnclosingClass();
参考资料
Java类加载机制的七个阶段,加载、验证、准备、解析、初始化、使用、卸载
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具