Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Final variable from resources file

I have an activity with some final variables. I extracted their values (let's assume they're all Strings) into a resources file.

The problem:

If I directly assign them on the instantiation (as following):

private final String PREFERENCE_NAME = getResources().getString(R.string.preference_name);

I get the following error:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.res.Resources android.content.Context.getResources()' on a null object reference

I understand the problem; the onCreate() method has not been called yet which means I cannot access context-related methods (getResources()).

If I want to assign the value in the onCreate() method of the activity, I get the error Cannot assign value to final variable 'PREFERENCE_NAME'

The question is: How can I get my final variables to be assigned from the resources file? And if this is not possible, what is the best practice for a solution?

Thanks in advance.

like image 541
Shishdem Avatar asked Apr 28 '15 08:04

Shishdem


3 Answers

The simple answer is that you can't.

Variables declared final can only be set when the object is instantiated (i.e. in the constructor or with initialiser code).

Either use getResources().getString(R.string.preference_name); all the time or use a non-final variable.

The complex answer is that you can but that you shouldn't.

When you declare a variable final the compiler and VM uses this to make optimisations and assumptions. It can do this because the variable is guaranteed to never change. Changing it after it has been initialised can cause really weird bugs so you absolutely should not do that.

Here's how you do it:

public class FinalMessage {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        FinalMessage f = new FinalMessage("Hello World!");
        System.out.println(f.getMessage());
        f.changeFinalMessage("Hello Mars!");
        System.out.println(f.getMessage());
    }

    private final String message;

    public FinalMessage(String message) {
        this.message = message;
    }

    void changeFinalMessage(String newMessage) throws IllegalAccessException, NoSuchFieldException {
        final Field field = FinalMessage.class.getDeclaredField("message");
        field.setAccessible(true);

        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

        field.set(this, newMessage);
    }

    String getMessage() {
        return message;
    }
}

This will output:

Hello World!
Hello Mars!

Great, so we changed a final variable. No problem right?

Well, take this example instead:

public class FinalMessage {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        FinalMessage f = new FinalMessage();
        System.out.println(f.getMessage());
        f.changeFinalMessage("Hello Mars!");
        System.out.println(f.getMessage());
    }

    private final String message = "Hello World!";

    void changeFinalMessage(String newMessage) throws IllegalAccessException, NoSuchFieldException {
        final Field field = FinalMessage.class.getDeclaredField("message");
        field.setAccessible(true);

        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

        field.set(this, newMessage);
    }

    String getMessage() {
        return message;
    }
}

This will output:

Hello World!
Hello World!

Wait, what?

The problem is that the compiler can see that the variable message is always going to be "Hello World!" so it inlines "Hello World!" instead of our call to f.getMessage(). If you run this in a debugger you will see the debugger reflect the updated message in the instance to "Hello Mars!" but since the variable is actually never accessed it won't affect the outcome of the program.

So to summarize: You can update final fields via reflection (granted that there is no Security Manager present that prevents you from doing it), but you should not do it since it can have very, very weird side-effects.

I am not responsible if your house gets termites or your cat catches on fire if you actually decide to implement this.

like image 96
Raniz Avatar answered Oct 18 '22 03:10

Raniz


In this case I think the best thing to do it just make multiple calls to the resources. You still only have to change the value in one place and the call to getResources() isn't an expensive one.

like image 4
Dreagen Avatar answered Oct 18 '22 03:10

Dreagen


Use your application context:

  1. Create an application class:

    public class MyApplication extends Application {
    
        private static Context mContext;
    
        @Override
        public void onCreate() {
            super.onCreate();
            mContext = getApplicationContext();
        }
    
        public static String getStr(int resId) {
            return mContext.getString(resId);
        }
    
    }
    
  2. Use it in your manifest:

    <application
        android:name=".MyApplication"
        ...
    
  3. Call it anywhere in your application:

    static final String NAME = MyApplication.getStr(R.string.app_name);
    
like image 2
Simas Avatar answered Oct 18 '22 02:10

Simas