在构造函数退出之前访问最终变量

问题描述 投票:0回答:4

好吧,我一直在摆弄优秀的 JodaTime 库,试图实现一个通用的零售/财政 (4-5-4) 日历。 我已经找到了适合我公司的具体案例,但一般情况(主要是确定年初和闰年)是杀手锏;例如,有一组日期,两个会计年度(通常长 364 天)将在 1 个 ISO 年度内开始。

在确定年份开始规则的过程中,我最终得到了一个抽象类和几个具体类,用于根据它们落在 ISO 闰日的哪一侧来确定年份开始。

(精简)抽象类:

private static abstract class SimpleFiscalYearEndPattern implements FiscalYearEndPattern {

    protected final int leapYearCountOffset;
    protected final int doomsdayOffset;

    private final int startingDayOfWeek;
    private final int yearOffset;
    private final long millisFromEpochToFiscalYearStart;
    private final long millisElapsedToEpochDividedByTwo;

    /**
     * Restricted constructor
     * @param fiscalYear
     * @param startingOn
     * @param inFirstWeek
     */
    protected SimpleFiscalYearEndPattern(final int fiscalYear, final LocalDate startingOn, final MonthDay inFirstWeek) {
        this.yearOffset = fiscalYear - startingOn.getYear();
        this.doomsdayOffset = getDoomsdayOffset(inFirstWeek);
        this.startingDayOfWeek = startingOn.getDayOfWeek();

        final int startingDoomsday = getDoomsdayOffset(new MonthDay(startingOn, REFERENCE_CHRONOLOGY));
        // If the starting doomsday is a later day-of-week, it needs to become negative.
        this.leapYearCountOffset = calculateLeapYearCountOffset(startingDoomsday : doomsdayOffset, doomsdayOffset);

        final int leapYearsBefore = getPreviousLeapYears(fiscalYearBeforeEpoch);
    }
}

(精简)具体课程(适用于 1/7 - 2/28 范围内的日期):

private static final class BeforeLeapYearEndPattern extends SimpleFiscalYearEndPattern {

    private static final int FIRST_YEAR_LEAP_YEAR_OFFSET = -1;

    private BeforeLeapYearEndPattern(final int fiscalYear, final LocalDate startingOn, final MonthDay onOrBefore) {
        super(fiscalYear, startingOn, onOrBefore);
    }

    public static final BeforeLeapYearEndPattern create(final int fiscalYear, final LocalDate startingOn, final MonthDay onOrBefore) {
        return new BeforeLeapYearEndPattern(fiscalYear, startingOn, onOrBefore);
    }

    /* (non-Javadoc)
     * @see ext.site.time.chrono.FiscalYearEndPatternBuilder.SimpleFiscalYearEndPattern#getPreviousLeapYears(int)
     */
    @Override
    protected int getPreviousLeapYears(final int isoYear) {
        // Formula gets count of leap years, including current, so subtract a year first.
        final int previousYear = isoYear - 1;
        // If the doomsday offset is -1, then the first year is a leap year.
        return (previousYear + leapYearCountOffset + (previousYear / 4) - (previousYear / 100) + (previousYear / 400)) / 7 + (leapYearCountOffset == FIRST_YEAR_LEAP_YEAR_OFFSET ? 1 : 0);
    }

如果您会注意到,我使用

leapYearCountOffset
,它是在抽象超类中定义(作为最终变量)的
getPreviousLeapYears()
,然后从超类构造函数调用。 我不想在超类构造函数中重复该公式 - 对于 3/1-12/31 范围内的日期来说它是不同的;我也不想将实例变量放在具体的子类中 - 其他计算仍然需要
leapYearCountOffset

问题是:从构造函数调用(子类)方法时

leapYearCountOffset
的状态是什么? 它是否以任何方式得到保证,或者是否可以根据编译器的突发奇想而改变? 我到底该如何测试才能找到答案? 我已经知道编译器可以自由地重新安排一些语句,但是会(可能吗?)在这里发生这种情况吗?

java
4个回答
4
投票

final
变量的保证之一是编译器在分配它们之前不会让您访问它们。 因此,如果它能够编译(应该如此),那么就可以开始了!


2
投票

由于在分配

getPreviousLeapYears
之后调用
leapYearCountOffset
,因此
leapYearCountOffset
将被正确初始化并且
getPreviousLeapYears
将看到正确的值。


Java 留下了确保构造函数期间调用的代码访问的变量在首次访问之前正确初始化的负担。 如果未正确初始化,在构造函数期间调用的代码将看到该字段类型的零值。


节目

final

打印

public class Foo { protected final int x; Foo() { foo(); this.x = 1; foo(); } void foo() { System.out.println(this.x); } public static void main(String[] argv) { new Foo(); } }

因为 
0 1

在第一次调用

x
期间未初始化,但如上所述,您不会遇到此问题。

JLS 规定每次在构造函数中使用 Final
都必须在初始化之后,但对其他方法没有这样的保证。考虑

foo

对于语言来说,要确保
abstract class C {
  public final int x;

  C() {
    this.x = f();
  }

  abstract int f();
}
在每次使用之前都被初始化,它需要确保不存在像

这样的子类

x

这需要对类进行全局推理,这与 Java 的动态链接不一致,并且会增加语言规范的复杂性。
    

虽然

0
投票
保证有其最终值,但这仍然是一个等待发生的事故。方法

leapYearCountOffset

 在子类初始化开始之前执行,因此子类中的任何变量都将具有默认值(0 或 null)。
现在没有危险,但如果有人进来并改变
getPreviousLeapYears

,也许通过添加一个新的

BeforeLeapYearEndPattern

实例变量,然后在
final
中使用,你会受伤。

看起来这个问题是由于线程内和线程间语义之间的混淆引起的。

0
投票
只要您在单线程中运行相关代码,一切都会按您的预期工作:代码重新排序不会产生明显的影响。

getPreviousLeapYears

字段也是如此。

final

字段为并发访问提供了额外的保证,并且这些保证仅在构造函数完成后才生效。这就是为什么不建议在构造函数完成之前使 
final
 字段可供其他线程访问。但只要您不尝试从其他线程访问有问题的字段,这并不重要。
但是,我同意从超类的构造函数调用子类方法是一种不好的做法,因为此时子类字段尚未初始化。

© www.soinside.com 2019 - 2024. All rights reserved.