news 2026/10/13 1:53:23

建造者模式详解:解决Java构造器膨胀与不可变对象构建的利器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
建造者模式详解:解决Java构造器膨胀与不可变对象构建的利器

先说个真实感受。工作里我维护过好几个“构造参数十几个”的业务对象,每次 new 出来都像在打密码,传错一个参数编译期根本发现不了,等跑到线上才炸出一堆 NullPointerException。后来把对象改成建造者模式实现,代码评审的时候同事都松了口气。今天这篇就把建造者模式掰开揉碎讲清楚,从它解决什么问题、核心角色怎么划分,到经典实现、链式写法、与工厂模式的区别,再到面试题和实战里踩过的坑,一次讲透。不管你是准备 Java 面试、软考,还是单纯想把代码写得干净点,这篇都适合你。

1. 为什么需要建造者模式

1.1 构造器膨胀问题

先还原一个最常见的场景。假设你在开发一个订单系统,订单对象一开始只有订单号、用户 ID、金额三个字段,构造器写起来很干净:

public Order(String orderId, Long userId, BigDecimal amount) { this.orderId = orderId; this.userId = userId; this.amount = amount; }

过两个迭代,需求加了收货地址、优惠券 ID、支付方式、备注、预计送达时间、是否开发票、发票抬头……字段从 3 个涨到 12 个。这时候你面临几个选择。

第一种,继续堆构造器重载。3 个参数的、5 个参数的、8 个参数的,疯狂重载。调用方根本分不清new Order(a, b, c, d, e)里的c到底是优惠券 ID 还是支付方式,可读性直接归零。

第二种,用 JavaBean 风格,也就是无参构造加一堆 setter:

Order order = new Order(); order.setOrderId("123"); order.setUserId(456L); order.setAmount(new BigDecimal("99.00")); order.setAddress(address); order.setCouponId("c001");

JavaBean 风格解决了参数顺序问题,但牺牲了不可变性。对象在创建之后可以被任意修改,一个本该在构造阶段就固定下来的订单号,可能被后续业务代码覆盖,排查起来非常痛苦。而且你没法在 setter 阶段做“必填字段校验”,因为对象是逐步拼起来的,什么时候才算拼完,代码里没有任何约束。

第三种还见过更极端的,直接 new 一个空对象,字段全靠反射赋值,代码跑起来跟猜谜一样。

建造者模式就是冲着这两个痛点来的。

1.2 建造者模式的核心价值

建造者模式的核心思想,是把“复杂对象的构建过程”和“对象的表示”分离开。翻译成大白话:你不需要关心对象内部有多少零件、这些零件怎么拼装,你只需要告诉建造者自己要什么,由建造者负责一步步把对象组装好。

它的三个直接好处特别明显:

第一,参数语义清晰。orderBuilder.withCouponId("c001")一眼就知道在配什么,不用再去数构造器参数位置。

第二,保证对象不可变。builder 构建完成后调用build()生成最终对象,所有字段用 final 修饰,之后没人能改,线程安全性也更好。

第三,校验时机可控。你可以在 build() 方法里做集中校验,比如订单金额必须大于 0、订单号不能为空,不满足条件直接抛异常,而不是让一个半成品对象流入业务层。

还有一个常被忽略的价值:客户端代码几乎不受对象内部结构变化影响。今天订单加了“是否加急”字段,用到 builder 的调用方只需要在需要的地方加一行.withUrgent(true),其他代码不用动。如果还停留在构造器重载阶段,这一步能把所有调用处都改一遍。

2. 建造者模式核心角色与经典实现

2.1 四个角色的职责划分

GoF 对建造者模式的定义是:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。这里涉及四个角色,我一个个说。

Product(产品):最终要构建的复杂对象,比如上文的 Order。产品类里通常只提供 getter,不提供 setter,字段通过构造函数传入,保证不可变。

Builder(抽象建造者):定义构建产品各个部件的抽象接口,比如buildOrderId()、buildAddress(),以及一个返回产品的方法build()。

ConcreteBuilder(具体建造者):实现 Builder 接口,具体负责每个部件怎么构建,同时记录构建过程中的中间状态。

Director(指挥者):控制构建的步骤和顺序。它定义一个construct()方法,内部按固定流程调用 builder 的方法,保证“构建过程”稳定不变。

