恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Java入门:从String比较到三大特性,搞懂内存与对象才算会写代码

  • 首页
  • 资讯中心
  • /
  • Java入门:从String比较到三大特性,搞懂内存与对象才算会写代码

相关资讯

为什么@Mapper标在接口而@Service标在实现类?底层机制解析 2026/10/12 21:25:21
MGRE与RIP实验排错:NHRP注册、水平分割与Spoke互通全解析 2026/10/12 21:25:21
高校学生管理系统课程设计:Delphi+SQL Server实现与避坑指南 2026/10/12 21:25:21

最新资讯

MCP Server On FC 之旅第四站:长连接闲置计费最高降低87%成本的技术内幕
大模型 Agent 变天了!OpenClaw 创始人实测 32 款 LLM:最贵的竟然不是最能干的?
STM32F103+ESP8266+机智云:物联网节点从零搭建与避坑指南
AI Agent如何从手机屏幕走向可穿戴硬件:Muse SDK实践指南
ESP32 AP+STA混合模式实战:Web控制服务器搭建与避坑指南
京东淘宝竞品分析全解:从商业模式到运营策略

今日推荐

Debian新手入门:从部署到日常操作的完整指南
MongoDB复制集扩缩容实战:从rs.add到选主事故复盘
条形码目标检测数据集实战:从YOLOv8训练到部署

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Java入门:从String比较到三大特性,搞懂内存与对象才算会写代码

