Почему конечные константы в Java могут быть переопределены?

Рассмотрим следующий интерфейс в Java:

public interface I {
    public final String KEY = "a";
}

И следующий класс:

public class A implements I {
    public String KEY = "b";

    public String getKey() {
        return KEY;
    }
}

Почему класс A может прийти и переопределить конечную константу интерфейса I?

Попробуйте сами:

A a = new A();
String s = a.getKey(); // returns "b"!!!

person Yuval Adam    schedule 15.10.2008    source источник


Ответы (6)


Несмотря на то, что вы скрываете переменную, довольно интересно узнать, что вы можете изменить конечные поля в java, как вы можете прочитать здесь:

Java 5 — "final" больше не является окончательным

Narve Saetre из Machina Networks в Норвегии вчера прислал мне записку, упомянув, что очень жаль, что мы можем изменить дескриптор на окончательный массив. Я неправильно понял его и начал терпеливо объяснять, что мы не можем сделать массив постоянным и что нет никакого способа защитить содержимое массива. «Нет, — сказал он, — мы можем изменить финальную метку, используя отражение».

Я попробовал пример кода Narve, и, невероятно, Java 5 позволил мне изменить окончательный дескриптор, даже дескриптор примитивного поля! Я знал, что в какой-то момент это было разрешено, но затем было запрещено, поэтому я провел несколько тестов со старыми версиями Java. Во-первых, нам нужен класс с финальными полями:

public class Person {
  private final String name;
  private final int age;
  private final int iq = 110;
  private final Object country = "South Africa";

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String toString() {
    return name + ", " + age + " of IQ=" + iq + " from " + country;
  }
}

JDK 1.1.x

В JDK 1.1.x мы не могли получить доступ к закрытым полям с помощью отражения. Однако мы могли бы создать другого человека с публичными полями, затем скомпилировать наш класс для него и поменять местами классы Person. Не было проверки доступа во время выполнения, если мы работали с классом, отличным от того, с которым мы скомпилировали. Однако мы не могли перепривязать окончательные поля во время выполнения, используя обмен классами или отражение.

В JDK 1.1.8 JavaDocs для java.lang.reflect.Field было сказано следующее:

  • Если этот объект Field обеспечивает управление доступом к языку Java, а базовое поле недоступно, метод выдает исключение IllegalAccessException.
  • Если базовое поле является окончательным, метод создает исключение IllegalAccessException.

JDK 1.2.x

В JDK 1.2.x это немного изменилось. Теперь мы можем сделать приватные поля доступными с помощью метода setAccessible(true). Доступ к полям теперь проверялся во время выполнения, поэтому мы не могли использовать трюк с заменой классов для доступа к закрытым полям. Однако теперь мы можем внезапно переназначить конечные поля! Посмотрите на этот код:

import java.lang.reflect.Field;

public class FinalFieldChange {
  private static void change(Person p, String name, Object value)
      throws NoSuchFieldException, IllegalAccessException {
    Field firstNameField = Person.class.getDeclaredField(name);
    firstNameField.setAccessible(true);
    firstNameField.set(p, value);
  }
  public static void main(String[] args) throws Exception {
    Person heinz = new Person("Heinz Kabutz", 32);
    change(heinz, "name", "Ng Keng Yap");
    change(heinz, "age", new Integer(27));
    change(heinz, "iq", new Integer(150));
    change(heinz, "country", "Malaysia");
    System.out.println(heinz);
  }
}

Когда я запустил это в JDK 1.2.2_014, я получил следующий результат:

Ng Keng Yap, 27 of IQ=110 from Malaysia    Note, no exceptions, no complaints, and an incorrect IQ result. It seems that if we set a

Последнее поле примитива во время объявления, значение встроено, если тип примитивный или String.

JDK 1.3.x и 1.4.x

В JDK 1.3.x компания Sun немного усложнила доступ и запретила нам изменять финальное поле с отражением. То же самое было и с JDK 1.4.x. Если бы мы попытались запустить класс FinalFieldChange для повторной привязки окончательных полей во время выполнения с помощью отражения, мы бы получили:

