Является окончательным окончанием во время выполнения?
Я играл с ASM, и я считаю, что мне удалось добавить окончательный модификатор в поле экземпляра класса; однако затем я начал создавать экземпляр указанного класса и вызывать на нем сеттер, который успешно изменил значение поля, которое теперь является окончательным. Я что-то делаю неправильно с моими изменениями в байткоде или окончательно соблюдаю только компилятор Java?
Обновление: (31 июля) Вот вам какой-то код. Основные части
- простое POJO с
private int x
и private final int y
,
- MakeFieldsFinalClassAdapter, который делает каждое поле посещением окончательного, если оно уже отсутствует,
- и AddSetYMethodVisitor, что приводит к тому, что метод setX() POJO также устанавливает y в то же значение, что и x.
Иными словами, мы начинаем с класса с одним конечным (x) и одним не конечным (y) полем. Мы делаем x final. Мы устанавливаем setX() множество y в дополнение к установке x. Мы бегаем. И x и y устанавливаются без ошибок. Код находится в github. Вы можете клонировать его с помощью:
git clone git://github.com/zzantozz/testbed.git tmp
cd tmp/asm-playground
Две примечательные вещи: Причина, по которой я задал этот вопрос в первую очередь: как поле, которое я сделал окончательным, так и поле, которое уже было окончательным, могут быть установлены с тем, что я считаю быть нормальными инструкциями по байт-коду.
Другое обновление: (1 авг.) Протестировано с 1.6.0_26-b03 и 1.7.0-b147 с теми же результатами. То есть, JVM с радостью изменяет окончательные поля во время выполнения.
Финальное (?) обновление: (19 сентября)
Я удаляю полный источник из этого сообщения, потому что он довольно длинный, но он все еще доступен на github (см. Выше).
Я считаю, что я убедительно доказал, что JVK7 JVM нарушает спецификацию. (См. отрывок из ответа Стивена.) После использования ASM для изменения байт-кода, как описано ранее, я написал его обратно в файл класса. Используя отличный JD-GUI, этот файл класса декомпилируется по следующему коду:
package rds.asm;
import java.io.PrintStream;
public class TestPojo
{
private final int x;
private final int y;
public TestPojo(int x)
{
this.x = x;
this.y = 1;
}
public int getX() {
return this.x;
}
public void setX(int x) {
System.out.println("Inside setX()");
this.x = x; this.y = x;
}
public String toString()
{
return "TestPojo{x=" +
this.x +
", y=" + this.y +
'}';
}
public static void main(String[] args) {
TestPojo pojo = new TestPojo(10);
System.out.println(pojo);
pojo.setX(42);
System.out.println(pojo);
}
}
Краткий взгляд на это должен сказать вам, что класс никогда не будет компилироваться из-за переназначения конечного поля, и все же запуск этого класса в простой ваниле JDK 6 или 7 выглядит следующим образом:
$ java rds.asm.TestPojo
TestPojo{x=10, y=1}
Inside setX()
TestPojo{x=42, y=42}
- Есть ли кто-нибудь еще, прежде чем сообщать об ошибке?
- Кто-нибудь может подтвердить, что это ошибка в JDK 6 или только в 7?
Ответы
Ответ 1
Является окончательным окончанием во время выполнения?
Не в том смысле, о котором вы говорите.
AFAIK, семантика модификатора final
применяется только компилятором байт-кода.
Для инициализации полей final
нет специальных байт-кодов, и верификатор байт-кода (по-видимому) также не проверяет "незаконные" назначения.
Однако компилятор JIT может рассматривать модификатор final
как подсказку о том, что вещи не нужно повторять. Итак, если ваши байткоды изменяют переменную, помеченную как final
, вы можете вызвать непредсказуемое поведение. (И то же самое может произойти, если вы используете отражение для изменения переменной final
. Спецификация явно говорит так...)
И, конечно, вы можете изменить поле final
, используя отражение.
UPDATE
Я взглянул на спецификацию Java 7 JVM, и это частично противоречит сказанному выше. В частности, описание кода операции PutField говорит:
"Связывание исключений... В противном случае, если поле является окончательным, оно должно быть объявлено в текущем классе, а инструкция должна выполняться в методе инициализации экземпляра (<init>
) текущего класса. В противном случае вызывается IllegalAccessError
.
Итак, хотя вы могли (теоретически) присваивать поле final
несколько раз в конструкторе объекта, верификатор байт-кода должен помешать любой попытке загрузить метод, содержащий байт-код, который присваивает final
. Что... когда вы думаете о защищенных песочницах Java... это хорошо.
Ответ 2
Если поле окончательно, все равно может возникнуть ситуация, когда ему назначено. Например, в конструкторе. Эта логика применяется компилятором, как указано в этой статье . Сама JVM не обеспечивала бы соблюдение таких правил, поскольку цена исполнения была бы слишком высокой, а верификатор байтового кода не мог бы легко определить, назначено ли поле только один раз.
Таким образом, создание поля final
через ASM, вероятно, не имеет большого смысла.
Ответ 3
Вы можете перезаписать окончательные поля во время выполнения с помощью отражения.
Gson делает это все время, привязывая JSON к объектам Java.