ARTICLE DETAIL

资讯详情

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

自学编程第33天:从类到抽象类、接口与类加载,一文理清

自学编程第33天:从类到抽象类、接口与类加载,一文理清 1. 一个人学到第33天为什么还在跟“类”较劲今天是自学的第三十三天。晚上十一点屋子里只剩屏幕的光我打开笔记本写下今天要整理的主题各种类。说实话一个人坚持学到这个阶段最大的敌人不是难度本身而是那种“感觉自己什么都没学会”的焦虑。前两天看视频课讲面向对象老师敲了一个Dog类又敲了一个Cat类然后说“这就是类”。我看懂了关上视频自己动手写傻了。class后面跟什么、self到底指谁、为什么别人代码里的类长得那么奇怪——脑子里全是浆糊。于是今天哪都没去专门跟“类”这个词死磕了一天。从普通的类的定义到抽象类、接口、枚举类、匿名内部类再到类加载、类图、多线程下的类凡是搜索框里和“类”沾边的内容我几乎都翻了个遍。整理完才发现这个过程本身比结果重要得多一个人自学最大的障碍不是没人教而是没人为你划出“今天该学哪一片”。所以这篇东西不是一个系统性教程更像一个自学者的第33天复盘。我把今天理清楚的“各种类”按自己的理解重新讲一遍也把过程中踩到的坑、搜到的奇奇怪怪的东西都记录下来。如果你也在一个人学编程尤其学到面向对象这块觉得卡壳那这篇应该能给你一点参考。先说一下今天为什么选“类”而不是别的主题。搜“类”这个关键词的时候你能搜出一堆完全不相干的东西类宝可梦游戏源码、D类功放、SCI期刊分类、电赛控制类题目、短文本分类……这些都叫“类”但跟编程里的class完全是两码事。今天我只整理编程语境下的“类”也就是面向对象里的那个核心概念。1.1 自学容易掉进的“假懂”陷阱什么叫“假懂”就是你看了视频、看了书觉得自己懂了但合上资料让你自己写写不出来或者写出来了但不知道为什么这么写。我在“类”这个主题上假懂过好几次。最典型的就是对“类”和“对象”的关系。书上说“类是对对象的抽象对象是类的实例”这话我背得滚瓜烂熟但真要我说“抽象”体现在哪里我答不上来。后来我拿现实生活类比才稍微开窍类像是“一张房屋设计图”对象是根据这张图盖出来的“具体房子”。设计图规定了所有房子都有门、有窗、有面积但每扇门什么颜色、窗户朝哪个方向每栋房子都不一样。代码里的类也一样它规定一类的数据属性和行为方法但每次实例化出来的对象可以各自有不同的属性值。另一个假懂的点是“为什么要用类”。不用类也能写程序顺序往下写不就行了我从Python开始学一开始确实觉得函数就够了。但后来自学着做一个小项目要同时管理好几个角色的数据每个角色有名字、血量、攻击力还有各自的攻击动作。如果全用变量和函数散着写代码很快就乱成一团。这时候才体会到的意义它把数据和操作数据的方法打包在一起你拿到一个对象就同时拿到了“它有什么”和“它能做什么”。1.2 今天整理类的思路代码为主概念为辅今天的整理方法我没按教材的章节顺序来而是按自己实际写代码时遇到的顺序来先搞清楚类和对象的基本关系再看类有哪些常见变体抽象类、接口、枚举类、内部类然后用类图把关系画出来最后处理几个我确实遇到过的报错。这样整理的好处是每一个概念都有对应的代码场景不容易“背了概念用不上”。下面每一节都是我今天反复敲过、画过、查过的内容我把能直接抄的示例代码和我的理解放在一起希望能帮你把“类”这团线头扯开。2. 普通类、抽象类、接口三种容易混淆的“类”“类”这个主题绕不开的一个坎就是抽象类、接口和普通类到底有什么区别。我一开始以为这三样是三种完全不同的东西后来才发现它们都是“类家族”里的成员只是各自承担的责任不一样。2.1 普通类最基础的设计图普通类就是我们平时写最多的那个东西。它像一张完整的设计图既规定数据属性也规定行为方法而且可以被直接拿来创建对象。我用Python写了一个最朴素的例子class Dog: species Canis familiaris # 类属性所有实例共享 def __init__(self, name, age): self.name name # 实例属性每个实例各存各的 self.age age def bark(self): print(f{self.name} 在叫) d1 Dog(旺财, 3) d2 Dog(来福, 5) d1.bark() print(d1.species, d2.species)这个例子里最关键的是区分“类属性”和“实例属性”。species写在类体里是类属性所有实例共享name和age写在__init__里是实例属性每个实例独立。你创建一万个Dog对象species只有一份但每个对象都有自己的name和age。这个道理我一开始完全没注意直到后面写线程相关的代码才意识到类属性是天生的共享变量一个不留神就会被多线程搞出并发问题。这个坑后面专门讲。Python里还有一种classmethod修饰的类方法和staticmethod修饰的静态方法。类方法接收类本身作为第一个参数静态方法既不接收类也不接收实例。它们都可以通过类名直接调用区别在于类方法能访问、修改类属性静态方法更像一个放在类名字空间里的普通函数。class Dog: count 0 def __init__(self, name): self.name name Dog.count 1 classmethod def get_count(cls): return cls.count staticmethod def breed_info(): return 犬科动物2.2 抽象类不允许盖样板房抽象类是“半成品设计图”。它最大的特点是不能被直接实例化。你写了一个Animal抽象类里面可能有eat这种已经被实现了的方法也有make_sound这种只有方法声明、没有方法体的抽象方法。子类必须把抽象方法实现完才能被实例化。这样设计的目的是强制统一接口你不实现make_sound你的类就没法用。拿Java举例可能更清晰public abstract class Animal { public abstract void makeSound(); public void eat() { System.out.println(吃东西); } } public class Dog extends Animal { Override public void makeSound() { System.out.println(汪汪); } }这里我只写了Dog如果你想写一个Cat就必须也实现makeSound。抽象类的好处是把“所有动物都要会叫”这个约束定死同时允许eat这样的通用行为被直接继承。它和普通类最大的区别就一句话普通类是盖好的房子抽象类是半张图纸加几条硬性要求。2.3 接口一份纯契约不是“类”但总被拿来比严格说接口不算“类”但学“各种类”的时候根本绕不开它因为它经常和抽象类放在一起对比。接口把所有方法都只写成声明谁来实现谁自己填内容。Java里接口的关键词是interface一个类可以实现多个接口弥补了单继承的局限。public interface Flyable { void fly(); } public interface Singable { void sing(); } public class Bird implements Flyable, Singable { Override public void fly() { System.out.println(飞); } Override public void sing() { System.out.println(唱歌); } }我自己的理解是抽象类强调的是“是什么”is-a接口强调的是“能做什么”can-do。Dog继承Animal说Dog是一种AnimalBird实现Flyable接口说Bird具备了飞的能力。实际开发里的习惯是能用接口的地方优先用接口因为接口的约束更松、组合更灵活这也是为什么搜“抽象类和普通类的区别”时答案里总是会连带着讲接口。3. 枚举类和内部类写代码时真正高频的“各种类”普通类、抽象类和接口是概念上的重头戏但说实话日常写代码真正用得多的还有另外两种“类”枚举类和内部类。它们解决的问题非常具体属于那种“不知道有它们也能写但知道了会舒服很多”的东西。3.1 枚举类把散落的常量收进一个有边界的集合你有没有遇到过这种情况一个用户状态你用字符串表示一会写success一会又写成SUCCESS到了别的文件里又换成1。假设是个小型demo还能忍但代码一多这种散落的常量就是定时炸弹。枚举类enum解决的就是这个问题把所有可能的值集中定义在一个类型里编译器帮你检查拼写错误。Java枚举类的写法public enum OrderStatus { CREATED(已创建, 100), PAID(已支付, 200), SHIPPED(已发货, 300), DONE(已完成, 400); private final String desc; private final int code; OrderStatus(String desc, int code) { this.desc desc; this.code code; } public String getDesc() { return desc; } public int getCode() { return code; } }调用的时候就是OrderStatus.PAID.getDesc()你永远不会写错一个不存在的状态。我搜“java枚举类”的时候看到一句话说得特别到位枚举类是把一组有限常量升格成了类型安全的类。它也是类可以有构造方法、字段和方法只是实例的个数在定义时就已经固定。所以它本质上是一张“限量版设计图”——图纸上的房子只有那么几栋不能再多了。Python里虽然没有单独的enum语法但也有专门的模块from enum import Enum class OrderStatus(Enum): CREATED 100 PAID 200 SHIPPED 300 DONE 400用起来就是OrderStatus.PAID.value得到200支持遍历也支持通过值反查枚举成员。3.2 内部类藏在类里面的类主要用来解决“局部复杂度”内部类就是定义在另一个类内部的类。它存在的意义是有些类只服务于它的外部类没必要单独放在一个文件里放在外面反而污染命名空间。Java的内部类通常分四种成员内部类、静态内部类、局部内部类、匿名内部类。我学习时容易晕的是成员内部类和静态内部类的区别。成员内部类持有外部类实例的引用所以可以直接访问外部类的字段和方法静态内部类不持有外部类实例的引用它更像一个普通的独立类只是名字待在外部类的命名空间里。public class Outter { private String msg 外部类消息; class Inner { void print() { System.out.println(msg); } } static class StaticInner { void print() { System.out.println(静态内部类); } } }成员内部类需要用outer.new Inner()来创建静态内部类用new Outter.StaticInner()就能创建。这个差异的背后其实是引用关系成员内部类天生带着一个指向外部类对象的引用所以它不静态静态内部类没有这个引用所以它在内存布局上更轻。3.3 匿名内部类与lambda从啰嗦到简洁匿名内部类是我今天觉得最绕的一个概念。后来我也理解它的本质了它就是一个没有名字的内部类通常用来一次性实现某个接口或父类。老式Java里创建一个线程要写匿名内部类代码很啰嗦Thread t new Thread(new Runnable() { Override public void run() { System.out.println(线程执行); } });后来Java引入了lambda表达式专门简化这种“接口里只有一个抽象方法”的场景Thread t new Thread(() - System.out.println(线程执行));搜“java lambda调用内部类示例”的时候我看到一种更实际的应用lambda把外部类的局部变量捕获进来时要求这个变量必须是final或“事实final”也就是赋值后不再改变。比如在Android里写一个点击事件要在回调里使用局部的position这个变量就不能再被改。倒是匿名内部类也有同样的限制毕竟捕获变量本质上是在编译器层面复制了一份“不可变副本”。搞懂了匿名内部类和lambda的关系很多旧的教程代码看起来就没那么吓人了。它们不是两套完全不同的机制lambda只是其中一个特例的语法糖接口只有一个抽象方法既然实现代码就一行那连类名和方法名都不用写了写逻辑就完事。4. UML类图把脑子里纠缠的类关系画到纸上今天学到一半我发现光看代码已经理不清类跟类之间的关系了。Dog继承了AnimalBird实现了FlyableOrderStatus又是一个枚举类谁能记住谁依赖谁这时候就得用图。UML类图就是画“类的结构图”的标准语言。4.1 为什么一定要画类图有经验的开发者可能已经习惯了脑子里直接建模但自学的人阶段性地画一画类图真的能让思路清晰不少。画图相当于把脑子里隐性的设计判断显性化你画一个类就得明确它有哪些字段、哪些方法、方法的可见性你画类之间的关系就得想清楚它是继承还是聚合还是简单依赖。这些思考如果不画图很容易在写代码时被“先写再说”糊弄过去。4.2 六种常见关系的箭头含义UML类图里的关系核心是记住六种箭头的画法继承泛化实线加空心三角箭头从子类指向父类强调“is-a”。实现虚线加空心三角箭头从实现类指向接口强调“can-do”。关联实线普通箭头表示一个类知道另一个类的存在强调长期引用比如一个员工类持有部门类的引用。依赖虚线普通箭头表示临时使用比如一个方法参数中用到某个类强调“用到就用一下”。聚合实线加空心菱形菱形在整体那端表示整体和部分可以分离比如班级与学生班级没了学生还在。组合实线加实心菱形菱形在整体那端表示强归属关系整体没了部分也没了比如人与心脏。这个区别是我今天画图时最容易搞混的。聚合和组合很像关键判断点就是“部分能不能脱离整体单独存在”。学生离开班级还能存在这是聚合心脏离开人就没法存在这是组合。4.3 工具用staruml和IDEA反向生成类图画类图的工具搜“staruml类图怎么画”的人不少staruml确实是一款免费开源工具适合不写代码、专门画UML图的人。我的经验是画图时一定要把“属性、方法、可见性”这三个层次的展示都打开否则画出来的图只是一个概念图没法对应到代码。如果你用的是IntelliJ IDEA那有一个更省事的做法直接在类名上右键选择Diagrams选择Show DiagramIDE会自动从代码生成类图。我拿自己写的一个小demo试了一下所有继承、实现、依赖关系都用彩色实线画得清清楚楚比自己手工画准确得多。还有一个“idea打开的类可以排两行”的搜索词说的其实是在IDEA里同时打开多个类时标签可以设置成两行显示这样类与类之间的对照会舒服很多和画类图配合起来挺好用。画完图再看代码原来那种“类型设计是不是合理”的模糊感觉会变得特别具体。我有次看到一个类有五个箭头指向别的类立刻就觉得这个类负担太重了应该拆成两个。类图的价值就在这里它让“设计得好不好”这件事变得可见。5. 两个让我差点放弃的报错类加载失败与“找不到类”今天除了学习和整理概念还花了很多时间在报错上。我搜“类”的时候热搜词里清理出一大堆报错相关的内容比如“eclipse 找不到或无法加载主类 org.apache.catalina.startup.bootstrap”“maven编译项目报找不到类com.sun.image.codec.jpeg.jpegcodec”。这些报错都指向同一个基础机制类加载。5.1 ClassNotFoundException 与 NoClassDefFoundError 的区别这两个异常的名字中文翻译过来都叫“找不到类”但本质完全不同。ClassNotFoundException是Exception是程序在运行过程中主动用Class.forName()之类的方式去动态加载一个类但对应的.class文件在整个classpath里都找不到。它强调的是“运行时你试着去加载结果没有”。NoClassDefFoundError是Error是JVM在编译或类初始化阶段明明能找到这个类但真正要用它的时候发现它已经不在classpath里了或者这个类的静态初始化块抛了异常导致类初始化失败。它强调的是“类本来存在但用的时候没了”。有一个很常见的场景可以同时解释这两个问题项目里有A类和B类B类被A类引用。你把B类对应的源码删了但又没重新编译只把A类的.class文件拷到新环境运行时就会报NoClassDefFoundError因为你光有A的字节码但B的字节码没被带过去。5.2 Eclipse里Tomcat启动失败org.apache.catalina.startup.Bootstrap搜“eclipse 找不到或无法加载主类 org.apache.catalina.startup.bootstrap”的时候我查了一下这类问题的最常见原因eclipse配置Tomcat时项目运行库缺了servlet-api.jar或者Tomcat运行库的路径在移动后已经失效。解决办法通常是把Tomcat从服务器列表里删掉重新添加一遍让eclipse重新生成运行配置然后在项目构建路径确认servlet相关依赖存在。这个问题的本质还是classpath不对。Tomcat的Bootstrap类是启动入口它不在你的项目里而在Tomcat安装目录的lib文件夹里。如果你在运行配置里把Tomcat运行库指定到一个已经不存在的路径那eclipse自然就找不到主类。遇到这种报错先检查运行配置里的服务器路径别急着改代码。5.3 Maven编译报错找不到com.sun.image.codec.jpeg.JPEGCodec这个报错是另一个类型的问题。com.sun.*是JDK内部APIJPEGCodec曾经是处理JPEG图片的类但它在JDK 9之后已经被移除。如果你的项目里用了JPEGCodec在旧JDK 8上还能编译换到高版本JDK就报“找不到类”。解决方式不是去硬找一个老jar而是把代码从com.sun.image.codec.jpeg迁移到标准API比如用javax.imageio.ImageIO。这里我想多说一句搜到这类报错时要分清楚是代码本身出问题还是依赖环境出问题。JPEGCodec这种属于前者——代码用了不该用的内部API就算你暂时把编译搞定将来换环境还会再炸。同样的问题也存在于很多老教程里遇到老代码报“找不到类”时先搜一下“这个类是什么时候被移除的”往往比找jar更有效。5.4 “明明代码都在启动却报类不存在”还有一个热搜词是“java启动项目报类不存在明明代码都在这是为啥”这个我太有感触了。前几天我自己就遇到一次代码文件在IDEA里看得清清楚楚但一运行就提示找不到某个类。后来发现是多模块Maven项目的问题当前模块依赖了另一个未安装到本地仓库的模块IDE里虽然能识别出依赖关系但运行时的classpath里压根没有那个模块的产物。这种情况的常规处理方法是先对依赖的模块执行mvn install把构建产物装进本地仓库再重新加载项目。如果还不行就要做一次彻底的mvn clean因为有可能旧版本的.class文件卡在target目录里IDEA的增量编译并没把删除掉的类同步移除。清理旧的编译产物让编译器从零开始重新生成所有类文件通常能解决很多“明明代码都在却找不到”的诡异问题。我现在的习惯是遇到类加载类报错第一步先看classpath配置第二步看JDK版本第三步看产物目录最后才考虑是不是代码写错。这个顺序能省掉很多瞎折腾的时间。6. 多线程下的类静态属性和实例属性可不是一回事今天整理“各种类”时有一类特殊的问题让我特别留了个心眼类的线程安全。搜“java类线程安全”和“python类属性、类方法和静态方法”的时候我才意识到单线程下我们再熟悉不过的类一旦放到多线程环境里很多默认行为都会变成坑。6.1 一个让我后背发凉的并发案例假设你写了一个计数器用类属性来存当前计数public class Counter { public static int count 0; public static void increment() { count; } }然后你开十个线程每个线程调用increment()一万次你猜count最后等于多少直觉上应该是十万但实际跑下来经常会比十万少。原因很简单count在JVM层面不是一步完成的而是“读取出当前值、加一、写回去”三步。两个线程同时读到count0各自加一后又同时写回1于是明明加了两次结果只加了1。这就是典型的竞态条件。问题出在哪出在我前面提到的类属性它是被所有实例共享的。如果count是实例属性那每个实例各自维护各自的计数互不干扰但静态的类属性天然就是全局共享的任何一个线程都能改它而且没有额外同步机制的话修改不是原子的。6.2 类属性、实例属性各自的使用边界通过这个案例我对类属性和实例属性有了更深的体会实例属性每个对象一份天然是线程隔离的。如果你写业务逻辑时能尽量把状态放在实例属性而不是类属性里并发出问题的概率会小很多。类属性所有对象共享一份适合放全局配置、常量、注册表这类数据但在多线程里必须主动处理同步问题。Python里的类属性也是同样的逻辑。我之前用class Dog写一个记录所有实例数量的count在单线程demo里没问题但一旦将来项目里出现多线程创建Dog对象那个Dog.count 1就会遇到和Java里一样的竞态。Python的GIL虽然让单个字节码操作看起来安全但count 1在GIL释放和重新获取之间同样能被打断依然有并发风险。6.3 线程安全的三条实用解法如果确实需要在多线程环境下共享一个类级别的计数或状态我学到三条比较靠得住的方案改用原子类。Java的AtomicInteger把自增操作做成了原子操作直接用count.incrementAndGet()比加锁更轻量。Python里可以用threading.Lock配合with语句。加同步块。Java可以用synchronized修饰方法或者用ReentrantLock锁住关键区域。好处是灵活坏处是自己必须记得锁和释放锁容易出死锁。重新设计能不用共享状态就不用。最常见的替代思路是每个线程维护自己的局部变量最后再合并结果。比如计数器案例可以让每个线程各算各的最后把十个结果加起来。这其实就是分治思想的朴素版也是很多高性能框架避免锁竞争的底层思路。我自己现在写代码时多了一个习惯凡是看到静态变量先问一句“这个变量真的需要被所有实例共享吗”然后问第二句“多线程下这个共享安全吗”。这个习惯帮我避免了不止一次线上级别的失误。7. 第33天关于“一个人坚持”这件事的真实体会最后这段不聊技术了聊聊我自己。一个人学到第33天最难的是什么不是某个语法看不懂不是报错调不出来而是那种“我到底在干嘛”的虚无感。没有同班同学可以比较进度没有老师布置作业没有代码评审告诉你哪里写得不好。你写完一段代码跑通了然后呢它没有上线没有用户没有任何正反馈。全靠自己给自己找目标自己给自己当监督。我盘点了一下自己能坚持33天的几个土办法未必科学但真实有效。第一个办法是把目标切成特别小的块。我不再定“一个月学会Java”这种空目标而是定“今天搞懂抽象类和接口的区别”“今晚写一个OrderStatus枚举类”这种当天就能见到结果的任务。小任务的好处是结束时有明确的完成感哪怕只有十分钟的“松了一口气”也能支撑你进入第二天。第二个办法是写自己的答案而不是抄答案。今天这篇整理其实就是在逼自己把“各种类”的知识用自己的话讲出来。很多人觉得做笔记是把教程里的重点抄一遍不是的那样抄完第二天照样忘。真正的笔记是“我能让明天的自己看懂这段代码”。我自己重看一周前的笔记如果完全看不懂当时在写什么说明那次学习其实是无效的。第三个办法是接受“慢”是常态。33天我在类这个主题上花了整整一天才勉强理出框架换成培训班的教学进度这大概是课堂上45分钟的内容。但我最近越来越认可一个说法自学的进度不能按知识点数量算要按“你能独立讲清楚多少”来算。慢不丢人假懂才是真浪费时间。我看过有人两个月刷完“Java全套视频课”但连一个简单的Maven项目都跑不起来。这应该不是学习能力问题是节奏问题。今天是第三十三天一个人坐在电脑前屏幕的光照得眼睛发酸。我关掉备忘录顺手写下了明天的计划把今天画的那张类图用代码实现一遍。然后跑起来再改坏再修好。我想这就是一个人坚持的全部秘密不是突然变得多自律而是每天都把“再学一点”变成一件不太难的事。
返回列表