Java-匿名内部类
我们先分析匿名内部类的结构,然后逐一解释,最后以下罗列的问题都会在下面的内容中一一得到解答 :
匿名内部类到底是什么?
我们为什么要学习匿名内部类 ?
匿名内部类都有怎样的作用 ?
匿名内部类应用的场景又有哪些 ?
匿名内部类是否有缺陷?
让我们带着这些问题来学习Java中的匿名内部类吧 !
结构决定性质
结构
- 匿名内部类基本语法 :
new 父类构造器(参数) / 实现接口() {// 类的主体部分
};
-
解释匿名内部类语法中所有的概念:
-
new 父类构造器(参数)
表示匿名内部类是某个类的子类实例。 -
实现接口()
表示匿名内部类是某个接口的实现实例。 -
{ ... }
内部是匿名内部类的主体部分,包含类的字段、方法等定义。
-
性质
匿名内部类(Anonymous Inner Class)是一种在声明和创建对象的同时定义类的方式,它没有显式的类名。通过 匿名内部类
看这几个字的字面意思我们都知道这是个没有名字的类,即 非具名类 . 以下是匿名内部类的具备的一些性质 :
- 可以实现接口或继承类: 匿名内部类可以实现接口或继承某个类,从而提供具体的实现。
- 没有显式的类名: 匿名内部类没有显式的类名,因为它是一种临时的、一次性的实现。
- 一次性使用: 通常用于临时的、一次性的场景,不需要复用。因为匿名内部类没有类名,所以无法在其他地方重复使用。
- 可以访问外部类的成员: 匿名内部类可以访问外部类的成员,包括成员变量和方法。对于外部类的局部变量,有一些规则,比如必须是
final
或者事实上是final
的。 - 可以包含字段和方法: 在匿名内部类的主体部分,可以包含字段(成员变量)和方法的定义。
- 不可以包含静态成员: 匿名内部类不能包含静态成员,包括静态方法和静态变量。
我们来一个一个的解释,同时我也会拿具体的代码演示.
可以实现接口或继承类
- 匿名内部类可以用来实现接口或者继承类,这也是匿名内部类最常用的一个用法,我们在实际开发中,如果需要实现(重写)某个接口(类)的方法,而且这个方法只是临时使用,不需要复用,而且会出现很多这样的场景,我们肯定是不想一一都去额外的封装一个类去实现接口或者继承类然后再创建类的实例,取调用我们需要的方法,这个时候我们就 匿名内部类 就排上用场啦,我们可以通过匿名内部类去临时创建一个接口的实例(接口不能实例化,这里只是形象比喻一下)或者创建一个临时的子类重写了父类方法的实例. 总结一下 : 当匿名内部类需要实现接口或者继承类时,它可以直接在创建对象的地方定义类的主体,而不需要显式地声明一个具名的类。
package src.demo;//定义一个接口
interface Chef{void cook();
}class Sever{public void shangCai(){System.out.println("服务员上菜");}
}public class Demo01 {public static void main(String[] args) {//使用匿名内部类来实现接口Chef chef1 = new Chef() {@Overridepublic void cook() {System.out.println("炒蛋炒饭");}};//调用我们使用匿名内部类实现接口实例中的抽象方法chef1.cook();//使用匿名内部类重写父类方法Sever sever = new Sever() {@Overridepublic void shangCai() {System.out.println("服务员上菜->蛋炒饭");}};//调用我们使用匿名内部类重写父类的方法sever.shangCai();}
}
通过上述的代码,我演示了两种使用匿名内部类的情况:
-
实现接口: 通过匿名内部类实现了
Chef
接口,提供了cook
方法的具体实现。Chef chef1 = new Chef() {@Overridepublic void cook() {System.out.println("炒蛋炒饭");} };
这部分代码演示了匿名内部类用于实现接口的情况,通过创建一个实现了
Chef
接口的匿名内部类的实例,重写了接口中的抽象方法cook
。 -
重写父类方法: 通过匿名内部类重写了
Sever
类的shangCai
方法。Sever sever = new Sever() {@Overridepublic void shangCai() {System.out.println("服务员上菜->蛋炒饭");} };
这部分代码演示了匿名内部类用于重写父类方法的情况,通过创建一个继承自
Sever
类的匿名内部类的实例,重写了Sever
类的方法shangCai
。
通过这两种使用情况,你展示了匿名内部类在实现接口和重写父类方法时的便利性。匿名内部类可以直接在创建对象的地方提供类的定义,避免了显式地声明一个具名的类,尤其适用于一次性的实现或重写。
没有显式的类名
没有显式的类名,这意味这我们使用匿名内部类创建出来的实例是无法通过类名去访问的,如果我们需要实现函数回调的话,我们就可以使用父类引用或者接口引用去接收. 所以,我们可以知道 **匿名内部类是必须基于已存在的类或者接口,因为它的实质是在这个基础上创建一个新的匿名子类或者实现一个匿名实例,**这样做的优势就是我们省略了显式的创建一个具名子类的步骤,这对于我们一些简单的或者一次性的任务是非常方便的,因我们我们可以在需要的地方直接实现类的功能,而无需为此专门定义一个新的类,这使得代码可以更为紧凑和直观.
一次性使用
匿名内部类具备这个性质的理由就是 : 因为匿名内部类没有类名,所以无法在其他地方重复使用。
可以访问外部类的成员
实际需求中如果我们需要使用匿名内部类访问外部类的一些成员(包括属性和方法) , 那么其中是否有什么限制或者规则吗 ? 有的 ! 我们后续通过代码测试也可以知道,在这里我们先提前说明有怎样的规则 :
匿名内部类可以访问外部类的成员,包括成员变量和方法。对于外部类的局部变量,有一些规则,比如必须是final
或者事实上是final
的.
(1)外部类的成员变量:
匿名内部类中可以访问外部类的成员变量,并且可以进行修改。这是因为外部类的成员变量在匿名内部类中有完整的作用域,而且匿名内部类实际上是外部类的一个扩展,可以直接访问外部类的成员。(需要注意的是 : 在匿名内部类的方法中,可以访问和修改外部类的成员变量。这些修改在匿名内部类的方法中生效,但不会影响外部类实例之外的其他代码。这是因为匿名内部类实际上是一个新的类,其代码块被包含在外部类的方法内部,而成员变量的修改是在匿名内部类的方法中执行的。)
实例代码如下 :
package src.main;public class Example {private int memberVar = 10;public void modifyMemberVar() {// 匿名内部类Runnable r1 = new Runnable() {@Overridepublic synchronized void run() {// 访问外部类的成员变量System.out.println("Before modification: " + memberVar);// 修改外部类的成员变量memberVar = 20;System.out.println("After modification: " + memberVar);}};Runnable r2 = new Runnable() {@Overridepublic synchronized void run() {// 访问外部类的成员变量System.out.println("Before modification: " + memberVar);// 修改外部类的成员变量memberVar = 20;System.out.println("After modification: " + memberVar);}};// 使用匿名内部类的实例new Thread(r1).start();new Thread(r2).start();}public static void main(String[] args) {Example example = new Example();example.modifyMemberVar();}
}
- 代码中,匿名内部类实现了
Runnable
接口,其中的run
方法中访问并修改了外部类Example
的成员变量memberVar
。 - 匿名内部类的作用域包括了它所在的方法,也就是
modifyMemberVar
方法。在这个方法中,您创建了两个不同的匿名内部类的实例(r1
和r2
),每个实例都有自己的run
方法。因此,每个实例的run
方法中的对memberVar
的修改是在各自匿名内部类的作用域内完成的。 - 在
modifyMemberVar
方法中创建了两个线程,每个线程都启动了一个匿名内部类实例。这意味着两个线程可以并发地执行各自匿名内部类的run
方法,但彼此之间的修改不会相互干扰。
(2)外部类的局部变量:
匿名内部类可以访问外部类的成员变量和方法,但如果要访问外部方法中的局部变量,这个局部变量必须是 final 或者是 effectively final。这是因为匿名内部类的实例可能会在方法执行完毕之后仍然存在,而且对局部变量的引用是在匿名内部类中存储的。如果局部变量不是 final 或者 effectively final,那么在方法执行完毕后,外部局部变量的生命周期结束,但匿名内部类的实例可能仍然存在,这时如果访问这个局部变量就会出现问题。
这也就是我们说的 变量捕获机制,确切地说是对局部变量的捕获机制。
变量捕获机制的关键是,编译器会生成一个匿名内部类的构造函数,并将外部的局部变量传递给这个构造函数,以确保匿名内部类在之后仍然能够访问这些变量。这种捕获方式确保了在匿名内部类中对外部局部变量的访问是安全的。
示例代码如下:
public class Example {public void someMethod() {final int localVar = 42;//局部变量
//局部变量必须是 final 或者是 effectively finalRunnable r = new Runnable() {@Overridepublic void run() {//localVal = 1; 报错,无法修改System.out.println(localVar);//在匿名内部类中访问外部类的局部变量}};// 使用匿名内部类的实例new Thread(r).start();}
}
- 在外部类修改局部变量的值(报错)
- 在匿名内部类中修改局部变量的值(报错)
- 不修改外部类局部变量的值,而是显式的让其被final修饰
- 不修改外部类的局部变量的值,而是隐式的意味着事实上是final,也就是局部变量一旦被定义就不再被修改,那么在事实上就可以认为这个局部变量是一个final修饰的了,而且就是修改了,在匿名类中访问这样的被修改的外部类的局部变量是会报错的!!! 因为变量捕获机制的存在 !!!
在上述代码中,localVar
是一个局部变量,因为匿名内部类 Runnable
中引用了这个局部变量,所以必须声明为 final
。
(3) 所以,总结一下:
- 外部类的成员变量可以在匿名内部类中被访问和修改。
- 外部类的局部变量必须要求是
final
的,或者是事实上是final
的,才能在匿名内部类中被访问。(变量捕获机制)
可以包含字段和方法
贴代码
interface Greeting {void greet();
}public class AnonymousInnerClassExample {public static void main(String[] args) {// 使用匿名内部类实现接口并包含字段和方法Greeting greeting = new Greeting() {//字段private String message = "Hello from anonymous inner class";@Overridepublic void greet() {System.out.println(message);sayGoodbye(); // 调用匿名内部类中定义的方法}//方法private void sayGoodbye() {System.out.println("Goodbye from anonymous inner class");}};// 调用实例方法greeting.greet();}
}
不可以包含静态成员
这是我的理解 :
匿名内部类的本质是一个实例,这个实例可以用来重写父类中的实例方法,也可以用来实现接口中的抽象方法,也可以用来实现抽象类中的抽象方法,但是不可以声明静态成员变量或者定义静态方法,虽然可以重写父类的静态方法,但是重写的静态方法是通过实例调用的,这违背了java面向对象编程的理念.所以匿名内部类是一个非具名的实例,之所以叫做类是因为匿名内部类既完成了拓展原有类或者接口的目的又完成了实例的创建.
( 匿名内部类是一个实例,而不是一个类。静态成员是属于类的,而匿名内部类没有类名,无法定义属于自己的静态成员。)
贴代码 :
package src.main;public class AnonymousInnerClassExample {public static void main(String[] args) {// 尝试在匿名内部类中定义静态成员(编译错误)Runnable myRunnable = new Runnable() {// 尝试定义静态成员变量(编译错误)// static int staticVariable = 20;@Overridepublic void run() {// 尝试定义静态方法(编译错误)// staticMethod();System.out.println("Inside Runnable");}};// 调用匿名内部类中的 run 方法myRunnable.run();}// 尝试在外部类中定义静态方法(正常)public static void staticMethod() {System.out.println("Static method");}
}
分析 : 在这个例子中,我们尝试在匿名内部类中定义静态成员变量和静态方法,但这会导致编译错误。匿名内部类无法包含静态成员,因为它本身是一个实例,而静态成员是属于类的,必须在类级别上声明。
-
通过分析匿名内部类的结构和性质,我们已经解决了文章开头我们提出的疑问了,现在还剩下最后一个疑问,那就是 : 匿名内部类是否有缺陷? 如果有缺陷是否有更好的替代方案 ? (答案是有缺陷,而且有更好的替代方案)
匿名内部类是一种便捷但有局限性的编程方式。虽然它在短期、轻量级的情况下很方便,但它的一次性使用、可读性较差、不适合复杂继承关系、对外部变量的访问限制、不能包含静态成员以及可能导致生命周期延长等缺陷,使得在一些复杂或长期维护的场景中,更倾向于使用具名类或Lambda表达式等更灵活的替代方式。
使用场景
在以下的场景中,匿名内部类的简洁语法和临时性的特性使得它成为一种方便的编码方式。然而,需要在使用时权衡其便利性和一些限制,选择适合当前情况的实现方式。
-
事件处理
在 GUI 编程中,匿名内部类常用于处理用户界面上的事件,比如按钮的点击事件。这简化了代码,避免了为每个事件都创建一个独立的类。
button.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {// 处理按钮点击事件的逻辑} });
-
线程创建
匿名内部类可以用于创建简单的线程对象,尤其在某个地方需要一次性的线程执行逻辑时。
new Thread(new Runnable() {@Overridepublic void run() {// 线程执行的逻辑} }).start();
-
实现接口或抽象类
当只需要实现某个接口或继承某个抽象类的单一实例时,匿名内部类可以提供简洁的语法。
SomeInterface instance = new SomeInterface() {@Overridepublic void someMethod() {// 实现接口的逻辑} };
-
测试和调试
在单元测试或调试时,有时需要临时性地实现某个接口或者继承某个类,匿名内部类能够方便地提供这种快速的实现。
TestingTool.runTest(new TestInterface() {@Overridepublic void runTest() {// 测试逻辑} });
-
简化工厂方法
在工厂方法中,如果只需要创建一个实例,匿名内部类可以用于快速创建。
Factory.createInstance(new SomeInterface() {@Overridepublic void someMethod() {// 实现接口的逻辑} });
-
回调函数
在某些设计模式或异步编程中,匿名内部类可以作为回调函数,用于定义异步操作完成后的回调逻辑。
asyncOperation.doAsync(new Callback() {@Overridepublic void onComplete() {// 异步操作完成后的回调逻辑} });
通过上述的分析, 相信大家应该对Java中的匿名内部类有了很深刻的理解了,如果文章中有什么地方写的不对,或者理解有误,欢迎大家在评论区留言, 教学相长 ~
如果觉得这篇文章有帮助到您,您的三连是对我最大的支持 !