ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

类、对象、方法全解析:面向对象编程核心概念与实战避坑

类、对象、方法全解析:面向对象编程核心概念与实战避坑 类、对象、方法这三个词几乎每个刚接触编程的人都会在第一天碰到也几乎每个人在第一天都没真正搞明白。我第一次写Java的时候把类当成“文件夹”把对象当成“变量”把方法当成“函数”结果写着写着就乱了套一个类里塞了几十个方法对象到处new改一个字段牵动半个项目。后来做Python、写JavaScript重新回头看这三个概念才发现它们不是语法糖而是组织代码的一套世界观。这篇文章我想认真把这件事讲透什么是类什么是对象什么是方法它们之间的关系是什么怎么用才不容易翻车。不管你是刚学编程的新手还是写了几年代码但一直“凭感觉”用面向对象的老手应该都能从里面找到点有用的东西。1. 从零搞懂类、对象、方法到底在说什么1.1 用生活里的例子把“类”和“对象”说明白先说类。类这个概念如果只看定义——“类是对一类事物的抽象描述”——大部分人看完还是懵。我更愿意用“模板”或者“图纸”来理解它。比如汽车图纸图纸上规定了这辆车有四个轮子、一个方向盘、一个发动机能前进、能刹车、能转向。图纸本身不是车它只是一份描述告诉你“一辆车应该长什么样、能做什么”。对象就是按照这张图纸真正造出来的那辆车。你可以按同一张图纸造十辆车每一辆都是独立的个体颜色可以不同车牌号可以不同跑起来的里程数可以不同。但它们都具备图纸上规定的那些属性和行为。放到代码里这个对应关系非常直接class Car: def __init__(self, color, plate): self.color color # 属性颜色 self.plate plate # 属性车牌 self.mileage 0 # 属性里程 def drive(self, km): # 方法行驶 self.mileage km car_a Car(红色, 京A12345) # 对象A car_b Car(白色, 沪B67890) # 对象BCar是类是那张图纸car_a和car_b是对象是按图纸造出来的两辆具体的车。它们共享同一套结构定义但各自持有自己的数据。这就是类和对象最核心的关系类是抽象的模板对象是具体的实例。很多人会问那我为什么不直接写两个变量car_a_color、car_b_color就完了非要搞个类小规模当然可以但只要车的数量超过三辆或者需要给车加上“行驶”“加油”“保养”这些行为散落的变量就会迅速失控。类的价值在于把“数据”和“操作数据的方法”绑在一起形成一个自洽的单元。1.2 方法不是普通函数它绑在对象身上方法这个词最容易和函数混淆。函数是一段可以被独立调用的逻辑方法则是“属于某个类或对象”的函数。区别看起来很小实际影响很大。普通函数这样写def drive(car, km): car.mileage km方法这样写class Car: def drive(self, km): self.mileage km调用的方式不一样drive(car_a, 100) # 函数调用需要手动把车传进去 car_a.drive(100) # 方法调用车自动作为第一个参数传进去car_a.drive(100)这一行背后的实际含义是“让 car_a 这辆车执行 drive 这个动作参数是 100”。方法里的self就是“当前这辆车自己”。Python 把car_a自动塞给了self所以你在方法内部可以随意访问self.color、self.mileage不需要外部再传一次。Java 里对应的是thispublic class Car { private String color; private int mileage; public Car(String color) { this.color color; this.mileage 0; } public void drive(int km) { this.mileage km; } }this.color color这一句是新手最容易写错的地方。左边this.color是对象的字段右边color是构造方法的参数。如果不加this写成color color那只是把参数赋值给自己对象的字段根本没被设置。这种错误不会报错只会让你在打印对象属性时发现全是 null。提示方法的第一参数Python 的 self、Java 的 this、JavaScript 的 this永远指向“当前正在操作的那个对象”。理解这一点方法就不再神秘。2. 为什么现代编程绕不开面向对象这套思路2.1 从面向过程到面向对象痛点到底在哪我早期写脚本的时候习惯面向过程一步一步往下写需要什么就定义什么函数所有数据用变量存着。写几百行的工具脚本没问题但一旦代码上千行问题就来了。第一个问题是数据和方法分离。比如处理订单我有一堆订单相关的变量order_id、order_amount、order_status散落在文件各处又有一堆处理订单的函数calculate_total()、update_status()、print_order()。数据和函数之间没有强制关联谁都能改改错了也看不出来。第二个问题是复用困难。想再做一个“退款单”大部分逻辑和订单一样只是多了几个字段。面向过程的做法基本就是复制粘贴一整段代码改吧改吧结果两份代码越走越远维护成本翻倍。面向对象解决的就是这两个痛点。把订单相关的数据和处理逻辑装进一个Order类里数据被保护起来外部只能通过类提供的方法来操作需要扩展退款单直接继承Order再加字段和方法即可公共逻辑不用重写。这不是“为了面向对象而面向对象”而是代码规模上去之后自然而然的选择。2.2 封装、继承、多态在实际代码里长什么样这三个词是面向对象的三大支柱但光背定义没用得看它们在代码里具体是什么样。封装说白了就是“不让外面随便碰我的数据”。Python 里用下划线约定class BankAccount: def __init__(self, balance): self._balance balance # 约定外部不要直接访问 def deposit(self, amount): if amount 0: raise ValueError(金额必须大于0) self._balance amount def get_balance(self): return self._balanceJava 里用private关键字强制public class BankAccount { private double balance; public void deposit(double amount) { if (amount 0) { throw new IllegalArgumentException(金额必须大于0); } this.balance amount; } public double getBalance() { return balance; } }为什么要封装因为直接改balance可以让账户变成负数而通过deposit方法改就有一道校验关卡。封装不是为了“显得专业”而是为了把规则集中在一个人手里避免外部乱来。继承是“子类自动拥有父类的能力”。比如SavingsAccount继承BankAccount就自动有了deposit和get_balance只需要补充自己特有的add_interest方法。多态是“同一个方法调用在不同对象上表现不同”。比如Animal有个speak()方法Dog重写成“汪汪”Cat重写成“喵喵”。你写animal.speak()的时候不用关心它到底是狗还是猫运行时自动选对应版本。这就是多态的威力调用方只依赖抽象不依赖具体实现。提示新手常有一个误区觉得继承越多越好。实际上继承层次一旦超过三层代码就开始难以追踪。优先用组合把对象作为字段而不是继承是更稳妥的做法。3. 手把手写一个完整的类从定义到实例化3.1 类的结构拆解属性、构造、方法各司其职一个标准的类通常由三部分组成属性字段、构造方法、普通方法。我拿一个“学生”的例子完整走一遍。class Student: school 第一中学 # 类属性所有学生共享 def __init__(self, name, age): # 构造方法 self.name name # 实例属性 self.age age self.courses [] # 每个学生自己的课程列表 def enroll(self, course): # 普通方法 if course not in self.courses: self.courses.append(course) def introduce(self): # 普通方法 return f我叫{self.name}今年{self.age}岁就读于{self.school} classmethod def from_string(cls, info): # 类方法 name, age info.split(,) return cls(name, int(age))这里有几个细节值得单独说。类属性 vs 实例属性school是类属性写在类的顶层所有实例共享同一份name、age、courses是实例属性写在__init__里每个对象各有一份。混用会出问题比如把courses []写在类属性位置所有学生就会共享同一个列表一个学生选课全班都显示有课这个坑我踩过不止一次。构造方法__init__在对象创建时自动调用负责初始化实例属性。Java 里构造方法名必须和类名一致Python 用固定的__init__JavaScript 用constructor概念是一样的。类方法用classmethod装饰第一个参数是cls代表类本身常用作“备用的构造入口”。比如上面的from_string可以从张三,18这样的字符串直接创建学生比在外面手动拆字符串再 new 更整洁。3.2 对象的创建过程与内存里发生了什么执行s Student(张三, 18)的时候背后发生的事情大致是这样的解释器找到Student这个类。分配一块内存用来存放这个新对象。自动调用__init__把张三、18传进去。__init__内部把name、age、courses设置到这块内存上。把这块内存的引用赋给变量s。理解这个过程能解释很多现象。比如为什么两个对象改属性互不影响——因为它们指向不同的内存块。再比如为什么s1 s2在 Java 里可能是false——因为比的是引用地址不是内容要比较内容得用equals。Java 里创建对象还要多一步“类加载”JVM 在第一次使用某个类的时候会把类的字节码加载进内存验证、准备、解析、初始化然后才轮到创建对象。这也是为什么 Java 启动时会有一段时间在“加载类”而 Python 这种解释执行的语言没有明显的这一步。Student s new Student(张三, 18);这行 Java 代码里new关键字负责在堆上分配内存、调用构造方法Student s只是声明了一个引用变量。如果你只写Student s;而不 news就是 null调用任何方法都会抛NullPointerException。这是 Java 新手最常见的报错之一。3.3 方法调用、参数传递与返回值处理方法的调用看起来简单但参数传递的细节经常让人翻车。Python 和 Java 在参数传递上都是“值传递”区别在于传的是值本身还是引用的副本。def change_num(n): n 100 def change_list(lst): lst.append(100) x 1 change_num(x) print(x) # 输出 1没变 y [1, 2] change_list(y) print(y) # 输出 [1, 2, 100]变了为什么数字没变而列表变了因为x传进去的是值的副本函数里改的是副本y传进去的是列表引用的副本副本和原变量指向同一个列表所以往列表里追加元素原列表能看到。但如果你在函数里写lst [9, 9]那只是把副本指向了新列表原列表不受影响。Java 里也是一样的道理public void modify(int n) { n 100; // 改的是副本 } public void modifyList(ListInteger list) { list.add(100); // 改的是同一个列表对象 // list new ArrayList(); // 这行不会影响外部 }方法的返回值也是需要注意的地方。Python 默认返回NoneJava 的void方法没有返回值有返回值的方法必须显式return。如果方法声明了返回类型却忘记 returnJava 会直接编译报错Python 则返回None导致后续调用出错。这种问题在调试时往往表现为“为什么我拿到的是 None”顺着调用链往回找就能定位。提示要修改外部变量就返回新值让调用方接收要修改对象内部状态就调用它的方法。别指望通过“重新赋值参数”来影响外部那是最常见的一种无效操作。4. 不同语言里的类与对象差异对照4.1 Python、Java、JavaScript 的写法对照同样一个“学生选课”的功能三种语言的写法差异能让你更清楚哪些是概念本身哪些是语法细节。对比项PythonJavaJavaScript定义类class Student:public class Student {}class Student {}构造方法def __init__(self, ...)public Student(...)constructor(...)当前对象引用selfthisthis访问修饰符靠下划线约定private/public/protected靠#或约定创建对象Student(...)new Student(...)new Student(...)继承语法class A(B):class A extends Bclass A extends B举一个具体例子对照class Student { constructor(name, age) { this.name name; this.age age; this.courses []; } enroll(course) { if (!this.courses.includes(course)) { this.courses.push(course); } } introduce() { return 我叫${this.name}今年${this.age}岁; } } const s new Student(张三, 18); s.enroll(数学); console.log(s.introduce());JavaScript 的this是最容易出问题的因为它跟调用方式绑定。s.introduce()里的this是s但如果你把introduce单独拿出来赋值给一个变量再调用this就丢了会变成undefined。这也是为什么现代 JavaScript 里经常看到箭头函数箭头函数不绑定自己的this会沿用外层的。Python 的self虽然也是显式的但通常不会丢因为它就是第一个参数位置固定。Java 的this更安全编译器会帮你盯着。所以如果对this没有把握写 JavaScript 的时候尽量用箭头函数或者用bind固定上下文。4.2 那些容易踩的坑this、self、静态方法、修饰符坑一静态方法里不能用 self / this。静态方法属于类本身不属于任何对象所以调用时没有“当前对象”这个东西。Python 里静态方法第一个参数不能是 selfJava 里静态方法里不能直接用this。如果你在静态方法里想访问实例字段编译或运行一定会报错。class Counter: total 0 staticmethod def add(a, b): return a b # return self.total a # 错误静态方法里没有 self坑二类属性被所有实例共享。前面提过的空列表问题再强调一次class Bad: items [] # 类属性 def add(self, x): self.items.append(x) a Bad() b Bad() a.add(1) print(b.items) # 输出 [1]b 也被影响了正确做法是把可变类型放到__init__里作为实例属性。这个坑不只是新手会踩写了好几年的人偶尔也会中招尤其是从其他语言转过来、凭直觉写的时候。坑三Java 里构造方法不能被继承。父类的构造方法不会自动成为子类的构造方法子类需要自己写构造方法并用super(...)调用父类构造。如果父类没有无参构造子类又不显式调用super编译就会失败。public class Person { protected String name; public Person(String name) { this.name name; } } public class Student extends Person { public Student(String name) { super(name); // 必须显式调用 } }坑四Python 里类名和变量名重名。比如list [1,2,3]之后list(...)就用不了了因为内建的 list 类被覆盖了。类似地type 学生之后想调用type()也会失败。给变量起名字时避开内建名称是 Python 写代码的一条基本纪律。5. 常见问题与排查技巧实录5.1 新手最常犯的五个错误错误一忘记实例化直接对类调用方法。class Greeter: def hello(self): print(你好) Greeter.hello() # 报错缺少 self Greeter().hello() # 正确先创建对象再调用Python 里如果直接对类调用实例方法解释器会把类本身当成第一个参数传进去但方法期望的是一个实例于是报错。看到“missing 1 required positional argument: self”这种提示八成就是忘了加括号实例化。错误二Java 里 null 调用方法。Student s null; s.introduce(); // NullPointerExceptionJava 里 null 表示“没有指向任何对象”对它调用任何方法都会抛NullPointerException。排查方法是在调用前加判空或者用Optional包装。日志里看到NullPointerException第一反应就是去找哪个对象没被正确初始化。错误三改了对象字段但发现没生效。如果你在方法里写obj new SomeClass()只是把局部引用指向了新对象外部的obj还是指向老对象。要真正修改得改对象内部的字段或者把新对象返回给调用方。错误四子类方法签名写错导致没覆盖父类方法。Java 里Override注解能帮你检查。Python 没有编译期检查只能靠测试。如果发现多态没生效先确认方法名、参数个数是否和父类一致。错误五类属性被意外修改。前面讲过可变类属性共享是高频事故。写类的时候如果某个属性每个对象应该有独立的一份就放__init__里不要图省事写在类顶层。5.2 调试时怎么快速定位类和对象的问题遇到跟类、对象、方法相关的问题我一般按这个顺序排查排查方向具体做法常见结论对象是否为 null / None打印对象本身忘了 new 或忘了实例化方法是否真正被调用在方法开头加打印调用链写错或条件没满足属性是否被正确初始化构造方法里逐个打印构造参数顺序写错方法签名是否正确对比父类定义名字或参数不匹配类属性是否被共享创建两个对象对比可变类型写成了类属性还有一个很实用的技巧用dir()看对象有哪些属性和方法Python或者用 IDE 的类图视图Java 里 IDEA 按 CtrlH 能看继承关系。很多时候你以为是 bug实际上是方法名记错了查看一下方法列表就清楚了。提示调试类相关问题时最有效的习惯是“先打印对象身份”。Python 用id(obj)Java 用System.identityHashCode(obj)能快速区分你操作的是不是同一个对象。6. 我在这几年里总结出来的几条经验写到这里关于类、对象、方法的硬核内容基本讲完了剩下几条是我自己用血泪换来的体会不算技术点但可能比技术点更值钱。第一条不要为了用类而用类。如果一个功能只是几个函数加几个变量就能解决没必要硬套面向对象。类适合管理有状态、有生命周期、需要复用的东西不适合包装无状态的工具函数。我见过太多项目里堆着一大堆只有一个方法的类读起来比面向过程还累。第二条一个类只做一件事。刚开始写类的时候很容易把“跟学生相关的所有操作”全塞进Student类里结果它既管选课又管考试还管发通知几百行代码滚在一起。后来我学会了按职责拆分Student只管自己的数据选课逻辑放CourseService通知逻辑放Notifier。类小了改起来才敢动手。第三条先想清楚对象之间怎么交互再动手写代码。这一点很多人忽略。写之前先问自己这件事里有几个角色每个角色需要维护什么数据它们之间谁调用谁。想清楚了再开写比写一半推倒重来省太多时间。第四条多看成熟项目的类设计。语言自带的类库就是最好的教材。比如 Java 的ArrayList、Python 的dict看看它们暴露了哪些方法、哪些数据被藏起来了慢慢就能找到设计的手感。
返回列表