发布时间:2026/10/12 21:25:21
Java入门:从String比较到三大特性,搞懂内存与对象才算会写代码 新手学Java几乎每个人都会在“String怎么比较相等”这道题上报废一轮血。然后紧接着就是类与对象、各种关键字、封装继承多态。知识本身不难难的是每个概念都散落在一堆教程里看得懂但合上书写不出能跑的代码。我自己带过不少刚入行的同事包括看他们的代码和面试反馈发现所有人卡住的点高度一致不是语法没背住而是没把“内存模型”“对象生命周期”这些底层的东西和日常编码连接起来。这篇就拿String、类与对象、关键字、三大特性这几个最基础、最常考、也最容易被忽略细节的知识点把每一块讲透带上面试里常问的变体尽量做到看完能直接用到项目里。1. String的不可变性初学者的第一个大坑1.1 字符串常量池new String(hello)到底创建了几个对象String在Java里为什么叫“引用类型”又为什么类比基础类型更好用一个最常见的坑来自和equals混用。先看最简单的代码String s1 hello; String s2 hello; System.out.println(s1 s2); // true System.out.println(s1.equals(s2)); // true String s3 new String(hello); String s4 new String(hello); System.out.println(s3 s4); // false System.out.println(s3.equals(s4)); // trues1 s2的结果是true很多人解释成“Java对相同字符串做了缓存”这个说法方向对但不够准确。准确的原因是字符串常量池String Pool。JVM在启动时会在堆内部的字符串常量池里维护一个表程序里写的所有字面量字符串在首次遇到时会被放到池中。之后再有同样内容的字面量出现直接复用同一个对象引用。所以s1和s2指向的是同一个池内对象自然成立。new String(hello)的情况完全不同。new强制在堆上新建一个对象s3和s4是两个不同的对象它们的引用值不同比较的是引用地址结果就是false。那么同一条语句里“hello”这个字面量会在池里存一份吗会。可以理解为new String(hello)做了两件事把hello放入常量池如果还没有然后在堆里额外创建一个新的String对象。这也是面试中常问“String str new String(abc)产生了几个对象”的来源一般回答一个或两个取决于池中是否已有abc。这个知识点不是背结论而是帮你想清楚引用和对象的区别。1.2 equals与判断字符串相等的正确姿势日常开发里我们真正关心的几乎都是“内容是否相同”而不是“是不是同一个对象”。所以字符串比较一律推荐equals。但equals也有需要注意的地方如果前面的字符串可能为null那么调用equals会抛空指针异常。String target null; if (target.equals(hello)) { // 空指针程序直接崩 }安全的写法是把常量放前面if (hello.equals(target)) { }因为字面量不会为null这种写法天然避免NPE。再进一步当你需要忽略大小写时用equalsIgnoreCase这个API在业务里常用于验证码、邮箱前缀等场景。此外并不是完全不能用如果你能确认两个变量都指向同一个池对象或同一个对象比如比较枚举常量、比较某些单例引用反而比equals更高效因为直接比较地址省去了方法调用。但业务中“能确认同一引用”的场景不多加锁、缓存、单例复用时才会这么干。新手不要为了性能牺牲正确性默认用equals。1.3 String拼接的性能账为什么循环里不要用字符串用拼接时Java编译器会优化成StringBuilder.append。但如果在循环里反复做拼接每次循环都会生成新的StringBuilder性能开销被放大。假设循环一万次你等于创建了一万个临时对象GC压力很大。String result ; for (int i 0; i 10000; i) { result i; // 每次循环都新建StringBuilder和String }正确写法StringBuilder sb new StringBuilder(); for (int i 0; i 10000; i) { sb.append(i); } String result sb.toString();StringBuilder是线程不安全的单线程场景下最快StringBuffer是线程安全的内部方法都加了synchronized多线程环境下才用它。绝大多数业务方法里不涉及多线程同时修改同一个拼接对象所以默认选StringBuilder。还有一个细节如果你已经知道大概长度可以给StringBuilder指定初始容量例如new StringBuilder(1024)减少扩容时的数组拷贝。注意String本身有一个intern()方法可以把堆上的String对象手动加入常量池并返回池中的引用。这个方法的典型用途是在大量重复字符串的场景下节省内存比如读入几十万条记录其中某一列只有几百种取值。但intern用不好会拖慢性能因为池是哈希结构太多字符串会加大哈希冲突。除非你做过内存对比分析否则不要为了炫技去用intern。2. 类与对象把内存套到代码上2.1 new一个对象时栈、堆、常量池各自发生了什么类与对象的本质是“模板”与“实例”的关系。类描述属性字段和行为方法对象是类的具体体现。但很多初学者写类的时候并不清楚自己new出来的对象到底放在哪里。以这个User类为例public class User { String name; int age; }执行User u new User()时内存层面发生三件事栈上创建引用变量u它是一个局部变量保存的是堆中对象的地址。堆上分配一块内存存放User对象内部有两个字段name引用类型的 null 默认值、age基本类型int的0默认值。如果给name赋值为字面量张三那么张三这个字符串对象在常量池里对象的name字段只是存了一个池内引用。理解这一点很有用。比较两个User对象时如果用比较的是栈上引用的地址两个new出来的对象地址必然不同。要做内容比较就得自己重写equals。我见过不少同事把两个内容完全一样的实体类用比较查了半天查不出问题最后发现是比较地址而不是内容。再有一点局部变量与成员变量的默认值也不同。类成员变量有默认值int是0、boolean是false、引用类型是null但方法内的局部变量不赋值直接用会编译失败因为编译器要求你必须显式初始化。这个细节也常出现在笔试里提供给初学阶段容易混淆的人一条清晰的判断标准。2.2 参数传递Java到底是不是“纯传值”这个问题面试出现频率极高。结论Java只有值传递没有引用传递。但引用类型传的“值”是对象地址所以方法内部修改对象属性外部能看到变化。public void changeAge(User user) { user.setAge(30); // 外部对象被修改 user new User(); // 换一个新的引用外部不受影响 }调用changeAge(u)后外部u.getAge()变成30但u本身仍然是原来那个对象没有变成方法里新建的User。原因user是栈上新建的局部变量拷贝了外部u的地址通过这个地址可以操作原对象但把user重新赋值只是让局部变量指向了新对象没有改外部u的指向。写代码最容易犯的错就是把参数对象直接替换掉而不是修改其内容。比如在方法里写obj new ...然后抱怨外层没变。理解了值传递这类问题秒破。常见的安全策略是如果方法需要返回变化后的对象就通过返回值传出去不要指望修改形参引用。这种思路在写工具类、DTO转换时特别重要。2.3 static、final、this、super这些关键字不是装饰品回到关键字。static表示“属于类而不属于某个对象”。静态字段所有实例共享一份适合存类级别的常量、连接池实例、工具方法等。static方法里不能直接访问实例字段因为它没有this指向任何具体对象。这个限制可以帮你在设计时想清楚一个方法如果没有用到实例数据就应该声明为static例如Math.max、Collections.sort这类纯工具逻辑。final有三个常见用法修饰变量表示值不可变、修饰方法表示子类不能重写、修饰类表示不能被继承。其中最容易忽略的是修饰引用类型的含义final User u new User()你不能再把u指向另一个User对象但可以u.setName(...)修改内部属性。这里的“不可变”指的是引用地址不能变对象本身可以变。把这一点搞错的人不少。this指代当前对象主要用来区分局部变量和字段典型的构造器赋值场景this.name name也可以用来在一个构造器里调用另一个构造器public User() { this(默认名, 0); }super则指向父类对象。它在子类构造器里调用父类构造器是隐式完成的如果你不写super()编译器会在子类构造器第一行自动插入。但父类若没有无参构造器编译就直接报错这时候必须显式写super(参数)。注意this(...)和super(...)调用构造器时两者不能同时出现都必须放在构造器第一行。关于this和super再补充一点在方法内部this可以被正常使用但super不能用于父类私有字段的访问因为私有字段在子类里本来就不具备直接访问权。3. 封装private不只是“不让动”3.1 数据校验这才是封装的真正价值很多教材把封装解释成“属性私有化提供getter/setter”这就让学习者以为封装是语法规定。其实封装的本质是隐藏内部实现细节统一对外提供可信赖的操作入口。最简单的例子是年龄字段public class User { private int age; public void setAge(int age) { if (age 0 || age 150) { throw new IllegalArgumentException(年龄不合法); } this.age age; } }字段是public时外部可以直接写成user.age -5系统里出现脏数据还查不到是谁改的。通过private加setter外部只能经由校验逻辑赋值非法数据直接抛出异常或拒绝写入。这样就把数据安全性收在了可控范围内。我在实际项目里见过因为实体字段直接暴露前端传了一个负数进来后面的统计报表整整乱了一周的情况。如果当时有这层封装问题根本不会发生。3.2 怎么设计对外方法面向调用者的思维封装不只是“字段private getter/setter”还涉及业务方法的暴露粒度。假设有个订单类内部方法calculateTax()和applyDiscount()都是计算折扣与税费的实现细节主流程checkout()需要依次调用它们。如果三个方法都设为public调用方就可能跳过主流程单独调用applyDiscount()造成业务逻辑被绕过。正确的做法是只有checkout()设为public其余两个方法设为private。外部看到的只是一个“下单”入口内部怎么计算是类的自由。未来改计算逻辑不影响外部调用。这样达到“修改内部不影响调用方”的效果。所以封装的设计思路可以从“这个类对外承诺什么能力”出发而不是“把所有字段都暴露成getter/setter”。如果一个字段只在类内部使用连getter都不需要暴露。看代码时的一个参考标准是某个类public方法如果超过七八个就要检查它是否在承担超过一个职责。职责过多意味着封装失败需要拆分。3.3 包级私有与访问修饰符的选择Java提供四种访问控制级别public、protected、default包级私有、private。其中protected是初学时容易踩迷糊的难点它允许同包下的任意类访问也允许不同包下的子类访问。这意味着如果你所在包外的某个类继承了这个类就能通过继承关系访问protected成员但不是该包内的普通类也能直接访问。不要在写工具类时滥用protected默认能用private就用private需要给子类扩展预留能力时才用protected。这是一个实践中非常重要的选择。判断方式可以记成能见度扩散的顺序是private → default → protected → public越往右传播范围越大。不必要地扩大访问修饰符长期看是埋雷行为。4. 继承代码复用的甜头和代价4.1 构造器链为什么子类对象创建时会先执行父类构造器继承让子类自动拥有父类的非私有成员这确实能省很多重复代码。但同步地也带来了一个隐蔽点——初始化顺序。看下面的代码class Animal { Animal() { System.out.println(Animal构造器); } } class Dog extends Animal { Dog() { System.out.println(Dog构造器); } }执行new Dog()输出顺序是“Animal构造器”再“Dog构造器”。这是Java强制规定的构造子类对象时必须先完成父类部分的初始化因为子类对象内部“含着”一个父类子对象。编译器在子类构造器的第一行自动加了super()。如果父类没有无参构造器就必须显式调用父类的有参构造器class Animal { Animal(String name) { System.out.println(name); } } class Dog extends Animal { Dog(String name) { super(name); // 不写这一行编译直接报错 } }继承体系里还有一个初始化顺序问题父类静态代码块 → 子类静态代码块 → 父类实例代码块/构造器 → 子类实例代码块/构造器。执行一次new Dog()就能看到完整轨迹。这个顺序在面试里也很常问明白了构造器链的由来基本能推理出来不用死记。4.2 重写Override的规则比想象中更严格子类重写父类方法时有四个限制容易忽略方法名、参数列表必须完全一致。参数变了是重载不是重写。返回类型可以相同也可以是父类返回类型的子类型协变返回。例如父类返回Animal子类可以返回Dog。子类方法的访问权限不能比父类更严格。父类是public子类就不能改为private。子类方法不能抛出比父类更宽泛的受检异常。为什么不能把public重写成private因为调用方可能是透过父类引用调用的如果子类把方法偷偷改成私有运行时会破坏多态调用的规则。这条限制是JVM为了保障“面向父类编程”能成立。还要额外注意重写时要不要调用super.xxx()。如果重写的是业务方法建议在开头保留原有逻辑再扩展。如果重写的是一个生命周期回调方法如框架里的init()不调用父类版本可能导致状态没初始化完成。写完后强烈建议加上Override注解它不只是注释编译器会帮你校验是否真的重写了父类方法。如果方法签名写错又没有Override代码会当成重载静默通过运行行为完全不符合预期这种bug特别难查。4.3 继承不是万能的组合优先于继承新手尝到继承的好处后容易什么都不想用继承这是另一个极端。继承最大的风险是“脆弱的基类问题”父类一改所有子类行为都受影响。比如订单和退货单都可以继承订单基类但如果退货单不允许某些字段修改而基类的修改逻辑又写在公共方法里子类就不得不用重写去“纠正”代码越来越拧巴。经验法则是只有当子类和父类存在明确“is-a”关系且子类确实继承了父类的大部分行为而不需要大量禁用父类方法时才使用继承。否则用组合Composition一个类持有另一个类的引用通过调用来复用能力。比如一个PaymentService不一定继承Logger而是内部持有Logger对象需要记日志时调用其方法。组合的耦合度更低后续维护也更容易。很多大型项目长期演进后都会把过深的继承体系重构为组合结构这与可读性关系不大主要在于改动时的影响面更容易控制。5. 多态动态绑定是Java最值钱的机制5.1 向上转型写一次代码处理多种对象多态最经典的应用场景是面向父类编程。假设我们有父类Animal和子类Dog、Catpublic void makeSound(Animal a) { a.sound(); }传Dog、传Cat都可以。方法内部不关心具体是哪种动物只要它有sound()行为。运行时JVM会根据实际对象类型去调用对应子类重写后的sound()。这就是动态绑定方法调用的目标是在运行期确定的而不是编译期的修饰符类型。很多人背过这句话但理解不透本质上可以拆成两个层面编译期编译器只看变量声明的类型能确认sound()方法存在运行期JVM则根据堆里对象的真实类型选择方法版本。因此多态成立的前提是必须有方法重写并且通过父类引用调用。如果子类没有重写父类方法那就执行父类实现。重载不参与动态绑定它是在编译期就决定好调用哪个版本。实际业务里这种模式大量应用。比如一个消息推送系统定义PushStrategy接口短信、邮件、App推送各自实现后续新增加微信推送时只需要新增实现类不需要改动主流程代码。这就是多态带来的扩展性。5.2 重载Overload与重写Override区分核心要看“方法签名”重载要求方法名相同、参数列表不同类型、个数或顺序不同返回类型不参与判断。两个方法如果只有返回类型不同不能构成重载会编译报错。重载是编译期的静态分派编译器根据实参类型确定调哪个。重写的核心则是方法签名完全一致而且发生在继承体系中。区分它们最简单的方式看方法签名是否相同签名相同且在父子类间则是重写签名不同、在同一类或父子类间都是重载。千万不要在重写时把参数列表改了还期待子类行为生效。比如Override public boolean equals(User other) { // 这不是重写Object的equals因为参数类型是User }这只是参数为User的重载方法。写这个方法的人想比较两个User但容器调用的是Object.equals(Object)版本于是永远不会进入这个逻辑调用方还一脸懵。手动重写equals时参数类型必须写Object。5.3 向下转型的安全姿势先instanceof再强转向上转型丢失了子类特有方法比如Dog有fetch()Animal引用调不到。这时需要向下转型恢复成Dog类型Animal a new Dog(); if (a instanceof Dog) { Dog d (Dog) a; d.fetch(); }instanceof就是用来做类型检查的运算符。不安全的强转直接抛ClassCastException典型的报错场景是把一个Cat实例强转成Dog。在新版Java里instanceof功能也增强了支持模式匹配比如if (a instanceof Dog d) { d.fetch(); }省掉一次显式强转。instanceof左边为null时返回false而不是异常可以顺手防NPE。有一点要提醒在继承分支多的场景里大量用instanceof和向下转型是代码坏味道的信号说明该引入多态了。比如你写if (x instanceof Dog) ... else if (x instanceof Cat) ...完全可以改成在父类里声明抽象方法每个子类各自实现外部调用时多态就自动分发不需要手动分支。新手学多态最大的进步就是逐渐减少代码里的instanceof把类型判断交给JVM。6. 综合案例用封装、继承、多态重构一段业务代码最后把这些内容串成一个能跑的实战案例。假设现在有一个员工薪资计算模块原始代码可能是这样的public double calculateSalary(Employee e) { if (e.getType() 1) { // 普通员工 return e.getBaseSalary(); } else if (e.getType() 2) { // 经理 return e.getBaseSalary() e.getBonus(); } }这种写法有三个问题type类型字段让不可靠的魔法数字出现每新增一种员工类型都要改这个方法bonus这种子类专属字段被塞到基类里不符合封装。现在用三大特性重写。先定义一个抽象基类Employee封装通用字段public abstract class Employee { private String name; private double baseSalary; public Employee(String name, double baseSalary) { this.name name; this.baseSalary baseSalary; } public abstract double calculateSalary(); protected double getBaseSalary() { return baseSalary; } }两个子类继承并实现各自薪资逻辑public class Developer extends Employee { private int projectBonus; public Developer(String name, double baseSalary, int projectBonus) { super(name, baseSalary); this.projectBonus projectBonus; } Override public double calculateSalary() { return getBaseSalary() projectBonus; } } public class Manager extends Employee { private double bonusRate; public Manager(String name, double baseSalary, double bonusRate) { super(name, baseSalary); this.bonusRate bonusRate; } Override public double calculateSalary() { return getBaseSalary() * (1 bonusRate); } }主流程完全不关心类型public void printSalary(Employee e) { System.out.println(e.calculateSalary()); }新增岗位类型时只需要增加一个Employee的子类不用改动printSalary一行代码。这就是多态。baseSalary保持private子类通过protected的getBaseSalary()获取这是封装配合继承的典型用法。abstract方法强制所有子类必须实现薪资计算避免忘记重写。整个结构相比type判断的版本扩展性、可读性和可维护性都明显提升。回头看最开始的String、类与对象、关键字、封装继承多态这些知识点其实最终都指向同一个核心理解对象的生命周期、理解每个修饰语带来的边界、理解方法分发发生在运行期。掌握了这些很多“为什么这样写”的问题就能自己推导出答案。基础阶段的代码量不需要很大但每写一段都值得多问自己一句这段代码里对象是怎么创建的方法调用在哪个阶段被决定字段的可见边界是否合理带着这些问题敲代码比刷三遍教程都有用。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号