Java面向对象编程核心:类、封装、继承与多态实战入门
发布时间:2026/9/28 14:42:28来源:尧图网络
1. 类与对象从“基本类型”到“引用类型”的思维跃迁Day1搞定了JDK安装、环境变量配置也把int、double、boolean这些基本类型和流程控制捋了一遍。如果你跟完了那套流程今天进入面向对象编程OOP正合适。为什么这么说因为Java和C语言最大的分水岭就是从“面向过程”转向“面向对象”这一跃。这个跃迁不是语法层面的堆叠而是思维模式的转变——原本我们写代码是“定义变量、写方法、按顺序调用”现在要换成“设计类、创建对象、让对象之间协作”。举个例子Day1里我们可能会写一个printStudent(String name, int age, String className)方法把学生信息打印出来。到了Day2你会发现这种写法有个问题学生的所有属性散落在不同的变量和参数里逻辑一多就乱。面向对象的做法是先把“学生”这个事物抽象成一个类把姓名、年龄、班级这些数据以及相关的操作全部收拢到一起。这个收拢的过程就是你第一篇真正意义上的面向对象代码。1.1 类一张“设计图纸”类Class不是实体它是一张设计图纸规定了这类事物有哪些属性成员变量和哪些行为方法。我常用一个生活化类比类是“蛋糕模具”对象是用模具做出来的“蛋糕”。模具本身不能吃但它决定了蛋糕的形状类本身不占内存逻辑空间但它规定了对象拥有什么。定义一个最简单的类语法比Day1的HelloWorld多不了几行public class Student { String name; int age; void printInfo() { System.out.println(姓名 name 年龄 age); } }这里name和age就是成员变量printInfo()是成员方法。注意这些变量不在main方法里而是直接写在类体中。很多初学者在这里犯迷糊Day1里变量不都是写在main方法里的吗因为main是程序的入口局部变量归main管而成员变量归对象管。你创建一个学生对象这个对象自带name和age两个空位等着填值。1.2 对象把图纸变成实体有了类之后通过new关键字创建对象public class TestStudent { public static void main(String[] args) { Student s1 new Student(); s1.name 张三; s1.age 20; s1.printInfo(); Student s2 new Student(); s2.name 李四; s2.age 21; s2.printInfo(); } }new Student()做了三件事在堆内存中给对象分配空间、成员变量初始化默认值null、0、返回对象的引用地址。s1和s2是引用变量它们存的是对象在堆内存中的地址而不是对象本身。这个机制我建议从第一天就开始建立直觉否则后面学集合、学JVM的时候会有很多“灵异事件”。这里有一个非常经典的坑如果你写Student s3 s1;那不是复制了一个对象而是让s3和s1指向同一个对象。修改s3.names1.name也会变。我见过不止一个新手在这里栽跟头后面学“深拷贝”和“浅拷贝”时更是头疼。所以第一天建立“引用”概念第二天会让你的调试效率快很多。1.3 构造方法对象出生的“初始化仪式”给成员变量赋值最笨的方式是一个一个s1.name 张三这样写三个对象就受不了写一百个更崩溃。Java提供了构造方法Constructor在对象创建时自动执行专门做初始化。public class Student { String name; int age; // 构造方法方法名和类名相同没有返回值类型 public Student(String name, int age) { this.name name; this.age age; } }创建对象变成Student s1 new Student(张三, 20);注意this关键字。this代表“当前正在创建的那个对象”。this.name name的意思是“把参数name的值赋值给当前对象的name成员变量”。如果不加this写成name nameJava会认为左右两边都是参数成员变量根本没被赋值结果就是对象创建后名字永远是null。这个坑在面试基础题中出现的频率极高。构造方法有几个细节值得记在笔记里如果你没有写任何构造方法Java会默认提供一个无参构造方法一旦你写了有参构造方法默认的无参构造方法就消失了。这时如果你还写new Student()编译器直接报错。解决办法是手动补一个无参构造方法。构造方法可以重载即写多个参数列表不同的构造方法比如Student(String name)和Student(String name, int age)可以共存。构造方法里可以调用另一个构造方法this(name, 18)但这条调用语句必须出现在第一行而且不能循环调用。2. 封装把对象内部的细节“锁起来”类与对象是OOP的第一层感觉封装则是第一个真正体现工程味道的特性。封装的核心思想是把成员变量私有化通过公共方法对外提供受控的访问入口。为什么要这么做直接public String name不是更方便吗原因有两个。第一数据安全。如果你把age直接暴露成public外部代码可以写出student.age -100这种荒谬值。而如果用方法控制写入就可以在校验逻辑上拦一道。第二可维护性。有一天你想把age改成birthday来推导外部所有直接访问age的代码全部报错但如果有getAge()和setAge()你只需要改方法内部的实现外部完全无感。这种“伸手要”的姿势在团队协作里尤其重要。2.1 访问修饰符与getter/setterJava有四个访问修饰符public、protected、default不写就是默认、private。对于初学者先重点掌握public和private就够用了private只有本类内部能访问public谁都能访问。protected和default的知识可以后面学继承和包时再补。标准封装写法如下public class Student { private String name; private int age; public Student() {} public Student(String name, int age) { this.name name; this.age 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; } }这样外部代码只能通过setAge()设置年龄恶意值会被挡在门外。IDEA提供了快捷键AltInsert一键生成getter/setter但新手阶段我还是建议手敲几遍感受一下这个模式不然面试时让你手写你都反应不过来。2.2 包与import组织类的“文件夹”封装延伸到类的组织层面就是包package机制。包相当于文件系统的文件夹解决了两个问题同名类的冲突、类的归类管理。类全名其实是包名.类名比如java.util.Scannerjava.util是包名Scanner是类名。在项目里我们一般用公司域名倒写作为包前缀比如com.myblog.student.model。包的命名规范全小写层级之间用点分隔。带了包之后跨包使用类需要import导入import com.myblog.student.model.Student;或者用全限定名com.myblog.student.model.Student s new ...后者麻烦一般只在import冲突时才这么用。还有一个细节同包下的类不需要importjava.lang包下的类比如String、System也不需要import它们是自动导入的。很多教程会把包的知识往后放但我建议Day2就接触因为你一旦开始写多个类不建包的话文件管理很快就乱了。3. 继承让类与类之间产生“血脉关系”封装解决的是单个类的数据安全继承解决的是多个类之间的共性抽取。假设现在要开发一个教务系统有Student和Teacher两个类它们都有name、age都需要打印信息。如果各写各的代码会重复。继承允许你建一个父类Person把公共属性和方法放进去然后Student和Teacher通过extends继承它们。public class Person { protected String name; protected int age; public Person(String name, int age) { this.name name; this.age age; } public void printInfo() { System.out.println(姓名 name 年龄 age); } } public class Student extends Person { private String studentId; public Student(String name, int age, String studentId) { super(name, age); // 调用父类构造方法 this.studentId studentId; } public void study() { System.out.println(name 正在学习); } } public class Teacher extends Person { private String subject; public Teacher(String name, int age, String subject) { super(name, age); this.subject subject; } public void teach() { System.out.println(name 正在教 subject); } }3.1 super关键字与构造方法链super(name, age)这行代码是继承中的“新手第一坑”。Java规定子类构造方法中如果没写super(...)会默认调用父类的无参构造方法。而父类一旦写了有参构造方法无参构造方法就没了于是子类编译报错。所以要么在父类里手动补一个无参构造方法要么在子类构造方法第一行显式调用super(name, age)。super有两个用途一是调用父类构造方法二是调用父类被重写的成员或方法。比如父类printInfo()里打印了姓名年龄子类想多打印一个学号可以这样public void printInfo() { super.printInfo(); System.out.println(学号 studentId); }构造方法是不能继承的父类的构造方法只能在子类构造方法里通过super(...)调用而且必须放在第一行。同时this(...)调用本类其他构造方法也必须放第一行所以this和super调用构造方法不能同时出现。3.2 方法重写Override与方法重载Overload方法重写是继承里最容易和“重载”混淆的概念。我用一张表帮你彻底分清对比维度方法重载Overload方法重写Override发生位置同一个类中父子类之间方法名相同相同参数列表必须不同必须相同返回值类型可以不同必须相同或子类类型即协变返回类型访问修饰符任意子类不能严于父类比如父类是public子类不能改成private用途提供多个类似功能的方法子类对父类方法进行扩展或替换重写还有一个隐藏细节父类方法如果加了private修饰子类写一个同名同参数的方法那不是重写是“重新定义”。因为private方法对子类不可见子类等于自己新写了一个方法。检查是否重写成功可以在方法上放Override注解如果编译器不报错说明确实重写了。这个习惯我建议从第一天写代码就坚持它能帮你省下很多排查问题的时间。3.3 单继承与继承层次Java的类是单继承一个子类只能有一个直接父类。但继承可以传递A extends BB extends C那么A也能使用C中的非私有成员。这种传递形成了类继承的层次结构最顶层的父类叫Object——所有类都直接或间接继承自Object。初学阶段我遇到过有人试图用class A extends B extends C来“多继承”Java直接不支持。实现类似“多继承能力”的方式有两种一种是接口Interface这是后面要学的重要概念另一种是内部类或用组合替代继承。这里先记住结论优先使用组合而非继承除非你确定“B确实是一种A”的语义成立。比如Student是一种Person是成立的但如果你只想复用Person里的方法更稳妥的做法是让Student持有Person的引用而不是继承它。4. 多态同一个方法调用多个行为表现继承的一个强大副产品就是多态。所谓多态用一句话概括父类类型的引用指向子类对象。这句话在面试里的出现频率极高它的核心意义在于解耦——你拿到一个Person类型的引用但实际调用的方法会根据“真实对象”的类型不同而执行不同版本。Person p1 new Student(张三, 20, S001); Person p2 new Teacher(李四, 35, 数学); p1.printInfo(); // 调用的是Student重写后的printInfo p2.printInfo(); // 调用的是Teacher重写后的printInfo这里p1的声明类型是Person但它实际指向的是Student对象。调用printInfo()时Java虚拟机在运行阶段根据实际对象类型来决定调用哪个方法这叫动态绑定。动态绑定是理解多态的关键也是后面理解JVM方法调用和反射机制的基础。初学者容易困惑的问题是“我明明拿的是Person类型的引用为什么不能调用study()”因为编译期检查时编译器只认声明类型PersonPerson里没有study()方法所以直接编译报错。4.1 向上转型与向下转型Person p new Student(...)这种写法叫向上转型子类对象赋给父类引用安全且自动发生。但向上转型后你只能调用父类中定义的方法子类独有的方法就看不到了。如果想重新调用子类独有的方法需要向下转型if (p1 instanceof Student) { Student s (Student) p1; s.study(); }instanceof用于判断对象是否为某个类型的实例。这里先判断再强转是为了避免ClassCastException。如果不判断直接(Student) p1而p1实际是Teacher对象运行时就直接抛异常。这个点面试基础题里也常考判断条件和强转逻辑几乎是标准答案。4.2 instanceof、getClass() 与 equals的关系instanceof还有一个特殊之处如果左边是nullnull instanceof 任何类都返回false不会报空指针。这个特性在写equals方法时很有用。先看一个常见写法public boolean equals(Object obj) { if (this obj) return true; if (obj null || getClass() ! obj.getClass()) return false; Student student (Student) obj; return age student.age Objects.equals(name, student.name); }这里用的是getClass() ! obj.getClass()而不是obj instanceof Student。区别在于instanceof对子类对象也返回true而getClass()要求类型完全一致。实际业务里两者都有用但你需要知道它们的语义差异。Day2阶段不要求你写完整的equals但至少要明白比较的是引用地址重写equals才能比较对象内容。String在Day1用过如果那时你没注意new String(abc) abc返回值是false今天可以顺带把这个坑填了。5. 抽象类与接口OOP的“规范制定者”讲到继承和多态如果不提抽象类和接口知识结构是不完整的。但Day2的内容本来就多所以我建议你先把抽象类理解到位接口可以在Day3再深入。为什么现在就要提因为后面你写一点稍微复杂的项目就会遇到“父类方法体根本没法写”的场景。假设Person类里有work()方法但Student的work()是学习Teacher的work()是教学父类里写什么实现都不合适。这时候可以把Person定义成抽象类work()定义成抽象方法——只有方法签名没有方法体子类必须重写。public abstract class Person { protected String name; protected int age; public abstract void work(); } public class Student extends Person { Override public void work() { System.out.println(name 的主要任务是学习); } }抽象类和抽象方法要记住三条规则抽象方法不能有方法体连大括号都不能写直接以分号结尾。包含抽象方法的类必须是抽象类但抽象类里可以有普通成员变量、普通方法、构造方法。子类如果不想实现全部抽象方法它自己也得声明为抽象类否则必须全部实现。接口Interface和抽象类有很多相似之处但语义不同抽象类表达“is-a”关系接口表达“can-do”能力。比如Student继承Person是合理的但如果Student需要“唱歌”“编程”这些能力用接口更合适因为一个类可以实现多个接口。接口里的方法默认都是public abstract的从Java 8开始接口里可以写带方法体的默认方法default但这个内容放到后面学Lambda和函数式编程时再去消化Day2先建立“接口是一种规范”的认知就够了。6. 实操用今天学的知识写一个学生成绩管理系统理论说再多不如动手写一个综合项目。这个“学生成绩管理系统”不需要数据库也不需要框架纯粹用类、封装、继承、多态来实现。大概两三百行代码但能把今天学到的所有知识点串起来。6.1 需求描述与类设计需求很简单有学生、教师、课程三种基础数据。学生选修课程课程有成绩。按学生查看课程成绩按课程统计平均分、最高分。用多态方式统一处理“人员”信息。类结构设计如下类名类型关键成员说明Person抽象类name,age,work()抽象方法人员公共属性和行为规范Student继承PersonstudentId,ListCourse选课和成绩的容器Teacher继承Personsubject教师只负责教授一门课Course普通类name,score课程和成绩Main入口main方法组装测试数据验证功能6.2 核心代码实现与走读public abstract class Person { protected String name; protected int age; public Person(String name, int age) { this.name name; this.age age; } public abstract void work(); public String getName() { return name; } } public class Student extends Person { private String studentId; private ListCourse courses new ArrayList(); public Student(String name, int age, String studentId) { super(name, age); this.studentId studentId; } public void addCourse(Course course) { courses.add(course); } public ListCourse getCourses() { return courses; } Override public void work() { System.out.println(name 是学生当前学年修读 courses.size() 门课程); } } public class Course { private String name; private double score; public Course(String name, double score) { this.name name; this.score score; } public String getName() { return name; } public double getScore() { return score; } }Main方法里做组装和统计public class Main { public static void main(String[] args) { Student s1 new Student(张三, 20, S001); Teacher t1 new Teacher(李老师, 35, Java); s1.addCourse(new Course(Java程序设计, 92.5)); s1.addCourse(new Course(数据结构, 85.0)); t1.work(); s1.work(); // 多态测试 Person p s1; // 向上转型 p.work(); // 统计平均分 double sum 0; for (Course c : s1.getCourses()) { sum c.getScore(); } System.out.println(平均分 sum / s1.getCourses().size()); } }这段代码你亲手敲一遍用到的知识点包括抽象类与继承、super调用父类构造方法、Override重写、ArrayList存储对象、增强for循环、向上转型。如果这些都跑通了你的Day2就算真正过关。6.3 程序执行结果与验证点运行Main控制台会输出类似李老师是教师负责教授 Java 课程 张三是学生当前学年修读 2 门课程 张三是学生当前学年修读 2 门课程 平均分88.75我在验证时有两个关键观察点第一t1.work()和s1.work()走的是各自的实现而p.work()因为多态也输出了学生的版本——这验证了动态绑定。第二如果你把Course的score改成int平均分会变成整数除法结果所以成绩字段是double这个看起来不起眼的类型选择恰恰是实际开发中容易出bug的地方。7. 常见问题速查表与排坑心得下面这些坑都是我在带新手和实际开发中反复遇到的整理成速查表供你参考。症状原因解决办法Implicit super constructor Person() is undefined父类没有无参构造方法子类也没显式调用super(...)父类补无参构造方法或子类构造方法第一行写super(参数)子类可以访问父类private成员吗不能private只在声明它的类内部可见改成protected或者通过父类提供的publicgetter/setter访问ClassCastException: Student cannot be cast to Teacher向下转型时对象真实类型本身不是目标类型强转前用instanceof判断toString()打印出来是com...Student3f0类没有重写Object的toString()重写toString()拼接类名和信息equals比较两个对象内容总是false没重写equals默认比较引用地址重写equals和hashCodeArrayListStudent直接打印输出也是地址同上集合里存储的是对象在实体类重写toString()或者循环遍历打印构造方法调用自己造成死循环this(...)链式调用里循环了调整参数确保最终调用的是父类构造方法最后一个问题值得展开说有些初学者在重写equals时会把hashCode忘掉。在HashMap、HashSet这些容器里如果equals相等但hashCode不同对象会被放进不同的“桶”导致即使内容相同也找不到。这个理论你现在不用完全消化但要养成习惯重写equals就要重写hashCode这是Java世界的“连坐法”。调试上还有一个实用技巧在IDEA里给Person和Student打上断点用Debug模式逐步执行p.work()那一行你会看到调用栈里真实绑定到了Student.work()。用这个方式观察动态绑定比单纯看文字解释直观得多。我建议有条件的读者都去实际操作一下眼见为实。8. Day2收尾把面向对象的思维带到后天到现在为止你已经掌握了类与对象、封装、继承、多态、抽象类这五个OOP的核心板块。这些内容是Java面试里“基础八股文”的重头戏也是后面学习集合框架ArrayList、HashMap、IO流、异常处理、多线程的基石。如果你今天把代码敲完并且都能跑通后天的Day3建议直接进入接口、内部类和常用集合。如果今天的内容有点吃力不要急着赶进度把上面的学生管理系统多敲两遍代码量上来之后很多概念会自然通顺。我在实际带新人的过程中发现面向对象最大的障碍不在于语法本身而在于“分析问题的视角”。语法不过关多敲几天就能解决但是“拿到需求先想有哪些类、哪些对象、它们之间什么关系”这种建模能力需要不断用真实项目去磨。所以从Day2开始你就应该有意识地练习这个思路拿到一个功能描述先不急着写main方法先在纸上列类、列属性、列方法、列继承关系。这一步做顺了后面写任何项目都会顺利很多。最后分享一个小技巧每天学完新知识点不要急着学下一个章节把当天学的内容用“自己的话”写成一段笔记。比如今天你可以写“继承让我不用重复写公共代码多态让同样的调用表现出不同行为封装保护了数据不被乱改。”如果你能用大白话把这三个特性解释清楚说明你真正懂了。如果你解释不出来说明哪里还有没吃透的地方翻回去再看看。这是我从学Java到现在一直坚持的学习方法比闷头刷一百道题都管用。
网站建设高端定制企业官网