☰
类与对象怎么学?从模板到实例,掌握封装、构造方法与实战练法
2026/10/20 17:43:13 网站建设 项目流程

每当讲到类与对象这一章,我总会想起刚带第一门编程基础课时的场景。当时课后答疑,某同学拿着代码问我:“老师,类我知道,就是一个模板;对象我也知道,就是实例。可自己写的时候还是会卡住,不知道从哪一行开始写。” 这个问题我后来听过太多次,因为类与对象这块内容确实和前面的变量、循环、函数都不一样,它不只是一个新语法,而是看待程序的一种新方式。这章内容(很多教材把它放在第11章左右)常常是初学者的一道分水岭——跨过去了,后面学习数据结构、设计模式、框架开发都会顺很多;卡在这里,后面越学越吃力。这篇内容我说说实操里最容易落地的那套理解方式和练法,尽量把原理讲透,把步骤写清楚,适合正在学编程基础、准备用类来写小型项目的人参考。

1. 为什么前面的知识突然不够用了:类和对象在解决什么问题

1.1 从“数据 + 操作”到“把数据和行为装在一起”

在学类之前,我们写程序的思路基本都是:先定义变量保存数据,再用函数处理这些数据,数据和处理逻辑是分开的。比如做一个学生成绩管理系统,我们会写一个数组存所有学生的分数,再写几个函数分别负责计算平均分、找最高分、打印成绩单。功能简单时这样写没有问题,可一旦数据种类变多,函数之间需要共享的数据一多,代码就会慢慢变得难维护,因为没有任何机制保证“这个函数只能改它该改的数据”。类这个概念的出发点,就是把一组相关的数据和操作这组数据的函数打包成一个整体,外界只能通过预留的入口使用它,内部细节不暴露。这个“打包”的动作,就是面向对象编程里最基础也最核心的思想。

可以这样理解:前面学的变量像散落的积木块,循环和函数是拼搭积木的规则;类则像一座小小的成品模型——它有自己固定的零件(属性),也有自己固定的玩法(方法),你拿到手只需要按说明操作,不必每次从零开始拼。类还允许你批量造出很多个结构相同但数据不同的成品,这正好对应“对象是类的实例”这句话。

1.2 类和对象到底是什么:一次说清“模板”和“实例”

类与对象的关系,最经典的说法就是“模板和实例”。但只有这句话还不够,我更喜欢用“建筑图纸和建筑”来类比:图纸上定义了一栋楼有几层、几户、几部电梯(属性),以及水电气怎么走、消防通道怎么开(方法)。同一张图纸可以盖出很多栋结构完全一样的楼,只是每栋楼具体住的人不一样。图纸不是楼,它就是用来指导盖楼的;楼才是实际存在、能住人的东西。映射到代码里,图纸就是类,楼就是对象,而“按照图纸盖一座楼”这个动作,就是new一个对象。

这里有一个初学者容易忽略的点:类只定义了结构,不单独占用运行时内存,真正占用内存的是对象。我们写class Student只是告诉编译器“以后凡是学生的数据,都会有 name、score 这些部分”,但此时内存里什么都没有。只有执行了Student s = new Student();,才真正产生了一个可供操作的学生对象。所以写类的时候思维要切换成“我在定义一张图纸”,写new的时候才想“我在盖一座具体的楼”。把这两个动作分开想清楚,很多概念就自然理顺了。

2. 亲手定义一个类:语法骨架背后藏着什么设计思路

2.1 一个最基本的类是怎么写出来的

以 Java 为例,定义一个学生类最少需要三块内容:类名、属性、方法。类名是这张图纸的标题,属性是这栋楼有哪些结构,方法是这个结构支持哪些操作。最基本的写法如下:

public class Student { String name; int score; public void printInfo() { System.out.println(name + " 的分数是 " + score); } }

