分享好友 移动开发首页 频道列表

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

Android开发  2016-11-23 14:220

在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,才衍生出自己这样的思考,才能理解的更加透彻!

查看更多关于【Android开发】的文章

展开全文
相关推荐
反对 0
举报 0
评论 0
图文资讯
热门推荐
优选好物
更多热点专题
更多推荐文章
SDK热更之如何在SDK代码中自动插桩及如何生成补丁包
写在前面本文是SDKHotfix相关的SDK热更系列文章中的一篇,以下为项目及系列文章相关链接:SDKHotfix整体介绍:http://blog.bihe0832.com/sdk_hotfix_project.htmlSDKHotfix对应github地址:https://github.com/bihe0832/SDKHoxFix这篇文章主要介绍一下SDK热更

0评论2017-02-05358

ASimpleCache
ASimpleCache 是一个为android制定的 轻量级的 开源缓存框架。轻量到只有一个java文件(由十几个类精简而来)。1、它可以缓存什么东西?普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java对象,和 byte数据。2、它有什么特色?特色主要是

0评论2017-02-05376

原生App与javascript交互之JSBridge接口原理、设计与实现
前期调研调研对象:支付宝,微信,云之家调研文档:Android中JS与Java的极简交互库 SimpleJavaJsBridge设计需求阅读类型的业务功能页面需要由前端H5实现,需要做到服务端可控;页面界面更改减少重新发布新版本的频率;功能页面部分原型需求无法实现,需要原生

0评论2017-01-063119

RxJava系列番外篇:一个RxJava解决复杂业务逻辑的案例
之前写过一系列RxJava1的文章,也承诺过会尽快有RxJava2的介绍。无奈实际项目中还未真正的使用RxJava2,不敢妄动笔墨。所以这次还是给大家分享一个使用RxJava1解决问题的案例,希望对大家在使用RxJava的时候有一点点启发。对RxJava还不了解的同学可以先去看看

0评论2017-01-06508

使用Smalidea对无源码APK调试简介
阅读:8最近正好也用了Smalidea,就ZZ的原贴做一些补充。可调试APP如果Android的系统属性ro.debuggable等于1(用getprop ro.debuggable验证),则所有APP都可调试。如果ro.debuggable等于0,某APP的AndroidManifest.xml中有android:debuggable=”true”,该APP

0评论2017-01-06295

[Java] Retrofit2.0 如何进行GBK编码
对Retrofit + OkHttp还不熟悉的人可以点传送门,先看下这两个东西的使用。Retrofit:https://github.com/square/retrofitOkHttp:https://github.com/square/okhttp分析接口文档要求Post请求,字段使用GBK编码我们先按照Retrofit的规范和接口文档来写接口: @PO

0评论2016-12-231093

从 Retrofit 源码学习 Java 的动态代理的使用
Retrofit 是当前 Android 最流行的 HTTP 网络库之一了,其使用方式比较特殊,是通过定义一个接口类,通过给接口中方法和方法参数添加注解的方式来定义网络请求接口。这种风格下定义一个网络接口变得很简单。不过 Retrofit 是如何使用一个接口的 Class 创建出

0评论2016-11-13181

更多推荐