这四个角色里,Director 是很多初学者最容易忽略的。实际写代码时,很多人直接把 Director 省掉,由客户端直接操作 builder。这种做法不叫错,但要分场景:如果构建步骤永远只有一套流程,省略 Director 反而更简洁;如果同一套原料要支持多种构建顺序或多种产品变体,Director 就很有必要。

2.2 经典 GoF 风格实现

我用一个简化版的“电脑组装”例子来写经典实现。产品是 Computer,部件有 CPU、内存、硬盘,另外客户端还关心是否配备显卡、是否配显示器。直接看代码。

首先是产品类:

public class Computer { private final String cpu; private final String memory; private final String disk; private final boolean hasGpu; private final boolean hasMonitor; Computer(String cpu, String memory, String disk, boolean hasGpu, boolean hasMonitor) { this.cpu = cpu; this.memory = memory; this.disk = disk; this.hasGpu = hasGpu; this.hasMonitor = hasMonitor; } public String getCpu() { return cpu; } public String getMemory() { return memory; } public String getDisk() { return disk; } public boolean isHasGpu() { return hasGpu; } public boolean isHasMonitor() { return hasMonitor; } @Override public String toString() { return "Computer{" + "cpu='" + cpu + '\'' + ", memory='" + memory + '\'' + ", disk='" + disk + '\'' + ", hasGpu=" + hasGpu + ", hasMonitor=" + hasMonitor + '}'; } }

注意构造器我用了包级私有,外部不能直接 new,只能通过 builder 创建。

抽象建造者:

public interface ComputerBuilder { ComputerBuilder withCpu(String cpu); ComputerBuilder withMemory(String memory); ComputerBuilder withDisk(String disk); ComputerBuilder withGpu(boolean hasGpu); ComputerBuilder withMonitor(boolean hasMonitor); Computer build(); }

具体建造者和指挥者:

