一、介绍
匿名内部类
的本质就是隐藏了名字的内部类。
它的格式如下:一开始写一个 new
关键字,然后再加上 类名或接口名
,在大括号中,我们可以去重写 类中或者接口中
的方法。
new 类名或者接口名() {
重写方法;
};
整体的这个格式,它里面包含了三部分
1、继承
或者是 实现
关系
2、方法的重写
3、创建对象
我们来举个例子:例如我现在有个接口,叫做 Inter
,Inter接口
中有个 show()
抽象方法。
此时我们就可以来写这样的一个匿名内部类
new Inter() {
// 直接重写接口里的方法 show() 就可以了
public void show() {
}
}
二、代码示例
Swim.java接口
package com.itheima.a06innerclassdemo6;
public interface Swim {
public abstract void swim();
}
Test.java
匿名内部类需要重写接口中所有的方法
package com.itheima.a06innerclassdemo6;
public class Test {
public static void main(String[] args) {
// 编写匿名内部类的代码
new Swim(){
@Override
public void swim() {
System.out.println("重写了游泳的方法");
}
}; // 注意分号不能丢!
}
}
三、格式详解
代码写完了,感觉还是有点看不懂,接下来通过画图板,将这个格式详细信息完完,整整解析一下。
按顺序阅读每段代码的含义,接下来我们就以第4个格式为基础,一点一点的去推导成左边的匿名内部类
的格式。
首先,第4个代码的类名是 Student
,这个类实现了 Swim
这个接口,因此,它需要在自己的类中,去重写 Swim接口
中的抽象方法。
所以说通过这个我们就知道了,在这段代码当中,第一行其实就是类的名字而已。
从下面的这个大括号开始,到最下面的大括号结束才是类真正的内容。
![image-20240419105334884](https://img-blog.csdnimg.cn/img_convert/2ee1b640c855d02bda473704a9f43094.png)
但如果,我将上面的名字给删掉,那么下面的这个,就变成了一个没有名字的类了。
如果我想要让这个没有名字的类
,去实现 Swim接口
其实也是可以办到的。
此时我们只需要将 Swim
写在前面就可以了。
现在就变成了:这个没有名字的类,去实现 Swim
这个接口。
那么也就是说紫色框起来的这一部分,是没有名字的类,它就是 Swim接口
的实现类,所以说它是不是要在自己的类中去重写 Swim接口
中所有的抽象方法。
那如果我还想要创建这个没有名字的类的对象,那该怎么办?
以前创建对象的时候,都是通过 new关键字 + 类名()
来创建对象的。
new 类名();
现在我们也可以直接套用进去:把 new
放在 Swim
的前面,但由于这个类没有名字,因此类名就用接口去替代,再把小括号放到 Swim接口
的后面。最后的一个分号放到末尾即可。
现在再来看,这个格式,跟左边的代码基本一模一样。
所以通过刚刚的分析,就可以真正的去解释,刚刚书写的这段代码了。
四、理解代码
首先,我要告诉你的是:匿名内部类,那个真正没有名字的类,并不是这个整体。
new Swim(){
@Override
public void swim() {
System.out.println("重写了游泳的方法");
}
};
而是大括号后面的这一堆(即紫色框框起来的这部分),这个类实现了 Swim接口
,因此它需要在自己这个类当中去重写 Swim接口
中所有的抽象方法。
而前面的 new
,它 new
的不是这个接口,而是后面这个没有名字的对象
因此在刚刚我们说了,这个整体它包含有三样东西
1、继承
或者是 实现
关系:紫色框框起来的 这个没有名字的类
实现了 Swim接口
2、方法的重写:既然实现了一个接口,就需要在自己的类中去重写里面的抽象方法,如果你不重写,代码就会报错。
3、创建对象:前面的 new关键字
创建了后面紫色框框起来的 这个没有名字的类
的对象
中间还剩下一个小括号,就表示我用 new
在创建这个没有名字的对象的时候,我使用的是空参构造来创建的。
因此,如果是从语法的角度来解释这个整体的话,这个整体不应该叫做匿名内部类,而是叫做:匿名内部类的对象
,它这个整体是一个对象,真正没有名字的这个类应该是紫色的区域。
![image-20240419112122354](https://img-blog.csdnimg.cn/img_convert/bb17116f4c9aff5c39fb3eec696bc025.png)
说到这,有的同学就会想:如果 Swim
这个地方,我写的不是接口呢?而是类,那是怎么回事呢?
五、理解 匿名内部类
继承 类
的格式
首先新建 Animal类
,里面有个 eat
抽象方法,抽象方法所在的类必须是抽象类。
public abstract class Animal {
public abstract void eat();
}
在测试类中创建 匿名内部类
,然后重写 Animal抽象类
中所有的抽象方法。
new Animal() {
@Override
public void eat() {
System.out.println("重写了eat方法");
}
};
那么这个类我们又怎么去理解呢?同样的,也是画图去解释。同样的也已经将要用到的代码都粘贴进去了。
在一开始的时候我们讲了,这个代码的整体,其实包含了三样:
1、继承
或者是 实现
关系:
2、方法的重写
3、创建对象
在这个整体中,真正没有名字的类其实是后面的大括号,没有名字的这个类跟 Animal
是继承关系。
后面紫色框框起来的这个类,是 Animal
的子类,既然是子类,那就需要去重写里面所有的抽象方法。
因此在大括号中,我们需要去重写 eat
方法。
![image-20240419113226159](https://img-blog.csdnimg.cn/img_convert/516e8759e5be3b9af0ade818a9b54618.png)
前面的 new关键字
表示创建对象,创建的是后面这个没有名字的类
的对象。
并且我是通过空参构造去创建的,因此小括号中什么都不写,这个就是整体格式的解释。
![image-20240419113319631](https://img-blog.csdnimg.cn/img_convert/4ed158ddc2937a7bf3d35decae95b7a2.png)
因此,如果 new
后面是个类的话,跟刚刚接口其实是一样的,这个整体同样也是 匿名内部类的对象
。
只不过跟刚刚不一样的就是:这个没有名字的类,现在是继承前面 Animal类
而已。
六、验证结论
1)查看匿名内部类的字节码文件
我们刚刚写的代码,如果直接运行是没有结果的。因为这段代码只是创建了一个 匿名内部类的对象
,但是并没有执行里面的方法!
new Swim(){
@Override
public void swim() {
System.out.println("重写了游泳的方法");
}
};
因此我们真正想让你看的是:本地的字节码文件信息。
右击 ——> Open In ——> Exploer
然后找到 basic-code
(项目的名称)
在项目的根目录(basic-code)中找到 out
文件夹。
在这个文件夹中它装了所有的字节码文件,也就是编译之后的 class文件
。
双击点进去 ——> 点进 production文件夹
——> 找到模块 oop-innerclass
,双击点进去
然后一路寻找 com\itheima\a06innerclassdemo6
Animal.class
是我们刚刚写的抽象类的字节码文件,Swim.class
是接口的字节码,Test.class
是测试类的字节码。
与此同时它还多了一个:Test$1.class
,这个字节码文件。
然后我们将下面的匿名内部类打开,右键再来运行,此时本地又多了一个 Test$2.class
。
所以在这里我们看到了一个效果:匿名内部类并不是真的没有名字,只是这个名字不需要你起而已,这里的 外部类 + $ + 序号
,例如这里的 Test$1
、Test$2
这个就是匿名内部类的名字。
现在我就要想了:这个匿名内部类里面,到底长什么样呢?
现在我要带着你去做一个反编译,所谓反编译就是:将 字节码class文件
再变回 java代码
。
2)反编译
点击一下上面的路径,然后输入 cmd
回车
然后在 cmd
的界面中,输入 javap
,javap
是JDK给我们自带的一个工具,它可以用来进行反编译,我们就反编译 Test$1.class
,然后回车即可。
和IDEA中写的代码对比,输出的这一堆,就是没有名字的那个类中所有的内容。
class com.itheima.a06innerclassdemo6.Test$1 implements com.itheima.a06innerclassdemo6.Swim {
com.itheima.a06innerclassdemo6.Test$1();
public void swim();
}
------------------------------------------------------------------------------------------
new Swim(){
@Override
public void swim() {
System.out.println("重写了游泳的方法");
}
};
一路对比下来,可以发现和我们写的匿名内部类是一模一样的。
没有名字的类是红色圈起来的这一块,它实现了 Swim接口
。在反编译后的代码中也可以看见它的关键字是 implements
。
![image-20240419130119225](https://img-blog.csdnimg.cn/img_convert/fa8cd90709864e681766041aef629bc0.png)
继续往下,我们来反编译 Test$2.class
class com.itheima.a06innerclassdemo6.Test$2 extends com.itheima.a06innerclassdemo6.Animal {
com.itheima.a06innerclassdemo6.Test$2();
public void eat();
}
------------------------------------------------------------------------------------------
new Animal(){
@Override
public void eat() {
System.out.println("重写了eat方法");
}
};
来到IDEA中做一个对比。
没有名字的类是红色圈起来的这一块,它继承了 Animal类
。
![image-20240419130715447](https://img-blog.csdnimg.cn/img_convert/3e3eea6ea5e3c4efd0ec2bdc35c249db.png)
再次一路对比下来,也是一模一样的。
和我们平时写的类唯一不同就是:Java会自动帮我们加上类名,不需要我们自己起
3)总结
在刚刚,我们通过画图和反编译的方式已经验证了我们的结论。
以后别人问你:什么是匿名内部类的时候,你要非常有底气的去告诉它,这个整体其实不是一个类,而是 new
出来的对象。
![image-20240419131315548](https://img-blog.csdnimg.cn/img_convert/68a7d114b2ad161b09efc9c9cb3a1277.png)
真正没有名字的类是后面大括号中的。
![image-20240419131349368](https://img-blog.csdnimg.cn/img_convert/241b007ed3c5d5b90967293f60a1cb26.png)
这个大括号跟前面的 这个,要么是实现
关系,要么是继承
关系。
如果前面是接口,那就是实现关系;如果前面是类,那就是继承关系。
在 实现
/ 继承
之后,需要重写它里面所有的抽象方法,这个就是整体格式的理解。
七、应用场景
格式理解之后,那以后该如何去使用呢?
来看一个场景:如何在测试类中调用下面的method方法?
public class Test {
public static void main(String[] args) {
}
public static void method(Animal a){
a.eat();
}
}
在以前,我需要自己写一个子类,然后去继承 Animal类
,然后再创建子类的对象,传递给 mehtod方法
Dog.java
public class Dog extends Animal{
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
Test.java
Dog d = new Dog();
method(d);
但如果Dog类我只要用一次,那么还需要单独定义一个类,太麻烦了!
因此在这种情况下,我们就可以用匿名内部类的形式去简化代码。
public class Test {
public static void main(String[] args) {
method(
// 1.相当于就是把这个对象,当做参数传递给下面的 Animal a
// 2.而这个 new 出来的对象,又是后面这堆没有名字的类的对象,这个没有名字的类又继承了 Animal类,因此可以把下面这个整体看做是 Animal 的子类对象
new Animal() {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
);
}
// 3.当你把 Animal子类的对象 传递给这个方法的时候,就形成了:Animal a = 子类对象,这个就是一个标准的多态
public static void method(Animal a){
// 4.因此这里在调用eat方法的时候,它还是会执行规则:编译看左边,运行看右边
a.eat();
}
}
因此上面这段代码的执行结果就是:狗吃骨头
八、扩展
我们先来回顾一下匿名内部类的格式
new 类/接口(){
需要重写的抽象方法;
}
新建一个 Test2.java
public class Test2 {
public static void main(String[] args) {
// 在刚刚我们说了,下面这个整体是一个对象,这个对象又是通过后面这个没有名字的类创建出来的对象。
// 而这个没有名字的类又实现了Swim接口,因此这个整体我们可以理解为:Swim接口的实现类对象
// 那此时那我们能不能把这个对象赋值给一个 Swim类型的 变量呢?完全是可以的。
// 等号的左边是接口,等号的右边是接口实现类的对象
// 此时就形成了 接口多态
Swim s = new Swim(){
@Override
public void swim() {
System.out.println("重写之后游泳方法");
}
};
}
}
既然是接口多态,那就可以使用 s
去调用里面的方法。
此时它就会遵守:编译看左边,运行看右边的原则
s.swim();
并且,既然这个整体就是 Swim实现类 的对象,那不就可以直接在对象后面直接调用自己类中所有的方法了?
new Swim(){
@Override
public void swim() {
System.out.println("重写之后游泳方法");
}
}.swim();
九、总结
1、什么是匿名内部类?
匿名内部类:隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置。
匿名内部类
就是隐藏了名字的内部类。
但是 匿名内部类
并不是真的没有名字,只是不需要我们自己写而已,Java会给它起名字,格式是:外部类类名 + $ + 序号
,例如:Test$1
、Test$2
。
匿名内部类
可以写在成员位置,也可以写在局部位置。
这句话非常重要。
很多课程中都会这么去讲:匿名内部类
是 局部内部类
的一种,但是这句话是错的!
回到IDEA中验证一下,如果直接将 匿名内部类
写在方法里面,此时它是 局部内部类
的一种;但是如果你写在 方法外,类的里面(成员位置)
,这个时候它就不是 局部内部类
的一种了,而是 成员内部类
中的一种了。
package com.itheima.a06innerclassdemo6;
public class Test2 {
// 写在成员位置,就是一个没有名字的 成员内部类
Swim s = new Swim(){
@Override
public void swim() {
System.out.println("重写之后游泳方法");
}
};
public static void main(String[] args) {
// 写在方法里面,它就是一个没有名字的 局部内部类
Swim s = new Swim(){
@Override
public void swim() {
System.out.println("重写之后游泳方法");
}
};
}
}
2、匿名内部类的格式
new 类名/接口名(){
重写方法;
};
3、格式的细节
整体的这个格式,它里面包含了三部分
1、继承
或者是 实现
关系
2、方法的重写
3、创建对象
因此这个整体,其实是一个类的子类对象
或者接口的实现类对象
。
4、使用场景
当一个方法的参数是接口或者类时,
以接口为例,可以传递这个接口的实现类对象,
如果这个实现类只需要使用一次,此时就没有必要再去定义一个新的类的,可以直接以 匿名内部类
的形式来简化代码。