深入理解 final 在 Java 和 Android 中修饰局部变量的意义

3,067 阅读5分钟

       Android的日常编程中,我们会经常使用匿名内部类,比如给Button设置点击事件时,setOnClickListener(new OnClickListener(){...})。如果此时,我们需要在匿名内部类中外部方法中的局部变量,我们必须手动对将这个局部变量用final关键字修饰(在JDK1.8之后不再需要显示的声明为final,因为这种情况下这个局部变量默认是final的,这是编译器为我们做的,这是JDK1.8的新特性,所以前面的结论仍然成立)。代码写了那么久,为什么?最近的即时通讯项目中就被这个final坑的好苦,正是由于这个坑,才促进自己对它进一步的理解,也才有今天的博客,来记录一下。

首先,在Java中,有四种内部类:

  • 静态内部类(static inner class)
  • 成员内部类(Method inner class)
  • 局部内部类(Local inner class)
  • 匿名内部类(Anonymous inner class)

我们在后面两种内部类中如果访问了外部方法中的局部变量,都需要加final。为了弄清本质,我翻开了《Thinking in Java》,找到了如下这样一段话:

If you’re defining an anonymous inner class and want to use an object that’s defined outside the anonymous inner class, the compiler requires that the argument reference be final, as you see in the argument to destination( ).

这里确实给出了结论,和我们在前面陈述的是一样的,但是没有说清楚为什么,然后我又去翻开了《Java核心卷》,这里面才找到我想要的,首先它贴出这样的代码:

public void start(int interval, final boolean beep)
{
    class TimePrinter implements ActionListener
    {
        public void actionPerformed(ActionEvent event)
        {
            Date now = new Date();
            System.out.println("At the tone, the time is " + now);
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    }

    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
}

注意这个beep,在局部内部类中使用了,而且使用了final,接下来它做了一件事:反射这个TimePrinter:

class TalkingClock$1TimePrinter
{
    TalkingClock$1TimePrinter(TalkingClock, boolean);
    public void  actionPerformed(java.awt.event.ActionEvent);
    final boolean val$beep;
    final TalkingClock this$0;
}
Note the boolean parameter to the constructor and the val$beep instance variable. When an object is created, the value beep is passed into the constructor and stored in the val$beep field. The compiler detects access of local variables, makes matching instance fields for each one of them, and copies the local variables into the constructor so that the instance fields can be initialized.

以上文档就很好的阐述了理由:原来,我们在局部内部类中访问的这些final修饰的局部变量,都会作为局部内部类的由final修饰的成员变量,并在构造中传入值初始化。

原来,编译器是这么处理的,渐渐有了眉目,但是为什么必须声明是final的呢?还是核心卷里的一段话启发了我:

From the programmer’s point of view, local variable access is quite pleasant. It makes your inner classes simpler by reducing the instance fields that you need to program explicitly.

也就是说,我们在局部内部类中访问的实际上是这个var$beep(它的值等于beep),它是局部变量beep一份拷贝,并不是局部变量本身,但是为了方便编程,编译器允许我们直接使用beep来指代var$beep。那到这里就能解释为什么要是final了。

我们来试想这样的场景:如果我们在局部内部类中对访问的这个局部变量进行了修改,例如在上面的actionPerformed方法中,我添加了这样的一行代码:

        public void actionPerformed(ActionEvent event)
        {
            beep = false;
            Date now = new Date();
            System.out.println("At the tone, the time is " + now);
            if (beep) Toolkit.getDefaultToolkit().beep();
        }

那么这个时候,就会出现矛盾,在actionPerformed中将beep置为false,这个时候,这个beep本质上是我们前面提到的var$beep,而不是局部变量beep,那么接下来的代码中,到底以谁为准,就会造成不一致,就会给程序员带来困扰,那么这个时候规定,此时只能使用这个局部变量,而不允许修改它,(后面要高亮)因此,局部变量必须声明为final,而且内部类中的这份拷贝,这个成员变量也是final的。到这里我们已经能够解释原因了。

接下来我就把项目中遇到的问题在这里与大家分享一下:

先贴出关键代码:

public class GroupDetailAdapter extends BaseAdapter {
    private UserInfo userInfo;

     @Override
    public View getView(final int position, View convertView, ViewGroup parent) {

        userInfo  = mUsers.get(position);

        ......

        holder.ivGroupDetailDelete.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    // 删除群成员
                    mOnGroupDetailListener.onDeleteMember(userInfo);
                }
            });
    }

这是最初造成bug的代码,很简单,就是在ListView的适配器getView方法中绑定数据,熟的不能再熟了,平时想取集合中的数据时,都用局部变量,如果在内部类要使用,就声明为final,但是这次就不知为啥心血来潮将这个userInfo声明为成员变量,最终导致onClick方法会出现问题,传入onDeleteMember方法里的userInfo始终是集合中最后一个。后来各种debug,最后把userInfo = mUsers.get(position);这行代码放入到onClick方法中就好了,或者将这个userInfo换成局部变量然后使用也能解决问题。为什么?先解释bug出现的原因:

由于我的userInfo是在getView方法中获取的,而getView方法只会在视图显示的时候被调用,显示完毕后,position的值肯定到达了它的最大值(即集合的size - 1),那么这个时候userInfo自然保存的就是集合中的最后一个元素,然后静静的等待着onClick方法的被回调,一旦回调就把userInfo传给onDeleteMember方法执行相应的逻辑,而userInfo此时肯定是集合中的最后一个元素,最终导致这个bug的诞生。

这实际上才是这篇博客最初的触发点,这里来分别解释下这两种解决问题的办法:

  • userInfo为成员变量,userInfo = mUsers.get(position);放到onClick中:这种情况下,如果这么去使用,那么position肯定为final的,也就是这个匿名内部类中会维护这样一份拷贝,注意,这里是position的拷贝,要注意和第二种方法的区分。
  • userInfo为局部变量,userInfo = mUsers.get(position);还在原来的位置:这种情况下,如果这么去使用,那么userInfo肯定为final的,同样在这个匿名内部类中维护一份拷贝,但这里是userInfo的拷贝。

谢谢这个bug,才衍生出自己这样的思考,才能理解的更加透彻!