Java: переменная, возможно, уже была инициализирована, но я не понимаю, как

Сначала немного контекста: весь вставленный ниже код находится в другом классе, объявленном как public class TheClass extends SomeProprietaryClass. Я не могу объявить эти классы в другом файле по разным причинам... И сообщения журнала на французском языке. И я "последний счастливый" программист. Вот в чем суть проблемы...

Теперь код... (вероятно, его слишком много - удаление по требованию, чтобы сохранить только соответствующие части)

Пользовательское исключение:

private static final class BreadCrumbException
    extends Exception
{
    private BreadCrumbException(final String message)
    {
        super(message);
    }

    private BreadCrumbException(final String message, final Throwable cause)
    {
        super(message, cause);
    }
}

Перечисление для «материализации» видимости элемента хлебной крошки:

private enum Visibility
{
    MAINPAGE("R"),
    MENU("M"),
    BREADCRUMB("A"),
    COMMERCIAL("C");

    private static final Map<String, Visibility> reverseMap
        = new HashMap<String, Visibility>();

    private static final String characterClass;

    static {
        final StringBuilder sb = new StringBuilder("[");

        for (final Visibility v: values()) {
            reverseMap.put(v.flag, v);
            sb.append(v.flag);
        }

        sb.append("]");
        characterClass = sb.toString();
    }

    private final String flag;

    Visibility(final String flag)
    {
        this.flag = flag;
    }

    static EnumSet<Visibility> fromBC(final String element)
    {
        final EnumSet<Visibility> result = EnumSet.noneOf(Visibility.class);

        for (final String s: reverseMap.keySet())
            if (element.contains(s))
                result.add(reverseMap.get(s));

        return result;
    }


    static String asCharacterClass()
    {
        return characterClass;
    }

    static String asString(final EnumSet<Visibility> set)
    {
        final StringBuilder sb = new StringBuilder();

        for (final Visibility v: set)
            sb.append(v.flag);

        return sb.toString();
    }

    @Override
    public String toString()
    {
        return flag;
    }
}

Элемент хлебной крошки:

private static class BreadCrumbElement
{
    private static final Pattern p
        = Pattern.compile(String.format("(%s+)(\\d+)",
        Visibility.asCharacterClass()));

    private final String element;
    private final String menuID;
    private final EnumSet<Visibility> visibility;

    BreadCrumbElement(final String element)
    {
        final Matcher m = p.matcher(element);

        if (!m.matches())
            throw new IllegalArgumentException("Élément de fil d'ariane invalide: " + element);

        this.element = element;
        visibility = EnumSet.copyOf(Visibility.fromBC(m.group(1)));
        menuID = m.group(2);
    }

    public boolean visibleFrom(final Visibility v)
    {
        return visibility.contains(v);
    }

    @Override
    public boolean equals(final Object o)
    {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        final BreadCrumbElement that = (BreadCrumbElement) o;

        return element.equals(that.element);
    }

    @Override
    public int hashCode()
    {
        return element.hashCode();
    }

    @Override
    public String toString()
    {
        return element;
    }

    public String getMenuID()
    {
        return menuID;
    }
}

Панировочные сухари:

private static class BreadCrumb
    implements Iterable<BreadCrumbElement>
{
    private static final BreadCrumb EMPTY = new BreadCrumb();

    private final List<BreadCrumbElement> elements
        = new LinkedList<BreadCrumbElement>();

    private String bc;

    BreadCrumb(final String bc)
        throws BreadCrumbException
    {
        final Set<BreadCrumbElement> set = new HashSet<BreadCrumbElement>();
        BreadCrumbElement e;

        for (final String element: bc.split("\\s+")) {
            e = new BreadCrumbElement(element);
            if (!set.add(e))
                throw new BreadCrumbException("Élément dupliqué "
                    + "dans le fil d'Ariane : " +  element);
            elements.add(e);
        }

        if (elements.isEmpty())
            throw new BreadCrumbException("Fil d'ariane vide!");

        if (!elements.get(0).visibleFrom(Visibility.MAINPAGE))
            throw new BreadCrumbException("Le fil d'Ariane ne "
                + "commence pas à l'accueil : " + bc);

        set.clear();
        this.bc = bc;
    }

    private BreadCrumb()
    {
    }

    BreadCrumb reverse()
    {
        final BreadCrumb ret = new BreadCrumb();
        ret.elements.addAll(elements);
        Collections.reverse(ret.elements);
        ret.bc = StringUtils.join(ret.elements, " ");
        return ret;
    }

    public Iterator<BreadCrumbElement> iterator()
    {
        return elements.iterator();
    }

    @Override
    public String toString()
    {
        return bc;
    }
}

