Reputation: 33
I study reflection and try to change field's value in Record.
public record Account(Integer id, String login, Boolean blocked) {}
public class Main {
public static void main(String[] args) {
Account account = new Account(null, null, null);
setFieldValue(account, "id", 1);
setFieldValue(account, "login", "admin");
setFieldValue(account, "blocked", false);
System.out.println(account);
}
public static void setFieldValue(Object instance,
String fieldName,
Object value) {
try {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(instance, value);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
If I convert Record to Class everything works, but with Record I get Exception
java.lang.IllegalAccessException: Can not set final java.lang.Integer field Account.id to java.lang.Integer
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:79)
at java.base/java.lang.reflect.Field.set(Field.java:799)
What do I have to do to make the code work with records?
Upvotes: 3
Views: 10076
Reputation: 761
If for some reason you need a field of a record to be mutable, there is a workaround (not using reflection though). You can add methods to a record:
public record Account(Integer id, String login, Boolean blocked) {
public Account setLogin(String newLogin) {
return new Account(id, newLogin, blocked);
}
}
It doesn't update the record, but returns a new one with the field updated. So the variable needs to be reassigned to the result.
public static void main(String[] args) {
Account account = new Account(1, "admin", false);
System.out.println(account);
account = account.setLogin("root");
System.out.println(account);
}
output:
Account[id=1, login=admin, blocked=false]
Account[id=1, login=root, blocked=false]
If you need mutable fields, you probably should make it a class though.
Upvotes: 1
Reputation: 319
I know you are asking about Java records, but if you really need to change a single field in a record you could user a Kotlin data class which allows you to easily create a copy of the data with just one field changed. The Kotlin class can easily be called from Java
Kotlin data class:
data class Account(val id :String, val login: String, val blocked: Boolean) {
fun setId(id:String) = copy(id = id)
fun setLogin(login:String) = copy(login = login)
fun setBlocked(blocked:Boolean) = copy(blocked = blocked)
}
Java class:
public class AccountSetter {
public static void main(String[] args) {
Account account = new Account("oldId", "foo", false);
account = account.setId("newId");
}
}
Upvotes: 0
Reputation: 121
Field::set
method doesn't work for record class fields by design. From the docs,
If the underlying field is final, this Field object has write access if and only if the following conditions are met:
setAccessible(true)
has succeeded for this Field object;To be able to modify the fields reflectively, convert Account
from a record class to a regular POJO class. Records are truly immutable, not even reflection will do.
public class Account {
private final Integer id;
private final String login;
private final Boolean blocked;
public Account(Integer id, String login, Boolean blocked) {
this.id = id;
this.login = login;
this.blocked = blocked;
}
public Integer getId() {
return id;
}
public String getLogin() {
return login;
}
public Boolean getBlocked() {
return blocked;
}
}
Upvotes: 2
Reputation: 45
In general the commenters saying that this "can't be done" or is "impossible" aren't wrong... unless you are willing to slightly bend the rules of the JVM :) For example, by using unsafe reflection to change the relevant value directly at the memory location in the record like this:
public static void setFieldValue(Object instance, String fieldName, Object value) {
try {
Field f = instance.getClass().getDeclaredField(fieldName);
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe");
theInternalUnsafeField.setAccessible(true);
Object theInternalUnsafe = theInternalUnsafeField.get(null);
Method offset = Class.forName("jdk.internal.misc.Unsafe").getMethod("objectFieldOffset", Field.class);
unsafe.putBoolean(offset, 12, true);
unsafe.putObject(instance, (long) offset.invoke(theInternalUnsafe, f), value);
} catch (IllegalAccessException | NoSuchFieldException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
Upvotes: -3