Java 中的接口是什么

张开发
2026/4/11 20:57:09 15 分钟阅读

分享文章

Java 中的接口是什么
刚接触 Java 接口时很多人会觉得它有点别扭。因为它看起来不像普通类那样能直接创建对象也不像一般方法那样拿来就能执行。它更像是一种“先把规则写出来再让别人按规则去实现”的机制。要真正理解接口不能只盯着语法看而是要先明白它解决的到底是什么问题。从最直观的角度说接口可以理解为一种统一约定。它先告诉你一个类如果想具备某种能力那么它至少要提供哪些方法。至于这些方法内部到底怎么实现接口本身并不负责它只负责把标准定下来。这样一来不同的类虽然实现方式各不相同但只要遵守同一套接口外部使用它们时就可以用同一种方式去调用。一、先从生活中的感觉来理解接口如果直接给初学者下定义通常不太容易听进去所以不妨先借助一个生活化的类比。比如你可以把接口想象成现实中的“插座标准”或者“USB 规范”。电脑并不关心你接上来的是鼠标、键盘还是 U 盘它真正关心的是你是不是符合 USB 这个标准。如果符合那么电脑就知道应该怎样跟你交互。Java 中的接口也是同样的思路。程序在很多时候不希望过早地绑定某一个具体类而是更希望先约定好“你需要具备哪些能力”。只要某个类满足了这个约定它就可以被当成这类能力的提供者来使用。比如支付场景就很适合说明这个问题。对于订单系统来说它需要的是“支付能力”而不是“必须是支付宝类”或者“必须是微信类”。支付宝、微信、银行卡支付虽然实现流程完全不同但从业务角度看它们都应该能完成“支付”这件事。于是我们就可以先定义一个支付接口把“支付”这个动作规范下来。interfacePayment{voidpay(doubleamount);}这个接口的意思并不复杂它只是规定任何声称自己是支付方式的类都应该能完成pay(double amount)这个动作。至于支付时内部是跳转页面、调用远程接口还是做参数签名这都属于具体实现的事情。二、接口的专业解释如果用稍微正式一点的语言来概括接口是 Java 中的一种引用类型它主要用来定义一组行为规范。它的核心作用不是保存对象状态也不是直接提供完整实现而是描述某类对象应该具备哪些公共能力。因此接口最适合表达的不是“某个对象是什么”而是“某个对象能做什么”。这一点和类、尤其是父类区别非常明显。父类通常更适合描述共性身份比如动物、员工、交通工具而接口更适合描述行为能力比如会飞、会支付、可比较、可运行。换句话说如果你想表达“它是一种什么东西”通常更偏向用类和继承如果你想表达“它具备什么能力”通常更偏向用接口。三、接口最基本的写法理解接口并不难先从最简单的语法开始就可以了。下面定义了一个动物接口它规定实现者必须具有“吃东西”的行为。interfaceAnimal{voideat();}这里有一个方法eat()但没有方法体。因为接口在这个层面只负责提出要求不负责给出具体实现。然后我们可以写一个类去实现它。classCatimplementsAnimal{Overridepublicvoideat(){System.out.println(猫在吃鱼);}}implements这个关键字的含义可以理解成“实现接口”。既然Cat说自己实现了Animal接口那么它就必须把接口中规定的方法真正补全否则编译器就会报错。接着写一个简单的测试程序publicclassMain{publicstaticvoidmain(String[]args){CatcatnewCat();cat.eat();}}程序运行后会输出猫在吃鱼这一部分本身并不复杂关键在于你要意识到接口只是规定了“必须有eat()这个行为”而“猫怎么吃”则是由Cat类自己决定的。四、为什么实现接口后必须重写方法很多初学者在这里会有一个疑问为什么实现了接口以后里面的方法一定要重写原因其实很简单因为接口本身只定义了规则却没有给出完整实现。程序只知道“你应该会这个动作”但并不知道“这个动作具体怎么做”。比如同样是eat()猫吃鱼、狗吃骨头、鸟吃虫子行为名可以相同但具体内容肯定不同。如果接口把这些细节都写死那它就不再是规则而变成某一个具体实现了。正因为接口不负责细节所以实现类必须自己把细节补上。五、接口真正重要的地方在于多态如果只是把接口理解成“写一个没有方法体的东西”那其实还没有抓到重点。接口真正的价值在于它特别适合和多态配合使用而多态正是 Java 面向对象中非常重要的一种思想。先看一组代码interfaceAnimal{voideat();}classCatimplementsAnimal{Overridepublicvoideat(){System.out.println(猫在吃鱼);}}classDogimplementsAnimal{Overridepublicvoideat(){System.out.println(狗在吃骨头);}}接着这样使用publicclassMain{publicstaticvoidmain(String[]args){Animala1newCat();Animala2newDog();a1.eat();a2.eat();}}这里要重点注意的是变量类型写的是Animal而不是Cat或Dog。这说明在使用时程序更关心“你是否具备 Animal 接口规定的能力”而不急着关心你到底是哪一种具体动物。这样写的好处是调用方可以统一面向接口编程而底层具体使用哪个实现类可以根据需要自由替换。这正是接口在实际开发中非常重要的原因。它让程序从“依赖具体类”变成了“依赖能力约定”整个系统会变得灵活得多。六、为什么要使用接口很多人在小项目里会觉得直接写类也能完成需求似乎没必要专门抽出一个接口。这种感觉在初学阶段很正常因为项目规模小时问题还不明显。可是一旦系统复杂起来接口的价值就会越来越突出。首先接口能够降低耦合。所谓耦合说简单一点就是代码之间绑定得有多死。如果一个业务类里直接写死了某个具体实现类那么以后只要底层实现一变上层业务代码往往也得跟着改。这样的设计不够灵活也不利于维护。比如下面这种写法就耦合得比较紧classOrderService{publicvoidcheckout(){AliPayaliPaynewAliPay();aliPay.pay(100);}}这里OrderService直接依赖AliPay。如果以后改成微信支付那这个类本身就得动。而如果先定义一个支付接口再让不同支付方式实现它情况就不一样了。interfacePayment{voidpay(doubleamount);}classAliPayimplementsPayment{Overridepublicvoidpay(doubleamount){System.out.println(支付宝支付amount);}}接下来订单服务只依赖接口classOrderService{privatePaymentpayment;publicOrderService(Paymentpayment){this.paymentpayment;}publicvoidcheckout(){payment.pay(100);}}这样一来OrderService根本不关心传进来的是支付宝、微信还是别的支付实现它只关心对方是不是一个Payment。这就明显减少了代码之间的直接绑定关系。其次接口特别适合扩展。如果今天系统里只有支付宝实现明天要增加微信支付那么只需要新写一个实现类即可原来的上层业务代码通常不需要大改。这一点在企业项目里很重要因为业务需求变化是常态而不是例外。再者接口还方便替换和测试。比如真实环境下你可能希望调用正式支付逻辑但在测试环境下你不想真的扣款那就可以写一个模拟实现类只输出日志不执行真实支付。由于业务层依赖的是接口因此替换实现的成本会很低。七、接口和类、抽象类的区别理解接口时最容易混淆的就是它和普通类、抽象类之间的关系。普通类很好理解它既可以定义属性也可以提供具体方法实现它是真正“能干活”的对象模板。接口则不同它更偏向于一套对外规范。它关注的是公共行为而不是内部状态。一个接口通常不会拿来描述“某种完整的对象”而是拿来描述“某种可供外部调用的能力”。至于抽象类它和接口有相似之处但用途并不完全一样。抽象类更适合在一组关系明确的类之间提取公共部分比如动物都有名字、年龄都可以吃东西但吃法不同这时用抽象类就比较自然。接口则更适合表达横向能力比如一个类是否可比较、是否可运行、是否支持支付。也就是说抽象类通常是“共同祖先”的抽象而接口通常是“共同能力”的抽象。如果用一句更容易记住的话来区分可以这样理解继承强调“它是什么”实现接口强调“它会什么”。八、一个类可以实现多个接口Java 中类不能多继承但可以实现多个接口这也是接口非常实用的地方。因为在现实世界中一个对象往往会同时具备多种能力而这些能力并不一定都适合放到同一个父类里。比如鸭子既会飞也会游泳这两个能力就很适合用两个接口来表示。interfaceFlyable{voidfly();}interfaceSwimmable{voidswim();}classDuckimplementsFlyable,Swimmable{Overridepublicvoidfly(){System.out.println(鸭子会飞);}Overridepublicvoidswim(){System.out.println(鸭子会游泳);}}这样的设计方式很自然也比通过类继承去硬凑层次更清晰。接口在这里提供的是“能力组合”的表达方式这一点在很多框架和库设计中都很常见。九、接口中的成员有哪些特点对于初学者来说接口中的成员规则也需要掌握否则看别人代码时容易疑惑。接口里的变量本质上默认都是常量也就是说它们默认带有public static final修饰。比如下面这段代码interfaceMyInterface{intAGE18;}实际上等价于interfaceMyInterface{publicstaticfinalintAGE18;}因此这个值是不能被修改的。接口中的普通方法默认都是public abstract。也就是说像下面这种写法interfaceMyInterface{voidtest();}实际上等价于interfaceMyInterface{publicabstractvoidtest();}所以在实现接口时方法必须写成public不能把访问权限缩小否则会报错。十、现在的接口不只是“纯抽象方法”如果你看的是比较老的资料可能会看到一种说法接口里只能有抽象方法。这个说法在早期 Java 版本里可以成立但放到现在已经不完整了。从 Java 8 开始接口中除了抽象方法以外还可以有默认方法和静态方法从 Java 9 开始还允许有私有方法。对初学者来说先重点掌握默认方法和静态方法就够用了。默认方法使用default关键字定义它的意义在于接口在某些情况下也可以提供一个默认实现这样已有实现类即使不重写这个方法也照样能正常使用。interfaceAnimal{voideat();defaultvoidsleep(){System.out.println(动物会睡觉);}}Cat实现这个接口时只需要实现eat()即可classCatimplementsAnimal{Overridepublicvoideat(){System.out.println(猫在吃鱼);}}然后就可以直接调用默认方法publicclassMain{publicstaticvoidmain(String[]args){CatcatnewCat();cat.eat();cat.sleep();}}之所以会设计默认方法主要是为了在扩展旧接口时尽量不破坏已有代码。否则如果一个已经被大量实现的接口突然增加一个新方法那么所有旧实现类都要跟着修改代价会很大。接口中的静态方法则更像是和接口相关的工具方法它们必须通过接口名来调用而不能通过对象去调用。例如interfaceTool{staticvoidprintInfo(){System.out.println(这是工具接口);}}调用方式是Tool.printInfo();十一、一个很典型的入门案例USB 设备讲接口时USB 设备这个例子特别经典因为它很容易把“统一标准”和“具体实现”的关系讲清楚。先定义一个 USB 接口interfaceUSB{voidconnect();voiddisconnect();}然后让鼠标和键盘都去实现它classMouseimplementsUSB{Overridepublicvoidconnect(){System.out.println(鼠标已连接);}Overridepublicvoiddisconnect(){System.out.println(鼠标已断开);}}classKeyboardimplementsUSB{Overridepublicvoidconnect(){System.out.println(键盘已连接);}Overridepublicvoiddisconnect(){System.out.println(键盘已断开);}}再写一个电脑类classComputer{publicvoiduseDevice(USBdevice){device.connect();System.out.println(设备正在工作...);device.disconnect();}}最后测试publicclassMain{publicstaticvoidmain(String[]args){ComputercomputernewComputer();USBmousenewMouse();USBkeyboardnewKeyboard();computer.useDevice(mouse);computer.useDevice(keyboard);}}这个例子里电脑完全不需要知道自己接入的是鼠标还是键盘它只认一件事对方是否符合 USB 这个接口。只要符合它就知道可以怎样去调用。这就是接口设计最核心的价值之一——让使用者依赖统一标准而不是依赖具体实现细节。十二、接口在实际开发中怎么用如果只是停留在语法层面接口会显得有点空。真正到了实际开发里你会发现接口几乎无处不在尤其是在分层设计、框架设计和业务扩展点设计中非常常见。以支付系统为例通常会先定义一个支付服务接口interfacePaymentService{voidpay(doubleamount);}然后不同支付渠道分别实现它classWeChatPaymentServiceimplementsPaymentService{Overridepublicvoidpay(doubleamount){System.out.println(微信支付amount);}}classAliPaymentServiceimplementsPaymentService{Overridepublicvoidpay(doubleamount){System.out.println(支付宝支付amount);}}业务类依赖的不是某个具体支付类而是这个支付接口classOrderService{privatePaymentServicepaymentService;publicOrderService(PaymentServicepaymentService){this.paymentServicepaymentService;}publicvoidcreateOrder(){System.out.println(订单创建成功);paymentService.pay(200);}}这种设计的意义在于订单服务只关心“有人能完成支付”而不是“到底是谁完成支付”。这样以后切换实现、增加渠道、做测试替身都会容易很多。日志系统里接口也很常见。你可能希望日志能输出到控制台、写入文件、发送到日志平台甚至写入数据库。表面上看它们的输出位置不同但从业务角度说它们都在完成“记录日志”这件事。于是完全可以定义一个日志接口再让不同实现各自去处理。数据库访问层也是一样。业务层往往不应该直接依赖某种数据库实现而更应该依赖一个抽象的数据访问接口。这样如果以后数据库换了或者持久化方案改了影响就不会一下传导到整个业务层。十三、你以后会频繁见到的接口等你学 Java 集合、线程和常用框架时会发现接口不是某个冷门语法点而是日常代码里非常常见的东西。比如List、Set、Map、Runnable、Comparable、Comparator这些都是你以后会经常碰到的接口。最典型的一句代码就是ListStringlistnewArrayList();这里List是接口ArrayList是实现类。之所以很多人喜欢这么写而不是直接写成ArrayListString list new ArrayList();就是因为前一种写法更灵活。将来如果你想换成LinkedList通常只需要改右边的实现部分而左边的使用方式基本可以保持不变。这其实就是“面向接口编程”最常见的实际体现。十四、初学者容易犯的错误学习接口时有几类错误特别常见。第一类是把接口想得过于玄乎觉得它一定是什么特别高级、特别复杂的设计。实际上它的本质并不神秘你完全可以把它先理解成“统一规范”或者“能力约定”这就已经抓住重点了。第二类错误是分不清接口和继承的职责导致一会儿拿接口去表示身份一会儿又拿父类去表示能力代码结构会显得别扭。这个时候最好反复问自己一个问题我现在想表达的是“它是什么”还是“它会什么”。这个问题往往能帮你做出比较清晰的判断。第三类错误是实现接口时忘记把方法写成public。因为接口中的抽象方法默认就是public所以实现类不能把访问权限缩小。如果你少写了public编译器通常会直接报错。第四类错误是滥用接口。并不是所有类都必须先抽出一个接口。如果一个功能非常简单未来也没有多实现、扩展、测试替换这些需求那么直接写具体类完全没有问题。接口真正有价值的地方在于系统需要更强的扩展性和解耦能力时它能明显改善代码结构。十五、最后把接口的核心思想真正记住如果把前面的内容压缩成一个完整的认识框架那么可以这样总结接口本质上是一种行为规范它用来规定“实现者必须提供哪些公共方法”。它更关注能力而不是身份更适合描述“能做什么”而不是“是什么”。在语法上接口通过interface定义类通过implements来实现在设计上它最重要的价值并不是“多写一层”而是帮助系统解耦让调用方不依赖具体实现从而获得更好的扩展性、替换性和可维护性。所以真正学会接口不只是会写下面这样的代码interfaceAnimal{voideat();}也不只是会写classCatimplementsAnimal{Overridepublicvoideat(){System.out.println(猫在吃鱼);}}更重要的是你要理解它背后的思路程序很多时候不应该过早地绑死某一个具体类而应该先依赖一套稳定的能力约定。只要这个约定不变底层实现就可以自由替换和扩展。

更多文章