Java change field type

Changing a Field’s Type in Recent JDKs

Nicolas is a developer advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). . Learn more

A couple of years ago, I attended a talk by my former colleague (but still friend) Volker Simonis. It gave me the idea to dig a bit into the subject of how to secure the JVM. From the material, I created a series of blog posts as well as a talk.

From that point on, I submitted the talk at meetups and conferences, where it was well-received. Because I like to explore different areas, I stopped to submit other proposals. Still, the talk is in my portfolio, and it was requested again in 2021. I have already presented it twice since the beginning of the year at the time of this writing.

It allowed me to update the demo with version 16 of the JDK. In this blog post, I want to share some findings regarding the security changes regarding changing a field’s type across JDK versions.

Fun with JDK 8

Let’s start with the JDK. Here’s a quiz I show early in my talk:

Foo foo = new Foo(); Class clazz = foo.getClass(); Field field = clazz.getDeclaredField(«hidden»); Field type = Field.class.getDeclaredField(«type»); AccessibleObject.setAccessible( new AccessibleObject[], true); type.set(field, String.class); field.set(foo, «This should print 5!»); Object hidden = field.get(foo); System.out.println(hidden); class Foo

Читайте также:  Python read file names

Take some time to guess the result of executing this program when running it with a JDK 8.

Here’s the relevant class diagram to help you:

Simplified Field class diagram

As can be seen, Field has a type attribute that contains. its type. With the above code, one can change the type of hidden from int to String so that the above code executes and prints «This should print 5!» .

With JDK 16, the snippet doesn’t work anymore. It throws a runtime exception instead:

Exception in thread "main" java.lang.NoSuchFieldException: type at java.base/java.lang.Class.getDeclaredField(Class.java:2549) at ch.frankel.blog.FirstAttempt.main(FirstAttempt.java:12)

The exception explicitly mentions line 12: Field.class.getDeclaredField(«type») . It seems as if the implementation of the Field class changed.

Looking at the Source Code of JDK 16

Let’s look at the source code in JDK 16:

public final class Field extends AccessibleObject implements Member < private Classclazz; private int slot; // This is guaranteed to be interned by the VM in the 1.4 // reflection implementation private String name; private Class type; // 1 // . >

If the field is present, why do we get the exception? We need to dive a bit into the code to understand the reason.

Here’s the sequence diagram of Class.getDeclaredField() :

Class.getDeclaredField() sequence diagram

The diagram reveals two interesting bits:

  1. The Reflection class manages a cache to improve performance.
  2. A field named fieldFilterMap filters out the fields that reflective access return.

Let’s investigate the Reflection class to understand the runtime doesn’t find the type attribute:

For this reason, none of the attributes of Field are accessible via reflection!

An Alternative Way to Change the Type

Since version 9, the JDK offers a new API to access fields as part of the java.lang.invoke package.

Here’s a quite simplified class diagram focusing on our usage:

VarHandle simplified class diagram

One can use the API to access the type attribute as above. The code looks like the following:

var foo = new Foo(); var clazz = foo.getClass(); var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup()); var type = lookup.findVarHandle(Field.class, "type", Class.class); var field = clazz.getDeclaredField("hidden"); type.set(field, String.class); field.setAccessible(true); field.set(foo, "This should print 5!"); var hidden = field.get(foo); System.out.println(hidden);

But running the code yields the following:

Exception in thread "main" java.lang.IllegalArgumentException: Can not set int field ch.frankel.blog.Foo.hidden to java.lang.String at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167) at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171) at java.base/jdk.internal.reflect.UnsafeIntegerFieldAccessorImpl.set(UnsafeIntegerFieldAccessorImpl.java:98) at java.base/java.lang.reflect.Field.set(Field.java:793) at ch.frankel.blog.FinalAttempt.main(FinalAttempt.java:16)

Though the code compiles and runs, it throws at field.set(foo, «This should print 5!») . We reference the type field and can change it without any issue, but it still complains.

The reason lies in the last line of the getDeclaredField() method:

public Field getDeclaredField(String name) throws NoSuchFieldException, SecurityException < Objects.requireNonNull(name); SecurityManager sm = System.getSecurityManager(); if (sm != null) < checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true); >Field field = searchFields(privateGetDeclaredFields(false), name); if (field == null) < throw new NoSuchFieldException(name); >return getReflectionFactory().copyField(field); // 1 >

Since the JDK code returns a copy of the field, the change happens on this copy, and we cannot change the original field’s type.

Conclusion

Though Java touts itself as a statically-typed language, version 8 of the JVM allows us to change the type at runtime dynamically. One of my favorite jokes during the talk mentioned above is that though we have learned that Java is statically-typed, it is dynamically-typed in reality.

We can track the change precisely in Java 12: the version 11 of the Reflection class shows a basic fieldFilterMap ; the version 12 shows a fully-configured one. Hence, if you want to avoid nasty surprises, you should upgrade to the latter, if not the latest.

To go further:

Orginally published at A Java Geek on April 4 th , 2021

Nicolas is a developer advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps.…

Источник

Java change field type

Каждый базовый тип данных занимает определенное количество байт памяти. Это накладывает ограничение на операции, в которые вовлечены различные типы данных. Рассмотрим следующий пример:

int a = 4; byte b = a; // ! Ошибка

В данном коде мы столкнемся с ошибкой. Хотя и тип byte, и тип int представляют целые числа. Более того, значение переменной a, которое присваивается переменной типа byte, вполне укладывается в диапазон значений для типа byte (от -128 до 127). Тем не менее мы сталкиваемся с ошибкой на этапе компиляции. Поскольку в данном случае мы пытаемся присвоить некоторые данные, которые занимают 4 байта, переменной, которая занимает всего один байт.

Тем не менее в программе может потребоваться, чтобы подобное преобразование было выполнено. В этом случае необходимо использовать операцию преобразования типов (операция () ):

int a = 4; byte b = (byte)a; // преобразование типов: от типа int к типу byte System.out.println(b); // 4

Операция преобразования типов предполагает указание в скобках того типа, к которому надо преобразовать значение. Например, в случае операции (byte)a , идет преобразование данных типа int в тип byte. В итоге мы получим значение типа byte.

Явные и неявные преобразования

Когда в одной операции вовлечены данные разных типов, не всегда необходимо использовать операцию преобразования типов. Некоторые виды преобразований выполняются неявно, автоматически.

Автоматические преобразования

Преобразования типов в языке Java

Стрелками на рисунке показано, какие преобразования типов могут выполняться автоматически. Пунктирными стрелками показаны автоматические преобразования с потерей точности.

Автоматически без каких-либо проблем производятся расширяющие преобразования (widening) — они расширяют представление объекта в памяти. Например:

byte b = 7; int d = b; // преобразование от byte к int

В данном случае значение типа byte, которое занимает в памяти 1 байт, расширяется до типа int, которое занимает 4 байта.

Расширяющие автоматические преобразования представлены следующими цепочками:

Автоматические преобразования с потерей точности

Некоторые преобразования могут производиться автоматически между типами данных одинаковой разрядности или даже от типа данных с большей разрядностью к типа с меньшей разрядностью. Это следующие цепочки преобразований: int -> float , long -> float и long -> double . Они производятся без ошибок, но при преобразовании мы можем столкнуться с потерей информации.

int a = 2147483647; float b = a; // от типа int к типу float System.out.println(b); // 2.14748365E9

Явные преобразования

Во всех остальных преобразованиях примитивных типов явным образом применяется операция преобразования типов. Обычно это сужающие преобразования (narrowing) от типа с большей разрядностью к типу с меньшей разрядностью:

Потеря данных при преобразовании

При применении явных преобразований мы можем столкнуться с потерей данных. Например, в следующем коде у нас не возникнет никаких проблем:

int a = 5; byte b = (byte) a; System.out.println(b); // 5

Число 5 вполне укладывается в диапазон значений типа byte, поэтому после преобразования переменная b будет равна 5. Но что будет в следующем случае:

int a = 258; byte b = (byte) a; System.out.println(b); // 2

Результатом будет число 2. В данном случае число 258 вне диапазона для типа byte (от -128 до 127), поэтому произойдет усечение значения. Почему результатом будет именно число 2?

Число a, которое равно 258, в двоичном системе будет равно 00000000 00000000 00000001 00000010 . Значения типа byte занимают в памяти только 8 бит. Поэтому двоичное представление числа int усекается до 8 правых разрядов, то есть 00000010 , что в десятичной системе дает число 2.

Усечение рациональных чисел до целых

При преобразовании значений с плавающей точкой к целочисленным значениям, происходит усечение дробной части:

double a = 56.9898; int b = (int)a;

Здесь значение числа b будет равно 56, несмотря на то, что число 57 было бы ближе к 56.9898. Чтобы избежать подобных казусов, надо применять функцию округления, которая есть в математической библиотеке Java:

double a = 56.9898; int b = (int)Math.round(a);

Преобразования при операциях

Нередки ситуации, когда приходится применять различные операции, например, сложение и произведение, над значениями разных типов. Здесь также действуют некоторые правила:

  • если один из операндов операции относится к типу double , то и второй операнд преобразуется к типу double
  • если предыдущее условие не соблюдено, а один из операндов операции относится к типу float , то и второй операнд преобразуется к типу float
  • если предыдущие условия не соблюдены, один из операндов операции относится к типу long , то и второй операнд преобразуется к типу long
  • иначе все операнды операции преобразуются к типу int
int a = 3; double b = 4.6; double c = a+b;

Так как в операции участвует значение типа double, то и другое значение приводится к типу double и сумма двух значений a+b будет представлять тип double.

byte a = 3; short b = 4; byte c = (byte)(a+b);

Две переменных типа byte и short (не double, float или long), поэтому при сложении они преобразуются к типу int , и их сумма a+b представляет значение типа int. Поэтому если затем мы присваиваем эту сумму переменной типа byte, то нам опять надо сделать преобразование типов к byte.

Если в операциях участвуют данные типа char, то они преобразуются в int:

int d = 'a' + 5; System.out.println(d); // 102

Источник

Оцените статью