Интерфейс рендерера хлебных крошек:

public interface BreadCrumbRender
{
    List<CTObjectBean> getBreadCrumb()
        throws Throwable;

    String getTopCategory();

    String getMenuRoot();

    String getContext();
}

Реализация интерфейса выше, который является источником моих проблем:

private class CategoryBreadCrumbRender
    implements BreadCrumbRender
{
    private final BreadCrumb bc;
    private final CTObject object;

    CategoryBreadCrumbRender(final CTObject object)
    {
        this.object = object;
        final String property;

        // FIELD_BC is declared as a private static final String earlier on.
        // logger is also a private static final Logger
        try {
            property = object.getProperty(FIELD_BC);
        } catch (Throwable throwable) {
            logger.fatal("Impossible d'obtenir le champ " + FIELD_BC
                + " de l'objet", throwable);
            bc = BreadCrumb.EMPTY;
            return;
        }

        try {
            bc = new BreadCrumb(property);
        } catch (BreadCrumbException e) {
            logger.fatal("Impossible d'obtenir le fil d'Ariane", e);
            bc = BreadCrumb.EMPTY; // <-- HERE
        }
    }
    // ....

В точке, отмеченной // <-- HERE выше, Intellij IDEA, которую я использую, и javac (1.6.0.29) сообщают мне, что Variable bc might already have been assigned to, что считается ошибкой (и действительно, код не компилируется).

Беда в том, что я не понимаю почему... Мои рассуждения таковы:

  • в первом блоке try/catch (и да, .getProperty() вызывает Throwable), когда перехватывается исключение, bc успешно присваивается, а затем я возвращаюсь, пока все хорошо;
  • во втором блоке try/catch конструктор может дать сбой, и в этом случае я назначаю пустую цепочку, поэтому все должно быть в порядке, даже если bc является окончательным: присваивание не происходит (?) в блоке try, но происходит в вместо этого блок catch...

Кроме нет, это не так. Поскольку и IDEA, и javac со мной не согласны, они, безусловно, правы. Но почему?

(а также, BreadCrumb.EMPTY объявлен private static final в классе, интересно, как я вообще могу получить к нему доступ... Дополнительный вопрос)

EDIT: есть известная ошибка с ключевым словом final (здесь, спасибо @MiladNaseri за ссылку на него), однако следует отметить, что в этой ошибке переменная v всегда назначается только в catch блоках, но в приведенном выше коде я назначаю ее в try блоках и назначайте его в блоках catch только в случае возникновения исключения. Также следует отметить, что ошибка возникает только во втором блоке catch.


person fge    schedule 15.01.2012    source источник


Ответы (3)


Хорошо, предположим, что в первом блоке try при выполнении property = object.getProperty(FIELD_BC); возникает исключение. Итак, JVM войдет в блок catch и попутно инициализирует bc.

Затем во втором блоке try также возникает исключение, в результате чего BreadCrumb.EMPTY присваивается bc, эффективно переопределяя его исходное значение.

Вот как bc могло быть уже инициализировано. Надеюсь, вы понимаете, откуда я.

Поскольку механизм анализа JAVAC не делает различий между одним или несколькими операторами внутри блока try, он не видит в вашем случае никаких отличий от приведенного ниже:

try {
    bc = null;
    String x = null;
    System.out.println(x.toString());
} catch (Throwable e) {
    bc = null;
}

В этом случае bc будет назначено дважды. Другими словами, JAVAC не будет заботиться о том, где находится источник Throwable, его заботит только то, что он может быть там, и что bc может пройти успешное присваивание в этом блоке try.

person Milad Naseri    schedule 15.01.2012
comment
+1 Да, именно так. Вам нужно сделать локальную переменную localBC, и, в самом конце, присвоить ей значение bc один и только один раз. - person user949300; 15.01.2012
comment
Точно. Это способ обойти предупреждение. - person Milad Naseri; 15.01.2012
comment
да. Но алгоритм управления потоком, приводящий к этому предупреждению, довольно сомнительно относится к операторам return. Вы можете убедиться в этом сами. Поместите оператор возврата в середине метода, и вы получите ошибку недостижимого кода. Затем поместите перед ним всегда истинное условие, и все готово (даже если условие является исключением, которое никогда не может произойти). - person Milad Naseri; 15.01.2012
comment
Также следует отметить, что это известная ошибка. - person Milad Naseri; 15.01.2012
comment
Дайте определение сомнительному? И это ошибка, код не компилируется... То есть я присваиваю bc в первом блоке catch и return без проблем. Затем я пытаюсь назначить его во втором блоке try: если конструктор BreadCrumb терпит неудачу, bc не назначается, и тогда он должен быть назначен в блоке catch... - person fge; 15.01.2012
comment
Iffy здесь означает глючит. Он не понимает, что в четко определенной ситуации bc не может быть назначено более одного раза. Чтобы избежать ошибки, вы должны оценить значение для bc с помощью промежуточной переменной, а затем присвоить его bc. Кроме того, есть еще одна ситуация, которая может привести к ошибке, которую я сразу добавлю в ответ. - person Milad Naseri; 15.01.2012
comment
@MiladNaseri после прочтения ошибки, я действительно не думаю, что это относится к моему варианту использования - в отчете об ошибке переменная назначается только в блоках catch. Это не мой случай... - person fge; 15.01.2012
comment
Да ты прав. Эта ошибка точно не связана с этой ошибкой. Однако эта тема . Я добавил дополнительную информацию. Я надеюсь, что я был тщательным. - person Milad Naseri; 15.01.2012
comment
Вам не обязательно нужно. Я добавил суть в свой ответ. - person Milad Naseri; 15.01.2012
comment
@MiladNaseri Боюсь, я не вижу сути вопроса ... Вы, кажется, лучше меня понимаете, даже после того, как я прочитал ветку и страницу IBM, более того, ваш первый абзац все еще не кажется учитывайте тот факт, что я возвращаюсь в первом блоке catch. Не хотите уточнить ответ? - person fge; 15.01.2012
comment
Конечно. Предположим, что в первом блоке try ошибок не возникает, поэтому bc все еще не инициализирован. Затем вы достигаете второго блока try, в котором bc может быть инициализирован или не инициализирован (поскольку JAVAC не может знать, когда или где может возникнуть исключение), поэтому, когда он достигает присваивания внутри блока catch, он не может быть уверен, было ли оно уже инициализирован или нет. Кроме того, эта ошибка гарантирует, что вы оставите конструктор только с одним исполняемым оператором присваивания для конечной переменной. Вам нелегко понять это, потому что вы ЗНАЕТЕ, что блок try предназначен только для задания. - person Milad Naseri; 15.01.2012
comment
Но если бы он сопровождался вторым оператором, скажем, String x = null; System.out.println(x.toString());, тогда этот оператор мог быть причиной Исключения, оставляя bc инициализированным, а затем переходя к блоку catch, где ему снова было бы назначено новое значение. Однако JAVAC не может так просто предположить, что блок catch существует только для этого единственного назначения, и в этой особой ситуации игнорировать возможность такой ошибки. - person Milad Naseri; 15.01.2012
comment
@MiladNaseri Хорошо, спасибо за ваше время. В итоге я использовал временную переменную и только один блок catch (боже, как же я ненавижу API-интерфейсы, бросающие Throwable), но вы очень помогли мне понять, что здесь происходит! Из любопытства я, вероятно, попробую несколько тестовых классов с проверенными/непроверенными исключениями и т. д. и посмотрю, что к чему... Еще раз спасибо! - person fge; 15.01.2012

Я не думаю, что анализ достаточно глубокий, чтобы действительно понять, что в блоке try есть только одно выражение, и диагностика выдается несмотря ни на что, поэтому вы видите это в своем случае.

person Bill    schedule 15.01.2012

Попробуйте это вместо этого:

BreadCrumb tmp = null;
try {
    tmp = new BreadCrumb(property);
} catch (BreadCrumbException e) {
    logger.fatal("Impossible d'obtenir le fil d'Ariane", e);
    tmp = BreadCrumb.EMPTY;
}
bc = tmp;
person Óscar López    schedule 15.01.2012