Java面向对象编程入门:类、对象、构造器与封装核心解析
讲一个很多自学Java的人都会遇到的坎儿变量、if、for这些语法学得还算顺手一到“类和对象”就开始犯迷糊视频看了两三遍代码照着敲也敲了但总感觉脑子里缺一根弦不知道这玩意儿到底在解决什么问题。这个感觉太正常了因为面向对象不是一门“语法”而是一套组织代码的思维方式语法只是它穿在外面的衣服。这篇文章就专门拆一拆这座桥把我自己从“能写”到“能想”这一路踩过的坑、看过的代码、总结出的套路都捋一遍。内容聚焦在面向对象的第一阶段类和对象的关系、构造器、封装、this与static以及在实战里最容易翻车的几个细节点。适合刚学完Java基础语法、准备理解面向对象原理的自学者也适合准备面试前快速过一遍核心概念的开发者。1. 先破除对面向对象的心理障碍1.1 为什么语法都懂一到面向对象就懵很多自学者在学面向对象之前已经写了不少“从上到下”的代码定义几个变量写一个方法然后在main里按顺序调用。这种编程方式叫面向过程它的核心是“步骤”就像一份菜谱先备菜、再热锅、再下料、最后出锅。每一步都是独立的操作数据和操作是分开的。可到了面向对象画风突然变了你要先描述“菜”是什么再描述“锅”是什么然后让“菜”和“锅”之间发生关系。数据和方法被捆绑在了一起代码的组织单位不再是一行行语句而是一个个“对象”。这种思维上的跳转比语法本身难得多。再加上网上很多教程上来就念定义——“类是对象的抽象对象是类的实例”——念完读者更晕了。1.2 面向对象到底解决了什么问题我举个例子。如果你用面向过程的方式管理一个班级的成绩你要维护两个数组一个存学生姓名一个存学生分数。然后你写函数传入两个数组去操作。看起来还行但一旦人数变多、功能变多比如还要存性别、存学号、计算平均分、按分数排名数组就会越来越多函数参数越来越长改一个需求可能要牵连所有函数。代码就像一个大厨房所有厨具都摊在桌上谁都可以拿谁都可以乱放最后找不到东西。面向对象的做法是把“学生”定义成一种类型里面既有姓名、分数这些属性也有计算平均分、判断是否及格这些方法。你要操作某个学生就只针对那一个对象。数据和方法天然在一起谁负责什么清清楚楚。这就像是给每个厨师配了独立的料理台各自的工具自己保管。1.3 用“图纸与实车”理解类与对象关于类和对象的关系我见过最好的一个类比是图纸和汽车。类Class就像一张汽车设计图纸上面画好了车子有几个轮子、什么颜色、发动机多大排量但它不是车不能开。对象Object则是按照这张图纸实际生产出来的那辆车能驾驶、能加油、能载人。一张图纸可以生产无数辆车每辆车的颜色可能略有不同但它们都遵行同一套设计规范。在代码里class Car就是那张图纸new Car()就是生产一辆车。new这个关键字本质上就是“按照类去打造一个实例”的动作。Java里所有对象都是通过这种方式创建的这也直接解释了为什么Java里几乎一切皆是对象——除了八种基本数据类型int、double、boolean等之外其他东西都是某个类的实例。2. 类和对象的底层运转逻辑2.1 一个类的标准组成属性、方法、权限修饰符先看一段最普通的代码定义一个“用户”类public class User { private String username; // 属性用户名 private String password; // 属性密码 private int age; // 属性年龄 public User(String username, String password, int age) { this.username username; this.password password; this.age age; } public void showInfo() { System.out.println(用户名 username 年龄 age); } }这里面有两个核心组成部分属性成员变量和方法行为。属性描述“这个对象是什么样的”方法描述“这个对象能做什么”。private和public是权限修饰符private表示这个字段只能在本类内部访问public表示谁都能调。这种“属性私有、方法公开”的设计就是封装的基础。新手经常把private String username;写在方法外面就忘了导致后面赋值、取值全都乱了套。你记住一句话写在类里面、方法外面的变量叫成员变量写在方法里面的叫局部变量成员变量属于对象局部变量只属于那个方法。2.2 new 的时候内存里发生了什么这一节是面试最爱问、自学者最不容易去深究的部分但理解了它很多诡异的问题就自动解释了。当你在main中写下User u new User(tom, 123456, 20);这一行代码时JVMJava虚拟机在内存里做了三件事在**堆内存Heap**中划出一块区域用来存放新创建的对象。在这个对象内部为每个成员变量分配空间并赋予默认值。比如String初始是nullint初始是0。在**栈内存Stack**中创建一个引用变量u这个变量的值不再是数据而是指向堆中那块区域的地址。打个比方栈上的u是一个写着地址的便签堆上的对象才是真正的房子。你通过u去访问房子里的家具属性实际上是通过便签找到了房子。理解这一点之后一个经典问题就清楚了下面两行代码有什么区别User u1 new User(tom, 123456, 20); User u2 u1;很多人以为u2是一个新对象其实不是。u2只是复制了便签上的地址这时它和u1指向同一个堆里的对象。你改了u2.ageu1.age也会变。这就是“引用传递”和“值传递”最容易混淆的地方。2.3 写第一个类时最容易踩的三个低级坑第一类名和文件名不一致。Java规定public修饰的类必须和文件名相同。我第一次写的时候把文件命名为Test.java内部类名却叫User编译直接报错报错信息又长又吓人其实问题就这么简单。初学者在IDE里创建一个类时通常不会遇到但如果你用记事本写代码这个坑躲不开。第二方法里给成员变量赋值却没加this。比如构造器里写成username usernameJava解析的规则是“局部变量优先”结果两个username都指参数成员变量username根本没被赋值。对象创建后你打印它的用户名发现是null。用了this.username username之后前面的username才明确指向成员变量。第三属性不写权限修饰符。Java里不写修饰符时是“包级私有”同一个包内可以访问。初学阶段觉得反正都是自己的代码就图省事不写结果后面被网络请求、数据库实体类、序列化这些框架折磨得死去活来。养成习惯成员变量一律private对外提供公开方法。后面讲封装的时候详细说。3. 构造器、封装、this和static的正确理解方式3.1 构造器对象的入场券构造器是一个特殊方法名字和类名完全一样没有返回值类型连void都不写。它的作用就是在对象创建时做初始化。看这个例子public class Student { private String name; private double score; // 无参构造器 public Student() { name 未知; score 0.0; } // 带参构造器 public Student(String name, double score) { this.name name; this.score score; } }需要特别注意如果你一个构造器都没写编译器会悄悄给你加一个无参构造器但只要你写了一个带参构造器编译器就不再提供无参构造器了。知乎、CSDN上到处能看到这种问题“为什么我new对象时报错说不存在Student()构造器”答案往往就是你写了带参构造器把默认无参构造器顶掉了。构造器也可以重载也就是多个构造器参数不同比如上面的无参和带参版本可以并存。这是一种很常用的设计目的是让调用者根据手头信息的多少灵活地创建对象。3.2 封装不是“把变量私有化”这么简单封装是面向对象三大特性之一但很多人对它理解得很浅以为就是套一层getter/setter。其实封装的核心思想是控制外部对内部数据的访问方式。直接暴露属性会有什么问题看这个反例public class BankAccount { public double balance; // 余额直接公开 }如果有人写account.balance -10000;会发生什么没有任何校验余额可以是负数。这在真实业务里就是灾难。而封装的做法是public class BankAccount { private double balance; public void deposit(double amount) { if (amount 0) { throw new IllegalArgumentException(存款金额必须大于0); } balance amount; } public void withdraw(double amount) { if (amount balance) { throw new IllegalArgumentException(余额不足); } balance - amount; } public double getBalance() { return balance; } }把属性设为private对外只暴露deposit、withdraw、getBalance这些方法所有对余额的修改都必须经过校验逻辑。这才是封装的本质不是不让看而是不能让外部随意乱改。你可以把封装理解成银行柜台的玻璃窗——你能看到余额但不能伸进手去自己拿钱必须通过柜员方法办理。3.3 this与static实例成员和类成员的分界线this指向“当前这个对象”它的核心用途有两个一是在构造器和setter中区分参数和成员变量前面已经见过二是调用当前对象的其他方法或构造器。static就更有意思了。被static修饰的成员属于类本身不属于任何一个具体对象。同一类的所有对象共享同一个static成员它存放在方法区或叫类的元数据区域而不是堆中的对象里。举个例子public class Counter { public static int count 0; public Counter() { count; } }每创建一个Counter对象静态变量count就加1。不管创建了多少个对象count只有一份它统计的是“这个类一共被实例化了多少次”。很多人在这儿犯迷糊的点是能不能用对象去访问static成员语法上可以比如Counter c new Counter(); c.count;但实际工作里强烈不推荐因为这会让人误以为count属于某个对象而且IDE也会给出警告。正确写法是用类名访问Counter.count。4. 实操用面向对象写一个学生成绩登记系统4.1 先想设计再动手写码我自己的习惯是拿到一个小需求先不急着写代码而是先在纸上画三个东西有哪些“类”每个类里有哪些“属性”每个类有哪些“方法”以“学生成绩登记系统”为例目标功能是能添加学生姓名、学号、成绩能修改成绩能按平均分统计班级整体水平能打印所有学生信息那么我只需要两个类一个Student类负责单个学生的数据和行为一个ClassRoom类负责管理多个学生。main方法所在的类只负责运行测试像启动器一样。4.2 Student类的设计public class Student { private String name; private String studentId; private double score; public Student(String name, String studentId, double score) { this.name name; this.studentId studentId; setScore(score); // 复用校验逻辑 } public String getName() { return name; } public String getStudentId() { return studentId; } public double getScore() { return score; } public void setScore(double score) { if (score 0 || score 100) { throw new IllegalArgumentException(成绩必须在0到100之间); } this.score score; } public void showInfo() { System.out.println(学号 studentId 姓名 name 成绩 score); } }注意这里的一个小设计在带参构造器里我调用了setScore(score)而不是直接写this.score score。这样无论通过构造器创建对象还是通过setter修改成绩校验逻辑都只用维护一份。这种技巧叫“复用校验入口”真实项目里很常见能有效防止“构造器绕过了校验”这种隐蔽Bug。4.3 ClassRoom类的设计import java.util.ArrayList; import java.util.List; public class ClassRoom { private ListStudent students; public ClassRoom() { students new ArrayList(); } public void addStudent(Student student) { students.add(student); } public void updateScore(String studentId, double newScore) { for (Student s : students) { if (s.getStudentId().equals(studentId)) { s.setScore(newScore); return; } } throw new IllegalArgumentException(未找到学号为 studentId 的学生); } public double getAverageScore() { if (students.isEmpty()) { return 0; } double sum 0; for (Student s : students) { sum s.getScore(); } return sum / students.size(); } public void printAll() { for (Student s : students) { s.showInfo(); } } }这个类里出现了List和ArrayList这是在Java里保存一组对象的标配容器。为什么用List而不是数组因为数组长度固定而List可以动态增长。真实业务里“学生数量是多少”几乎不可能事先定死所以动态容器更贴合实际场景。4.4 在main里跑起来public class Main { public static void main(String[] args) { ClassRoom room new ClassRoom(); room.addStudent(new Student(张三, 001, 86.5)); room.addStudent(new Student(李四, 002, 73.0)); room.addStudent(new Student(王五, 003, 95.0)); room.printAll(); room.updateScore(002, 88.5); System.out.println(修改后); room.printAll(); System.out.printf(全班平均分%.2f%n, room.getAverageScore()); } }运行结果学号001姓名张三成绩86.5 学号002姓名李四成绩73.0 学号003姓名王五成绩95.0 修改后 学号001姓名张三成绩86.5 学号002姓名李四成绩88.5 学号003姓名王五成绩95.0 全班平均分90.0到这里你已经完成了一次完整的面向对象建模设计类、定义属性、封装方法、创建对象、组合对象、调用行为。这一套流程就是以后所有Java后端开发的基础动作无论你以后是写Spring Boot接口、写Android应用还是写纯Java工具库本质都是在做同一件事。4.5 实操中值得记录的调试瞬间我在自己敲这套代码时第一版addStudent没有加任何校验结果往里塞了一个null后面printAll遍历的时候直接NullPointerException。这个问题很经典。解决方案有两种一是在addStudent里对参数判空public void addStudent(Student student) { if (student null) { throw new IllegalArgumentException(学生对象不能为null); } students.add(student); }还有一种思路是使用Objects.requireNonNull(student)JDK自带的工具方法。这个细节说明一个道理对象的创建者和使用者往往是不同的人或者不同时间的你所以入口处的防御性检查极其重要。宁可让异常早点抛出来也不要让一个null在程序里飘几百行代码后炸掉到那时排查成本就高多了。5. 常见问题与排查技巧实录5.1 编译报错“找不到符号”或“类X是公共的应在名为X.java的文件中声明”前者最常见的原因是类名拼写不一致比如类定义是Student调用时写了studentJava对大小写敏感。后者是public类的文件名和类名不符。处理办法很简单把文件名改成和类名完全一致或者去掉该类的public修饰符。初学阶段用IDE一般能自动处理但我建议至少用命令行javac去手动编译一次你会对Java的编译机制有极其直观的理解。5.2 属性值一直是null或者0构造器“似乎没生效”大概率是构造器里没加this局部变量把成员变量遮住了。排查时在构造器里第一行加个System.out.println(this.name)立刻就能看出来。这个坑我在前面已经强调过但它出现的频率高到值得再标一次。5.3 该私有的属性到处都是public或者getter/setter全自动生成很多初学者IDE用多了右键一键生成getter/setter就以为“封装完成了”。但封装的核心不在方法生成而在业务校验是否被放进去了。你光生成getter/setter和直接公开字段相比并没有质的提升。真正的封装至少要保证对数据的每一次修改都经过合法性的检查、对数据的每一次读取都经过必要的转换或保护。比如setScore里判断0到100就是封装的一部分。如果你生成完setter拍拍手走人那这个封装就是空壳。5.4 为什么对象数组创建后一访问就报空指针这是一个特别好玩的坑。写Student[] arr new Student[10];之后你以为arr[0]是一个Student对象但实际上它只是null。数组在创建时只会给每个位置填入默认值——引用类型是null所以你必须逐个arr[0] new Student(...)赋值或者直接用ArrayList它内部帮你处理了扩容和新增的问题。很多初学者在这类问题上卡一下午其实只要理解“new Student[10]只创建了10个‘空位子’并没有创建10个学生”就够了。5.5 常见错误速查表症状可能原因处理方式编译报“找不到符号”类名拼写不一致、大小写错误检查类名与调用处拼写属性为null或0构造器里没加this用this. 明确指向成员变量默认构造器不存在自己写了带参构造器手动补充无参构造器所有对象共享了同一个成员值误用static修饰实例属性去掉static改成实例成员NullPointerException对象还没new就调用方法确认引用已指向堆中的对象修改成绩不生效传入的Student对象地址被覆盖检查是否重新new了对象5.6 一点独家的习惯养成建议我在自学的时候吃过一个大亏花了很多时间看网课以为听懂了就完事。后来发现听和写完全是两回事。对于面向对象这一块我给自己的规矩很简单——每学一个概念就写一个不少于50行的小程序去“消化”它。学构造器就写一个定时器类学封装就写一个银行账户类学static就写一个全局计数器。写多了以后你再看面试题里那些抽象概念会发现自己已经在用它们解决问题了只是没有意识到名字而已。另外一个特别值得做的练习是试着把你现实生活中接触到的某样东西用Java类建模比如“一台咖啡机”“一个电梯”“一个外卖订单”。你不必写得多么完整只要能把属性、方法、构造器、封装这些概念都填进去就已经赢过了绝大多数只在视频里“看过”的人。这篇内容作为面向对象第一部分就写到这里。类和对象、构造器、封装、this和static是后面学继承、多态、抽象类、接口的前置基础。尤其是封装它是你后续写所有真实业务代码时每天都要用的思维方式。把这些概念真正揉进自己的代码里比追着看多少集视频都重要。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →