聊聊Java中的of

45 阅读11分钟

从JDK到Spring到业务代码,of无处不在:Optional.of()、LocalDate.of()、Stream.of()、List.of()……翻开任何一个稍具规模的Java项目,of方法的数量可能比你想的多得多。

这篇文章尝试把of这个命名的前世今生聊一聊:它从哪来、JDK里怎么用的、开源框架怎么用的、我自己写的业务代码怎么用的,以及什么时候该用of、什么时候不该用。

of的命名由来

一个常见的猜测是:of是不是Guava带起来的?

不是。EnumSet.of()在Java 5就已经存在了,这个时候Guava还没开源。EnumSet是Java标准库中最早使用of命名的一批类之一,Joshua Bloch本人就是EnumSet的设计者,他同时也是Effective Java的作者。

Joshua Bloch在Effective Java第一版中就讨论过静态工厂方法的命名问题。到第三版,他对常见命名做了更系统的归纳,其中of的定义是:

An aggregation method that takes multiple parameters and returns an instance of this type that incorporates them.

翻译过来:of是一个聚合方法,接收多个参数,返回一个融合了这些参数的本类型实例。

这个定义很精确地描述了of的语义。LocalDate.of(2026, 6, 2)就是把年、月、日三个参数聚合成一个日期对象。Stream.of(1, 2, 3)就是把多个元素聚合成一个流。

Guava开源后,ImmutableList.of()、ImmutableSet.of()、ImmutableMap.of()这些方法让of命名在Java社区大规模流行。但Guava不是发明者,而是推广者。

真正让of成为Java标准库一等公民的是Java 8。java.time包由Stephen Colebourne主导设计,整个时间日期API全面采用of命名:LocalDate.of()、LocalTime.of()、Duration.of()、Period.of()……这些方法的可读性远好于new LocalDate(2026, 6, 2)这种构造器调用。同一年,Java 8还带来了Optional.of()、Stream.of()、Collector.of()等,of在JDK中的存在感一下子拉满了。

再往后,Java 9引入了List.of()、Set.of()、Map.of()这些不可变集合的工厂方法,of就成了JDK中最常见的静态工厂方法命名。

值得一提的是,Bloch在Effective Java中还定义了另一个常用命名from:A type-conversion method that takes a single parameter and returns a corresponding instance of this type。from是类型转换,of是聚合,两者分工明确。LocalDate.of(year, month, day)是聚合,LocalDate.from(temporal)是转换。这个区分在JDK的时间API中贯彻得很好。

JDK里的of方法

JDK中定义了of方法的类,数量远比大多数人印象中的多。我统计了一下JDK 17的源码,至少有40多个类提供了of方法,覆盖的of方法总量超过60个。

按场景来分,大致有五类。

值包装

Optional.of(T value)、OptionalInt.of(int value)、OptionalLong.of(long value)、OptionalDouble.of(double value)。

这里有一个值得注意的设计:Optional.of()传入null会抛NullPointerException,而Optional.ofNullable()才允许null值。这个区分不是多余的,它传递了一个明确的语义:当你用of的时候,你在声明这个值一定不为null。如果调用者传了null,那是一个编程错误,应该尽早暴露。

时间对象

java.time包是of方法最密集的区域。LocalDate有2个of重载,LocalTime有3个,LocalDateTime有7个。这些重载的参数从粗到细:可以只传时分,也可以精确到纳秒。

Duration.of(long amount, TemporalUnit unit)的设计也有意思。Duration.ofSeconds(30)比new Duration(30)语义清晰得多,你不需要猜30是秒还是毫秒。

集合创建

List.of()、Set.of()、Map.of()是Java 9引入的不可变集合工厂。它们替代了Arrays.asList()和Collections.unmodifiableList()的组合写法,一行搞定。

EnumSet.of()是个老面孔了,从Java 5就有。它有一个特殊的重载策略:5个固定参数的重载(of(E)、of(E,E)、of(E,E,E)……of(E,E,E,E,E))再加一个可变参数版本of(E first, E… rest)。这不是炫技,而是有实际原因的:可变参数的调用会创建数组对象,对于只有一两个枚举值的场景,固定参数重载避免了不必要的数组分配。在EnumSet这种追求极致性能的工具类上,这个优化是有意义的。

流操作

Stream.of(T t)和Stream.of(T… values),IntStream、LongStream、DoubleStream各有两个重载。单个元素版本返回的不是可变参数的退化形式,而是专门的Stream实现,性能更好。

枚举操作

Month.of(int)、DayOfWeek.of(int)这两个方法,把int值转换为对应的枚举常量。Month.of(6)返回Month.JUNE。比起直接用Month.JUNE,of方法的价值在于处理运行时数据:数据库里存的是int,需要转成枚举。

下面这张速查表把JDK中最常用的of方法按场景归类:

场景典型调用核心语义
值包装OptionalOptional.of(value)非null值包装
值包装OptionalIntOptionalInt.of(42)基本类型值包装
时间LocalDateLocalDate.of(2026, 6, 2)年月日聚合
时间LocalTimeLocalTime.of(14, 30)时分秒聚合
时间LocalDateTimeLocalDateTime.of(date, time)日期时间聚合
时间DurationDuration.ofSeconds(30)时间段创建
时间PeriodPeriod.of(1, 2, 3)年月日周期
时间YearMonthYearMonth.of(2026, 6)年月聚合
时间ZoneOffsetZoneOffset.of(“+08:00”)时区偏移
集合ListList.of(1, 2, 3)不可变列表
集合SetSet.of(1, 2, 3)不可变集合
集合MapMap.of(“k1”, 1, “k2”, 2)不可变映射
集合EnumSetEnumSet.of(A, B)枚举集合
StreamStream.of(1, 2, 3)流创建
IntStreamIntStream.of(1, 2, 3)基本类型流
收集器CollectorCollector.of(supplier, accumulator, …)自定义收集器
枚举MonthMonth.of(6)int转枚举
枚举DayOfWeekDayOfWeek.of(1)int转枚举
格式化HexFormatHexFormat.of()默认实例

开源框架里的of

JDK之外,主流Java框架也大量使用of命名。不过不同框架对of的采用程度差异很大。

Spring Framework

Spring对of的使用不算多,但每个都有明确的设计意图。

DataSize是Spring自定义的值对象,用来表示数据大小。它没有只提供一个of方法,而是提供了ofBytes、ofKilobytes、ofMegabytes、ofGigabytes、ofTerabytes一组方法。这是of命名的一个变体:of + 单位名。DataSize.ofMegabytes(512)比new DataSize(536870912)或DataSize.of(512, DataUnit.MEGABYTES)可读性好太多。这种ofXxx的命名方式在值对象设计中很实用:把数值和单位绑在一起,消除歧义。

LogMessage.of(Supplier)是另一个有意思的设计。Spring的日志工具类用of接收一个Supplier,实现延迟求值。日志消息只在真正需要输出时才被构建,避免了不必要的字符串拼接开销。

RegisteredBean.of(ConfigurableListableBeanFactory, String)是Spring 6.0为AOT编译引入的,用来表示一个已注册但尚未实例化的Bean的元数据。

RocketMQ

RocketMQ 4.9.8中,CompressionType.of(String name)是唯一自定义的of方法。它从配置字符串转换为压缩类型枚举,支持大小写不敏感。典型的用法是CompressionType.of(System.getProperty(“compress.type”, “ZLIB”)),从系统属性读取压缩配置。

这个设计把配置和枚举解耦了:配置层只需要传字符串,运行时通过of方法转成类型安全的枚举。

业务代码里的of

讲JDK和开源框架的of方法,网上已经有很多文章了。真正少有人聊的是:在自己的业务代码里,of方法能怎么用、怎么用得好。

我自己撸的基础框架里,也是有用of的。

异常创建

这是我们框架里of方法最密集的场景。

BusinessException.of()有6个重载:可以只传错误码,可以传错误码加错误信息,可以传ErrorCode接口实现,可以传ErrorEnum枚举,可以传Result对象。调用者不需要关心BusinessException的构造器签名是什么,只需要把自己手上已有的错误信息扔给of方法就行。

BusinessTaskHandleException更进一步,除了of方法还提供了ignore()、warning()、close()、hangUp()这些语义化的便捷方法。这些方法内部仍然是创建异常对象,但调用者读代码时一眼就能看出意图:BusinessTaskHandleException.warning(“STOCK_NOT_ENOUGH”, “库存不足”)比new BusinessTaskHandleException(true, “STOCK_NOT_ENOUGH”, “库存不足”, false)清晰得多。

