新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java基础核心总结:封装、继承、多态、接口与异常详解

发布时间:2026/9/28 15:01:49来源:尧图网络
Java基础核心总结:封装、继承、多态、接口与异常详解
很多学 Java 的人都有过这种经历面向对象的概念倒背如流可真的上手写项目又分不清什么时候该封装、什么时候该继承、接口和抽象类到底怎么选。这篇是 Java 基础篇三我把封装、Java 包结构、继承、多态、抽象类和接口、常用类、异常这几个高频考点和日常开发里最常用的知识点串在一起讲一遍。每个部分都配上代码例子和实际开发中容易踩的坑希望能帮你把这些基础真正落到代码里而不是只停留在面试八股文层面。1. 封装先让对象学会管理自己的数据1.1 封装到底封装了什么很多新手刚接触封装时以为“给字段加个 private 就是封装”写完一堆 getter/setter 就觉得完事了。其实只做了一半。封装的核心不是私有化属性而是暴露必要的行为隐藏内部实现细节。换句话说调用方只需要关心“这个对象能做什么”不需要关心“它怎么做”。举个例子写一个账户类余额字段不能是 public因为一旦公开任何人都能直接把 balance 改成负数业务规则就被绕过了。正确的做法是把字段设为 private对外提供 deposit 和 withdraw 方法在方法里做校验和控制。public class BankAccount { private double balance; public void deposit(double amount) { if (amount 0) { throw new IllegalArgumentException(存款金额必须大于0); } balance amount; } public boolean withdraw(double amount) { if (amount 0 || amount balance) { return false; } balance - amount; return true; } public double getBalance() { return balance; } }这里的关键点是调用方看到的只有 deposit、withdraw、getBalance 三个方法至于 balance 是怎么存储的、内部有没有做额外校验这些实现细节可以被随时替换。业务规则放进方法里比暴露字段让外部自己判断要安全得多。这种“行为封装 数据隐藏”才是封装的本意。1.2 访问修饰符与包结构里的“内部资料”Java 的访问修饰符一共有四种private、缺省默认、protected、public很多初学者搞不清 protected 和缺省的区别经常在面试里被问到。我用一句话给你理清private只有当前类能访问。缺省也叫 package-private同一个包内的类可以访问。protected同一个包内的类以及不同包的子类可以访问。public任何地方都能访问。这里最容易混淆的是 protected。“同一个包内的类”和“子类”这两个条件是“或”的关系不是“且”。也就是说子类即使不在同一个包也能访问父类的 protected 成员但子类访问父类 protected 成员时有个限制只能通过子类自身的对象去访问不能通过父类引用去访问别的对象的 protected 成员。实际开发里我的习惯是状态字段一律 private对外只暴露必要的方法同一个包内需要协作的类之间才用缺省修饰如果是给别的包做扩展用的钩子方法才考虑 protectedpublic 只给真正对外提供的 API 使用。别一上来全 public那不是封装是裸奔。从设计层面看访问修饰符配合包结构其实是在划分模块边界。包就像是项目里的部门缺省权限相当于“部门内部资料”protected 相当于“部门内文件可以给关联部门的特定人员看”public 才是“对外公开的公告”。把这些边界定清楚后面维护代码时才能知道哪些改动会影响外部依赖。2. Java包结构不只是文件夹2.1 包不仅仅是个文件夹很多人刚学 Java 时以为 package 只是把类分类放好方便找文件。其实包的作用远不止这些第一它构成了命名空间避免了类名冲突第二它定义了访问控制的范围和上面的缺省修饰符直接相关第三它是 Java 模块化、组件化设计的基础。举个例子一个项目里可能出现两个 User 类一个在com.shop.entity.User一个在com.shop.dto.User名字相同但业务含义不同。如果没有包编译器根本没法区分。有了包我们可以用全限定名明确指定到底用哪个。日常写代码时导入 import 语句本质上就是告诉编译器“我说 User指的是哪个包下的 User。”还有一个细节我经常看到新人写错包名应该全部小写而且要反写公司域名。比如com.example.project.module。这样做的原因是保证全局唯一因为域名本身是唯一的。虽然现在的个人项目不太在意这种约束但公司内部和开源项目还是会按这套规范来建议早点养成习惯。2.2 import、全限定名和避免重名实际编码时我们通常不会写全限定名而是用 import 把类导入当前文件中比如import java.util.List;。但如果两个包下有同名的类而你又都需要用到那就只能在一个类里用全限定名了。我以前处理过一个订单模块代码里面同时使用了java.util.Date和java.sql.Date一开始只 import 了 util 的结果 sql 的 Date 直接用全限定名代码看起来有点啰嗦但至少不会出错。这里有个容易犯的错import 的类名冲突时如果你写import java.util.*;和import java.sql.*;并且同时用 Date编译器会直接报错。所以通配符导入虽然省字但遇到同名类就会麻烦。我的建议是平时尽量用单类导入IDE 自动完成的 import 不是拿来好看是帮你降低冲突风险。包结构还有一个实际用途它决定了编译后的 class 文件目录结构。不管是用 javac 还是 Maven、Gradle最终生成的 class 文件都会按照包路径存放。如果你手工编译就要记得javac -d . 文件名.java这样类文件才会正确落到对应目录里。很多新手在这里栽过跟头编译出来的 class 文件全部堆在同一个目录运行时直接 ClassNotFoundException。3. 继承代码复用的甜头与代价3.1 继承的构造链和 super继承是面向对象里最直观的复用手段子类继承父类的字段和方法可以直接复用父类已有的逻辑。但继承也带来一个容易被忽略的地方——构造方法不会被继承。Java 规定子类的构造方法必须直接或间接调用父类的构造方法这是为了保证父类先完成初始化。如果你在子类构造方法里没有显式写super(...)编译器会自动加一个无参的super()。这时候如果父类没有无参构造方法代码就会编译失败。这是很多新人第一次写继承最常见的报错。public class Animal { private String name; public Animal(String name) { this.name name; } } public class Dog extends Animal { public Dog(String name) { super(name); } }看到没父类只提供了带参构造子类就必须用 super(name) 把参数传上去。这个规则没什么捷径也不建议通过给父类硬塞一个无参构造来绕过因为那样会让父类字段可能处于“不完整初始化”状态。设计继承关系时一开始就把父类需要的核心参数定义清楚反而能逼你把对象模型想明白。3.2 重写与重载面试最爱问的一组概念重写Override和重载Overload看起来差不多实际是两个层面的东西重写发生在父子类之间子类用相同的方法签名方法名 参数列表覆盖父类方法方法的访问权限不能比父类更严返回类型可以是父类返回类型的子类型协变返回。重载发生在同一个类里多个方法同名但参数列表不同调用时由编译器根据参数数量和类型来决定调用哪个。面试时我经常拿一个例子考人父类方法返回 Object子类方法返回 String这算不算重写答案是算因为 String 是 Object 的子类型符合协变返回。但如果父类方法抛出的异常比子类宽子类可以缩小异常范围甚至不抛异常。这些细节代码评审时也常见。还有一个值得注意的点重写时如果忘了加Override注解而方法签名写错了你以为是在重写其实是在子类里新写了一个方法。这会导致多态失效而且编译器不会给出任何报错。所以我的习惯是所有重写方法一律加Override这不仅是规范更是防呆。3.3 继承里的几个常见坑第一坑继承层次过深。我维护过一个项目实体类继承链长达五层最顶层的字段在底层子类里根本没人知道从哪来的。后来做字段变更影响面巨大。继承虽然能复用但深度超过三层以后可读性和维护性会断崖式下降。这个时候更适合用组合把需要复用的对象作为成员变量持有而不是一直去继承。第二坑在构造方法里调用可重写的方法。父类构造方法执行时子类对象还没有完全初始化此时如果调用了被子类重写的方法可能会访问到子类中还没有赋值的字段结果读到 null 或默认值。这个坑在 Java 里很隐蔽编译器不会提示运行期才暴露。正确做法是构造方法里只调用 private/final 方法或者干脆只做初始化。第三坑没有把父类设计为可继承。Effective Java 里有一条规则如果类不是为了被继承而设计就应当设计成 final。因为默认继承会让私有构造、受保护钩子等设计意图被破坏。平时写业务代码不必过于教条但自己想要复用逻辑时优先考虑组合工具类而不是见到重复代码就抽一个父类出来。4. 多态为什么说它是面向对象的灵魂4.1 一个引用两种类型多态最直观的表现就是父类引用指向子类对象。你写代码时用的是父类类型但运行时的实际对象是子类类型调用相同的方法会执行子类重写后的逻辑。Animal animal new Dog(); animal.speak();这里animal的编译时类型是 Animal运行时类型是 Dog。animal.speak()调用的不是 Animal 里实现的版本而是 Dog 里重写后的版本。这就是“编译看左边运行看右边”。很多人学多态时死记这句话不理解为什么这么设计。其实多态解决的核心问题是对扩展开放你可以写一个方法参数是 Animal然后在完全不改这个方法的情况下往里传任何 Animal 的子类。新增一个 Cat、一个 Bird这个方法照样工作这就是面向接口编程的底层逻辑。4.2 向上转型与向下转型的边界向上转型是安全的因为子类一定具备父类的所有公共能力把它当成父类看待调用父类定义的方法一定没问题。但向上转型后你会失去子类特有的方法比如 Dog 的 fetch()Animal 引用调不到。如果真的需要调用子类特有方法只能向下转型if (animal instanceof Dog) { Dog dog (Dog) animal; dog.fetch(); }这里必须要先判断instanceof否则直接强转如果 animal 实际是 Cat运行时会抛ClassCastException。从 Java 16 开始可以用instanceof的模式匹配语法简化这个代码if (animal instanceof Dog dog) { dog.fetch(); }这种语法能把类型判断和变量声明合并到一行代码更清爽。不过要注意模式匹配里的变量作用域在 if 块内退出 if 后就不能用了。这是新语法和旧写法的显著区别面试时提到 Java 新特性可以顺便展示一下。4.3 多态的实际应用参数多态和返回多态多态在真实代码里最常见的应用方式是把父类或接口作为方法参数、方法返回值的类型。比如一个全局的日志策略接口可以注入不同实现public interface MessageSender { void send(String message); } public class SmsSender implements MessageSender { Override public void send(String message) { System.out.println(发送短信: message); } } public class EmailSender implements MessageSender { Override public void send(String message) { System.out.println(发送邮件: message); } } public class NotificationService { private final MessageSender sender; public NotificationService(MessageSender sender) { this.sender sender; } public void notify(String message) { sender.send(message); } }当你在 main 方法里传入不同 sender整个通知逻辑完全复用不需要写 if-else 分支去判断应该发短信还是发邮件。这就是多态的巨大价值策略选择被放到了调用方业务核心代码不再需要频繁修改。我自己在重构老项目时经常把一堆if (type 1) ... else if (type 2) ...的逻辑抽成这种策略接口再配合简单工厂或者枚举去选择实现类。改完之后每个分支的代码量少了一半测试也好写多了。这就是多态从“概念”变成“生产力”的过程。5. 抽象类和接口从设计层面做选择5.1 抽象类把公共骨架抽出来抽象类用 abstract 修饰它不能被实例化但可以被继承。抽象类最有价值的地方是它可以包含已经实现的方法也可以包含只有声明、没有实现的抽象方法。子类继承抽象类时要么实现所有抽象方法要么自己也声明为抽象的。我在设计一些模板流程时特别喜欢用抽象类。比如一个数据导入模板public abstract class BaseDataImporter { public final void importData(String filePath) { ListString lines readFile(filePath); validate(lines); ListDataItem items parse(lines); save(items); } protected ListString readFile(String filePath) { // 公共的文件读取逻辑 } protected abstract void validate(ListString lines); protected abstract ListDataItem parse(ListString lines); protected void save(ListDataItem items) { // 公共的保存逻辑 } }这里模板方法importData定义了整个流程骨架固定的步骤比如读取文件、保存数据在父类实现可变的步骤比如校验逻辑、解析逻辑交给子类去实现。这种设计叫模板方法模式是抽象类非常典型的应用场景。子类只需要关注自己的差异化逻辑公共步骤完全复用父类。5.2 接口从能力约定到默认方法接口在早期 Java 里的定义是“纯抽象约定”只能有公开抽象方法和常量。后来从 Java 8 开始接口里可以写静态方法和 default 方法Java 9 又允许写 private 方法。这让接口从“能力契约”变成了一种“带默认实现的能力约定”。接口最大的优势是支持多实现。一个类只能继承一个抽象类但可以实现多个接口。比如一个机器人既可以实现Walkable接口又可以实现Speakable接口类的功能可以按能力维度自由组合。public interface Walkable { void walk(); } public interface Speakable { void speak(); } public class Robot implements Walkable, Speakable { Override public void walk() { System.out.println(机器人走路); } Override public void speak() { System.out.println(机器人说话); } }在实际项目里接口往往拿来定义模块之间的通信边界。比如 Maven 多模块工程中核心业务模块定义一个 Repository 接口数据访问层提供具体的实现类上层业务代码只依赖接口不依赖具体实现类。这样做的好处是将来换数据库实现时上层代码不需要改动。5.3 抽象类 vs 接口一张表说清楚我经常被问到“什么时候用抽象类什么时候用接口”这里用一张表直接总结对比维度抽象类接口关键字abstract classinterface继承/实现数量单继承多实现成员变量可以有各种访问修饰符的字段默认 public static final 常量方法类型可以有具体方法、抽象方法、静态方法默认抽象方法可以有 default、static、private 方法构造方法可以有构造方法但用于子类初始化不能有构造方法设计目的复用共性代码定义模板骨架定义能力契约支持行为组合我的选择原则很简单如果是“A 是一种 B并且这些类之间共享很多具体实现”用抽象类如果只是“A 有某些能力具体怎么做不关心”用接口。比如猫和狗都是动物它们共享很多生理属性可以用抽象类 Animal而“会飞”是一种能力不该让蝙蝠和鸟都去继承一个飞机类所以用接口 Flyable 更合适。很多设计原则比如“面向接口编程”本质上是希望你在模块之间用接口来解耦而不是处处依赖具体类。接口有点像一个插座的规格只要符合规格哪个厂商的插头都能插进来替换起来也方便。6. 常用类那些天天在写的类6.1 String不可变与字符串池String 应该是 Java 里使用频率最高、也最容易被低估的类。它被设计成 final 不可变这意味着一旦创建就不能修改。每次做字符串拼接本质都是创建新字符串对象旧对象等着被 GC。所以循环里做大量字符串拼接时用 String 会非常浪费正确做法是用 StringBuilder 或 StringBuffer。String s ; for (int i 0; i 10000; i) { s i; // 不推荐每次循环都产生新对象 }改成这样StringBuilder sb new StringBuilder(); for (int i 0; i 10000; i) { sb.append(i); } String result sb.toString();这里有个性能细节StringBuilder 是线程不安全的但速度快StringBuffer 加了 synchronized线程安全但慢。绝大多数场景我们是在方法局部拼接字符串不存在线程竞争直接用 StringBuilder 就够了。我见过有人为了“稳妥”在每个场景都用 StringBuffer反而白白损失性能。字符串常量池也是面试常客。直接字面量赋值会在编译期把字符串放进常量池而通过 new 创建的字符串是运行时在堆上新建对象。比较字符串时永远不要用要用equals。我记得有次帮同事排查 bug他用了比较两个内容一样的字符串在某种场景下结果时对时错就是因为一个来自常量池、一个来自堆。知道了这个底层机制你就明白为什么规范里反复强调字符串比较必须用 equals。6.2 包装类缓存和拆装箱的隐藏开销Java 的八种基本类型都有对应的包装类Integer、Long、Boolean 等。它们存在的意义是让基本类型也能作为对象使用比如放入集合、参与泛型。从 Java 5 开始有了自动装箱和拆箱代码写起来方便了但隐藏的性能问题也随之而来。比较典型的坑是 Integer 的缓存机制。默认情况下Integer 会缓存 -128 到 127 之间的对象也就是说在这个范围内用Integer a 100; Integer b 100; a b会返回 true一旦超出 127相等比较就可能返回 false。很多人第一次被面试官用这个例子问懵就是因为不知道包装类有缓存机制。正确做法仍然是凡是对象比较一律用 equals值比较出结果之后如果非要判断相等再细究缓存的范围。另一个坑是循环里反复拆装箱。比如写Long sum 0L; for (long i 0; i 100000; i) { sum i; }每次加法都会把一个 long 装箱成 Long循环结束可能创建十万个 Long 对象。这种代码在低数据量时没感觉一旦上到千万级GC 压力会非常明显。所以涉及高频数值运算时优先用基本类型不要让包装类参与计算。6.3 日期时间从 Date 到 LocalDateTime老项目里到处都是 java.util.Date 和 java.sql.Date这两个类设计得很粗糙月份从 0 开始计数而且非线程安全。Java 8 引入了 java.time 包才算是真正好用的日期时间 API。如果你还在新代码里用 SimpleDateFormat建议尽快改成 DateTimeFormatter。LocalDate today LocalDate.now(); LocalDateTime now LocalDateTime.now(); DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); String formatted now.format(formatter); LocalDateTime parsed LocalDateTime.parse(2025-01-01 12:30:00, formatter);LocalDateTime 的线程安全是它最大的优点因为它是不可变对象每次操作都会返回新的实例。SimpleDateFormat 则相反它是可变且线程不安全的在多线程环境下共享一个实例会出现解析错乱。我之前在线上就遇到过因为并发调用同一个 SimpleDateFormat 导致的日期解析异常排查了很久才定位到是线程安全问题。还有时间戳和格式化不同场景要用不同类型LocalDate 只管日期LocalDateTime 含日期和时间Instant 适合做时间戳、跨时区计算。如果把带时区的需求搞混就容易出现用户看到的时间比服务器时间差几个小时的怪问题。建议写日期处理时把所有时间都统一存成 UTC展示时再转本地时区这样最能避免混乱。7. 异常别让错误静默发生7.1 异常体系受检与非受检Java 把异常分成两类受检异常checked exception和不受检异常unchecked exception。受检异常是编译期强制要求处理的异常比如 IOException、SQLException不受检异常包括 RuntimeException 及其子类比如 NullPointerException、IllegalArgumentException编译期不强制处理。受检异常的设计初衷是逼迫程序员重视可能发生的异常状态。但也正因为它强制很多初学者的第一反应是全用try-catch包住然后把异常吞掉。我最怕看到这种代码try { Files.readAllBytes(path); } catch (IOException e) { // 什么都不做 }这种代码等于告诉后来人这里可能出问题但我无所谓程序会继续跑。往往最后线上出 bug查日志时发现关键信息早被吞了根本无从下手。正确处理方式要么是向上抛出让上一级统一处理要么至少要记录日志。哪怕只是log.error(读取文件失败, e)也比什么都不做强得多。7.2 try-catch-finally 和 try-with-resources管理资源时经典的写法是 try-catch-finallyfinally 块负责释放资源比如关闭流。但 finally 里如果写的关闭代码再抛异常就会覆盖原始异常导致真正的问题被掩盖。Java 7 开始提供的 try-with-resources 语法能很好地解决这个问题只要资源实现了 AutoCloseable 接口就可以在 try 块结束时自动关闭。try (InputStream in new FileInputStream(file.txt); OutputStream out new FileOutputStream(out.txt)) { byte[] buffer new byte[1024]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } } catch (IOException e) { log.error(文件复制失败, e); }这种写法不仅代码简洁还自动处理了关闭顺序和异常抑制逻辑。我在代码评审里经常要求新写的资源相关代码一律使用 try-with-resources禁止手工在 finally 里关闭。这是可读性和安全性的双重提升。还有一点必须提醒catch 块里的异常参数在 Java 7 之前是隐式 final无法重新赋值现在可以直接用多异常捕获比如catch (IOException | SQLException e)但注意这些异常类型不能有父子类关系否则编译器直接报错。这一点在面试中偶尔会问平时编码也容易踩。7.3 自定义异常与日志规范实际业务里光靠 JDK 自带的异常类型往往不够通常需要自定义异常来表达业务错误。自定义异常比较简单继承 Exception受检或 RuntimeException非受检都可以。我的建议是业务异常尽量继承 RuntimeException好处是方法签名里不用强制声明调用方也不会因为处理机制复杂而被迫写一堆没意义的 catch。public class OrderNotFoundException extends RuntimeException { public OrderNotFoundException(String message) { super(message); } }定义异常时最好带有清晰的错误码和上下文信息。比如抛出异常时把订单号、用户ID这些关键信息塞进 message 里排查问题时会非常省力。千万不要只写一个空 message那样日志里只有个异常类型等于没有信息。最后说一个和异常相关的实践经验异常处理要遵循“谁处理谁负责”的原则不要在每层代码里重复 catch。如果底层代码已经处理并打了日志上层业务就不需要再 catch 一遍否则日志会重复刷屏排查问题时反而分不清最初的异常源头。正确的做法是在系统的入口处或框架层统一处理异常业务代码里绝大多数地方只需要关心那些真正能恢复或者需要特殊处理的场景。这个原则做到位了线上日志会干净很多告警也会准确很多。个人经验来看把基础概念学扎实最好的方式不是背题而是拿今天讲的这些知识点去盘点自己写过的代码哪些地方其实该用接口却用了具体类哪些地方继承层次太深该改成组合哪些异常不该被吞掉。每次重构一点慢慢就会有手感。Java 基础看起来散其实底层是一套自洽的工程哲学理解了设计动机代码自然就越写越顺。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

AI日报搭建实战:从信息源筛选到深度拆解的全流程指南 2026/9/28 15:52:08

AI日报搭建实战:从信息源筛选到深度拆解的全流程指南

1. 从“每日流水账”到“决策情报站”:一份AI日报的价值重塑先别急着往下翻,我想先跟你聊聊一个挺反常识的观察:在信息爆炸到人人喊“AI疲劳”的今天,我反而觉得,一份高质量AI日报的价值,比三年前重要了不止…

阅读更多 →
个人开发者LLM领域适配实战:从继续预训练到RAG部署 2026/9/28 15:52:08

个人开发者LLM领域适配实战:从继续预训练到RAG部署

做这行的时间长了会发现,很多人一提到“预训练语言模型”就自动把它和“几千张显卡、几百亿参数”绑定在一起,觉得这跟个人开发者毫无关系。但实际情况完全不是这样。开源生态成熟之后,个人开发者完全有能力走通一条从预训练到领域适配的完整…

阅读更多 →
STM32低成本音频播报方案:用PWM加RC滤波实现DAC输出 2026/9/28 15:52:08

STM32低成本音频播报方案:用PWM加RC滤波实现DAC输出

想给STM32加个音频播报功能,第一反应是用芯片自带的DAC。结果一查手册,手上这块F103C8T6只有一个12位DAC,两路输出还分别占用PA4和PA5,如果这两根引脚已经被其他外设占用了,就得另想办法。后来试了用PWM加RC滤波当DAC用…

阅读更多 →
Python虚假新闻检测:BERT向量融合LightGBM与CatBoost的混合模型实战 2026/9/28 15:52:08

Python虚假新闻检测:BERT向量融合LightGBM与CatBoost的混合模型实战

简介:基于Python搭建的多模态虚假新闻检测项目,融合文本与图像特征对新闻真实性进行自动识别,面向计算机、人工智能、通信工程、自动化等专业的高校学生和开发者。资源可在毕设答辩、课程设计、项目初期演示中直接使用,也适合作为…

阅读更多 →
斯坦福CS224R深度强化学习跟课指南与PyTorch实战 2026/9/28 15:52:02

斯坦福CS224R深度强化学习跟课指南与PyTorch实战

最近很多读者在问我同一个问题:斯坦福 CS224R 深度强化学习(Deep Reinforcement Learning)这门课到底该怎么跟?尤其是 2025 春季学期的视频资源陆续放出后,标题里大多带着“英文原声|中英字幕”这样的说明。…

阅读更多 →
60个工具下Agent挑花眼?工具路由与动态检索三招解决 2026/9/28 15:52:02

60个工具下Agent挑花眼?工具路由与动态检索三招解决

六十个工具堆在 Agent 面前的时候,问题不是它“不知道选哪个”,而是它开始乱选、反复横跳、甚至干脆不干活。这段时间我在折腾一个内部办公助手,把各类接口从 PDF 处理、表格解析、定时任务、图片压缩到会议纪要全挂上去,前前后后…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