У меня такой код:

view.setPressed(true);
view.postDelayed(new Runnable() {
    @Override
    public void run() {
        view.setPressed(false);
    }
}, 50);

Объявление переменной просто:

private View view;

Единственное место, где я присваиваю ему значение, находится внутри onInterceptTouchEvent:

view = parentView.findChildViewUnder(event.getX(), event.getY());

Из Crashlytics я получаю исключение нулевого указателя при вызове setPressed (false), стек:

Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view
.View.setPressed(boolean)' on a null object reference
       at com.MyApp.common.ui.RecyclerItemClickListener$GestureListener$1.run(SourceFile:119)
       at android.os.Handler.handleCallback(Handler.java:739)
       at android.os.Handler.dispatchMessage(Handler.java:95)
       at android.os.Looper.loop(Looper.java:158)
       at android.app.ActivityThread.main(ActivityThread.java:7229)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)

Я добавил нулевую проверку, и все выглядит нормально, никаких предупреждений в Android Studio.

Переместил код в какую-то функцию и отправил представление в качестве аргумента. В объявлении функции объявлено final View. Теперь Android Studio подчеркивает, что условие "view! = Null" всегда ложно (ну, в этой строке оно разбилось, так что .. :)

enter image description here

Интересно, связано ли это со сборкой мусора и как он работает с конечными переменными, или это просто ложная ошибка проверки.

3
ox_ 5 Янв 2017 в 13:44
Возможно, это потому, что доступ к представлению осуществляется за пределами потока пользовательского интерфейса.?
 – 
Tharaka Devinda
5 Янв 2017 в 13:54
Если вы объявляете атрибут класса final и инициализируете его, он действительно больше не может стать нулевым в том же самом экземпляре объекта. В этой ошибке проверки нет ничего ложного.
 – 
Gimby
5 Янв 2017 в 13:55
Я не думаю, что NPE там происходит. Переменная view захватывается анонимным внутренним классом, поэтому не может иметь значение NULL в методе run.
 – 
john16384
5 Янв 2017 в 14:00
Это не сборка мусора, GC не очищает ссылки, которые используются классом (даже если это анонимный внутренний класс).
 – 
john16384
5 Янв 2017 в 14:01
Однако я заметил следующее: view имеет в вашем редакторе 2 разных цвета. Возможно, одна из них - поле, а другая - локальная переменная?
 – 
john16384
5 Янв 2017 в 14:03

3 ответа

Вы не предоставляете достаточно контекста для точного диагноза. Однако вот один возможный сценарий, в котором view может быть null в методе run().

Рассмотрим этот код:

public class MyClass {
    private View view = ... // not null

    public void someMethod() {
        view.setPressed(true);
        view.postDelayed(new Runnable() {
            @Override
            public void run() {
                view.setPressed(false);
            }
        }, 50);
    }

    public void someOtherMethod() {
       view = null;
    }
}

Теперь предположим, что создается экземпляр MyClass и вызывается someMethod(), за которым следует someOtherMethod(). Если последний вызов происходит до завершения задержки, тогда метод run() будет видеть view как null.

Причина в том, что анонимный класс не захватывает view. Скорее это захват ссылки на экземпляр MyObject, членом которого является view.

Предположительно, ваша проверка того, что view не null, выполняется в вызове someMethod() (или аналоге в вашем коде)

Если вы объявите view как final, назначение в someOtherMethod() будет предотвращено, а view останется ненулевым.

Другой подход таков:

    public void someMethod() {
        final View localView = view;
        localView.setPressed(true);
        localView.postDelayed(new Runnable() {
            @Override
            public void run() {
                localView.setPressed(false);
            }
        }, 50);
    }

Здесь мы принудительно захватываем локальную переменную вместо (или так же) this. Если затем присвоить поле this.view, это не изменит захваченное значение localView.

Или, что еще проще, поместите тест null в метод run() ... хотя тогда вам нужно будет подумать, может ли быть состояние гонки, включающее то, что присваивает null объекту Поле view.


Интересно, связано ли это со сборкой мусора и как он работает с конечными переменными

Нет. Сборщик мусора не будет мешать доступным объектам. Экземпляр MyClass доступен, потому что он был захвачен, а захватывающий объект (экземпляр анонимного класса) доступен через инфраструктуру postDelay.

или это просто ложная ошибка проверки.

Это правдоподобно ... но (ИМО) не самое вероятное объяснение.

1
Stephen C 5 Янв 2017 в 14:49
Я думаю, что два моих лучших варианта: (1) этот ответ. или (2) добавить нулевую проверку. Поправьте меня если я ошибаюсь :)
 – 
ox_
5 Янв 2017 в 16:48
Лучше правильно реализованная нулевая проверка. (Правильно реализовано в том смысле, что вам нужно выполнить проверку в методе run().)
 – 
Stephen C
6 Янв 2017 в 05:56

Если view является окончательным, а строка postDelayed отсутствует в конструкторе, view гарантированно уже инициализирован. Он никогда не может быть нулевым, поэтому view != null всегда принимает значение true. Предупреждает, что это бессмысленная проверка.

Единственное исключение из этого было бы, если бы у вас было что-то вроде

private final Integer myInt;

MyCtor()
{
   myInt = null;
}

Что было бы совершенно бессмысленно (но почему-то возможно).

0
Michael 5 Янв 2017 в 13:58
view.postDelayed(new Runnable() {
    @Override
    public void run() {
        if (view != null) {
          view.setPressed(false);
        }
    }
}, 50);

В вашем коде, если view будет null, у вас будет NPE в вызове метода view.postDelayed(), и поскольку ссылка является окончательной, она не может быть изменена, поэтому в Runnable она не может быть нулевым никогда, два варианта:

  1. View - это null и вылетает на view.postDelayed, поэтому вам не нужно проверять его через Runnable
  2. View не null, поэтому вам не нужно проверять его через Runnable
1
Orest Savchak 5 Янв 2017 в 13:58
Правильно, и view пока что не может быть установлен на null, так как он должен быть установлен для вызова final во "внутреннем объявлении" (не могу вспомнить имя JLS для этого).
 – 
AxelH
5 Янв 2017 в 14:07
Ну, это именно то, о чем я думал, прежде чем опубликовать этот вопрос :) но как-то так бывает
 – 
ox_
5 Янв 2017 в 16:46