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

   2016-11-23 0
核心提示:在Android的日常编程中,我们会经常使用匿名内部类,比如给Button设置点击事件时,setOnClickListener(new OnClickListener(){...})。如果此时,我们需要在匿名内部类中外部方法中的局部变量,我们必须手动对将这个局部变量用final关键字修饰(在JDK1.8之后不

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

 
标签: Java 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • SDK热更之如何在SDK代码中自动插桩及如何生成补
    写在前面本文是SDKHotfix相关的SDK热更系列文章中的一篇,以下为项目及系列文章相关链接:SDKHotfix整体介绍:http://blog.bihe0832.com/sdk_hotfix_project.htmlSDKHotfix对应github地址:https://github.com/bihe0832/SDKHoxFix这篇文章主要介绍一下SDK热更
  • ASimpleCache
    ASimpleCache 是一个为android制定的 轻量级的 开源缓存框架。轻量到只有一个java文件(由十几个类精简而来)。1、它可以缓存什么东西?普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java对象,和 byte数据。2、它有什么特色?特色主要是
    02-05 Java开源
  • 原生App与javascript交互之JSBridge接口原理、
    前期调研调研对象:支付宝,微信,云之家调研文档:Android中JS与Java的极简交互库 SimpleJavaJsBridge设计需求阅读类型的业务功能页面需要由前端H5实现,需要做到服务端可控;页面界面更改减少重新发布新版本的频率;功能页面部分原型需求无法实现,需要原生
  • RxJava系列番外篇:一个RxJava解决复杂业务逻辑
    之前写过一系列RxJava1的文章,也承诺过会尽快有RxJava2的介绍。无奈实际项目中还未真正的使用RxJava2,不敢妄动笔墨。所以这次还是给大家分享一个使用RxJava1解决问题的案例,希望对大家在使用RxJava的时候有一点点启发。对RxJava还不了解的同学可以先去看看
  • 框架Robust原理解析(下)
    框架Robust原理解析(下)
    一、回顾框架原理本篇继续来看热修复框架Robust原理,在之前的一篇文章中已经详细讲解了:Robust框架原理,因为这个框架不是开源的,所以通过官方给出的原理介绍,咋们自己模拟了案例和框架逻辑的简单实践。最后在通过反编译美团app进行验证咋们的逻辑实现是
  • 使用Smalidea对无源码APK调试简介
    阅读:8最近正好也用了Smalidea,就ZZ的原贴做一些补充。可调试APP如果Android的系统属性ro.debuggable等于1(用getprop ro.debuggable验证),则所有APP都可调试。如果ro.debuggable等于0,某APP的AndroidManifest.xml中有android:debuggable=”true”,该APP
    01-06 JavaLinux
  • 年度盘点(四) | 2016 年十大 Java / Android 开发者必读好文
    年度盘点(四) | 2016 年十大 Java / Android
    2016 年已经过去,感谢大家支持开发者头条。 年度盘点第四篇: 2016 年十大 Java / Android 开发者必读好文 。 长按识别文章摘要下方二维码,即可进入文章评论页。0. 推荐几个自己写的 Java 后端相关的范例项目这里推荐几个自己写的范例项目,主要采用 SSM(S
  • 8个华丽而实用的Java图表类库
    8个华丽而实用的Java图表类库
    学习Java的同学注意了!!! 学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码: 589809992 我们一起学Java! 前段时间我们为大家分享过一些最常用的Java图表应用和Android图表应用,无论是在PC平台上还是移动平台上,图表和报
  • 第151期:一个RxJava解决复杂业务逻辑的案例
    第151期:一个RxJava解决复杂业务逻辑的案例
    第151期:一个RxJava解决复杂业务逻辑的案例深度讨论 基本特效:饿了么丝滑无缝过度搜索栏的实现 diycode 帖子优先,就给上个头条吧。Android开发 一个RxJava解决复杂业务逻辑的案例 本文给大家分享一个使用RxJava解决问题的案例,希望对大家在使用RxJava的时
  • Eclipse 集成ijkplayer demo
    Eclipse 集成ijkplayer demo
    接着上一篇在Mac上编译ijkplayer的.so,现在将这些文件夹拷贝到windows上。(在mac和winds上集成到eclipse上是一样的,只是我这mac上没有安装eclipse)。现在开始说集成到Eclipse的步骤:1 更改目录结构 以 ijkplayer-armv7a 文件夹为例,删除选中的这四个文件
    12-23 EclipseJava
点击排行