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

StackOverflow https://stackoverflow.com/questions/205239

Вопрос

Рассмотрим следующий интерфейс на 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"!!!
Это было полезно?

Решение

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

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

Нарве Саэтре из 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 с полями public , затем скомпилировать наш класс с учетом этого и поменять местами классы Person .Не было проверки доступа во время выполнения, если мы запускали для класса, отличного от того, для которого мы скомпилировали.Однако, мы не можем привязать окончательной поля во время выполнения, используя либо замена класса или рефлексии.

В JDK 1.1.8 JavaDocs для java.lang.reflect.Поле содержало следующее чтобы сказать:

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

JDK 1.2.x

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

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

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

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.java: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, то увидим, что изменять поля final следует только в рамках нашего процесса построения.Вариант использования - когда мы десериализуем объект, а затем, как только мы создадим объект, мы инициализируем конечные поля, прежде чем передавать его дальше.После того, как мы сделали объект доступным для другого потока, мы не должны изменять конечные поля, используя отражение.Результат был бы непредсказуем.

Там даже написано вот что:Если конечное поле инициализировано константой времени компиляции в объявлении поля, изменения в конечном поле могут не наблюдаться, поскольку использование этого конечного поля заменяется во время компиляции константой времени компиляции.Это объясняет, почему наше поле 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.х и JDK 5.х, мы получим следующее выходной:

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

java версия "1.5.0" Поток исключений "main" IllegalAccessException:Поле является окончательным в java.lang.reflect.Field.set(поле.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"?

Другие советы

Вы скрываете это, это особенность "Области видимости".Каждый раз, когда вы находитесь в меньшей области, вы можете переопределить все переменные, которые вам нравятся, и внешние переменные области будут "Затенены".

Кстати, вы можете просмотреть его еще раз, если хотите:

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

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

Теперь КЛАВИША вернет "c".;

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

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

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

    }
}

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

I.KEY //returns "a"
B.KEY //returns "b"

В качестве конструктивного соображения,

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"
    }
}

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top