用of方法创建异常有几个好处。第一,异常类的构造器可能变,但of方法的签名可以保持稳定,调用者不受影响。第二,of方法可以做参数适配,同一个异常类支持多种入参格式。第三,of方法的名字本身就在告诉调用者这是一个工厂操作,比new更语义化。

上下文构建

ChainContext.of(T param)、BusinessTaskHandlerContext.of(Long taskId)、FlowCtrlContext.of(String channelKey)……

这些上下文对象的共同特征是:字段多、初始化逻辑简单、调用频繁。用of方法替代构造器,调用者不需要记住参数顺序,不需要写一大段setter链,一行搞定。

工具类初始化

ConcurrentDateFormat.of(String format)创建线程安全的日期格式化器。Lazy.of(Supplier)创建延迟计算容器。XmlUtils.of(InputStream)或XmlUtils.of(String)创建XML解析器。QuickJsonReader.of(Object)创建JSON读取器。

这些工具类的共同模式是:内部初始化逻辑比较复杂(创建线程池、解析文档、构建XPath对象等),但对外只暴露一个简洁的of方法。调用者不需要知道内部细节,of方法把复杂度封装在背后。

DTO构建

ErpNumber.of(Object number)创建与ERP系统交互的数字对象。InnerSkuStock.of(skuCode, shopCode, qty)创建库存变化DTO中的SKU对象。

这类of方法的特点是参数少、逻辑简单,但用of替代构造器后,代码的可读性提升明显:ErpNumber.of(123456)比new ErpNumber(123456)更口语化,读起来更自然。

模式归纳

从这些业务代码中,可以归纳出of方法在业务场景的四种典型用法:

用法典型类特征
异常工厂BusinessException.of多重重载适配不同入参,语义化便捷方法
上下文创建ChainContext.of字段多但初始化逻辑简单,一行创建
工具类初始化ConcurrentDateFormat.of内部复杂但对外接口简洁
DTO构建ErpNumber.of参数少,可读性优于构造器

of的实践建议

聊了这么多of的用法,最后说说什么时候该用of、什么时候不该用。

该用的场景

创建本类型实例,参数到对象的聚合关系明确。LocalDate.of(year, month, day)比new LocalDate(year, month, day)可读性好,因为of这个介词天然表达了一种组合关系。

替代多参数构造器。当构造器参数超过3个,调用者很难记住参数顺序。of方法可以通过命名和重载来消除歧义。

需要隐藏实现细节。of方法可以返回子类型,调用者不需要知道实际创建的是哪个实现类。

不该用的场景

类型转换场景用from更合适。LocalDate.from(temporalAccessor)是从另一种时间类型转换而来,不是从零开始聚合。如果你方法的语义是把A类型转成B类型,from比of更准确。

需要返回不同类型时用getInstance或create。of方法的隐含约定是返回本类型实例。如果你的工厂方法返回的是别的类型,用of会让调用者困惑。

逻辑太重不适合of。of方法给人的预期是轻量级的对象创建,如果内部有复杂的校验、IO操作或远程调用,用of会误导调用者。

设计技巧

重载不宜过多。BusinessException.of有6个重载,这已经偏多了。超过4个重载的时候,考虑用Builder模式或引入参数对象。

配合泛型保持类型安全。ChainContext.of(T param)的泛型签名让调用者不需要强转,编译器帮你保证类型正确。

考虑是否需要缓存实例。Boolean.valueOf(boolean)会缓存实例,但大多数of方法不需要。如果你的of方法创建的是不可变对象且创建成本不低,可以考虑缓存。JDK中Boolean、Integer等包装类的valueOf做了缓存,但of方法一般不做,因为of的语义是聚合创建,每次聚合的参数通常不同。

of和from的区分判断:如果你的方法是多个参数聚合成一个对象,用of;如果是从一个已有对象转换类型,用from。  记住这一条,命名就不会纠结。

小结

of不是什么高级设计模式,它就是一个命名约定,但它是一个被整个Java生态验证过的好约定。从2004年EnumSet.of()开始,经过Guava的推广、JSR-310的确立、Java 9集合工厂方法的跟进,of已经成为Java开发者最熟悉的静态工厂方法命名。

在实际项目中,of方法最有价值的地方不在于它比构造器优雅多少,而在于它把创建对象的意图表达得更明确。new BusinessException(code, msg)你看不出这是在创建异常还是在做别的什么,BusinessException.of(code, msg)一眼就知道这是工厂创建。这种语义上的清晰度,在项目规模大了之后,价值会越来越明显。