这段代码定义了一个 Student 类,它有两个属性(name、score)和一个方法(printInfo)。注意这里没有任何main方法,因为类本身不负责启动程序,它只是被其他代码使用的一种类型。很多同学第一次写完类就急着去运行,发现报错“找不到主方法”,其实是因为还没有创建对象,也没有调用这个类的入口。这个阶段最合理的练法是另写一个Main类或者直接在带main方法的类里临时创建对象,用来当测试入口。

定义类时的类名有一个约定:用名词命名,首字母大写,多词组合时每个词首字母大写(比如StudentInfo、OrderManager)。这不是语法强制要求,而是整个行业共同的阅读习惯。类名本质上是给别人看的“这是什么类型的东西”,名词化、驼峰式命名能让你和读代码的人一眼就明白它的用途。

2.2 属性设计的核心:不是照着需求堆字段,而是想清楚“这块数据属于谁”

属性就是类的数据部分。初学者最常见的问题是把所有可能用到的数据一股脑全塞进类里,比如给 Student 类加一个teacherName字段。老师确实是学生系统里用得到的信息,但它不属于学生个体,应该放到 Teacher 类里。设计属性时先问一个问题:这个字段是描述这个对象自身状态的,还是描述另一个事物的?如果是后者,就应该在类之间建立关联,而不是把数据堆进当前类。

属性本身也有生命周期和可见性的考量。默认情况下,Java 的字段是包内可见的,其他包的类直接访问会报错。确定属性用public、private还是默认访问权限,其实是后续封装的内容,但初学者从第一天写属性时就应该养成习惯:默认把数据字段设为private,不要图省事全写成public。我见过太多练习项目因为所有字段都是public,导致后面想限制非法赋值时,要满世界找哪里直接改了字段,非常痛苦。

属性的数据类型选择也很关键。比如存“分数”,用int够用且清晰;存“姓名”,用String;存“是否通过”,用boolean比int加注释更安全。这些选择看似简单,但选对类型本身就是建模能力的一部分——类型选得准,后面方法里少写很多防御性判断。

2.3 方法:类里的函数和普通函数有什么不同

类里的方法,写法上和前面学的函数几乎一样,区别在于它是“属于这个对象的函数”,天然能访问这个对象自己的属性。这一点特别重要:普通函数如果要处理某个学生的数据,得把数据作为参数传进去;而 Student 类里的方法,直接写name就能访问到当前对象的姓名,因为方法是在对象内部运行的,天然绑定了“我是谁”。

刚才的printInfo方法就是例子,它没有接收任何参数,却可以直接使用name和score。原因在于:调用s.printInfo()的时候,看起来是调一个函数,实际是让对象s对自己执行一次操作。这个“绑定”是理解面向对象的关键跳板——方法里出现的每个属性名,指的都是当前这个对象的属性,不是别处的同名变量。

方法设计有一个判断标准:一个方法只做一件完整的事情。比如printInfo只负责打印,不负责修改分数;修改分数应该由另一个方法setScore负责。新手写类方法时容易把几件事揉在一起,打印完顺便把分数改一下,这类“顺手行为”在项目变大后会让调试变得很难受,因为你根本不知道哪个方法在什么时候偷偷改了数据。

3. 对象创建背后的事情:构造方法、引用与内存机制

3.1 为什么需要一个构造方法

定义好类之后,创建对象用的是new。但new后面往往还要跟一个构造方法,比如new Student()。构造方法名字和类名相同,没有返回值,它的特殊之处在于:对象创建出来的瞬间,构造方法负责完成初始化工作。如果你不在类里写任何构造方法,编译器会送一个默认的无参数构造方法,属性保持默认值(String为null,int为0)。很多同学一开始忽略这一点,结果打印对象时发现name是null,一脸疑惑,其实只是因为根本没给对象赋过初值。

构造方法真正的价值是让对象一出生就处于“可用状态”。比如要求 Student 对象创建时必须有姓名和分数,就可以这样写:

public class Student { private String name; private int score; public Student(String name, int score) { this.name = name; this.score = score; } }