public class StandardComputerBuilder implements ComputerBuilder { private String cpu; private String memory; private String disk; private boolean hasGpu; private boolean hasMonitor; @Override public ComputerBuilder withCpu(String cpu) { this.cpu = cpu; return this; } @Override public ComputerBuilder withMemory(String memory) { this.memory = memory; return this; } @Override public ComputerBuilder withDisk(String disk) { this.disk = disk; return this; } @Override public ComputerBuilder withGpu(boolean hasGpu) { this.hasGpu = hasGpu; return this; } @Override public ComputerBuilder withMonitor(boolean hasMonitor) { this.hasMonitor = hasMonitor; return this; } @Override public Computer build() { if (cpu == null || memory == null || disk == null) { throw new IllegalStateException("cpu、memory、disk 为必填项"); } return new Computer(cpu, memory, disk, hasGpu, hasMonitor); } } public class ComputerDirector { private final ComputerBuilder builder; public ComputerDirector(ComputerBuilder builder) { this.builder = builder; } public Computer construct() { // 这里控制组装顺序和默认配置 return builder.withCpu("i5-12400") .withMemory("16GB") .withDisk("512GB SSD") .withGpu(false) .withMonitor(true) .build(); } }

客户端使用:

ComputerDirector director = new ComputerDirector(new StandardComputerBuilder()); Computer computer = director.construct();

这套实现里,Computer 的构造器是包级私有的,意味着除了 builder,其他类没法直接 new。外部如果想自定义配置,也可以不经过 Director,直接把 builder 拿来用。这就是“构建过程和表示分离”的体现。

2.3 经典实现与链式实现的取舍

上面是标准的 GoF 风格。但说实话,现在 Java 项目里更常见的是把 builder 写成产品类的静态内部类,并且 setter 方法返回 this,形成链式调用。

我个人的经验是:经典四角色适合“构建过程特别复杂、有多种组装流程”的场景,比如配置引擎、报表生成器;而静态内部类链式写法适合大多数业务对象,因为你会发现,多数业务对象的构建流程根本没有那么多“顺序”可言,大家都是按参数名一个一个赋值,再集中校验。

链式写法的典型结构是这样:

public class Order { private final String orderId; private final Long userId; private final BigDecimal amount; private final String address; private final String couponId; private final boolean urgent; private Order(Builder builder) { this.orderId = builder.orderId; this.userId = builder.userId; this.amount = builder.amount; this.address = builder.address; this.couponId = builder.couponId; this.urgent = builder.urgent; } public static Builder builder() { return new Builder(); } public static class Builder { private String orderId; private Long userId; private BigDecimal amount; private String address; private String couponId; private boolean urgent; public Builder orderId(String orderId) { this.orderId = orderId; return this; } public Builder userId(Long userId) { this.userId = userId; return this; } public Builder amount(BigDecimal amount) { this.amount = amount; return this; } public Builder address(String address) { this.address = address; return this; } public Builder couponId(String couponId) { this.couponId = couponId; return this; } public Builder urgent(boolean urgent) { this.urgent = urgent; return this; } public Order build() { if (orderId == null || orderId.isEmpty()) { throw new IllegalStateException("orderId 不能为空"); } if (userId == null) { throw new IllegalStateException("userId 不能为空"); } if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalStateException("amount 必须大于 0"); } return new Order(this); } } }

使用的时候:

Order order = Order.builder() .orderId("20250101001") .userId(10086L) .amount(new BigDecimal("99.00")) .address("某市某区某街道") .couponId("c001") .build();

这段代码里有个细节值得注意:原订单是“可选的优惠券、可选的地址”,客户端不必调address(),build 后对应字段就是 null。这是建造者模式相对构造器重载的巨大优势——你不用为每一种可选参数组合专门写一个构造器。

3. 建造者模式与工厂模式的区别

3.1 一图理清两种模式的侧重点

这个问题在面试里出现频率极高,也经常有人搞混。我先说结论:工厂模式侧重“创建哪种产品”,建造者模式侧重“怎么一步步创建产品”。

工厂模式的核心意图是封装创建逻辑,让调用方不用关心具体实现类。比如你有一个LoggerFactory,调用getLogger("file")给你返回文件日志实现,调用getLogger("console")给你返回控制台日志实现,调用方拿到的都是Logger接口,不需要知道背后是 FileLogger 还是 ConsoleLogger。

建造者模式的核心意图是,即便产品是同一个实现类,它的构建过程也极其复杂,或者参数极多,需要分步骤设置,并且希望构建过程校验能集中管理。建造者 mode 并不解决“选哪个实现类”的问题,它解决的是“一个对象怎么从零到完整装配起来”的问题。

如果用生活类比:工厂模式是餐厅点餐——“我要一份宫保鸡丁”,后厨决定给你做哪道菜;建造者模式是 DIY 组装——“我要一台电脑,CPU 选 i5,内存选 16G,硬盘选 512G,加显卡,不加显示器”,每一个部件由你指定,最后拼装校验。

3.2 什么时候选择哪个

从实际开发角度,我总结了一个选择清单。

如果创建对象时主要变化点是实现类不同,优先考虑工厂模式。比如接口有多个实现,希望通过一个统一入口创建。

如果创建对象时主要变化点是内部参数不同、组合方式不同,优先考虑建造者模式。比如同一个实体类,不同场景要配不同字段,且可选字段很多。

如果对象既有多种实现类、每个实现类又有复杂的内部结构,可以考虑两种模式结合:用工厂模式封装不同实现类的创建入口,用建造者模式封装单个实现类的内部装配。

举个实际例子,我之前写过一个消息推送服务,PushSender有短信、邮件、站内信三种实现,这里用简单工厂;但每种消息都有标题、内容、模板参数、定时发送时间、重试次数等一堆字段,这时候消息对象本身用建造者模式。两个模式不冲突,反而互补。

4. 建造者模式在 JDK 与开源框架中的身影

4.1 JDK 里的典型应用:StringBuilder 与 StreamBuilder

Builder 模式在 JDK 里最出名的就是StringBuilder。你可能没想过,StringBuilder的设计动机之一就是解决字符串拼接时产生大量不可变中间对象的问题。它本质上是把“字符串的构建过程”独立出来,你不断append,最后toString()产出最终字符串。这和建造者模式的“一步步装配,最终 build”如出一辙。

另一个容易忽略的是java.util.stream.Stream的Builder接口:

Stream<String> stream = Stream.<String>builder() .add("a") .add("b") .add("c") .build();

这也是典型的构建者思想。中间add一个元素就相当于装配一个部件,最后build()固定成一个不可变视角的 Stream。

4.2 Guava、Lombok 与 Spring 中的实践

Guava 的ImmutableList、ImmutableMap系列大量使用 Builder:

ImmutableList<String> list = ImmutableList.<String>builder() .add("a") .add("b") .build();

它的设计意图很明确:创建不可变集合,同时避免构造器参数过长。

Lombok 的@Builder注解更是把建造者模式变成了 Java 开发者的日常。给实体类标上@Builder,Lombok 会帮你生成静态内部 builder 类和链式方法。但我要提醒一句,Lombok 生成的是简化版 builder,它默认不做 build 期校验。如果构建时真的有必填约束,要么写自定义构造器加校验,要么在业务入口处校验,别把 Lombok 当成万能药。

Spring 框架里的UriComponentsBuilder、MockMvcRequestBuilders也都是典型的 builder。UriComponentsBuilder.fromPath("/api/user").queryParam("id", 1).build(),每一步都在装配 URI 的组成部分,非常直观。

看这些框架级案例,你会发现它们不是“为了用设计模式而用”,而是确实需要解决传入参数多、可选参数多、构建后对象不可变这些实际问题。设计模式从来不是装饰品,是现实约束逼出来的解法。

5. 实战纯手写 Builder 的细节与规范

5.1 手写 Builder 类的步骤清单

如果项目里不方便引入 Lombok,或者想完全掌控 builder 行为,手写 builder 的套路我基本固定为这几步。

第一步,产品类的构造器设为私有,参数是一个 Builder 类型。目的是不让外部绕过 builder 直接创建对象,确保所有对象都经过校验逻辑。

第二步,Builder 类和产品类同包或作为静态内部类。静态内部类能直接访问产品类的私有构造器,省去很多访问权限的麻烦。

第三步,Builder 的每个字段与产品字段一一对应,类型保持一致。setter 方法命名用字段名本身,比如orderId(String orderId),不要画蛇添足写setOrderId再返回 this,链式调用读起来更自然。

第四步,build 方法内做字段校验。必要字段缺失时抛IllegalStateException,错误信息要写清楚,比如“orderId 不能为空”,而不是笼统的 “build failed”。

第五步,返回产品对象时,通过产品类的私有构造器把 Builder 的字段整体拷贝进去。这里有个关键点:如果产品字段里有List、Map这类可变对象,要在构造器或 builder 里做防御性拷贝,否则外部仍可以通过修改传入的集合来破坏对象不可变性。这个问题我在生产环境真实踩过,后面会细说。

5.2 防御性拷贝与不可变性陷阱

很多讲 builder 的文章不会提防御性拷贝,但这个坑真的会出线上事故。举个场景,订单对象有个List<OrderItem>字段:

public class Order { private final List<OrderItem> items; private Order(Builder builder) { this.items = builder.items; } }

builder 里:

public Builder items(List<OrderItem> items) { this.items = items; return this; }

看起来没问题,但调用方如果这样写:

List<OrderItem> itemList = new ArrayList<>(); itemList.add(item1); Order order = Order.builder().items(itemList).build(); itemList.add(item2);

因为order.items和itemList指向同一个 ArrayList,第二步itemList.add(item2)之后,订单对象里的 items 也多了个元素,不可变性被击穿。正确做法是在赋值时拷贝:

public Builder items(List<OrderItem> items) { this.items = items == null ? new ArrayList<>() : new ArrayList<>(items); return this; }

build 时再包一层Collections.unmodifiableList:

this.items = Collections.unmodifiableList(new ArrayList<>(builder.items));

这样即使外部拿到order.getItems(),调用 add 也会抛 UnsupportedOperationException,不可变才算真正落地。

5.3 Builder 中的继承问题

再聊一个让很多人懵掉的点:Builder 的继承。产品类有父类和子类时,简单的手写 builder 很容易破功。

假设有个BaseOrder和子类GiftOrder,如果希望GiftOrder.builder()也能设置父类的字段,需要用到泛型自继承技巧,也就是常见的Self<T>模式:

public abstract class BaseOrderBuilder<T extends BaseOrderBuilder<T>> { protected String orderId; @SuppressWarnings("unchecked") public T orderId(String orderId) { this.orderId = orderId; return (T) this; } public abstract BaseOrder build(); } public class GiftOrderBuilder extends BaseOrderBuilder<GiftOrderBuilder> { private String giftName; public GiftOrderBuilder giftName(String giftName) { this.giftName = giftName; return this; } @Override public GiftOrder build() { return new GiftOrder(this); } }

使用:

GiftOrder order = new GiftOrderBuilder() .orderId("001") .giftName("优惠券礼包") .build();

orderId返回泛型T,实际类型是GiftOrderBuilder,所以链式调用不会断。这个技巧在框架源码里很常见,写业务代码时如果遇到继承结构,可以直接照搬。没有这个技巧的话,子类 builder 一旦调用父类方法,返回类型变成父类 builder,后面就接不上子类的独有方法了,很烦。

5.4 性能与对象开销问题

关于性能,网上有一类说法是“建造者模式有额外对象开销,不建议在高性能场景用”。这个说法对,但需要分清楚影响范围。

每个Order.builder()都会 new 一个 Builder 对象。在创建频率极高的场景,比如每秒创建几万个订单对象,这个开销确实不能忽略。但它也仅仅是多一次对象分配,大多数业务系统远没有到这个量级。更重要的是,Builder 模式带来的代码可维护性提升,远超那点微小的 GC 压力。

如果你真的在极端高性能场景,比如网关转发日志、埋点上报,可以考虑复用 Builder 实例,每次 build 前重置字段。但这样做的代价是 Builder 不再是线程安全的,必须用 ThreadLocal 或严格串行调用。我一般不建议提前做这种优化,等压测数据出来再动手也不迟。先让代码正确、清晰,然后用基准测试说话。

6. 面试与软考高频问题速查

6.1 面试官到底在考什么

建造者模式几乎是 Java 面试必问的设计模式之一。面试官问“讲讲建造者模式”,通常不只是想听你背四个角色,他们更想看这几件事:

第一,能不能说清楚和工厂模式的区别。这个前面已经讲透,核心就是“创建什么”和“怎么创建”的差别。

第二,有没有实际用过。很多人一上来就背概念,问他项目里哪里用了,就说“看过但是没写过”。这种回答非常减分。你完全可以提 StringBuiler、Lombok @Builder、或自己封装过复杂查询参数对象。

第三,能不能现场写一个链式 Builder。建议把第 2 节的完整例子记熟,并且能讲清楚 build 方法里为什么要做校验、产品类构造器为什么要私有、集合字段为什么要拷贝。

第四,对不可变对象的理解。建造者模式和不可变对象经常连在一起考,如果顺带说出final字段、Collections.unmodifiableList、防御性拷贝这些点,面试官会眼前一亮。

6.2 软考设计模式速记技巧

软考(比如软件设计师)会考设计模式分类。建造者模式在 GoF 分类里属于创建型模式(Creational Pattern)。创建型模式一共五个:单例、工厂方法、抽象工厂、建造者、原型。记忆口诀五花八门,我自己用的是“打工抽箭圆”这种谐音梗,但你得找到自己能记住的那套。

关键要记住建造者的三个关键词:复杂对象构建、分步组装、构建与表示分离。软考如果出选择题,问你“以下哪个模式适合创建参数较多的复杂对象”,基本就是建造者。如果问你“将复杂对象的构建与表示分离”,是原话,也能直接对应。

另外软考还可能给一段 UML 类图让你识别模式。建造者的 UML 特征很鲜明:Product 和 Director 都依赖 Builder 接口,ConcreteBuilder 实现 Builder,Product 不直接依赖 Director。看到这种结构,答案基本锁定建造者。

6.3 八股文里的几个变体问题

除了基础问题,分享几个我见过的变体面试题。

“建造者模式为什么不用抽象类?”答案是 Builder 接口往往是稳定的语义定义,用接口更灵活,可以让一个类同时实现多个 Builder 接口;但如果你想在 Builder 基类里放公共的字段和默认实现,抽象类也有优势。所以这是一个“按场景选”的问题,不是非黑即白。

“build() 方法和工厂方法有什么区别?”build 方法处理的是同一个产品对象内部的装配,工厂方法返回的是不同类型的产品;如果某个 build() 方法内部根据参数判断返回不同类型,那它就更像工厂方法了。

“Builder 模式能否解决构造函数参数过多的问题?和 JavaBean 相比有什么优势?”这个问题前面已经给过答案:可读性、不可变性、校验集中。

“如果产品需要支持多种配置组合,Builder 是不是最佳选择?”分情况。如果组合是有限的几种,用 Director 或枚举配置更清晰;如果组合近乎无限,Builder 更合适。

7. 一些踩坑经验与实际感受

7.1 别为了用模式而用模式

在团队里看过不少过度设计的代码。一个只有两个字段的简单 VO,也硬套 Builder,然后每个调用处都是Xxx.builder().a(a).b(b).build()。说实话,这种代码不如直接写构造器。设计模式是工具,不是教条。字段不超过三个、参数顺序稳定、没有校验需求的时候,构造器就是最清晰的方案。

我判断一个对象是否需要 Builder 的经验法则:

  • 字段数量在 4 个以上,或者未来大概率增长。
  • 存在多个可选字段,客户端只关心其中一部分。
  • 构建之后对象不允许被修改,需要不可变保证。
  • 构建阶段需要做参数校验或字段间联动校验。

满足两条以上,才值得引入 Builder。只有一条,可以再想想。

7.2 Lombok 与手写 Builder 的实际选择

团队里用 Lombok 很普遍,我自己的态度是:简单 DTO、VO、请求参数对象,直接用@Builder提高效率;核心领域对象、有严格不可变性要求的,建议手写 Builder 并自定义校验逻辑。

Lombok 的 @Builder 生成代码时有个细节很多人不知道:它默认生成的是builder()静态方法和全字段构建方法,如果你需要忽略某些字段或者设置默认值,可以用@Builder.Default。但说到底,Lombok 生成 builder 的过程中是不会有校验逻辑的。

团队规范里还可以通过自定义注解处理来扩展 Lombok 的行为,比如在 build() 时自动调用实体的 validate 方法。这块能力很强,也特别适合规范统一的团队。如果不放心,就手写。

7.3 现场排查案例:一个因可变字段引发的线上问题

最后讲一个真实案例,也是我最初对防御性拷贝如此敏感的原因。

之前做营销系统,发奖接口创建用户礼包对象,礼包里有个List<RewardItem>。当时只图快,Builder 的字段直接赋值,没有做拷贝。结果有一个定时任务在发奖过程中往同一个rewardItemList里追加了新的奖励项,因为内存里引用共享,礼包对象里的奖励列表跟着变了,最直观的影响是对账数据对不上,用户实际收到的和记录里的不一致。排查了很久,最后发现是共享集合引用搞的鬼。

从那之后我给自己定了条规矩:凡是 Builder 里出现集合或可变对象,一律拷贝进产品对象,返回时再包不可变视图。这个习惯救了我不少次。

8. 最后分享一个提升 Builder 实用性的技巧

如果你手写的 Builder 已经用得很顺手,可以再给产品类加一个静态方法from,用于把已有对象转换为 Builder,方便做对象拷贝或部分字段修改。

比如订单有“复制一单,但金额改成新的”这种需求:

Order copy = Order.builder() .orderId("NEW-001") .userId(original.getUserId()) .amount(newAmount) .address(original.getAddress()) .couponId(original.getCouponId()) .build();

每个字段都手写一遍很啰嗦。可以先在 Order 里写一个builderFrom(Order existing):

public static Builder builderFrom(Order existing) { return Order.builder() .orderId(existing.getOrderId()) .userId(existing.getUserId()) .amount(existing.getAmount()) .address(existing.getAddress()) .couponId(existing.getCouponId()) .urgent(existing.isUrgent()); }

之后复制订单就变成:

Order copy = Order.builderFrom(original) .orderId("NEW-001") .amount(newAmount) .build();

减少重复代码,也让复用意图一目了然。这个模式在很多框架里都有,叫 “copy builder”,做法虽小,但日常开发里非常提效。建造者模式说到底不是什么高深理论,它就是在“对象构造越来越复杂”的时候,帮我们把代码变得可读、可靠、可维护的一套成熟解法。真正用好了,代码评审都会轻松很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:52:14

第 13 章 选哪个材质:Standard vs PBR vs OpenPBR

本章实测环境&#xff1a;Babylon.js 9.30.0、Vite 8.3.4、TypeScript 5.9.3、Windows 11 Chrome&#xff08;WebGL2 后端&#xff09;。 本章代码&#xff1a;src/chapters/ch13-material-comparison.ts&#xff0c;运行后打开 http://localhost:18000/?ch13。 图中所有&quo…

作者头像 李华
网站建设 2026/10/13 1:49:49

SugarNMSTool:面向工业质检的轻量级可配置NMS工具包

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:48:37

DynamicLayout:TextView动态文本布局的核心机制与性能优化

1. 项目概述&#xff1a;为什么DynamicLayout是TextView性能优化的“隐形开关”你有没有遇到过这样的场景&#xff1a;一个展示长篇用户评论的列表页&#xff0c;滑动时偶尔卡顿&#xff0c;但Profile工具里又看不出明显耗时函数&#xff1f;或者在做富文本编辑器时&#xff0c…

作者头像 李华