Java的static关键字的定义
在Java中static是个修饰符,用于修饰类(而非对象)的成员方法,成员变量,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问。
static 修饰的属于类,对于同一类共享内存,而成员变量,普通方法属于对象,不共享内存。
示例演示
-
以下以最近我在实际生产中写的static关键字的乱用带来的bug演示:
需求如下:我们做了一个多平台统一的管理系统,现在有一个功能我们需要根据给定时间生成计划,但是目前我们只有其中一个平台支持该功能,但是我们要让程序有足够的扩展性,方便后续其他平台的对接。 -
需求分析(内心独白):
既然是多平台的功能,我们大概率需要用到策略模式,但是现在又只有一个平台支持这个功能,所以简单点了就整个内部的策略模式一锅端了,后面又其他平台进来了我们再把策略接口单独拎出来。 -
开始写代码,以下可以看到我定义了一个用于计算plan的聚合类,提供了一个用于计算不同合作方的计划。
@AllArgsConstructor
public class UnifiedPlanAggregateRoot {
// 计算计划的基准时间
private LocalDate basedDate;
// 各个合作方的策略map
private static Map<String, PartnerPlanPolicy> partnerPoliciesMap = new HashMap<>();
// 初始化各个合作方的策略
{
partnerPoliciesMap.put("Google", new GooglePlanPolicy());
}
// 根据给定的合作方,调用对应的策略进行计算计划
public List<LocalDate> calculatePlan(String partner) {
return partnerPoliciesMap.get(partner).calculatePlan();
}
/**
* 这里我定义一个内部的policy接口,然后实现了一个google的policy,以后可能会有bing,facebook等等的policy
*/
interface PartnerPlanPolicy {
List<LocalDate> calculatePlan();
}
/**
* google的一个计算计划策略实现,我们简单返回一个后面一个星期的日期用来测试
*/
class GooglePlanPolicy implements PartnerPlanPolicy {
private static final int GOOGLE_PLAN_LIMITATION = 7;
@Override
public List<LocalDate> calculatePlan() {
List<LocalDate> plans = new ArrayList<>();
for (int i = 0; i < GOOGLE_PLAN_LIMITATION; i++) {
plans.add(basedDate.plusDays(1));
}
return plans;
}
}
}
- 单元测试如下,也如预期顺利通过。
@Test
void calculatePlan() {
UnifiedPlanAggregateRoot planAggregateRoot = new UnifiedPlanAggregateRoot(LocalDate.of(2022, 03, 15));
List<LocalDate> googlePlans = planAggregateRoot.calculatePlan("Google");
Assert.assertEquals(7, googlePlans.size());
googlePlans.forEach(plan -> Assert.assertTrue(plan.isBefore(LocalDate.of(2022, 03, 23))));
}
分析问题
这里我们定义的这个UnifiedPlanAggregateRoot中有一个静态的partnerPoliciesMap,有一段代码块,还有一个成员变量basedDate。
- 当你第一次创建这个对象时,首先会初始化这个静态的partnerPoliciesMap
- 然后执行了这个代码块,给这个map添加了K,V
- 最后执行构造方法初始化了成员变量basedDate
- 当你第二次创建这个对象时,静态的partnerPoliciesMap已经初始化过了,不会再运行,但是代码块是非静态的还是会再执行一遍,这会导致一个问题,它重新创建了一次policy对象,然而policy对象并非是个无状态的服务,它内部使用了一个basedDate的成员变量,也就是说现在这个静态的partnerPoliciesMap中的policy对象时是由第二次创建对象时创建的,这时如果第一次创建的UnifiedPlanAggregateRoot执行calculatePlan方法的话,其调用的policy是第二次对象创建的,并引用了第二个UnifiedPlanAggregateRoot的成员变量,导致Plan的基准日期并不是这个对象自己的基准时间,得不到预期值
验证分析
这里我先初始化两个UnifiedPlanAggregateRoot对象后再调用calculatePlan方法,Assert将会报错 googlePlans.forEach(plan -> Assert.assertTrue(plan.isBefore(LocalDate.of(2022, 03, 23))));
将会抛异常
@Test
void calculatePlanWithError() {
UnifiedPlanAggregateRoot planAggregateRoot = new UnifiedPlanAggregateRoot(LocalDate.of(2022, 03, 15));
UnifiedPlanAggregateRoot planAggregateRoot2 = new UnifiedPlanAggregateRoot(LocalDate.of(2022, 04, 15));
List<LocalDate> googlePlans = planAggregateRoot.calculatePlan("Google");
Assert.assertEquals(7, googlePlans.size());
googlePlans.forEach(plan -> Assert.assertTrue(plan.isBefore(LocalDate.of(2022, 03, 23))));
}
总结
- static是属于类的,而成员变量是属于对象的,属于类的共享内存,只会初始化一次,属于对象的每次都会初始化。
- 永远不要再非静态的代码块中修改静态变量
- 对于无状态的服务,不要引用外部有状态的变量。以以上的例子来说就是policy内部不要使用外部的basedDate,应该由调用方传入该参数。