资讯动态

接口都能写默认实现了,为什么还需要抽象类

发布时间:2026/8/30 16:31:15 来源:尧图企业网站定制
「Java 进阶之路」系列 Day19写在前面JDK 8 之前接口和抽象类的边界很清楚接口只能声明方法签名抽象类才能写具体实现。可 JDK 8 给接口加上了default方法之后接口也能带方法体了——这下不少人会犯嘀咕接口都能写实现了那抽象类是不是快没用了这篇就借 JDK 集合框架的真实设计把这条边界重新讲清楚。一、是什么一个只管契约一个自带半成品接口interface一组方法契约只声明能做什么不关心内部怎么实现。JDK 8 之前接口里只能有抽象方法和public static final常量JDK 8 允许写default/static方法JDK 9 又加了private方法给 default 方法内部拆分复用逻辑用。但接口从头到尾都不能有实例字段、不能有构造器撑不起跨方法共享的可变状态。抽象类abstract class不能被new出来的类可以有构造器、实例字段、已经写好实现的具体方法也可以有留给子类填的抽象方法——本质是一个没写完的类。二、为什么单继承逼出来的分工Java 类只能单继承一个类只能extends一个父类但一个对象常常要同时具备好几种互不相关的能力标签比如ArrayList同时实现了List、RandomAccess、Cloneable、Serializable。这几种能力之间没有共同状态可复用纯粹是行为契约所以用接口——一个类可以implements任意多个。反过来当一批子类之间有大量重复的样板代码、只是某一步骤实现不同就该用抽象类把公共逻辑收进去子类只填差异点。这正是模板方法模式在 JDK 里最常见的应用AbstractList帮所有List实现类写好了iterator、indexOf、equals、hashCodeArrayList/LinkedList只需要实现get和size这两个核心方法就能拿到一整套完整功能。骨架实现抽象类能力契约接口Cloneable 能被克隆Serializable 能被序列化AbstractList 已写好iterator indexOf equalsArrayList 只需实现get和sizeLinkedList 只需实现get和size一句话区分接口回答能做什么抽象类回答是什么——前者是无状态的行为契约可以多个一起实现后者是同一家族共享代码和状态的半成品只能单继承一个。三、怎么用四个最容易踩的坑坑一两个接口的同名 default 方法冲突interfaceFlyer{defaultvoidmove(){System.out.println(飞起来);}}interfaceSwimmer{defaultvoidmove(){System.out.println(游起来);}}classDuckimplementsFlyer,Swimmer{// 不重写这里编译直接报错两个父接口的 move 冲突无法确定用哪个Overridepublicvoidmove(){Flyer.super.move();Swimmer.super.move();}}Java 用编译期强制报错、必须显式重写的方式处理这种冲突不会像 C 多重继承那样产生隐式二义性——发现哪里报了这个错说明确实需要程序员自己决定优先用谁的实现或者两个都要调用。也正因为这样default方法的设计初衷只是让接口能平滑升级——比如 JDK 8 给Collection接口新增stream()方法时如果直接加一个抽象方法所有历史实现类都会编译报错改成default给个默认实现老代码就完全不用动。它不是让接口变成能攒状态的抽象类接口里堆太多default实现逻辑反而会让契约和实现混在一起。坑二接口里的字段不是普通变量interfaceConfig{intMAX_RETRY3;// 等价于 public static final int MAX_RETRY 3;}接口里写的字段编译器会自动加上public static final很多人以为能当普通共享变量改来改去实际上它是常量赋值之后不能再变。坑三接口加了一堆 default 方法还算函数式接口吗FunctionalInterfaceinterfaceGreeter{voidgreet(Stringname);// 唯一的抽象方法Lambda 就是补上这个方法的实现defaultvoidgreetTwice(Stringname){greet(name);greet(name);}}Greetergname-System.out.println(你好name);g.greetTwice(小明);函数式接口的判定标准是有且仅有一个抽象方法SAMSingle Abstract Methoddefault/static方法已经自带实现不需要调用方用 Lambda 补全所以不计入这个限制。这也是为什么 JDK 里的Comparator带reversed、thenComparing等 default 方法、Predicate带and、or、negate等 default 方法外加isEqual、not等 static 方法依然能直接用 Lambda 创建——它们都只有一个抽象方法。坑四抽象类只能单继承复用多套逻辑该怎么办抽象类的复用能力被单继承卡死。假设除了AbstractList这个迭代骨架项目里还有一个AuditLogger类封装了统一格式记录操作日志的公共逻辑——LoggingList想同时复用这两份逻辑但 Java 只允许extends一个类classAuditLogger{// 公共逻辑统一格式记录操作日志供持有者复用protectedfinalvoidaudit(Stringaction){System.out.println([audit] action);}}// LoggingList 本质是一个 List这个是什么的继承名额必须留给 AbstractList// AuditLogger 的逻辑只能靠组合来复用Java 不允许再继承第二个父类classLoggingListextendsAbstractListString{privatefinalListStringdelegatenewArrayList();privatefinalAuditLoggerloggernewAuditLogger();// 组合持有一个实例而不是继承它OverridepublicStringget(intindex){logger.audit(读取下标 index);// 复用AuditLogger的逻辑走组合而不是继承returndelegate.get(index);}Overridepublicintsize(){returndelegate.size();}}这就是组合优于继承的一个实际来源接口设计上放开了多实现但类的复用能力被限制在单继承——AbstractList这个继承名额留给了是什么LoggingList 确实是一个 ListAuditLogger这份额外的逻辑就只能改成组合持有它的实例、调用它的方法而不是硬挤继承链凑一个不伦不类的公共父类。四、面试追问Q1接口和抽象类最核心的区别是什么怎么选接口回答能做什么无状态的能力契约抽象类回答是什么同一家族、共享代码和字段。有公共状态、公共实现要复用就用抽象类只是行为约定、且一个类可能要同时具备多种能力就用接口。Q2JDK 8 之后接口能写 default 方法实现了是不是就可以取代抽象类不能。default 方法的设计目的是让接口能在不破坏历史实现类的前提下平滑新增方法不是让接口拥有状态。接口依然不能有实例字段、不能有构造器维护不了跨方法共享的可变数据这些场景还是必须用抽象类。Q3一个类实现的两个接口有同名 default 方法会发生什么编译报错Java 强制要求实现类显式重写这个方法解决冲突可以用接口名加.super.方法名()分别调用两个父接口的默认实现不会像 C 那样产生隐式的二义性调用。Q4抽象类里的抽象方法和接口里的方法本质区别是什么语法上都没有方法体但语义完全不同抽象类的抽象方法是这个继承体系内部有一步交给子类填服务的是模板方法这种内部实现机制接口方法是外部使用者能拿这个类型做什么的契约面向的是调用方而不是子类实现者。Q5JDK 集合框架里 List 是接口、AbstractList 是抽象类两者的分工是什么List 定义了是一个有序集合该有的行为契约面向所有使用者AbstractList 是这份契约的骨架实现模板方法模式把iterator、indexOf、equals等通用逻辑都写好了ArrayList/LinkedList继承它之后只需要实现get/size这几个核心方法就能拿到完整功能避免每个实现类都重复写一遍。Q6一个接口写了很多个 default 方法还能是函数式接口吗能。函数式接口的判定标准是有且仅有一个抽象方法default/static/private方法本身已经有实现不需要调用方通过 Lambda 补全不计入这个限制。JDK 里的Comparator、Predicate虽然带了不少default/static辅助方法依然是标准的函数式接口。Q7为什么说组合优于继承这跟接口和抽象类的选择有什么关系因为 Java 类只能单继承一旦某个类想同时复用两个不相关父类提供的逻辑比如既要继承AbstractList拿到迭代骨架又想复用另一个类的公共逻辑继承链根本装不下只能被迫找一个别扭的公共父类把两套逻辑硬拧在一起。更好的做法是把其中一个逻辑改造成组合持有它的实例、调用它的方法而不是继承它。这也是为什么接口设计上放开了多实现但类的复用能力被限制在单继承——复用多个不相关的逻辑本来就该用组合而不是硬挤继承链。下一篇预告Day20 讲一个更容易被问倒的细节为什么重写equals一定要重写hashCode——HashMap、HashSet这些基于哈希表的容器到底是怎么利用这两个方法定位元素的不重写会踩什么坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价