这样new Student("小明", 85)创建出来的对象立即具备完整数据,而不是先创建一个空壳再慢慢赋值。这里有一个常见认知误区:构造方法不是必须写的,但一旦你写了带参构造方法,默认的无参构造方法就会被覆盖,如果其他代码还用new Student()就会报编译错误。所以如果类允许无参创建,就需要手动补一个无参构造方法。

3.2this到底在指谁:消除“同名变量”的困扰

看到this.name = name;这一行,很多同学会愣住。等号左边和右边都叫name,怎么区分?this的作用是显式表明“左边这个name是当前对象的属性,右边的name是参数”。它的语义就是“当前对象”,谁调用这个方法或构造方法,this就代表谁。

这种写法不是为了炫技,而是因为在构造方法里,参数名取成和属性名一样是最自然的选择——参数叫studentName、属性叫name虽然也能凑合,但用久了会额外增加命名负担。this存在的意义就是帮你度过参数与属性同名的场景。在方法内部如果不需要区分,this可以省略,但建议初学阶段养成“访问实例属性时显式加this”的习惯,代码会清晰很多。尤其是代码量变多以后,这个小小的前缀能节省大量读代码的时间。

3.3 对象在内存里的真实样子:一个重要的直觉

初学者总会困惑对象变量和普通变量到底哪里不一样。其实关键差别在内存里:基本类型变量(比如int)直接存数值,而对象变量存的是一个引用,也就是“指向堆内存地址的指针式标识”。我们可以做一个对照实验:

int a = 10; int b = a; b = 20; System.out.println(a); // 输出 10 Student s1 = new Student("小明", 85); Student s2 = s1; s2.setScore(60); System.out.println(s1.getScore()); // 输出 60

第一组操作里改b不会影响a,因为两个变量各存各的数。第二组操作里改s2的分数,s1的分数也跟着变了,因为s1和s2保存的是同一个对象的引用,它们指向同一个堆内存区域。这里我给Student补一个setScore方法,方法内部做分数修改。这个实验在实际教学中几乎每个人都会做一遍,做完了才能真正理解“对象是按引用操作”的含义,而不是死记结论。

这个引用机制也解释了为什么对象可以作为参数传给方法,而且方法内部对对象的修改会“穿透”回调用方。理解这一点以后,你写代码就会有一个心理预期:你操作的对象可能被别人共享着,改之前要想清楚影响范围。

4. 封装:为什么不能像原来那样随手改字段

4.1 公开字段带来的麻烦:一个实际踩坑场景

先看一个反面例子。假设我们图省事把字段都设成public:

public class Student { public String name; public int score; }

做一个成绩系统的场景中,某同学在另外的代码里写了stu.score = -10;,程序不但不报错,还会很“听话”地接受这个负分。等到统计平均分的时候,这个负分就会把整批数据拉低,而且你根本不知道是谁、在什么地方把分数改成负数的——因为每个类都可以直接改公开字段,问题出现时连排查范围都圈不出来。这不是语法问题,而是设计问题:你的类没有把数据的合法性守住。

封装的本质是“给属性加上访问控制门槛”。具体做法就是属性一律private,外部无法直接访问,想读写数据只能通过类提供的方法。这样带来的第一个好处是:你可以把合法性校验集中在一个方法里。第二个好处是可排查性好——数据被修改时,只有那一两个方法里会出现赋值动作,其余地方都是只读调用。

4.2 getter 和 setter:入口和出口都自己控制

设置private后,外界不能直接obj.字段了,于是需要提供公开方法作为读写入口。最常见的就是 getter(读数据)和 setter(写数据):

public class Student { private String name; private int score; public String getName() { return name; } public void setScore(int score) { if (score < 0 || score > 100) { System.out.println("分数不合法,本次赋值无效"); return; } this.score = score; } public int getScore() { return score; } }

这个例子能看出 setter 的真实价值:setScore里加了范围校验,外部怎么赋值都不可能把分数改成负数或超过 100。你不必在每个调用处都写一遍判断逻辑,而是把规则收拢到一处。很多初学者觉得 getter/setter 多此一举,“直接访问字段不是更省事吗”,直到遇到这种需要加规则的场景才会意识到:省事的代价是失去控制。

顺带说一句,不是所有字段都必须配 getter 和 setter。如果一个字段只在类内部使用,完全没必要开放访问入口;如果字段的值初始化后就不该变,可以不提供 setter,甚至用final修饰。随心所欲地给每个字段都配上一对 getter/setter 反而会制造大量无用接口,增加阅读负担。

4.3 封装对初学者最大的思维转变

封装这件事,对写过一段时间脚本的人来说是个坎:原来代码里到处都是直接操作数据结构,现在结构被装进类里,外面的人只看到方法。这种约束一开始会让人感觉“不自由,凭什么我不能直接改”,但项目一旦超过一定规模,约束反而是一种保护。比如一个电商购物车类,内部有商品列表,外部只能调用addItem、removeItem、getTotalPrice这些方法,谁也绕不过你的校验和统计逻辑——这就是封装的价值。

这里需要强调一点:封装不是为了藏数据而不让别人用,而是为了让数据的使用方式规范化、可控化。本质上它是一种“接口即承诺”的设计思路:类对外承诺“你可以做这些操作,其他操作你不需要知道”,内部无论怎么改,只要接口不变,调用方代码就不用动。把这个理念想通了,后面学继承多态才会更有底气,因为那些高级特性都要建立在“类自己管好自己的内部状态”这个前提之上。

5. 一个完整的小案例:记账分类管理如何用类来演进

5.1 先写一个能跑起来的初始版本

学了这么多概念,最容易犯的毛病是纸上谈兵。我建议从一个小场景练起:做一个简单的记账分类管理程序,每条账目包含名称、金额、分类和日期。第一步,先定义账目类:

public class Record { private String name; private double amount; private String category; private String date; public Record(String name, double amount, String category, String date) { this.name = name; this.amount = amount; this.category = category; this.date = date; } public String getCategory() { return category; } public double getAmount() { return amount; } @Override public String toString() { return date + " " + name + " [" + category + "] " + amount; } }

这里只是一块“记录静态数据”的类,它管好自己的数据,不负责记账流程。不要把打印、统计逻辑全塞进来——类的职责单一,后面拆起来才容易。这是我写类的时候一直提醒自己的一句话。

5.2 再写一个管理类,感受“数据和操作的分离”

光有Record还不够,需要另一个类来管账目集合。建一个Ledger类,负责存记录、加记录、按分类统计金额:

import java.util.ArrayList; public class Ledger { private ArrayList<Record> records; public Ledger() { records = new ArrayList<>(); } public void addRecord(Record r) { records.add(r); } public double getTotalByCategory(String category) { double sum = 0; for (Record r : records) { if (r.getCategory().equals(category)) { sum += r.getAmount(); } } return sum; } }

这一版把两类职责分开了:Record描述一条账目长什么样,Ledger描述账目怎么被组织、怎么被统计。两者互相配合,但互不越界。这个拆分其实就是面向对象设计里很基础的原则——每个类只负责一类事情。练这个案例时不用刻意去套设计模式,只要保持“数据的类只做数据,管理的类只做管理”这个朴素感觉,后面再看专业设计原则时会豁然开朗。

对应的测试入口可以这样写:

public class Main { public static void main(String[] args) { Ledger ledger = new Ledger(); ledger.addRecord(new Record("早餐", 11.5, "餐饮", "2025-01-01")); ledger.addRecord(new Record("打车", 23.0, "交通", "2025-01-01")); System.out.println("餐饮支出:" + ledger.getTotalByCategory("餐饮")); } }

运行结果会输出一行餐饮支出。这个程序虽然小,但已经包含了类定义、构造方法、私有属性、方法调用、集合持有对象这些核心要素,是“类与对象”阶段最合适的练手闭环。

