Search code examples
javaandroidandroid-custom-view

NullPointerException happens with final member in custom view


I'm creating a custom view, and I write code like this:

public class TestView extends BaseCustomView<ViewTestBinding> {
...
    @NonNull
    private final Map<Boolean, Drawable> icon = new HashMap<>();

    private void onInit(Context context, @Nullable TypedArray attrs) {
        assert attrs != null;
        // NullPointerException throws at this line
        icon.put(true, attrs.getDrawable(R.styleable.TestView_icon));
        icon.put(false, attrs.getDrawable(R.styleable.TestView_iconUnselect));
...
}

Method onInit will be called in every constructor, but when I store the key-value pair in the icon, it thows NullPointerException:

java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.Map.put(java.lang.Object, java.lang.Object)' on a null object reference

I put the full code on Github. From my understanding, changing icon to null is impossible, so why is NullPointerException thrown? Thank you!


Update (2022/11/18 14:48 GMT +8):

According to the answer of @Cheticamp, I write a minify test case: ExampleUnitTest.java, and it reproduced the error, System.out.println() at line 31 printed null.

public class ExampleUnitTest {
    @Test
    public void test() {
        new ClassB();
    }

    public static abstract class ClassA {
        public ClassA() {
            method();
        }

        protected abstract void method();
    }

    public static class ClassB extends ClassA {
        @SuppressWarnings("FieldCanBeLocal")
        private final Integer integer = 114514; // print null
//        private final int integer = 114514; // print 114514

        @Override
        protected void method() {
            System.out.println(integer);
        }
    }
}

But I still can't understand why I got null, the code in this link initializes members in the constructor, and I assign the value directly after defining, Even if I initialize directly, is the actual assignment still performed in the subclass constructor?

If so, when I change the type to int like line 27, it should print the int's initial value of 0, but this time it correctly prints the value I initialized.


Solution

  • I have been caught by this same type of error in the past. This Stack Overflow answer is a decent explanation of what I believe is happening. As stated in the this Stack Overflow reference:

    A quote from Effective Java 2nd Edition, Item 17: Design and document for inheritance, or else prohibit it:

    There are a few more restrictions that a class must obey to allow inheritance. Constructors must not invoke overridable methods, directly or indirectly. If you violate this rule, program failure will result. The superclass constructor runs before the subclass constructor, so the overriding method in the subclass will be invoked before the subclass constructor has run. If the overriding method depends on any initialization performed by the subclass constructor, the method will not behave as expected.

    If you take a look at the base class BaseCustomView, it calls init() in its constructors. init() then calls onInit() which is overridden in the subclass TestView. Since the constructor for the base class runs before the constructor for the subclass, the class member icon has not yet been initialized.