簡體   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