5.3 进阶一下:把统计逻辑也封装进类里

初版跑通后,还可以试着把“统计各分类占比”加进去。注意不要拿到外面去算,而是继续往Ledger里加方法,比如返回一个字符串描述各分类的占比情况。这样主程序永远只需要几行代码,复杂逻辑都被类消化掉了。这一步能直观体会到面向对象写代码的一个红利:主程序越来越像“读说明书”,复杂逻辑都下沉到类内部,main 方法不再臃肿。

我会建议每个人原创地改一改这个案例,比如给Record加一个isExpense()方法判断收支类型,给Ledger加一个总金额方法。不要照抄,改过一次之后,类与对象的语法细节才能转化成你自己的东西。

6. 初学者最容易踩的五个坑:每个我都带人排过

6.1 在静态方法里访问实例字段

很多初学者刚学完main是静态方法,就开始在里面直接访问类的实例字段。比如想在main里写name = "小明";,结果编译报“非静态字段不能从静态上下文引用”。原因其实在上一节讲清楚了:静态方法不属于某个对象,而name属于对象,两者没有绑定关系。修正方法是先创建对象再通过对象访问,或者在类里把该字段也设为静态(但这一般不是好设计)。这个坑几乎每天答疑都会遇到,知道原因之后就不觉得神秘了。

6.2 创建对象时把属性的初始化顺序搞错

在构造方法里,给属性赋值的顺序如果和后面的业务逻辑耦合,容易出隐蔽问题。比如初始化时会调用一个依赖其他字段的方法,而这个字段还没赋值,就会出现空值。最稳妥的做法是:构造方法里先完成所有字段的直接赋值,再做任何依赖这些字段的调用。如果某些字段需要复杂初始化,可以拆成独立的初始化方法,在构造方法末尾调用。养成“先准备好所有原材料,再开始加工”的习惯,可以少踩这个坑。

6.3 把对象引用当成基本类型在传递

就是前面 3.3 节那个引用实验。很多同学写完函数改对象属性发现调用方也变了,惊讶之余才开始理解引用。反过来也有写完函数传一个基本类型进来,以为能改掉外部变量的值,结果改了没反应,一脸困惑。初学者先记住两条:基本类型变量传的是值的副本,对象引用传的是同一对象的引用。这两条用在所有常见场景都能得到正确预期。

6.4 类和文件名不一致导致的编译错误

在 Java 中,一个公开类的类名必须和所在的文件名一致。很多初学者把类名写错一个字母,或者在一个文件里定义了两个public class,编译报错时就只能干瞪眼。建议规则是:一个 Java 文件只放一个公开类,文件名和类名逐字一致,养成分文件存放类的习惯。这也符合“一张图纸一个文件”的直觉,后面代码多了也方便查找。

6.5 为了用类而用类,过度拆分

最后一个坑和前面几个相反——有些同学学会类之后,恨不得一个变量名就是一个类,导致代码被拆得七零八碎,读起来反而更费劲。类的意义在于把紧密相关的数据和操作打包在一起,不是制造碎片。判断标准很简单:如果一个类从头到尾只有一两个字段、一个方法,而且创建实例后几乎不提供额外价值,那它可能暂时不需要单独成类。初学阶段重点是“用得自然”,不用为了炫技强行拆分。

卡在类与对象这一关的人很多,我觉得彻底想明白的最好方式不是背概念,而是亲手从头定义几个类、创建几个对象,再故意踩几次坑。把错误报错信息看懂,比把教材翻三遍都管用。我后来每次带人入门,都会先让他们先把带参数的构造方法、this的用法、private加 getter/setter 这三件套写熟,再谈设计思想。等这三样东西不用想就能写对,类与对象的基础就算是真正打牢固了。接下来看继承、多态、接口这些进阶概念,你会发现之前打好的底子让新东西都变得好理解很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询