версия java "1.3.1_12": поток исключений "main" IllegalAccessException: поле является окончательным в java.lang.reflect.Field.set(собственный метод) в FinalFieldChange.change(FinalFieldChange.java:8) в FinalFieldChange.main(FinalFieldChange. ява:12)

версия java "1.4.2_05" Поток исключений "main" IllegalAccessException: поле является окончательным в java.lang.reflect.Field.set(Field.java:519) в FinalFieldChange.change(FinalFieldChange.java:8) в FinalFieldChange.main( FinalFieldChange.java:12)

JDK 5.x

Теперь мы подошли к JDK 5.x. Класс FinalFieldChange имеет тот же вывод, что и в JDK 1.2.x:

Ng Keng Yap, 27 of IQ=110 from Malaysia    When Narve Saetre mailed me that he managed to change a final field in JDK 5 using

Поразмыслив, я надеялся, что в JDK закралась ошибка. Однако мы оба чувствовали, что это маловероятно, особенно такая фундаментальная ошибка. После некоторого поиска я нашел JSR-133: модель памяти Java и спецификацию потоков. Большая часть спецификации трудна для чтения и напоминает мне о моих университетских днях (раньше я так писал ;-). Однако JSR-133 настолько важен, что его следует прочитать всем программистам Java. (Удачи)

Начните с главы 9 Семантика конечного поля на стр. 25. В частности, прочитайте раздел 9.1.1 Модификация конечных полей после построения. Имеет смысл разрешить обновления конечных полей. Например, мы могли бы ослабить требование иметь поля, не являющиеся окончательными в JDO.

Если мы внимательно прочитаем раздел 9.1.1, мы увидим, что мы должны изменять только конечные поля как часть нашего процесса построения. Вариант использования заключается в том, что мы десериализуем объект, а затем, когда мы создали объект, мы инициализируем окончательные поля перед его передачей. После того, как мы сделали объект доступным для другого потока, мы не должны изменять конечные поля с помощью отражения. Результат не был бы предсказуем.

В нем даже говорится следующее: если конечное поле инициализируется константой времени компиляции в объявлении поля, изменения в конечном поле могут не наблюдаться, поскольку использование этого конечного поля заменяется во время компиляции константой времени компиляции. Это объясняет, почему наше поле iq остается прежним, а страна меняется.

Как ни странно, JDK 5 немного отличается от JDK 1.2.x тем, что вы не можете изменить статическое конечное поле.

import java.lang.reflect.Field;

public class FinalStaticFieldChange {
  /** Static fields of type String or primitive would get inlined */
  private static final String stringValue = "original value";
  private static final Object objValue = stringValue;

  private static void changeStaticField(String name)
      throws NoSuchFieldException, IllegalAccessException {
    Field statFinField = FinalStaticFieldChange.class.getDeclaredField(name);
    statFinField.setAccessible(true);
    statFinField.set(null, "new Value");
  }

  public static void main(String[] args) throws Exception {
    changeStaticField("stringValue");
    changeStaticField("objValue");
    System.out.println("stringValue = " + stringValue);
    System.out.println("objValue = " + objValue);
    System.out.println();
  }
}

Когда мы запускаем это с JDK 1.2.x и JDK 5.x, мы получаем следующий вывод:

версия Java "1.2.2_014": stringValue = исходное значение objValue = новое значение

версия java "1.5.0" Поток исключений "main" IllegalAccessException: поле является окончательным в java.lang.reflect.Field.set(Field.java:656) в FinalStaticFieldChange.changeStaticField(12) в FinalStaticFieldChange.main(16)

Итак, JDK 5 похож на JDK 1.2.x, только отличается?

Заключение

Вы знаете, когда был выпущен JDK 1.3.0? Я изо всех сил пытался выяснить, поэтому я скачал и установил его. Файл readme.txt имеет дату 2000/06/02 13:10. Итак, ему больше 4 лет (боже мой, как будто вчера). JDK 1.3.0 был выпущен за несколько месяцев до того, как я начал писать Информационный бюллетень специалистов по Java(tm)! Я думаю, можно с уверенностью сказать, что очень немногие Java-разработчики могут вспомнить детали до JDK1.3.0. Эх, ностальгия уже не та! Вы помните, как впервые запустили Java и получили эту ошибку: «Невозможно инициализировать потоки: невозможно найти класс java/lang/Thread»?

