资讯动态

Java类与对象深度解析:内存图、封装与构造方法实战

发布时间:2026/10/15 10:16:50 来源:尧图企业网站定制
1. 从写个类到理解对象为什么这块骨头必须啃下来刚学Java那会儿我一度觉得类和对象这章特别虚。语法就那几行class、new、.敲一遍就会了。可真正开始写稍微像样的代码时问题全冒出来了为什么我改了对象A的属性对象B也跟着变了为什么方法里改了参数外面没反应为什么有的变量能直接用有的必须加this这些问题的答案全都藏在类和对象这一章里。这篇内容就是围绕Java面向对象最基础、也最容易被轻视的一块——类与对象、对象内存图、成员变量与局部变量、private、this、封装、构造方法。它适合刚接触Java、语法能看懂但一到自己设计类就发懵的朋友也适合学了一阵子、回头发现自己对对象到底在内存里长什么样始终没搞明白的人。我不会只给你语法定义而是把每个知识点背后的为什么讲透尤其是对象内存图这块很多人跳过它结果后面学继承、多态、集合时一路磕磕绊绊。先说清楚这一章到底在解决什么问题。面向对象编程的核心思想是把数据和操作数据的方法打包在一起形成一个独立的东西这个东西就是对象。而类就是生产这个东西的图纸。你写一个Student类不是写了一个学生而是写了一张学生应该长什么样、能干什么的图纸new Student()才是真正造出了一个具体的学生。这个图纸与实物的区分是后面所有概念的根。我见过太多人卡在这里把类当成对象用把静态的东西和实例的东西混着写构造方法乱写导致对象状态不对。这些坑本质上都是对类加载—对象创建—内存分配这条链路没有画面感。所以这篇我会花大篇幅讲内存图让你脑子里能看见对象是怎么被造出来、怎么被引用的。把这块吃透后面学封装、继承、多态会顺很多。下面我按先建立画面感再抠语法细节最后落到封装思想的顺序展开。每一块我都会配上能直接跑的代码和实测中容易踩的坑你可以边看边敲。2. 类与对象图纸和实物到底差在哪2.1 类不是对象但很多人第一行代码就混了先看一段最朴素的代码public class Student { String name; int age; public void study() { System.out.println(name 正在学习); } }这个Student是类它描述了两件事学生有name和age两个属性数据有study这个行为方法。注意此刻内存里并没有任何一个具体的学生name和age也没有值。类更像是一份设计规范。真正造出对象是这样public class Test { public static void main(String[] args) { Student s1 new Student(); s1.name 小明; s1.age 18; s1.study(); Student s2 new Student(); s2.name 小红; s2.age 17; s2.study(); } }s1和s2是两个独立的对象各自有各自的name和age。改s1.name不会影响s2.name。这就是图纸一张实物多个的含义。我常跟新手打这个比方类是你家楼盘的户型图对象是按这张图盖出来的一户户具体的房子。户型图只有一份但房子可以盖很多套每套里住的人、摆的家具都不一样。你不可能住在户型图里所以也不能直接给类赋值——必须new出对象才能操作。2.2 成员变量和局部变量位置决定命运这是初学者最容易混淆的一对概念。判断标准其实很简单看它写在哪个大括号里。成员变量写在类里面、方法外面。它属于对象随对象创建而存在随对象被回收而消失。局部变量写在方法里面或方法的参数、代码块里。它只在方法执行期间存在方法一结束就没了。public class Student { String name; // 成员变量 int age; // 成员变量 public void study() { int hours 2; // 局部变量 System.out.println(name 学习了 hours 小时); } }区别不止在位置还有几个关键点我用表格列清楚对比项成员变量局部变量定义位置类中、方法外方法内或方法参数内存位置堆内存随对象栈内存随方法默认值有默认值引用nullint为0等没有默认值必须先赋值生命周期与对象同生共死方法调用期间作用域整个类中视修饰符所在方法/代码块内这里有个特别容易踩的坑局部变量没有默认值。你写int x; System.out.println(x);直接编译报错因为编译器不知道x是多少。但成员变量int age;不赋值也能用默认是0。很多人第一次遇到为什么这个能编译那个不能就是栽在这。还有一个隐蔽的坑变量重名时的就近原则。如果方法里有个局部变量和成员变量同名方法内直接用这个名字访问的是局部变量成员变量被遮蔽了。这时候想用成员变量就得靠this——这正是后面要讲的。2.3 对象内存图把看不见的东西画出来这是整章最值钱的部分。很多人学Java卡壳就是因为脑子里没有内存画面。我用一段代码带你一步步画。public class Test { public static void main(String[] args) { Student s new Student(); s.name 小明; s.age 18; System.out.println(s.name); } }执行过程拆解类加载程序启动时Student.class和Test.class被加载到方法区JDK8以后叫元空间里面存着类的结构信息、方法代码。main方法入栈main方法被调用在栈内存中开辟一块栈帧里面准备放局部变量args和s。new Student()在堆内存中开辟一块空间存放这个对象。对象里包含成员变量name和age此时它们有默认值name是nullage是0。同时对象头里会记录它属于哪个类指向方法区的Student类信息。赋值引用堆里这个对象的地址比如0x001被赋给栈里的变量s。所以s存的不是对象本身而是对象的地址我们叫它引用。s.name 小明通过s找到堆里的对象把name从null改成小明。s.age 18同理age改成18。println(s.name)通过s找到对象取出name打印。画成文字版内存图大概是这样栈内存 堆内存 -------- ------------------ | main | | Student对象 | | s ---|----------| name 小明 | -------- | age 18 | ------------------ | v 方法区Student类信息name、age、study理解这张图你就能回答开头那些问题了。比如为什么改s1不影响s2——因为它们是堆里两个不同的对象栈里两个不同的引用指向不同地址。为什么方法传参改不了外面——因为传的是引用的副本改副本指向不影响原引用这个后面讲封装时会再提。提示画内存图不要偷懒。初学阶段每写一个涉及对象的程序都拿张纸画一遍栈、堆、方法区。画上二三十个你就有内存直觉了后面学多态、集合、GC会轻松一大截。3. private、this与封装把对象保护起来3.1 为什么不能让人随便改我的数据接着上面的Student。现在name和age都是公开的谁都能写Student s new Student(); s.age -100; // 年龄写成负数程序也不拦着 s.name ; // 名字写成空串也没人管这就是问题所在。对象的数据应该由对象自己负责维护不能让外部随便乱改。age -100这种明显不合法的值类本身应该有能力拒绝。这就是封装要解决的事。封装的核心动作就两步用private把成员变量藏起来外部不能直接访问。提供public的get/set方法外部通过方法间接访问方法里可以加校验逻辑。public class Student { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name name; } public int getAge() { return age; } public void setAge(int age) { if (age 0 || age 150) { System.out.println(年龄不合法); return; } this.age age; } }现在外部再想s.age -100编译直接报错因为age是私有的。只能走setAge而setAge里有校验非法值进不来。这就是封装的价值把数据的控制权收回类自己手里。3.2 this到底指谁一个被讲烂但没讲透的关键字上面setName里出现了this.name name。为什么不能直接写name name因为方法参数也叫name方法体里直接写name按就近原则访问的是参数不是成员变量。name name就变成把参数赋给参数自己成员变量纹丝不动。加上this.明确告诉编译器我要的是这个对象的成员变量。this的本质是当前对象的引用。谁调用这个方法this就指向谁。Student s1 new Student(); s1.setName(小明); // 此时setName里的this就是s1 Student s2 new Student(); s2.setName(小红); // 此时setName里的this就是s2同一个方法被不同对象调用this指向不同对象所以能正确操作各自的数据。这就是this存在的意义。this还有两个常见用法在构造方法中调用本类其他构造方法this(...)必须写在构造方法第一行。返回当前对象return this;用于链式调用。public class Student { private String name; private int age; public Student() { this(无名, 0); // 调用下面的有参构造 } public Student(String name, int age) { this.name name; this.age age; } public Student setName(String name) { this.name name; return this; // 返回当前对象支持链式调用 } }链式调用写起来是这样new Student().setName(小明).setName(小红)。虽然实际项目里不一定这么用但理解return this能帮你彻底搞懂this就是当前对象。注意this(...)调用其他构造方法时必须是构造方法里的第一条语句前面不能有任何代码。这是Java的硬性规定违反了直接编译报错。我见过有人想在this(...)前面打印一行日志结果编译不过就是这个原因。3.3 封装不是加个get/set就完事很多人对封装的理解停留在私有化get/set这其实只学到了皮毛。真正的封装是隐藏实现细节只暴露必要的接口。举个例子一个BankAccount类public class BankAccount { private double balance; public void deposit(double amount) { if (amount 0) { throw new IllegalArgumentException(存款金额必须为正); } balance amount; } public void withdraw(double amount) { if (amount 0 || amount balance) { throw new IllegalArgumentException(取款金额不合法); } balance - amount; } public double getBalance() { return balance; } }注意这里没有setBalance。为什么因为余额不应该被外部直接设置只能通过存款、取款这两个业务动作来改变。如果给了setBalance外部就能凭空把余额改成任意值封装就形同虚设。这就是封装的精髓该暴露的暴露不该暴露的坚决不暴露。判断标准是这个操作从业务上讲合不合理而不是能不能生成get/set。IDE一键生成get/set很方便但方便不等于正确你得想清楚每个字段到底该不该有setter。4. 构造方法对象出生的那一刻发生了什么4.1 构造方法的三个硬性特征构造方法是对象被new出来时自动调用的特殊方法用来初始化对象。它有三个必须记住的特征方法名和类名完全相同。没有返回值类型连void都不能写。通过new自动调用不能像普通方法那样手动调用。public class Student { private String name; private int age; public Student() { System.out.println(无参构造被调用); } public Student(String name, int age) { this.name name; this.age age; } }这里有个新手必踩的坑如果你不写任何构造方法Java会自动送你一个无参构造。但只要你写了任意一个构造方法哪怕是有参的这个免费的无参构造就消失了。public class Student { private String name; public Student(String name) { // 只写了有参构造 this.name name; } } // 下面这行会编译报错因为无参构造已经不存在了 Student s new Student();这个坑非常隐蔽。很多人写着写着加了个有参构造结果原来new Student()的地方全报错了一脸懵。解决办法很简单只要你写了有参构造就顺手把无参构造也补上除非你确定外部不需要无参创建。4.2 构造方法的重载与this链式调用构造方法可以重载也就是同一个类里可以有多个参数列表不同的构造方法。这给了对象创建很大的灵活性。public class Student { private String name; private int age; private String school; public Student() { this(无名, 0, 未定); } public Student(String name) { this(name, 0, 未定); } public Student(String name, int age) { this(name, age, 未定); } public Student(String name, int age, String school) { this.name name; this.age age; this.school school; } }这种写法的好处是所有初始化逻辑最终都汇聚到参数最全的那个构造方法其他构造方法通过this(...)委托过去。这样以后要改初始化逻辑只改一处就行不用每个构造方法都改一遍。这是实际开发中非常推荐的写法。但要注意this(...)只能出现在构造方法第一行而且不能形成循环调用A调BB又调A否则编译报错。4.3 构造方法和setter的分工既然有了构造方法为什么还要setter反过来有了setter为什么还要构造方法这俩到底怎么分工我的经验是构造方法负责必须的、一次性的初始化。比如一个User对象id和username是创建时就该确定的放构造方法里。setter负责后续可变的属性。比如用户的nickname、email创建后可能随时改用setter。public class User { private final long id; // 创建后不可变 private final String username; private String nickname; // 可变 public User(long id, String username) { this.id id; this.username username; } public void setNickname(String nickname) { this.nickname nickname; } }注意id和username用了final配合构造方法赋值创建后就锁死了没有setter。这就是不可变对象的雏形在多线程场景下特别有价值。这个思路在后面学并发时会反复用到。提示判断一个字段该不该有setter问自己一句这个值在对象创建后还应该被改吗。如果答案是否定的就别给setter用final构造方法固定下来。这比无脑生成get/set要专业得多。5. 那些年我在这一章踩过的坑5.1 空指针对象还没造出来就急着用Student s null; System.out.println(s.name); // NullPointerExceptions是null意味着它不指向任何对象。你让一个空引用去访问成员JVM只能抛空指针。这个错误在初学阶段出现频率极高根源就是没搞清楚引用和对象是两回事。排查思路看到空指针先定位是哪一行然后问自己这个变量到底有没有被new过。常见场景包括数组元素没初始化、集合里取出的元素是null、方法返回了null没判断。养成习惯用引用之前先确认它不为null或者用Objects.requireNonNull做防御。5.2 成员变量和局部变量同名导致的诡异bugpublic class Student { private String name; public void setName(String name) { name name; // 忘了this成员变量根本没被赋值 } }这段代码编译能过但setName完全没起作用。因为name name操作的是参数自己。这种bug特别难查因为不报错只是结果不对。只要方法参数和成员变量同名赋值时一律加this.这是铁律。5.3 构造方法里调用可被子类覆盖的方法这个坑稍微进阶但值得提前知道public class Parent { public Parent() { print(); // 构造方法里调用方法 } public void print() { System.out.println(parent); } }如果子类覆盖了print创建子类对象时父类构造方法先执行此时子类的字段还没初始化但print已经被覆盖成子类的版本可能导致访问到未初始化的字段。所以构造方法里尽量只做本类的简单初始化不要调用可能被覆盖的方法。这个坑在学继承时会真正爆发现在先埋个印象。5.4 把对象当参数传递时的改不动困惑public class Test { public static void main(String[] args) { Student s new Student(); s.setName(小明); change(s); System.out.println(s.getName()); // 还是小明 } public static void change(Student stu) { stu new Student(); stu.setName(小红); } }很多人以为change里改了stu外面的s也会变。实际上stu是引用的副本stu new Student()只是让副本指向了新对象原来的s还指向老对象。这就是Java值传递的体现——传对象时传的是引用的值地址的副本。但如果改成stu.setName(小红)不重新new那就会改到原对象因为副本和原引用指向同一个对象。这个区别一定要分清改引用指向 vs 改引用指向的对象内容是两码事。6. 把这一章串起来一个完整的小练习光看概念容易忘我建议你动手写一个Book类把这一章所有知识点都用上public class Book { private String title; private String author; private double price; private int stock; public Book() { this(未命名, 佚名, 0.0, 0); } public Book(String title, String author, double price, int stock) { this.title title; this.author author; setPrice(price); setStock(stock); } public String getTitle() { return title; } public void setTitle(String title) { this.title title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author author; } public double getPrice() { return price; } public void setPrice(double price) { if (price 0) { throw new IllegalArgumentException(价格不能为负); } this.price price; } public int getStock() { return stock; } public void setStock(int stock) { if (stock 0) { throw new IllegalArgumentException(库存不能为负); } this.stock stock; } public void sell(int count) { if (count 0 || count stock) { throw new IllegalArgumentException(销售数量不合法); } stock - count; System.out.println(售出《 title 》 count 本剩余 stock 本); } Override public String toString() { return Book{title title , author author , price price , stock stock }; } }写完后自己画一遍new Book(Java入门, 某作者, 59.9, 100)的内存图标出栈、堆、方法区各有什么。再写个测试类创建两个Book对象分别改价格、卖书观察互不影响。最后故意传个负数价格看异常有没有被抛出。这个练习做完你对这一章的理解会从知道变成会用。我当年就是靠反复画内存图、反复写这种小类才把面向对象的地基打牢的。后面学继承、多态、接口时你会发现它们全都是在这一章的基础上往上长——this、构造方法、封装思想一个都跑不掉。最后分享一个我自己的习惯每学一个新的面向对象概念就回头看看Book或Student这类小类能不能用上。能改就改改完跑一遍。这种小步迭代的方式比一次性啃大项目更适合打基础。等你把这一章的内存图能闭着眼睛画出来Java面向对象这扇门就算真正推开了。

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

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

免费获取报价 →
↑