繁体   English   中英

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

[英]Accessing a final variable before constructor exit

好吧,我一直在搞乱优秀的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的状态什么? 它是以任何方式得到保证,还是可以随着编译器的突发奇想改变? 我怎么测试才发现? 我已经意识到编译器可以自由地重新安排一些语句,但是会(可能吗?)发生在这里?

final变量的一个保证是编译器在分配之前不允许您访问它们。 所以,如果它编译(它应该),你很高兴!

由于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

因为在第一次调用foo没有初始化x ,但如上所述,你没有这个问题。


JLS表示构造函数中每个final的使用必须在初始化之后,但对其他方法没有这样的保证。 考虑

abstract class C {
  public final int x;

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

  abstract int f();
}

对于确保在每次使用之前初始化x的语言,需要确保不存在类似的子类

class D extends C {
  int f() {
    return this.x;
  }
}

这需要对类的全局推理,这与Java的动态链接不一致,并为语言规范增加了很多复杂性。

虽然leapYearCountOffset保证有最终值,但这仍然是一个等待发生的事故。 方法getPreviousLeapYears在子类初始化开始之前执行,因此子类中的任何变量都将具有其默认值(0或null)。

现在没有危险,但是如果有人进来并改变BeforeLeapYearEndPattern ,可能通过添加一个新的final实例变量,然后在getPreviousLeapYears ,你会受到伤害。

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

只要您在一个线程中运行有问题的代码,一切都按预期工作:代码重新排序可能没有明显的效果。

final字段也是如此。 final字段为并发访问提供了额外的保证,这些保证仅在构造函数完成后生效。 这就是为什么不建议在构造函数完成之前使其他线程可以访问final字段。 但只要您不尝试从其他线程访问相关字段,这无关紧要。

但是,我同意从超类的constuctor调用子类方法是一种不好的做法,因为子类字段在那时没有初始化。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM