
Java 匿名内部类藏在new后面的「无名英雄」你有没有盯着这样一段代码陷入过深深的困惑list.sort(newComparatorUser(){Overridepublicintcompare(Usera,Userb){returna.getAge()-b.getAge();}});这里的new ComparatorUser() { ... }到底是个啥Comparator不是个接口吗接口不是不能 new 吗怎么这里new得理直气壮后面还跟着一对花括号、里面还写了个方法再比如button.setOnClickListener(newOnClickListener(){OverridepublicvoidonClick(Viewv){// 点击后干什么}});这个new OnClickListener() { ... }同样让人摸不着头脑。别慌你遇到的这个东西有个专门的名字——匿名内部类Anonymous Inner Class。它是 Java 里一个「看起来怪、用起来香」的语法也是你理解Lambda 表达式之前必须先跨过的一道坎。一、什么是匿名内部类先回答那个最扎心的问题接口不是不能 new 吗为什么这里new Comparator()能编译答案是你 new 的不是接口本身而是「一个偷偷实现了这个接口、但没有名字的类」。1.1 它到底在干什么看这段代码其实它等价于「先定义一个实现了 Comparator 的类再 new 一个它的对象」// 匿名内部类的写法一步到位list.sort(newComparatorUser(){Overridepublicintcompare(Usera,Userb){returna.getAge()-b.getAge();}});// 等价于「先定义类再用类」传统写法要写两步classAgeComparatorimplementsComparatorUser{Overridepublicintcompare(Usera,Userb){returna.getAge()-b.getAge();}}list.sort(newAgeComparator());看明白了吗匿名内部类做的事就是把「定义类」和「创建对象」这两步压缩成了一步。匿名内部类一步new Comparator() { ... }定义类 创建对象一次搞定传统写法两步① 定义一个类AgeComparator implements Comparator② new 一个对象new AgeComparator()一句话匿名内部类就是一个「没有名字、用完就丢」的类。你new的其实不是接口而是「一个当场定义、当场实现接口、当场创建对象的无名类」。二、为什么需要匿名内部类那为什么要费这个劲搞一个「没名字」的类2.1 核心动机临时实现一个「只用一次」的接口很多时候你要实现的接口/抽象类只在这一个地方用一次。为了这「一次」专门去新建一个类文件、起个名字、写一堆样板代码太浪费了。匿名内部类就是为这种「一次性」场景而生的——当场定义、当场用完、不留痕迹。传统匿名内部类需要一个「只用一次」的实现新建类文件起名 写样板代码繁琐当场写用完就丢轻量2.2 典型场景场景例子排序比较Comparator线程任务Runnable/Thread事件监听按钮点击OnClickListener回调处理各种 Listener临时实现接口只在一处用的接口一句话匿名内部类解决的是「为了用一次不值得专门建个类」的尴尬。它是「一次性接口实现」的语法糖。三、语法拆解一个「公式」看懂所有匿名内部类匿名内部类的语法看起来乱其实就一个固定「公式」new 接口名/父类名(构造参数) { // 重写抽象方法 // 或新增自己的成员 }new接口名 / 父类名要继承/实现谁(构造参数)可选传给父类构造器{ ... }花括号里重写方法拆开看部分含义说明new创建对象老规矩接口名/父类名实现谁/继承谁匿名类就是它的「实现」或「子类」(构造参数)传给父类构造器实现接口时通常为空(){ ... }类体在这里重写抽象方法3.1 两种形态实现接口 vs 继承父类// 形态一实现接口RunnablernewRunnable(){Overridepublicvoidrun(){System.out.println(我是一个线程任务);}};// 形态二继承父类这里是抽象类ThreadtnewThread(){Overridepublicvoidrun(){System.out.println(继承 Thread 的匿名类);}};一句话匿名内部类有两种形态——要么实现一个接口要么继承一个父类通常是抽象类。记住公式new 谁(参数) { 重写方法 }所有匿名内部类都逃不出这个框。四、常见场景实战实操重点4.1 排序Comparator最经典ListUserusers...;// 按年龄升序排序users.sort(newComparatorUser(){Overridepublicintcompare(Usera,Userb){returna.getAge()-b.getAge();}});4.2 线程任务Runnable// 老式写法匿名内部类实现 RunnablenewThread(newRunnable(){Overridepublicvoidrun(){System.out.println(子线程执行);}}).start();4.3 事件监听按钮点击button.setOnClickListener(newOnClickListener(){OverridepublicvoidonClick(Viewv){// 用户点击按钮时这里的代码被触发System.out.println(按钮被点击了);}});4.4 临时实现一个回调接口// 比如定义一个「回调」执行完某件事后通知我doSomething(newCallback(){OverridepublicvoidonSuccess(Stringresult){System.out.println(成功了result);}});这些场景的共同点接口只在「这一个地方」被实现一次。这正是匿名内部类的主场。五、匿名内部类的「脾气」原理 限制匿名内部类不是万能的它有几个「硬脾气」面试也常考。5.1 只能实现一个接口 / 继承一个类匿名内部类没有名字也没有implements/extends关键字所以它只能「实现一个接口」或「继承一个父类」不能同时搞多个。// ✅ 实现一个接口newRunnable(){...};// ❌ 不能同时实现多个接口语法不支持// new Runnable(), Serializable() { ... }; ← 错误5.2 不能有构造器因为它没有名字所以无法定义构造方法。那参数从哪传——如果是继承父类可以通过new 父类(参数)把参数传给父类构造器。// 匿名内部类继承抽象父类通过 (参数) 传参给父类构造器newAnimal(小狗){Overridepublicvoidbark(){System.out.println(汪汪);}};5.3 访问外部局部变量必须 final或 effectively final这是匿名内部类最著名的「坑」publicvoidtest(){intnum10;RunnablernewRunnable(){Overridepublicvoidrun(){System.out.println(num);// ✅ 可以读外部变量// num 20; // ❌ 不能修改编译报错}};}为什么因为匿名内部类可能在「另一个线程」里执行如果外部方法结束了、局部变量被销毁了匿名内部类还得用它——Java 的解决办法是把变量「复制」一份给匿名内部类用。为了不让「复制品」和「原版」不一致就要求变量必须final只赋值一次不会被改。复制一份要求原版不能被改final / effectively final外部方法的局部变量 num10匿名内部类里的 num 副本10一句话匿名内部类能「借用」外部的局部变量只读但前提是这变量不能被修改final。这也是它和成员变量的重要区别——成员变量可以通过引用修改局部变量不行。六、匿名内部类 vs 普通内部类先搞清楚「内部类」这个大家族匿名内部类只是其中一员内部类成员内部类有名字属于外部类的成员局部内部类有名字定义在方法里匿名内部类没名字用完即丢静态内部类用 static 修饰类型有名字吗定义位置典型用途成员内部类✅类内部、方法外和外部类强关联的类局部内部类✅方法内部只在某个方法里用匿名内部类❌表达式里new 后面一次性实现接口静态内部类✅类内部static 修饰不依赖外部实例一句话匿名内部类是「内部类家族」里最轻量、最临时的一个——没名字、没构造器、用完就丢专为「一次性接口实现」而生。七、匿名内部类的本质编译后到底变成了什么好奇的话我们钻进底层看一眼。7.1 它其实「有名字」编译器起的虽然你写的时候没起名但编译器会偷偷给它起一个名字。编译后你会看到类似这样的 class 文件OuterClass$1.class // 第 1 个匿名内部类 OuterClass$2.class // 第 2 个匿名内部类编译源码里new Comparator() { ... }编译后OuterClass$1.class编译器自动起名所以「匿名」只是对你写代码的人匿名对编译器来说它照样是一个正经的类。7.2 它会生成独立的 class 文件这一点很重要特性匿名内部类编译产物生成独立的.class文件运行时真的 new 出一个对象本质是一个真正的类一句话匿名内部类「表面没名字骨子里是正经类」——编译器给它起名、生成 class 文件、运行时真的 new 对象。这也正是它比 Lambda「重」的原因。八、从匿名内部类到 Lambda一次「减负」之旅看到这里你可能会想匿名内部类好是好但还是太啰嗦了。// 匿名内部类7 行list.sort(newComparatorUser(){Overridepublicintcompare(Usera,Userb){returna.getAge()-b.getAge();}});里面真正有用的其实只有一句a.getAge() - b.getAge()。其余全是「样板代码」——new、Override、方法签名、括号……Lambda 表达式就是来砍掉这些样板代码的// Lambda1 行list.sort((a,b)-a.getAge()-b.getAge());进化再进化匿名内部类7 行样板代码Lambda 表达式1 行直达本质方法引用更简的 ::写法代码量本质传统类新建类文件定义一个类匿名内部类7 行临时的无名类Lambda1 行直接传「行为」方法引用更短Lambda 的缩写一句话匿名内部类是「从传统类到 Lambda 的过渡站」。它先把「定义类」简化成了「当场定义」Lambda 再进一步把「当场定义类」也砍掉直接传「行为」。理解了匿名内部类你就理解了 Lambda 为什么诞生。