拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Java面向对象编程:类与对象、封装、构造方法及static详解

写Java的人都知道面向对象是绕不过去的一道坎也是很多初学者心态崩掉的地方。语法倒是好背类对象封装继承这些词翻来覆去也都能说两句可一写代码就蒙不知道为什么要这么折腾。我见过太多人学了两个月Java能把集合、循环、数组写得飞起一到自己设计类就卡壳。这篇文章就把面向对象最核心的那几个知识点掰开揉碎讲清楚不整虚的全部是能直接拿去用、能帮你把代码写明白的东西。这篇主要针对零基础或者刚学完Java基本语法的读者也适合准备面试、想补一补面向对象底层逻辑的同学。内容聚焦在类和对象、封装、构造方法、this、static这几个关键点上也就是一般教程里面向对象上的部分。搞清楚这些后面学继承、多态、接口、抽象类就会顺很多。1. 先搞清楚一件事面向对象到底在解决什么问题很多教材上来就甩定义什么面向对象是一种程序设计思想听着高大上其实压根没解释清楚为什么要这么搞。我觉得想理解面向对象得先回头看面向过程是怎么回事。1.1 面向过程的困境代码越写越乱假设你要写一个程序管理一个班级的学生。面向过程的思路是定义一堆变量存数据再写一堆函数操作这些数据。比如// 伪代码面向过程风格 string studentName[50]; int studentAge[50]; string studentClass[50]; void printStudentInfo(int index) { printf(姓名: %s, 年龄: %d, 班级: %s, studentName[index], studentAge[index], studentClass[index]); }一个学生有三四个属性还能忍要是十个属性呢姓名、年龄、班级、学号、成绩、电话、地址、家长姓名……你得维护七八个平行数组每个函数都要记住下标对应的是谁。这还只是一个班要是一个年级、一个学校呢数据分散在各个角落函数满天飞改一个需求牵一发动全身。我当年写C语言课程设计的时候就是这么被折磨的。一个学生管理系统写了上千行后面想加个按成绩排序的功能发现至少要同时动四个数组稍不留神下标就串了bug改到怀疑人生。这就是面向过程的痛点数据和操作数据的逻辑是分离的数据一多代码的复杂度就指数级上升。1.2 面向对象的解法让数据和操作住在一起面向对象的思路完全不同把一个学生的所有属性和能做的事打包在一起形成一个类。类里面既有数据成员变量也有操作数据的方法成员函数谁的数据由谁来管井水不犯河水。// 一样的场景面向对象风格 class Student { String name; int age; String className; void printInfo() { System.out.println(姓名: name , 年龄: age , 班级: className); } }这样设计之后你想操作哪个学生就创建一个对应的对象出来直接调用它的方法就行。数据不再满天飞逻辑内聚在类里面人脑处理起来轻松得多。我经常打一个比方面向过程就像一个大车间所有零件数据都堆在地上工人函数需要什么零件自己去拿车间一大就乱套。面向对象则是把每个零件都装进独立的箱子箱子上写着标签和操作说明工人只需要对箱子说话箱子自己知道怎么处理里面的东西。这个类比不完美但很贴近实际感受。1.3 类的本质是模板对象才是真实的东西还有一个容易绕晕的点类和对象到底是什么关系一句话类是模板对象是模板制造出来的具体实例。拿盖房子举例。建筑师画了一张户型图这张图上有客厅、卧室、厨房的布局但户型图本身不能住人。你得照着这张图纸一砖一瓦盖出具体的房子。图纸就是类盖出来的每一套房子就是对象。同一个户型图可以盖出一模一样的很多套房这就对应同一个类能创建出多个对象。在Java里一切代码都是写在类里面的但你真正操作的都是对象。打个不太恰当的比方类是生产标准对象是产品。这个认知特别重要后面学内存分析的时候你会发现类和对象在JVM里存放的位置都不同理解了模板和实例的关系这些就很好懂。2. 类和对象的落地从写一个类开始概念说得再好不落到代码上都是白搭。这一节我带你手把手写一个完整的类把成员变量、成员方法、对象创建这些基础操作都过一遍。2.1 类的基本结构成员变量和成员方法定义一个类最核心的就是两样东西成员变量描述对象的状态和成员方法描述对象的行为。public class Dog { // 成员变量描述狗的状态 String name; int age; String color; // 成员方法描述狗的行为 public void bark() { System.out.println(name 汪汪叫); } public void eat(String food) { System.out.println(name 正在吃 food); } }这里有几个细节值得注意。成员变量定义在类里面、方法外面作用是描述这个类有什么。它的作用域是整个类类里任何一个方法都能直接访问。成员方法就是普通的方法只不过它定义在类里面天然可以访问本类的成员变量。很多新手会纠结成员变量到底要不要初始化。我的建议是先别急着给默认值Java会对成员变量自动赋默认值整数是0小数是0.0布尔是false引用类型是null。这个机制后面讲构造方法时会用到。建议试一下写一个类不初始化成员变量直接new出来打印看看结果印象会很深。2.2 new到底做了什么对象的创建过程有了类怎么得到对象用new关键字。这一行代码背后发生的事情比看起来要多。Dog myDog new Dog();这行代码可以拆成两部分理解new Dog()在内存中创建了一个Dog对象并调用Dog类的构造方法完成初始化。Dog myDog声明一个Dog类型的变量用来保存这个对象的内存地址。比较反直觉的一点是在Java中对象变量其实存的是引用也就是对象在堆内存中的地址不是对象本身。这个有点像你手机上存了朋友的电话号码电话号码不是朋友本人但你可以通过拨打这个号码跟朋友联系。从内存角度讲对象本身存放在**堆Heap中而局部变量myDog存在栈Stack**中栈里的变量存的是堆中对象的地址。这个知识点面试几乎必问后面排查空指针异常也要靠它。public class TestDog { public static void main(String[] args) { Dog myDog new Dog(); myDog.name 旺财; myDog.age 2; myDog.color 黑色; myDog.bark(); myDog.eat(骨头); } }输出结果是旺财 汪汪叫 旺财 正在吃 骨头注意我new了一个对象之后直接通过对象名.成员变量赋值。这种方式在现阶段没问题但后面讲封装的时候你会发现直接暴露成员变量是有隐患的。2.3 同一个类可以创造多个互不干扰的对象类的好处之一就是可以批量生产对象而且每个对象的数据都是独立的。Dog dog1 new Dog(); dog1.name 旺财; dog1.age 2; Dog dog2 new Dog(); dog2.name 来福; dog2.age 3; System.out.println(dog1.name); // 旺财 System.out.println(dog2.name); // 来福dog1改名字不会影响dog2。因为new了两次在堆内存中创建了两个完全独立的对象它们各自拥有一份成员变量。打个比方同一个户型图盖了两套房装修风格可以完全不同因为房子是独立的图纸只是参考。如果有新手问你为什么改dog1的名字dog2没变不用讲太深的理论让他记住new一次就创建一份独立的数据就够了。3. 封装为什么不让外部随便改数据前面那种直接对象名.成员变量赋值的方式看着很方便但在真实项目里这是大忌。因为你没法控制别人往变量里塞什么值。这一节就来讲讲面向对象的第一个大特性——封装。3.1 不封装的惨痛教训还是用Dog类举例。假设狗的年龄不能为负数不能超过30岁如果不做任何限制Dog dog new Dog(); dog.age -100; // 编译能通过但明显不合理年龄是负数这在业务上就是非法数据。没有封装的情况下任何代码都能随意修改age的值你根本拦不住。等到程序跑起来发现某只狗年龄是-100你都不知道是哪个模块改的。这就是为什么要封装把成员变量隐藏起来不让外部直接访问必须通过我们提供的公共方法来操作数据。这样就能在方法内部做校验保证数据永远处于合法状态。3.2 封装的三个步骤private、getter、setter封装的标准做法其实很简单三步走成员变量用private修饰让外部无法直接访问。提供公共的getter方法用于获取成员变量的值。提供公共的setter方法用于设置成员变量的值可以在setter里加校验。public class Dog { private String name; private int age; private String color; 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 30) { System.out.println(年龄不合法请检查); return; // 不合法就不赋值 } this.age age; } public String getColor() { return color; } public void setColor(String color) { this.color color; } public void bark() { System.out.println(name 汪汪叫); } }注意setter里的this关键字。this.age age;是指把参数age的值赋给当前对象的成员变量age。如果不加this就变成了age age;自己给自己赋值成员变量啥也没改到。这是新手最容易犯的错。封装的写法有点机械但这是Java的行业惯例几乎所有框架比如Spring、MyBatis都默认类要提供getter/setter。现在IDE基本都能一键生成右键 → Generate → Getter and Setter效率高很多。我用的是IntelliJ IDEAAltInsert快捷键就能呼出生成菜单。3.3 面试官眼里的封装好处和内幕封装是面试基础题的高频出题点常见问法就是封装有什么好处。回答不需要太花哨抓住核心四点提高安全性外部无法直接修改成员变量非法数据会被setter拦截。提高易用性调用者只需要知道怎么用方法不需要关心内部实现细节。提高可维护性内部实现改了只要方法签名不变外部代码不用跟着改。提高灵活性可以在setter里加入各种逻辑比如触发通知、记录日志、格式转换等。有一个细节值得注意对于boolean类型的成员变量IDE生成的getter方法名通常是isXxx()而不是getXxx()。比如private boolean hungry;对应的方法是isHungry()。这是JavaBean规范规定的很多框架做属性绑定的时候默认按这个规范来找方法。private boolean hungry; public boolean isHungry() { return hungry; } public void setHungry(boolean hungry) { this.hungry hungry; }我见过很多人手动写布尔getter的时候写成了getHungry()框架里报找不到属性排查半天才发现是命名规范的问题。这个小坑十个人里至少有两个人踩过。4. 构造方法对象出生的初始化程序new一个对象的时候到底发生了什么除了在堆里分配内存还会自动调用一个特殊的方法——构造方法。这个方法和类的名字一模一样没有返回值类型void也不用写作用是在对象创建时完成初始化工作。4.1 默认构造方法你没写Java帮你写了你可能发现前面的Dog类根本没写构造方法但new Dog()照样能跑。这是因为如果一个类没有定义任何构造方法Java会自动提供一个无参构造方法也叫默认构造方法方法体是空的不做任何事情。这个默认构造方法有个特点只要你自己写了任何一个构造方法默认构造方法就消失了。举个例子public class Cat { private String name; public Cat(String name) { this.name name; } } // 测试代码 Cat cat new Cat(); // 编译报错这段代码一编译就报错因为Cat类只定义了一个带参数的构造方法那个自动提供的无参构造方法已经不存在了。想要用无参构造就必须手动写一个public Cat() {}。这个坑非常经典新手经常踩定义了一个带参构造方法之后发现自己没法用无参方式new对象了一脸懵。实际上就是默认构造方法被覆盖掉的机制在作怪。4.2 构造方法的重载构造方法也支持重载也就是说一个类可以有多个构造方法只要参数列表不同就行。这给了调用者选择的余地可以只传必要参数也可以把所有参数都传上。public class Student { private String name; private int age; private String className; // 无参构造 public Student() { } // 只传名字 public Student(String name) { this.name name; } // 传名字和年龄 public Student(String name, int age) { this.name name; this.age age; } // 全参数构造 public Student(String name, int age, String className) { this.name name; this.age age; this.className className; } }构造方法重载在日常开发中非常常用尤其是写实体类的时候。不过要注意一个设计原则构造方法不要写太多业务逻辑它的职责就是初始化状态。如果构造方法里写了一堆复杂的计算出了问题很难排查而且违背了单一职责原则。4.3 this的两种标准用法附一个高频面试题在类的方法里this表示当前对象。它的两个核心用途前面已经涉及到了一是区分成员变量和局部变量二是调用本类的其他构造方法。public class Person { private String name; private int age; // 无参构造内部调用全参构造 public Person() { this(张三, 18); // 必须写在第一行 } public Person(String name, int age) { this.name name; this.age age; } }this(张三, 18)这种写法可以避免代码重复让多个构造方法最终都指向一个构造方法去执行初始化逻辑。但有一个硬性规则this()调用构造方法的语句必须放在构造方法的第一行否则编译报错。这是Java语法规定没什么道理可讲记住就行。面试题this和super的区别是Java八股文里的常客。这个小节先记着this是指向当前对象的引用super是指向父类对象的引用。等学了继承两者的对比会更清楚。4.4 代码规范建议写了带参构造就一定补一个无参构造这是我在实际项目中养成的习惯也是很多Java框架的隐性要求。JSON反序列化框架比如Jackson、Fastjson在创建对象的时候很多时候依赖无参构造方法。如果你的类只写了带参构造框架反序列化的时候直接报错或者要求你额外配置。为了避免这种莫名其妙的问题我的习惯是只要写了任何带参构造就顺手写一个无参构造哪怕方法体是空的。这个习惯看起来不起眼却在很多关键时候帮我避免了大麻烦。有次联调的时候对方的接口返回一个JSON数组映射到我们这边的List 结果一直报错查了半天发现就是Order类没写无参构造。从此以后我写实体类永远都是无参构造全参构造打底。5. static关键字属于类的东西不属于对象static是面向对象里特别容易让人晕的一个修饰符。它的含义一句话能说清被static修饰的成员属于类本身而不是属于某个对象。但真正理解这句话的含义还是得靠代码说话。5.1 static变量全体对象共享一份先看一段代码public class Counter { // 实例变量每个对象一份 private int count 0; // 静态变量所有对象共享一份 private static int totalCount 0; public void increment() { count; totalCount; } public void show() { System.out.println(当前对象count: count , 总totalCount: totalCount); } }测试一下Counter c1 new Counter(); c1.increment(); c1.increment(); Counter c2 new Counter(); c2.increment(); c1.show(); // count: 2, totalCount: 3 c2.show(); // count: 1, totalCount: 3count属于实例变量每个对象都有自己的一份c1改了不影响c2。totalCount是静态变量被所有对象共享不管通过哪个对象去修改改的都是同一份数据。所以最后totalCount变成了3。静态变量典型的使用场景是统计全局信息、定义常量配合final用、提供全局唯一的状态等。不过要注意在分布式架构里多个进程间的静态变量是不共享的别指望靠static在多个服务之间传递状态那已经超出单JVM的范畴了。5.2 static方法的限制不能直接用非静态成员static方法同样属于类所以有一个硬性限制静态方法中不能直接访问非静态成员实例变量和实例方法。public class TestStatic { private int num 10; public static void test() { // 编译报错不能从静态上下文中引用非静态变量num System.out.println(num); } }原因很简单静态方法属于类它不需要创建对象就能调用。但非静态成员必须依托于某个具体的对象才存在。你在一个没有对象的方法里怎么可能访问到某个对象才有的变量呢从逻辑上讲就不通。这也是main方法为什么必须是static的原因——程序一启动的时候还没有任何对象JVM必须能直接通过类名调用main方法所以它必须是非实例相关的静态方法。5.3 static的常见面试陷阱static修饰的成员能不能被覆盖重写这个问题面试用的很多。答案是不能。静态方法可以被继承可以被子类重新声明一个同名方法但这叫隐藏hide不叫重写override。调用的时候根据引用类型决定调哪个方法而不是根据实际对象类型。这个知识点现在记个结论就行等学了继承再回来验证。还有一个容易踩的坑static变量在多线程环境下存在线程安全问题。所有线程共享同一个静态变量如果多个线程同时读写不做同步控制就会出现数据不一致。所以我一般建议能用局部变量解决的问题别动不动就用static变量。5.4 static的应用工具类日常开发中static最常见的应用就是工具类。比如你想写一个数学工具里面全放静态方法调用的时候直接用类名点方法名不用创建对象。public class MathUtils { public static int max(int a, int b) { return a b ? a : b; } public static boolean isEven(int num) { return num % 2 0; } } // 使用 int max MathUtils.max(3, 7); boolean even MathUtils.isEven(4);Java标准库里的Math类、Arrays类、Collections类都是这个套路。这种设计在工具方法不依赖任何对象状态的时候非常合适。但要注意如果一个类有自己内部的状态成员变量就千万别把方法全部写成static否则就退化回面向过程了。6. 实战一步步封装一个Student类讲了这么多理论咱们动手写一个完整的例子把这一篇的所有知识点串起来。需求很简单做一个学生类包含姓名、年龄、班级三个属性要求年龄必须合法能打印学生信息。6.1 需求分析和类设计先想清楚要什么属性姓名、年龄、班级全部private。getter/setter年龄setter要做合法性校验。构造方法无参构造全参构造。方法打印学生详细信息。这个设计过程很关键。很多新手拿到需求就开写写到哪算哪。我建议先在注释里把类的结构列出来再动笔/** * 学生类 * 属性name, age, className * 方法getter/setter, showInfo * 构造无参构造 * 全参构造 */ public class Student { // 属性部分 private String name; private int age; private String className; // 构造方法部分 public Student() { } public Student(String name, int age, String className) { this.name name; this.age age; this.className className; } // getter/setter部分 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 120) { System.out.println(年龄不合法无法设置: age); return; } this.age age; } public String getClassName() { return className; } public void setClassName(String className) { this.className className; } // 业务方法 public void showInfo() { System.out.println(姓名: name , 年龄: age , 班级: className); } }6.2 测试类走一遍完整流程写好类之后再写一个包含main方法的测试类验证功能public class StudentTest { public static void main(String[] args) { // 方式一无参构造 setter Student stu1 new Student(); stu1.setName(王小明); stu1.setAge(18); stu1.setClassName(软件工程1班); // 方式二全参构造 Student stu2 new Student(李小红, 20, 软件工程2班); // 测试非法年龄 Student stu3 new Student(); stu3.setName(张小强); stu3.setAge(500); // 应输出年龄不合法 stu1.showInfo(); stu2.showInfo(); stu3.showInfo(); } }编译运行javac Student.java StudentTest.java java StudentTest预期的输出年龄不合法无法设置: 500 姓名: 王小明, 年龄: 18, 班级: 软件工程1班 姓名: 李小红, 年龄: 20, 班级: 软件工程2班 姓名: 张小强, 年龄: 0, 班级: null注意stu3这套数据因为我们拒绝了非法年龄age保持默认值0className没设置所以是nullname设置成功。这种拒绝非法赋值的方式会留下一个默认值对象在真实项目中可能需要结合异常处理来提示调用者。这篇先不做高级处理但你要意识到这个设计的问题静默拒绝在某些场景下会隐藏bug后面学异常时可以优化成throw new IllegalArgumentException(年龄不合法)。6.3 这段代码隐含的知识点检查模仿是最好的学习方式但得知道自己写了什么。我建议每个学到这里的人都能回答这几个问题为什么stu1和stu2的年龄都能正常打印但stu3的年龄是0private修饰的成员变量外部代码是怎么访问到的setAge方法里的if判断起到了什么作用如果只写无参构造不写全参构造stu2的创建方式还能用吗this.namename这句代码里的两个name分别指什么答不上来的回上面几节再看一遍。这些问题看着基础但都是面试八股文的变形换个马甲就出现在笔试题里。7. 常见问题与排查技巧实录最后这部分是我见过的初学者踩坑重灾区。整理成清单每一个都是真实发生过的问题。7.1 空指针异常最常见的运行时错误Student stu null; stu.setName(王小明); // 空指针异常NullPointerException对象变量是null表示它没有指向任何对象。这时候调用任何实例方法或访问任何实例变量都会抛NullPointerException。这是Java初学者遇到最多的异常没有之一。排查思路很简单找到出问题的行看看是哪个对象是null再往上看这个对象为什么没被初始化。常见原因有几个new对象忘了赋值给变量、构造方法里没初始化成员变量、方法返回了null但没有判断、从集合里取出来的元素是null。我排查空指针的经验是先在报错信息里找到at开头的行号定位到具体代码行然后在该行往上回溯检查变量是怎么来的。实在不行就在关键位置打日志输出变量的值。IDE的Debugger配合断点看变量值效率更高。7.2 构造方法名写错或没写无参构造我见过有新手把构造方法的方法名写成了小写或者拼错编译报错的时候还一脸迷茫。构造方法有两个硬性特征方法名必须和类名完全一致包括大小写、不能写返回值类型void都不能写。如果编译器提示invalid method declaration; return type required十有八九是构造方法写错了。另外别忘了前面说的默认构造方法消失机制。一旦你写了带参构造无参构造需要自己补。7.3 private成员变量在外部访问报错Student stu new Student(); stu.name 王小明; // 编译报错: name has private access in Student这个报错其实是在保护你。既然name是private的外部就不能直接访问。正确做法是通过setter方法赋值。如果真的需要外部可读可写要么把private改成public不推荐要么提供公共的getter/setter。绝大多数情况下后一种是正确答案。7.4 static方法里访问了非静态成员上面举过例子static方法里访问实例变量编译直接报non-static variable cannot be referenced from a static context。这个错误是编译期就能发现的所以不需要等到运行。解决办法把访问的成员改成static要考虑是不是真的应该改成static或者删掉static修饰符或者在该方法内new一个对象再去访问它的实例成员。7.5 常见问题速查表现象可能原因解决方案运行时抛NullPointerException对象变量为null就调用方法检查变量是否初始化判空处理编译报invalid method declaration构造方法名写错或写了void方法名必须与类名完全一致且无返回值类型编译报cannot be referenced from static context静态方法访问了非静态成员调整修饰符或创建对象后再访问默认无参构造不能用自己写了带参构造默认构造消失手动补一个无参构造this.namename没效果少写了this自己给自己赋值用this.成员变量区分参数static成员在并发生效错乱多个线程同时修改共享静态变量加锁或改用实例变量/局部变量7.6 我最后想再啰嗦的两点经验第一学面向对象阶段一定要不厌其烦地手写getter/setter和构造方法不要一上来就依赖IDE自动生成。你可能会觉得这很蠢但手写的过程中你会反复接触到this、参数传递、赋值方向这些最基础的东西它们会在你脑子里形成肌肉记忆。等真的理解了再用IDE生成也不迟。我自己带过的实习生里凡是基础扎实的无一例外都在这个阶段老老实实手敲过代码。第二强烈建议每学一个知识点就写一个小程序验证。学完封装就把上节课的学生管理系统改造成带private的版本学完构造方法就试试用构造方法给成员变量赋初值学完static就写一个统计工具类。面向对象不是靠背书学会的是靠不断写代码、踩坑、修正学会的。哪怕每天只写几十行坚持一个月你对Java的感觉会和现在完全不同。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门