person André    schedule 15.10.2008
comment
Также возможно изменить значения конечных полей из кода JNI. - person Alexander; 16.10.2008

Вы это скрываете, это особенность "Сферы". В любое время, когда вы находитесь в меньшей области, вы можете переопределить все переменные, которые вам нравятся, и переменные внешней области будут «затенены».

Кстати, вы можете снова прицелиться, если хотите:

public class A implements I {
    public String KEY = "b";

    public String getKey() {
        String KEY = "c";
        return KEY;
    }
}

Теперь KEY вернет «c»;

Отредактировано, потому что оригинал отстой при повторном прочтении.

person Bill K    schedule 15.10.2008
comment
Чтобы немного расширить, вы не можете переопределить ничего, кроме методов экземпляра. Переменные и методы класса (статические) скрыты или затенены, но не переопределены. - person Keeg; 15.10.2008
comment
Спасибо. У меня было кое-что об этом в моей исходной версии, но потом я понял, что он, возможно, не использовал Override для обозначения наследования, он мог просто иметь в виду изменение, поэтому я перефразировал это, чтобы удалить это предположение, но хорошо, что это указано явно. . - person Bill K; 15.10.2008
comment
+1. Затенение означает даже следующее I a = (I) new A(); a.KEY => "a", так как в этом случае ссылочный тип имеет решающее значение. - person Benjamin Peter; 19.06.2012

Похоже, ваш класс просто скрывает переменную, а не перезаписывает ее:

public class A implements I {
    public String   KEY = "B";

    public static void main(String args[])
    {
        A t = new A();
        System.out.println(t.KEY);
        System.out.println(((I) t).KEY);
    }
}

Это напечатает «B» и «A», как вы нашли. Вы даже можете присвоить ему значение, так как переменная A.KEY не определена как окончательная.

 A.KEY="C" <-- this compiles.

Но -

public class C implements I{

    public static void main (String args[])
    {
        C t = new C();
        c.KEY="V"; <--- compiler error ! can't assign to final

    }
}
person Steve B.    schedule 15.10.2008

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

I.KEY //returns "a"
B.KEY //returns "b"
person Jorn    schedule 15.10.2008
comment
Вы должны делать это только в том случае, если вы специально переопределяете доступ по умолчанию. - person RichieHH; 11.05.2014

Из соображений дизайна,

public interface I {
    public final String KEY = "a";
}

Статические методы всегда возвращают родительский ключ.

public class A implements I {
    public String KEY = "b";

    public String getKey() {
        return KEY; // returns "b"
    }

    public static String getParentKey(){
        return KEY; // returns "a"
    }
}

Как и заметил Джом. Разработка статических методов с использованием переопределенных элементов интерфейса может стать серьезной проблемой. В общем, старайтесь избегать использования одного и того же имени для константы.

person Arnaldo Ignacio Gaspar Véjar    schedule 04.02.2013

Статические поля и методы присоединяются к объявляющему их классу/интерфейсу (хотя интерфейсы не могут объявлять статические методы, поскольку они являются полностью абстрактными классами, которые необходимо реализовать).

Итак, если у вас есть интерфейс с общедоступным статическим (vartype) (varname), это поле прикрепляется к этому интерфейсу.

Если у вас есть класс, реализующий этот интерфейс, трюк компилятора преобразует (this.)varname в InterfaceName.varname. Но если ваш класс переопределяет имя_переменной, к вашему классу присоединяется новая константа с именем имя_переменной, и компилятор знает, что теперь нужно преобразовать (this.)имя_переменной в NewClass.имя_переменной. То же самое относится и к методам: если новый класс не переопределяет метод, (this.)methodName транслируется в SuperClass.methodName, в противном случае (this.)methodName транслируется в CurrentClass.methodName.

Вот почему вы столкнетесь с предупреждением «x поле/метод должны быть доступны статически». Компилятор сообщает вам, что, хотя он может использовать трюк, он предпочел бы, чтобы вы использовали ClassName.method/fieldName, потому что это более явно для удобства чтения.

person MetroidFan2002    schedule 15.10.2008