如何避免在初始化 class 之前访问静态字段引起的问题?
How do I avoid problems arising from accessing static fields before the class is initialized?
我有这个代码:
public abstract class Person {
public static final class Guy extends Person {
public static final Guy TOM = new Guy();
public static final Guy DICK = new Guy();
public static final Guy HARRY = new Guy();
}
public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY);
}
我知道,它看起来应该是一个枚举,它会是,但它需要继承Person,一个抽象class。
我的问题是:如果我尝试访问 Guys 列表,我没问题。但是我尝试访问任何一个人,特别是,我有一个问题:Guy
在 Person
之前加载。然而,由于Guy
继承了Person
,Person
被加载,并试图访问TOM
,但由于Guy
已经被加载,所以无法启动再次加载,所以我们有一个空引用。 (而且 ImmutableList
不接受 null,所以我们得到一个例外。)
所以我知道为什么会发生这种情况,但我不确定如何避免它。我可以将列表移到 Guy
class 中,但我将有多个 Person
的实现,并且我想要一个包含所有 Person
的主列表]s 在 Person
class.
加载静态内部 class 而不加载其包含 class 对我来说似乎很奇怪,但我想这是有道理的。有什么办法可以解决这个问题吗?
为解决方案而解决,这似乎可行(对于单线程)
public class Person
{
static Lazy tom = new Lazy();
static Lazy cat = new Lazy();
public static final List<Guy> GUYS = ImmutableList.of(tom.get(), cat.get());
public static final class Guy extends Person
{
public static final Guy TOM = tom.get();
public static final Guy CAT = cat.get();
}
static class Lazy
{
Guy v;
Guy get()
{
if(v==null)
{
Object x = Guy.TOM; // just to trigger Guy initialization
if(v==null)
v = new Guy();
}
return v;
}
}
OP 的设计本质上是递归的。尽管 Java 有一个明确定义的初始化过程(当检测到递归时会退出),但行为会根据首先初始化的 class 而有所不同。
字段为 final
的要求使问题变得更糟,这严重限制了我们分配它们的方式。否则,我们可以多处尝试if(null) assign
来解决问题。
上面发布的解决方案本质上是使用一个临时变量来解决 final
约束。
使用Map的修改版本,适用于更多字段数:
public class Person
{
static Map<String,Guy> cache = new HashMap<>(); // remove after both class are initialized
static Guy get(String name)
{
if(!cache.containsKey(name))
{
Object x = Guy.TOM; // just to trigger Guy initialization
if(!cache.containsKey(name))
cache.put(name, new Guy());
}
return cache.get(name);
}
public static final List<Guy> GUYS = ImmutableList.of(get("tom"), get("cat"));
static{ if(Guy.TOM!=null) cache=null; }
public static final class Guy extends Person
{
public static final Guy TOM = get("tom");
public static final Guy CAT = get("cat");
static{ if(Person.GUYS!=null) cache=null; }
}
多线程
有可能一个线程开始初始化 Person,而另一个线程开始初始化 Guy。死锁是可能的,它确实发生在现实世界中。所以这个设计本身就是有缺陷的。
一般来说,静态初始化时相互依赖的两个classes(不一定是父子)存在死锁的危险。
应用程序可以通过在应用程序启动时以可预测的方式小心地触发初始化来逃避做这种事情。尽管如此,这还是很危险的,它可能会让某人陷入调试地狱的日子。
您可以将不可变列表保存在单独的 class 中,例如 People.GUYS
:
public class People {
public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY);
}
通过这种方式,您仍然可以将个人保留在 Guy
class:
public abstract class Person {
public static final class Guy extends Person {
public static final Guy TOM = new Guy();
public static final Guy DICK = new Guy();
public static final Guy HARRY = new Guy();
}
}
使用 Person
抽象 class 的要求是否绝对(即它是否已经是遗留代码的一部分)?
如果不是,那么作为建议,我建议您将数据模型的设计重构为实现 Person 接口的 Guy enum
class。与基于 class 的设计相比,这种设计有很多优点。
public enum Guy implements Person {
TOM {
@Override
public void personMethod1(...) {...} // constant-specific
:
:
},
DICK {
@Override
public void personMethod1(...) {...} // constant-specific
:
:
};
@Override
public int personMethod2(...) {...} // general to the enum class
:
:
}
请注意,在这种情况下,没有理由使用常量引用定义 List
实例,因为枚举 API 提供了 GUY.values()
方法。
它将解决您所有的初始化问题,同时仍然启用 Person
的多个实现。缺点是代码重复。 Person
中的方法将需要在每个枚举 class 中实现(但如果有很多方法,您始终可以创建一个私有静态助手 class 并将方法调用转发给它。)
改变
public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY);
到
public static final List<Guy> GUYS = Collections.unmodifiableList(Arrays.asList(new Guy[] {Guy.TOM, Guy.DICK, Guy.HARRY}));
应该可以(我用 JDK 8 测试过),但它是否仍然满足您的要求?
public abstract class Person {
public static final class Guy extends Person {
public static final Guy TOM = new Guy();
public static final Guy DICK = new Guy();
public static final Guy HARRY = new Guy();
}
public static final List<Guy> GUYS = Collections.unmodifiableList(Arrays.asList(new Guy[] {Guy.TOM, Guy.DICK, Guy.HARRY}));
}
我有这个代码:
public abstract class Person {
public static final class Guy extends Person {
public static final Guy TOM = new Guy();
public static final Guy DICK = new Guy();
public static final Guy HARRY = new Guy();
}
public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY);
}
我知道,它看起来应该是一个枚举,它会是,但它需要继承Person,一个抽象class。
我的问题是:如果我尝试访问 Guys 列表,我没问题。但是我尝试访问任何一个人,特别是,我有一个问题:Guy
在 Person
之前加载。然而,由于Guy
继承了Person
,Person
被加载,并试图访问TOM
,但由于Guy
已经被加载,所以无法启动再次加载,所以我们有一个空引用。 (而且 ImmutableList
不接受 null,所以我们得到一个例外。)
所以我知道为什么会发生这种情况,但我不确定如何避免它。我可以将列表移到 Guy
class 中,但我将有多个 Person
的实现,并且我想要一个包含所有 Person
的主列表]s 在 Person
class.
加载静态内部 class 而不加载其包含 class 对我来说似乎很奇怪,但我想这是有道理的。有什么办法可以解决这个问题吗?
为解决方案而解决,这似乎可行(对于单线程)
public class Person
{
static Lazy tom = new Lazy();
static Lazy cat = new Lazy();
public static final List<Guy> GUYS = ImmutableList.of(tom.get(), cat.get());
public static final class Guy extends Person
{
public static final Guy TOM = tom.get();
public static final Guy CAT = cat.get();
}
static class Lazy
{
Guy v;
Guy get()
{
if(v==null)
{
Object x = Guy.TOM; // just to trigger Guy initialization
if(v==null)
v = new Guy();
}
return v;
}
}
OP 的设计本质上是递归的。尽管 Java 有一个明确定义的初始化过程(当检测到递归时会退出),但行为会根据首先初始化的 class 而有所不同。
字段为 final
的要求使问题变得更糟,这严重限制了我们分配它们的方式。否则,我们可以多处尝试if(null) assign
来解决问题。
上面发布的解决方案本质上是使用一个临时变量来解决 final
约束。
使用Map的修改版本,适用于更多字段数:
public class Person
{
static Map<String,Guy> cache = new HashMap<>(); // remove after both class are initialized
static Guy get(String name)
{
if(!cache.containsKey(name))
{
Object x = Guy.TOM; // just to trigger Guy initialization
if(!cache.containsKey(name))
cache.put(name, new Guy());
}
return cache.get(name);
}
public static final List<Guy> GUYS = ImmutableList.of(get("tom"), get("cat"));
static{ if(Guy.TOM!=null) cache=null; }
public static final class Guy extends Person
{
public static final Guy TOM = get("tom");
public static final Guy CAT = get("cat");
static{ if(Person.GUYS!=null) cache=null; }
}
多线程
有可能一个线程开始初始化 Person,而另一个线程开始初始化 Guy。死锁是可能的,它确实发生在现实世界中。所以这个设计本身就是有缺陷的。
一般来说,静态初始化时相互依赖的两个classes(不一定是父子)存在死锁的危险。
应用程序可以通过在应用程序启动时以可预测的方式小心地触发初始化来逃避做这种事情。尽管如此,这还是很危险的,它可能会让某人陷入调试地狱的日子。
您可以将不可变列表保存在单独的 class 中,例如 People.GUYS
:
public class People {
public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY);
}
通过这种方式,您仍然可以将个人保留在 Guy
class:
public abstract class Person {
public static final class Guy extends Person {
public static final Guy TOM = new Guy();
public static final Guy DICK = new Guy();
public static final Guy HARRY = new Guy();
}
}
使用 Person
抽象 class 的要求是否绝对(即它是否已经是遗留代码的一部分)?
如果不是,那么作为建议,我建议您将数据模型的设计重构为实现 Person 接口的 Guy enum
class。与基于 class 的设计相比,这种设计有很多优点。
public enum Guy implements Person {
TOM {
@Override
public void personMethod1(...) {...} // constant-specific
:
:
},
DICK {
@Override
public void personMethod1(...) {...} // constant-specific
:
:
};
@Override
public int personMethod2(...) {...} // general to the enum class
:
:
}
请注意,在这种情况下,没有理由使用常量引用定义 List
实例,因为枚举 API 提供了 GUY.values()
方法。
它将解决您所有的初始化问题,同时仍然启用 Person
的多个实现。缺点是代码重复。 Person
中的方法将需要在每个枚举 class 中实现(但如果有很多方法,您始终可以创建一个私有静态助手 class 并将方法调用转发给它。)
改变
public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY);
到
public static final List<Guy> GUYS = Collections.unmodifiableList(Arrays.asList(new Guy[] {Guy.TOM, Guy.DICK, Guy.HARRY}));
应该可以(我用 JDK 8 测试过),但它是否仍然满足您的要求?
public abstract class Person {
public static final class Guy extends Person {
public static final Guy TOM = new Guy();
public static final Guy DICK = new Guy();
public static final Guy HARRY = new Guy();
}
public static final List<Guy> GUYS = Collections.unmodifiableList(Arrays.asList(new Guy[] {Guy.TOM, Guy.DICK, Guy.HARRY}));
}