工具类构造器必须私有化以防止实例化,因其实例无意义且所有方法均为静态;需显式声明private构造器并抛出UnsupportedOperationException,否则新人可能误用new创建对象。

为什么工具类的构造器必须私有化
因为 Java 默认会给类加一个无参公有构造器,只要没显式定义任何构造器;一旦你写了公有构造器,别人就能 new 出实例——而工具类(比如 StringUtils、Objects)本就不该被实例化,所有方法都是静态的。不私有化,等于留了个后门,编译期不报错,但语义错误,团队里新人真会写 new DateUtils() 然后发懵。
常见错误现象:Cannot instantiate the type XXXUtils 这类报错其实不会出现——它反而能成功 new 出来,只是后续没人调用实例方法,代码审查时才被发现“这对象建了干啥?”
- 必须显式声明
private XXXUtils() {},哪怕空实现 - 建议在私有构造器里加
throw new UnsupportedOperationException("Utility class cannot be instantiated"),运行时兜底 - 如果类里已有其他构造器(比如带参的),默认无参构造器不会自动生成,但你仍得手动补上私有无参构造器,否则别人可能误用已有构造器
私有构造器和单例模式的强绑定关系
单例不是靠“只写一个实例变量”实现的,而是靠“阻止外部 new”+“提供唯一访问入口”。私有构造器是封锁 new 的第一道锁,没有它,Singleton.getInstance() 就是纸糊的门。
使用场景:配置管理、日志工厂、线程池封装等需要全局唯一状态或资源的类。
立即学习“Java免费学习笔记(深入)”;
- 漏掉私有构造器 → 外部可随意
new Singleton(),彻底破坏单例语义 - 构造器仅设为
protected或包级私有(default)不够,子类或同包类仍可继承/实例化 - 注意反序列化漏洞:即使构造器私有,
ObjectInputStream仍可能绕过它创建新实例,需配合readResolve()方法修复
私有构造器对继承和测试的影响
它直接切断继承链——子类无法调用 super(),编译报错 Implicit super constructor XXX() is not visible。这不是 bug,是设计意图:工具类和单例通常不该被继承。
但测试时容易踩坑:Mockito 无法 mock 私有构造器类的实例(因不能 new),也不能 spy 静态方法(除非用 Mockito.mockStatic())。
- 单元测试中,别试图 new 工具类去测静态方法,直接调用
XXXUtils.doSomething(...)即可 - 若真要测单例内部状态,确保测试前清空静态字段(如
Singleton.instance = null),否则测试间相互污染 - Android 开发中,混淆工具(R8/ProGuard)可能误删私有构造器(尤其没被反射调用时),需保留规则:
-keepclassmembers class * { private void <init>(...); }</init>
Java 14+ 记录类(record)与私有构造器的冲突
record 是隐式 final 的不可变数据载体,它的构造器天生是 public 的,且不能手动重写或私有化。所以 record 和“私有构造器用于禁实例化”完全互斥——你不能用 record 写工具类或单例。
错误尝试:public record StringUtils() { ... } 编译失败,因为 record 必须有组件字段,且构造器不可控。
- 想用 record 表达数据结构?可以,但它就是用来 new 的,别跟工具类混用
- 已有工具类想升级为 record?不行,重构方向应是拆分:数据用 record,逻辑用静态工具类,两者解耦
- IDEA 提示 “Constructor is never used” 别手抖删掉私有构造器——那是你刻意写的防御代码,不是冗余
真正难的不是写 private Utils() {},而是每次加新工具类时,下意识敲完 class 大括号就立刻补上它,像写分号一样自然。漏一次,就多一个可能被滥用的实例入口。










