
老实说Java学到类和对象这一块劝退的人比想象中多。很多人一开始把语法背得滚瓜烂熟一到写程序就抓瞎这个类该怎么设计、那个对象要new几个、属性到底放在哪里合适完全没头绪。我每次帮人调代码见得最多的就是这类问题根子上全是“Java面向对象的基础没揉进实际操作里”。所以这篇不打算灌概念我用一个完整的图书借阅Demo把类、对象、属性这几个词是怎么在代码里落地的从设计到调试全部过一遍。哪怕你现在只是刚学到标题里这些名词看完也能动手写出自己的第一个面向对象程序。1. 类和对象先理解“设计图纸”与“实物”的关系1.1 为什么“类就是模板”这句话远远不够很多人背概念的时候都把类理解成模板对象理解成根据模板创建出的实例。这句话没错但太笼统了落到代码里根本不知道该怎么用。我更愿意用“设计图纸”这个类比图纸上画出了汽车的外观、标注了发动机的参数但图纸不能开只有按图纸造出来的那台真实汽车才能上路。类就是那张图纸new关键字就是开工建造每次new出来的对象就是一台独立的汽车。class Car { String brand; String color; } Car car1 new Car(); Car car2 new Car();这里car1和car2都是按Car这个类造出来的对象。很多人会忽略一个关键点它们彼此独立。给car1的color涂成红色car2的颜色不会跟着变因为堆内存里存在两个完全独立的区域。这个“独立”如果没理解透后面写集合、写对象数组的时候十有八九会踩坑。真正要把类和对象的关系吃透心里得有一个画面类只有一个对象可以有一堆。所有对象都由同一个类定义来创建但每个对象都有自己的状态存储空间互不干扰。1.2 类的骨架字段、构造器、方法分别负责什么一张完整的类图纸基本由三样东西组成字段、构造器、方法。字段描述这个事物长什么样构造器负责创建对象时做初始化方法描述它还能干什么。还是拿汽车举例。一个稍微完整的类大概长这样public class Car { private String brand; private String color; private int speed; public Car(String brand, String color) { this.brand brand; this.color color; this.speed 0; } public void accelerate() { speed 10; } public void showInfo() { System.out.println(brand color 当前速度 speed); } }字段brand、color、speed是对象的数据部分构造器public Car(...)决定了对象在诞生的一瞬间应该处于什么状态方法accelerate()用来改变对象的行为状态。为什么要分这三块因为现实世界里描述一个事物本来就分“它是什么样”和“它能做什么”。类和对象的建模本质上就是把现实事物翻译成代码结构。新手的常见误区是把所有东西都堆在 main 方法里从头到尾用一堆局部变量做事情那样程序也能跑但随着代码变长状态散落各处根本没法维护。类的作用就是把这些状态和行为收拢到一起。1.3 new关键字背后的事引用变量与对象的本质区别new Car(...)到底发生了什么很多人其实没细想过。简单说它做了三件事在堆内存中分配一块空间用来存放这个新对象。调用对应的构造器把字段的值初始化好。把新对象的内存地址返回给等号左边的变量。注意第 3 点里说的是“地址”。Car car new Car()这句里的car本质上保存的是内存地址它本身并不是对象而是指向对象的引用。这一点极其关键。可以用遥控器理解car是遥控器new Car()出厂的那台电视才是对象。遥控器指向电视通过遥控器按钮控制电视。你可以拿着两个遥控器对准同一台电视也可以一个遥控器先后指向不同的电视但遥控器本身永远不是你控制的那台电视。Car car;如果只写这么一行相当于手里握着一个没有对准任何电视的遥控器这时car的值为null。如果贸然去按遥控器按钮也就是调用方法程序就会直接抛空指针异常。很多人一听到NullPointerException就慌其实原因往往就这么简单对象还没创建你就想操作它。明确了类和对象的关系接下来要看一个最容易被低估的部分对象的属性。2. 对象的属性从定义到封装把“状态”管起来2.1 属性该怎么设计先想清楚对象需要记录哪些状态属性在 Java 里也叫字段或成员变量它用来保存对象的静态特征。但到底该给一个类定义哪些属性没有现成公式得从业务场景反推。比如做一个图书借阅模型你要先问自己一本书在这个系统里需要被记录的信息有哪些至少要有书名、作者。可如果只是这两个字段功能就太单薄了书借出去了怎么知道几本书里哪本可以借所以还需要一个“借阅状态”字段。再比如一本书的页数、价格这些信息也会影响展示和业务判断。public class Book { private String title; private String author; private int pages; private boolean isBorrowed; }注意这里的isBorrowed它是典型的“状态属性”。很多初学者只关注书名、作者这类静态信息却忘了设计表达动态状态的字段结果借书这个行为只能靠外部临时变量记录数据一多就乱了。属性的选择能力直接决定了这个类的业务表达能力。一个设计良好的属性列表应该能完整回答“这个对象在当前场景下需要知道什么”。2.2 访问修饰符与封装属性为什么要用private包起来新人最开始见到private关键字时经常会觉得它麻烦属性都 private 了外面访问还要写 getter 和 setter这不是脱裤子放屁吗先看一个反例。如果属性直接 publicpublic class Book { public int pages; }外部代码就可以写book.pages -100;一本书的页数变成负数这在业务上是完全非法的。可因为属性是 public任何人都能绕过校验直接改字段整个对象的数据随时可能处于错误状态。一旦项目有几十个类、上百个调用点这种漏洞就像定时炸弹数据出问题时你甚至不知道是哪个地方改坏的。把属性改成 private外部就不能直接访问了只能通过 getter 和 setter 这两个“受控入口”读写数据。这样校验逻辑可以收敛在 setter 里public void setPages(int pages) { if (pages 0) { System.out.println(页数不能为负数); return; } this.pages pages; }所有写入页数的请求都会经过同一个安检门非法数据在入口处直接被拦住。这才是封装的价值它不是为了多写几行代码而是把数据的控制权收回对象自己手里。2.3 this关键字的常见用法和构造器重载this 关键字是 Java 初学者的老朋友可它的使用场景远不止“参数名和字段名相同时用来区分”。最常见的情况就是构造器里的赋值public Book(String title, String author) { this.title title; this.author author; }这里的this指代“当前正在创建的这个对象”。不加 this 的话title title只是把参数赋值给了参数自己类里的字段完全没有被操作这是新手最容易踩的坑之一。this 还有一个很实用的用法在构造器之间互相调用。如果你写了多个构造器可以用this(...)复用初始化逻辑public Book(String title, String author) { this.title title; this.author author; this.pages 0; this.isBorrowed false; } public Book(String title, String author, int pages) { this(title, author); this.pages pages; }第二个构造器的第一行this(title, author)会去调用第一个构造器先完成书名、作者、状态的初始化再补充页数信息。这样做的好处是不重复写初始化代码以后如果新增字段只需要改一个构造器就好。注意this(...)调用必须放在构造器的第一行这是 Java 的语法硬性规定。构造器重载本身的意义是让对象可以根据不同场景灵活初始化。有的场景只需要书名和作者有的场景需要完整信息多个构造器互相搭配调用方就能按需选择。2.4 初始化时记住默认值一半的空值问题都能避免Java 有一个非常容易忽略的细节类的成员变量是有默认值的。声明但不赋值它也会有一个默认初始值。数据类型默认值byte / short / int / long0float / double0.0char空字符\u0000booleanfalse所有引用类型String、数组、对象等null这条规则经常给新手带来错觉成员变量不用初始化也能用。确实能编译通过但“能用”不等于“结果正确”。比如声明了private String title;却没在构造器里赋值那么调用getTitle()返回的就是 null后面再去做字符串拼接或比较很容易出错。另一边方法里的局部变量没有这种默认值机制局部变量不赋值直接使用编译就会报错。这个不对称让很多人困惑“成员变量可以不初始化局部变量为什么不行”原因很简单成员变量有类的初始化流程兜底局部变量的生命周期很短编译器要求你明确它的初始状态。数组也有一套自己的默认值逻辑。new Book[3]只创建了一个长度为 3 的容器容器里的每个位置都是 null并不是三个现成的 Book 对象。这个概念后面排查 NullPointerException 时会反复用到。3. 手写一个图书借阅Demo类、对象、属性怎么组合成程序3.1 从需求出发设计Book类字段、构造器、业务方法一起上知识点拆到最后还是要回到“能写出完整程序”这个目标上。我挑一个特别适合练手的场景图书借阅管理。需求不复杂能创建几本书能借书、还书能查看每本书的状态。先设计 Book 类的字段。回顾前面说的属性要围绕业务需求来定书名title、作者author、页数pages是静态信息isBorrowed是动态状态。还需要一组业务方法borrow()借书、returnBook()还书、showInfo()展示信息。最后配 getter 和 setter。public class Book { private String title; private String author; private int pages; private boolean isBorrowed; public Book() { } public Book(String title, String author) { this.title title; this.author author; this.pages 0; this.isBorrowed false; } public Book(String title, String author, int pages) { this(title, author); this.pages pages; } 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 int getPages() { return pages; } public void setPages(int pages) { if (pages 0) { System.out.println(页数不能为负数); return; } this.pages pages; } public boolean isBorrowed() { return isBorrowed; } public boolean borrow() { if (isBorrowed) { System.out.println(《 title 》已被借出无法再次借阅。); return false; } isBorrowed true; System.out.println(你成功借阅了《 title 》); return true; } public boolean returnBook() { if (!isBorrowed) { System.out.println(《 title 》当前没有被借出无需归还。); return false; } isBorrowed false; System.out.println(你已归还《 title 》); return true; } public void showInfo() { System.out.println(书名 title 作者 author 页数 pages 借阅状态 (isBorrowed ? 已借出 : 在馆)); } }这个类里体现的其实是分层思想字段管状态构造器管初始化方法管行为。尤其注意borrow()方法内部的判断逻辑。它不是一上来就把isBorrowed改成 true而是先检查当前状态。这是业务代码的基本素养操作前校验前置条件避免状态被错误地覆盖。3.2 测试类中创建和操作对象完整代码加运行效果类写好了接下来用测试类把对象“用起来”。这是从“定义了类”到“程序能跑”的最后一步。public class LibraryDemo { public static void main(String[] args) { Book book1 new Book(Java Core 入门, 某开发者, 880); Book book2 new Book(并发实践手记, 某开发者, 460); Book book3 new Book(算法与数据结构基础, 某开发者, 780); Book[] books {book1, book2, book3}; System.out.println(借阅前); for (Book book : books) { book.showInfo(); } System.out.println(); book1.borrow(); book2.borrow(); book1.borrow(); System.out.println(); System.out.println(借阅后); for (Book book : books) { book.showInfo(); } System.out.println(); book1.returnBook(); } }运行结果大致如下借阅前 书名Java Core 入门作者某开发者页数880借阅状态在馆 书名并发实践手记作者某开发者页数460借阅状态在馆 书名算法与数据结构基础作者某开发者页数780借阅状态在馆 你成功借阅了《Java Core 入门》 你成功借阅了《并发实践手记》 《Java Core 入门》已被借出无法再次借阅。 借阅后 书名Java Core 入门作者某开发者页数880借阅状态已借出 书名并发实践手记作者某开发者页数460借阅状态已借出 书名算法与数据结构基础作者某开发者页数780借阅状态在馆 你已归还《Java Core 入门》这个 Demo 看起来简单但把面向对象的核心都串起来了类是图纸new出来的三个对象是实体每个对象通过自己的属性isBorrowed记着自己的状态。当第三次去借book1时方法能准确判断“这本书已经借出去了”靠的就是对象内部保存的属性值。这里还有一个细节值得注意Book[] books {book1, book2, book3};这个数组里的元素是对象的引用。数组本身不复制书本只是把三个遥控器放进了一个柜子里。遍历数组调用方法时实际操作的是同一个对象。能理解这一点你就算真正迈进面向对象的大门了。3.3 从单类Demo到多类协作下阶段的升级方向目前的 Demo 只有一个实体类加一个测试类属于最简单的结构。真实系统不可能只有一种对象下一步自然要往多类协作方向走。最典型的扩展是引入“读者”这个概念。新建一个Reader类包含姓名、学号等属性让读者对象拥有借书行为。比如public class Reader { private String name; // 其他字段和方法 public void borrowBook(Book book) { if (book.borrow()) { // 记录借阅信息 } } }这时候你会发现对象之间的关系开始产生读者对象拿着图书对象作为参数调用图书对象的方法两个对象之间发生消息传递。再往后可以用ArrayListBook代替数组来动态管理书库让新增、删除书本不再受数组长度限制。很多初学者学到这里容易陷入一个误区不停学习新语法却很少去拓展已经写过的 Demo。其实把这个 3 章的图书借阅程序往多类、动态集合方向迭代效果比做十个不同的“学生管理系统”练习要好得多。面向对象的能力就是在一次次扩展中慢慢长出来的。4. 高频报错与调试心得从空值到未生效的变动4.1 输出全是null先检查构造器和赋值环节代码跑起来调用getTitle()返回 null这是新手阶段最常见的问题之一。我第一次帮人排查这类 bug 时发现对方的构造器是这么写的public Book(String title) { title title; }参数名和字段名都是 title没有用 this 区分结果赋值给了参数自己字段从头到尾没被碰过。看起来只是少了this.两个字输出结果却天差地别。这种问题如果不了解 this 的指向挨个打印变量都看不出来。排查思路很简单先在构造器入口加一行打印确认构造器到底有没有被调用再检查每个字段是否在自己的作用域里被正确赋值。如果构造器里有一套this.xxx xxx字段就没理由仍然保持默认值。遇到 null 值第一反应永远是检查创建对象的路径而不是检查输出语句。4.2 方法里改了属性却没生效分清楚基本类型的复制和对象的共享另一个迷惑性很强的问题是把一个 int 变量传进方法方法里改成了新值回到 main 方法里一打印原值没变。原因是 int 属于基本类型方法拿到的是值的副本对副本做任何修改都不会影响原变量。static void modifyPages(int x) { x 99; } static void modifyBook(Book book) { book.setPages(999); } public static void main(String[] args) { int pages 100; modifyPages(pages); System.out.println(pages); // 100 Book book new Book(Java, 某开发者, 100); modifyBook(book); System.out.println(book.getPages()); // 999 }同样是传参为什么 Book 的属性发生了变化因为book变量保存的是对象地址方法拿到的是地址的副本副本指向的还是堆里那个同一个对象。通过地址去访问和修改对象属性自然会影响原对象。有人管这叫“引用传递”其实严格说 Java 只有值传递只不过引用类型的值本身就是地址。这个现象背后藏着对象共享的本质引用变量可以有很多份但对象实体只有一个。理解这一点后面写缓存、写列表转换才会心里有数。4.3 NullPointerException对象数组里每个元素都要单独创建空指针异常大概是 Java 初学者最讨厌的报错没有之一。它发生的根本原因只有一句话用一个值为 null 的引用去访问了属性或方法。前面提过new Book[3]的坑这里展开说。很多新手知道数组要 new于是写了Book[] books new Book[3]; books[0].showInfo(); // 空指针异常他以为自己已经创建了三个 Book实际上只创建了一个长度为 3 的数组容器容器里的三个格子默认都是 null。每个格子要想有对象必须再单独new Book(...)放进去。修复方式也很简单要么先new Book再放进数组要么在循环里逐个创建并填充。这也解释了为什么上面 Demo 里用Book[] books {book1, book2, book3};不报错因为数组初始化时已经装进了三个现成的对象。排查空指针时我习惯按一条线走找到报错那一行看是哪个变量是 null再反推这个变量为什么没有指向对象。绝大多时候不是语法问题而是创建对象的时机或位置错了。4.4 绕过封装后的代价一个没用private引发的连锁问题封装在代码里的体现就是属性默认用 private对外提供受控访问方法。可总有人觉得这是多余的。假设某个类的pages是 public 字段另一个开发者在业务代码里直接写book.pages -50。你可能已经加了 setter 校验但没关系他直接绕过 setter 修改了字段。等某个模块做数据汇总时发现一本书的页数是 -50开始全项目排查。这种 bug 最麻烦的地方在于你根本不知道数据是在哪个环节被改坏的因为绕过入口的写入路径是数不清的。如果属性是 private类似book.pages -50的代码根本编译不过去。编译器直接告诉你“不允许访问”问题在开发期就被消灭了。封装的本质不是限制自由度而是把危险操作挡在门外让数据的变化路径可预期、可审查。给 setter 里的校验加一个实用场景如果一本书的页数不但不能为负还不能超过 5000那么 setter 里就同时做两个判断。所有非法数据都在入口处被拦住而不是等到下游代码发现问题。4.5 新手最容易忽略的两个面向对象习惯最后说两个我观察到的、新手最容易忽略的好习惯。第一个习惯是写类之前先列字段清单。拿一张纸或者一个记事本把“这个对象在业务里需要记录哪些状态”写下来再在旁边写上“这个对象需要支持哪些行为”。字段清单写明白了类的结构自然就清晰了。不少人一上来就敲代码敲到一半发现缺字段又回头改类折腾几轮之后状态就开始乱。第二个习惯是学会 debug 看对象状态。很多人就靠 System.out.println 打日志排查问题这当然可行但不如直接在 IDE 里断点观察左侧变量面板里的对象属性值。在断点视图里你能直观地看到这个对象有哪些属性、每个属性当前是什么值、引用指向哪里。这个画面看得多了对内存布局的直觉会越来越准。我带新人时经常说一句话不要背类与对象的定义去画一张对象图。等你能画清楚new之后堆内存里那块区域长什么样、哪个变量持有它的地址类和对象这个概念就算真正过关了。之后再遇到空指针、封装失效、属性没同步这类问题你大概率一眼就能看出症结所在。