类和对象(下)实战:从构造到回收,把对象思维落到代码里 📅 发布时间:2026/9/9 18:32:14 👁 浏览次数: 类和对象下——把“对象思维”真正落地到代码里如果你能把“类是模板对象是实体”这句话背得滚瓜烂熟但在写项目时还是不知道该把某段逻辑放在哪个类里、该用抽象类还是接口、为什么别人的代码里到处都是匿名类和内部类——那这篇文章就是给你准备的。上篇把类与对象的基础概念、封装继承多态都过了一遍这篇不再重复那些入门知识。我直接按实战项目的思路把“类和对象”下半场的高频考点和日常开发容易踩的坑拆开讲构造方法的细节、static和final的边界、抽象类与接口怎么选、匿名类和内部类的使用场景、String等常用类的底层逻辑、对象从创建到回收的完整生命周期以及对象判断、去重、复制这些每天都在用但未必弄明白的操作。这篇更适合已经写过一些代码、能独立完成简单模块但想进一步提升代码质量和对象设计能力的人。当然如果你刚学完语法这篇也能帮你把知识点串起来省去很多试错时间。1. 类设计的几个高频细节很多人栽在这里类的设计不是把字段塞进去、生成getter/setter就完事了。平时写代码最频繁碰到的几个关键字和编码习惯恰恰决定了代码是能跑还是能维护。我先从构造方法和几个修饰符聊起。1.1 构造方法不只是new的时候才执行构造方法是对象诞生的第一道关卡但它有三个细节很容易被忽略。第一如果类里没有任何构造方法编译器会默认送一个无参构造。但只要你手写了任何一个带参构造默认的无参构造就没了。这个坑在Spring等框架里特别常见——框架要靠反射调用无参构造来创建对象你手写了一个带参构造又没补无参构造启动时直接报错。第二构造方法之间可以用this(...)互相调用子类构造方法里第一行必须调用父类构造方法用super(...)即使你不写编译器也会偷偷补一个super()。所以父类如果没有无参构造子类就会编译报错。我见过好几个人在这个问题上卡了半天报错信息明明写着“Implicit super constructor is undefined”愣是没反应过来是父类构造方法的问题。第三构造方法不是用来做复杂业务的。初始化字段、校验必要参数、建立初始状态这些没问题。但如果在构造方法里调用可被重写的方法就会触发动态绑定导致子类字段还没初始化就被调用抛出空指针。这段位高一点的开发者都会避开但新手很容易踩。1.2 static属于类不属于任何一个对象static修饰的成员属于类本身所有对象共享一份直接用类名.成员访问。这个规则简单但实际开发里有两个非常容易搞混的场景。第一个是static变量的初始化时机。static变量在类加载阶段就完成了准备和初始化跟new了多少个对象无关。你创建一万个对象static变量也只有一份你一个对象都不创建static变量照样存在。很多工具类里的计数器、全局配置、连接池用的就是这个特性。第二个是static方法里不能直接访问非static成员。原因很直白——static方法不依赖对象而实例成员的访问必须依托具体对象。如果你在static方法里非要访问实例字段必须自己先new一个对象或用参数传进来。Java的main方法之所以是static就是因为在main执行的那一刻还没有任何对象存在程序要在这个入口里自己创建对象再启动。还有一点static代码块在类加载时执行一次常用于初始化静态资源。我以前在项目里做过一个数据字典加载器就是把配置放在static块里一次性读进内存后续所有对象直接复用效率比每次new对象都去查一遍数据库高出不少。1.3 final不可变性的边界final可以修饰类、方法、变量含义都是“锁住”final类不能被继承final方法不能被重写final变量的值一旦赋值就不能改。这里最关键的两个应用场景一是定义常量比如public static final int MAX_SIZE 100这种写法在项目里到处都是二是设计不可变类典型代表就是String和包装类。不可变类的价值在于线程安全和数据可靠性——对象一旦创建内部状态就不会再变多个线程同时读不会有并发问题作为HashMap的key也不会因为hash值变化而丢失数据。但final变量有个小坑如果final修饰的是引用类型锁住的是引用指向而不是对象内部状态。也就是说final ListString list new ArrayList()你不能让list指向别的List但你仍然可以往list里add元素。很多人把这个搞混以为final了就彻底不能改了结果在并发环境下还是踩了数据被修改的坑。1.4 构造器、静态成员、final修饰符的应用对照知识点可访问性初始化时机典型用途常见坑构造方法public/protected/privatenew时执行初始化字段、建立对象状态手写带参构造后丢默认无参构造static成员类名直接访问类加载时工具方法、全局配置、共享计数static方法里直接访问实例字段final变量按声明位置决定声明时或构造器中常量、不可变引用final引用指向的对象内部仍可变static final常量类名直接访问类加载准备阶段系统常量、枚举替代布尔/数值常量编译期直接替换1.5 工具类、常量类的设计心得在实际项目中类可以分为实体类、业务类、工具类、配置类这几种角色。设计时有一个基本原则一个类只承担一种角色。工具类通常把构造方法私有化防止别人new再配上static final修饰的方法确保不依赖对象状态常量类统一管理魔法值比如订单状态、错误码写成public static final int ORDER_PAID 2比散落的数字字面量好维护十倍。我还见过一种做法把工具类的方法做得“太重”里面调用了一堆外部服务导致单元测试根本没法测。正确的思路是工具类尽量无状态、无依赖只处理传入的参数。一旦工具方法开始依赖数据库、缓存、第三方接口它就不该叫工具方法应该放到对应的业务服务类里。2. 抽象类与接口两个“不完整”的类怎么选才不拧巴抽象类和接口都是“不完整的类”都用来定义规范、让子类实现细节。但它们的设计意图完全不同选择的标准也不难记。2.1 抽象类和普通类的区别在哪里抽象类用abstract修饰核心特点是“可以有抽象方法也可以有普通方法、字段和构造方法”。抽象方法只有声明没有实现子类继承后必须实现除非子类也是抽象类。普通类没有抽象方法可以直接new抽象类不能被new只能被继承。这个区别决定了抽象类的定位它是一个“基类”把子类共有的状态和行为沉淀下来同时把某些步骤的差异部分留给子类去实现。举个例子做一个报表导出功能Excel和PDF的导出流程几乎一样都是查数据、组装、写文件只有“写文件”这一步不同。这时候就可以建一个抽象类ReportExporter把查数据、组装数据的公共逻辑写好把export(Data data)声明成抽象方法由ExcelExporter和PdfExporter分别实现。2.2 接口能飞、能游、能跑都可以接口在Java 8之后有了default方法和static方法但在核心定位上接口仍然是一份能力契约——只关心“能不能做这件事”不关心“怎么做”。一个类可以实现多个接口但只能继承一个抽象类。选择的标准很简单如果你要表达的是“是什么”is-a关系比如狗是动物用抽象类如果你要表达的是“能干什么”can-do关系比如狗能游泳、能看家用接口。实际开发里接口更常用因为接口解耦能力强——上层代码面向接口编程不关心底层的具体实现类是谁。Spring框架里你定义Service接口Controller里注入的是接口至于底层是用MyBatis还是JPA实现对Controller完全透明。2.3 抽象类还是接口三个真实场景帮你判断场景一多个类有大量公共代码。这时候优先用抽象类把公共方法放基类子类继承复用。如果硬用接口要么每个实现类重复写一遍要么靠default方法硬塞代码会很别扭。场景二只定义行为标准不关心内部状态。比如一个数据校验接口Validator里面就一个boolean validate(Object obj)方法谁想实现谁就实现完全没必要用抽象类。场景三既要共享代码又想实现多个能力。Java的类只能继承一个抽象类但可以实现多个接口。所以通常的组合拳是用一个抽象类做骨架让具体类继承它再用多个接口定义不同维度的能力让具体类去实现。JDK里的HashMap就是这么干的——继承AbstractMap同时实现Map、Cloneable、Serializable等接口。2.4 实战中的接口设计建议接口设计有一个容易犯的错叫“接口膨胀”。也就是接口里堆了一堆方法每个实现类都必须实现但很多方法跟当前实现类根本没半毛钱关系。解决思路是拆分接口——用户管理拆成读接口和写接口权限校验拆成独立的接口。这叫接口隔离原则比硬凑一个大接口好维护得多。另一个心得是接口命名不要用I开头比如IUserService这是比较老式的写法现在主流风格就是UserService。方法命名要动词开头findById、save、deleteById看一眼就知道干什么。接口上可以加FunctionalInterface注解表明这是一个函数式接口只允许一个抽象方法方便后面用Lambda表达式替代匿名类写更简洁的代码。3. 内部类与匿名类藏在类里的类到底图什么很多人第一次看到new Runnable() { Override public void run() { ... } }这种写法时一头雾水“这不是在new一个接口吗”其实这就是匿名类的典型形态——一个没有名字的、临时的类当场定义当场实例化。3.1 什么是内部类为什么需要它内部类就是定义在类内部的类。它能直接访问外部类的所有成员包括私有字段和方法因为它持有一个对外部类对象的隐式引用。这个特性在某些场景下非常好用比如迭代器——ArrayList的内部迭代器类直接访问ArrayList的底层数组不需要额外暴露任何内部结构。内部类按位置分为四种成员内部类定义在类里方法外、静态内部类加了static、局部内部类定义在方法里、匿名内部类没有名字的局部类。其中静态内部类不持有外部类引用和独立类的区别主要是命名空间上的组织比如Map.Entry就是一个典型的静态内部接口。3.2 Java匿名类在实际项目中的用法匿名类的核心价值是“一次性使用”不需要单独建文件也不需要起类名。Java里最经典的场景有三个创建线程、事件监听、自定义比较器。我用得最多的是比较器。对一个对象数组按某个字段排序以前要单独建一个类实现Comparator接口现在直接写ListUser userList getUserList(); userList.sort(new ComparatorUser() { Override public int compare(User u1, User u2) { return Integer.compare(u1.getAge(), u2.getAge()); } });这段代码的意思就是临时创建一个实现了Comparator接口的匿名类重写compare方法然后作为参数传给sort方法。Java 8以后这种写法可以进一步简化为Lambda表达式userList.sort((u1, u2) - Integer.compare(u1.getAge(), u2.getAge()));。匿名类有几个限制需要知道不能有显式的构造方法只能用默认构造器如果是匿名内部类它引用的外部局部变量必须是final或事实final的匿名类的字节码文件名会带数字比如Outer$1.class调试时如果堆栈里出现这类类名心里有数就行。3.3 C、Python、JavaScript、PHP里的“类中类”对照类中类这个概念不是Java专属。C没有内部类的语法糖但可以通过嵌套类nested class达到类似的组织效果C里嵌套类访问外部类的非静态成员需要显式传入外部类对象指针this因为它默认不持有外部类引用。Python的类定义里可以再定义类这种内部类常用于组织小工具类或常量分组访问外部类同样要显式传递。JavaScript的类本质是构造函数加原型链支持在方法里定义类或直接返回对象字面量函数式写法里更常见的是闭包而不是类。PHP的匿名类直接用new class关键字创建支持在定义时传构造参数。从这些语言对照可以看出内部类和匿名类的本质就是“把相关的代码放在离使用点最近的地方”Java做得最彻底其他语言各有自家的替代方案。跨语言写代码时拿到一个新语言先搞清楚它有没有匿名类、怎么写能少走很多弯路。3.4 内部类与内存泄漏一个必须警惕的坑内部类有个隐患非静态内部类会隐式持有外部类的引用如果内部类对象的生命周期比外部类还长就会导致外部类无法被垃圾回收这就是内存泄漏的常见来源之一。经典场景是在Activity或Fragment里写一个非静态内部类HandlerHandler持有外部类的引用而Handler又常驻在消息队列里结果Activity退出了内存还占着。解决办法有两个把内部类改成静态内部类不持有外部类引用需要访问外部类成员时用WeakReference包装一下或者像主流的MVVM架构里那样避免在长生命周期组件里使用非静态内部类。这个坑我当年排查了半天最后是堆转储dump堆内存才发现Handler一直在引用已经销毁的页面对象。4. 常用类的底层逻辑String和包装类凭什么特殊面向对象编程里有一批“类”太常用以至于很多人忘了它们也是类。String、包装类、Optional这些类不把它们的底层逻辑弄清楚写代码时会遇到各种莫名其妙的问题。4.1 String类的常用方法与不可变设计String类可能是Java里被用得最多、设计也最特殊的类。不可变性是它最核心的设计——一旦创建字符串内容就不能改。所以才会有字符串常量池的概念相同的字符串字面量在常量池里只有一份用比较常量池里的两个相同字符串时返回true用new String(abc)创建的字符串则是在堆里新建对象比较会返回false。日常开发里String类有几个高频方法必须熟练equals和equalsIgnoreCase比较内容split按分隔符切分字符串substring截取子串trim去两边空白indexOf找子串位置contains判断包含关系replace和replaceAll做替换String.join和StringBuilder做拼接。拼接字符串这个点要特别说少量的字符串拼接用没问题但循环里大量拼接一定不要用因为每次拼接都会创建新的String对象性能极差。正确做法是用StringBuilder或StringBuffer前者非线程安全但速度快后者线程安全但加锁有额外开销。单线程环境无脑用StringBuilder。4.2 包装类的自动装箱拆箱为什么会有隐藏的性能开销int、double这些基本类型不是类但它们有对应的包装类Integer、Double、Boolean。Java 5之后支持自动装箱和拆箱编译器会自动在基本类型和包装类之间转换写起来很方便但性能上有隐藏开销——装箱会创建对象拆箱有空指针风险。经典的坑是缓存范围Integer的valueOf方法对-128到127之间的值做了缓存在这个范围内Integer a 127; Integer b 127;用比较返回true但Integer a 128; Integer b 128;用比较返回false。这不是bug是JVM的缓存机制。判断两个包装类是否相等永远用equals不要用。还有包装类的空指针问题。比如Integer count null;如果代码里直接count 1编译不报错运行时报空指针因为自动拆箱调用的是count的intValue方法而count为null就会炸。在从数据库或前端接口拿值的时候这种空指针尤其常见。判空的习惯必须养成——后面我专门讲怎么优雅地判断对象为空。4.3 Optional用类来对抗空指针Optional是Java 8引入的一个容器类专门用来优雅地处理可能为空的值。它的核心思想是与其让你在代码里到处判null不如把“可能为空”这件事显式写进类型里强迫你处理。我常用的Optional操作有这些Optional.ofNullable(obj)把可能为空的对象包装成OptionalorElse(defaultValue)为空时返回默认值orElseGet(Supplier)为空时才去计算默认值比orElse更省资源map把一个值转换成另一个值空值不会执行filter按条件过滤ifPresent在值存在时执行操作。举一个实际例子从用户信息里拿用户的省份名称以前要写三层判空String provinceName null; if (user ! null user.getAddress() ! null user.getAddress().getProvince() ! null) { provinceName user.getAddress().getProvince().getName(); }用Optional可以写得更清晰String provinceName Optional.ofNullable(user) .map(User::getAddress) .map(Address::getProvince) .map(Province::getName) .orElse(未知);但注意Optional不是用来取代所有判空的它更适合做链式调用和返回值包装。不要给Optional对象本身再判空那属于画蛇添足。4.4 对象数组去重的三种写法对象数组去重是开发里非常高频的需求。比如从数据库查出一批用户对象要根据用户ID去掉重复的。这里的关键是不能直接new HashSet(list)因为HashSet默认用equals和hashCode判断重复而Object的默认equals比较的是内存地址。所以要让User类重写equals和hashCode方法或者自己去重。第一种写法重写equals和hashCode后用Stream去重。list.stream().distinct().collect(Collectors.toList());第二种写法用TreeSet加自定义比较器去重。TreeSetUser set new TreeSet(Comparator.comparing(User::getId)); set.addAll(list);第三种写法按某个字段手动去重用Map的merge方法简单粗暴MapInteger, User map new LinkedHashMap(); for (User user : list) { map.putIfAbsent(user.getId(), user); }如果你的需求是“同一个ID保留最新的一条”把putIfAbsent换成put就行。这个思路在很多数据清洗的场景里很实用。5. 对象的完整生命周期从类加载到垃圾回收对象不是凭空冒出来的从你写下一个类的代码到程序运行时new出一个对象再走到对象被回收销毁这条生命线值得完整过一遍。很多面试题问“类加载过程”“垃圾回收”其实就是在问对象的生死流转。5.1 new一个对象时JVM里发生了什么先澄清一个概念类加载发生在对象创建之前或同时。当JVM第一次使用某个类时会经过加载、验证、准备、解析、初始化这几个阶段把类的字节码读进内存在方法区生成Class对象并执行静态变量初始化和static代码块。然后是new关键字触发的实例化过程在堆内存中为新对象分配一块空间给对象的实例变量赋默认值int是0引用类型是null调用构造方法按构造方法里的赋值语句给字段赋真实值建立对象头信息包括锁状态、哈希码、GC分代年龄等。这个流程解释了前面提到的static初始化时机——static变量的初始化在类加载的准备和初始化阶段完成跟new多少个对象没有关系。也解释了为什么static方法不能直接访问非static字段——因为static方法执行时可能一个对象都还没创建出来。5.2 类加载的三层机制双亲委派模型说到类加载必须提双亲委派模型。JVM里有启动类加载器Bootstrap ClassLoader、扩展类加载器Extension ClassLoader、应用类加载器Application ClassLoader。一个类加载请求会先交给父类加载器父类加载不了子类才自己加载。这样设计是为了保证核心类库的稳定性——比如你自己写一个java.lang.String也不会把JDK的String顶掉因为启动类加载器会优先加载真正的String。这个机制在实际开发里的影响是如果依赖冲突导致同一份jar包里类不同版本并存或者自定义类加载器用的不当就会出现ClassCastException或NoSuchMethodError。排查时先看加载这个类的ClassLoader是谁往往能快速定位问题。5.3 对象何时才会被回收对象被回收的前提是“不再被任何存活的对象引用”。JVM的垃圾回收器会从GC Roots出发沿着引用链遍历能到达的对象是存活的不能到达的就是垃圾可以被回收。GC Roots包括虚拟机栈栈帧中的本地变量表中引用的对象方法区中静态属性引用的对象和常量引用的对象本地方法栈中JNI引用的对象以及活跃线程。这些对象是“根”从它们出发找不到的对象就是可回收的。所以一个常见的误解是局部变量赋null就能立刻让对象变成垃圾。实际上只要方法栈帧还没弹出局部变量表里的引用还指着对象对象就不会被回收。真正让对象可回收的是方法返回时栈帧弹出局部变量引用随之消失。写代码时不需要频繁把变量置null除非是某些内存敏感的长生命周期方法。5.4 不同语言的“对象销毁”差异Java、Python靠垃圾回收自动管理对象生命周期开发者基本不用操心对象销毁C里对象栈上分配时作用域结束就自动调用析构函数堆上分配的需要手动delete这是C最容易出内存泄漏的地方。现代C推荐用智能指针unique_ptr、shared_ptr管理堆内存做到“所有权清晰”。JavaScript的垃圾回收机制和Java类似但有一个经典陷阱闭包引用了外部对象导致外部对象一直被引用而无法回收。PHP脚本执行完进程结束所有内存都释放不太需要关心垃圾回收但长驻进程比如Swoole就得小心对象生命周期了。理解了这个差异写跨语言代码时就知道内存管理的关注点在不同语言里完全不一样面向对象的语言共性在于“对象创建需要初始化、对象结束要释放资源”只是方式不同。5.5 深拷贝和浅拷贝你真的会吗对象的“复制”是生命周期里最容易糊涂的一件事。在Java里直接newObj oldObj不叫复制只是把同一个对象的引用赋给了另一个变量两个变量指向同一个对象改一个另一个跟着变。这种叫“引用拷贝”。浅拷贝shallow copy会创建一个新对象但对象内部的引用类型字段仍然指向原来的对象。意思是外层换了地址内层还是共用。Java里实现浅拷贝的常见方式是重写clone()方法实现Cloneable接口或者用BeanUtils.copyProperties这种工具。深拷贝deep copy则会把对象内部引用的对象也一并复制内外全换新。深拷贝的实现有三种常见方案一是序列化对象流写入字节数组再读回来但要求所有相关类都实现Serializable二是用Jackson/Gson把对象转成JSON再反序列化回来这种方法对无参构造有要求三是递归手动复制每个引用字段。实际项目里如果对象嵌套层级不深用JSON序列化最省事如果性能要求高考虑专门的重构或手动复制。6. 工具与框架中的类对象技巧以及踩坑实录这部分聊聊我平时写代码、用IDE、调框架时攒下来的一些类和对象相关经验。不涉及太深奥的原理都是能立刻用起来的东西。6.1 IDEA生成类图和类配置的小技巧用IntelliJ IDEA做Java开发的人有没有试过快速查看一个类的继承结构和实现关系选中类名右键选择Diagrams选择Show Diagram就能生成类图接口、抽象类、实现类之间的关系一目了然。项目里类多了以后这个功能比读代码找关系快得多。IDEA里给类配置作者信息也很简单File - Settings - Editor - File and Code Templates在Includes里的File Header里写上创建时间、作者、描述新建类时自动带出。另外IDEA的Inspections默认会提示很多类设计的问题比如“Class with only private constructors should be final”“Unnecessary local variable”我建议保持默认开启它能在你写代码的时候就提醒一些设计味道问题。6.2 Spring等框架里的“对象”操作要点Spring框架本质上是一个对象容器——你定义BeanSpring帮你创建、注入、管理生命周期。几个高频问题值得玩味。第一Spring Bean的默认作用域是singleton整个容器里同一个Bean只有一个对象实例。如果你的Bean里放了可变状态多个线程共享这个状态就可能出并发问题。所以无状态的Service、Mapper用单例没问题绝不建议往Bean里塞有状态的可变字段。第二Bean的注入方式有构造器注入、Setter注入、字段注入。推荐构造器注入——它能让Bean在创建时就完整初始化且IDE和编译器都能帮忙发现依赖缺失test也更好写。第三“类型映射”和“对象转换”在Spring生态里是日常操作。比如从数据库查出的Entity要转成VO返回给前端手动写getter/setter太啰嗦用MapStruct或BeanUtils.copyProperties都可以。前者编译期生成映射代码性能好、类型安全后者反射拷贝写起来快但性能差点而且字段名不一致时会静默丢失。第四在网上搜“vite对象赋值页面不变”这类问题本质上是前端框架的响应式机制。Vue 2里直接给对象加新属性不会触发视图更新用this.$set才行Vue 3用Proxy实现响应式但如果你用reactive包裹对象后又整体重新赋值同样可能丢掉响应式。这类问题的核心是“赋值操作的时机和方式是否符合框架对响应式对象的设计预期”。遇到页面不更新的情况先去检查是不是新增了对象里原本不存在的属性。6.3 几个经典的类对象编写经验第一判断对象为空时能用工具类就不要手写。Java里推荐用Objects.isNull、Objects.nonNull或者Apache Commons的StringUtils.isBlank判字符串。自己手写if (obj ! null)没有错但代码里一多就啰嗦。重构方向是用Optional链式处理或者把判空抽成统一的方法。第二重写equals时一定要同时重写hashCode否则HashSet、HashMap等基于哈希的集合会出逻辑错误。这个规则上篇提过但值得再强调一次equals相等的两个对象hashCode必须相等hashCode相等的两个对象equals不一定相等。IDEA可以一键生成这两个方法但你要理解它的生成规则——通常按关键业务字段生成。第三定义实体类时行业惯例是只用包装类型Integer、Long、Double不要用基本类型int、long。原因是基本类型默认值是0无法区分“没被赋值”和“值正好是0”两种状态而包装类型为null说明字段“未设置”。数据库查询、JSON反序列化里这个区分很重要。第四避免过深的继承层次。三层以内是相对可控的超过五层的继承关系代码会变得很难读难维护。万一遇到必须复用但又不想硬继承的场景优先考虑组合——把公共逻辑抽成一个可注入的组件比硬拗继承关系优雅得多。6.4 实战里的对象操作答疑与避坑把搜索热词里几个典型问题串起来回答方便你直接对照自己的场景。问题一C模板类链表怎么写C里模板类和对象的关系是“编译时多态”创建一个模板类链表就是定义一个templatetypename T的节点结构然后实现插入、删除、遍历。模板类不是运行时多态所以不要在模板类里依赖虚函数实现链表的动态行为二者是两条路线。问题二python类对象最常用的特性是什么Python的类非常灵活可以随时给实例添加属性也支持类方法、静态方法、实例方法三种装饰器。我最常用的是类方法__init__和属性装饰器property——前者负责初始化后者把私有字段包装成只读属性做到“外部访问安全”。问题三orm框架里查询后对象转QueryWrapper是什么场景MyBatis Plus里QueryWrapper是专门用来构造查询条件的对象经常用Entity对象封装条件再转成QueryWrapper。比如QueryWrapperUser wrapper new QueryWrapper(new User().setName(张三));它会自动把非null的字段拼成等值条件。注意set进去的null字段默认不参与拼条件这在模糊查询、动态条件里很好用但也会导致某些本意是“条件为NULL”的查询失效需要显式调用isNull方法。问题四short文本分类里的对象怎么做文本分类和类对象的关系在于把文本转成向量、把标签转成分类对象。比如用一个字典类把文本映射成词索引再用Embedding对象查向量表示。本质上还是面向对象的设计——输入文本、特征提取、分类器、输出标签每一步都是一个对象互相协作完成任务。6.5 写类和设计类时的思维清单最后放一份我每次写类之前都会在心里过一遍的检查清单纯个人经验分享出来供你参考。这个类是不是承担了多个职责如果是先拆。 这个类的状态能不能保持不可变能的话就加final。 这个类需要被继承吗不需要就加final。 这个类的构造方法是否把对象初始化完整了有没有在构造方法里做复杂逻辑 这个类如果被多个线程共享有没有共享的可变状态 这个类的equals/hashCode/toString重写了吗重写得对不对 这个类暴露的public方法是不是都面向接口设计而不是依赖具体实现类 这个类的方法参数和返回值用包装类型了吗会不会有自动拆箱空指针风险每次写类的时候过一遍这些问题能达到8个以上满足基本就是一段经得起审查的代码。时间久了这些检查会变成肌肉记忆写出来的类自然清晰、稳定、好维护。我个人在实际项目里的深刻体会是类与对象从来不是语法问题而是思维问题。语法只是工具真正的对象思维是——每个类都有明确的边界每个对象都对自己负责类之间通过清晰的方法协作而不是互相抓字段。写完代码回头看那些“只做一件事”的类往往就是整个项目里最稳定、最好改、最不怕需求变动的部分。把一个类设计得刚刚好比堆出一堆花哨的继承层次和复杂的对象关系实用价值高得多。