Edward Northwind
Edward Northwind

Reputation: 33

Change field in Record

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

Answers (4)

Ben
Ben

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

isuru
isuru

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

bangeboss
bangeboss

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;
  • the field is non-static; and
  • the field's declaring class is not a hidden class; and
  • the field's declaring class is not a record class.

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.

Account.java

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

violet945786
violet945786

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

Related Questions