在日常开发中,使用final去修饰类、方法、成员变量等都是很常见的,final修饰一个类,表示这个类不能被继,也就是被它修饰的类没有子类;final修饰方法,表示这个方法不能被重写;final修饰变量,表示这个变量不能被修改。但是在使用过程中,可能就会因为使用不规范,导致一系列的意外发生;而我恰好遇到了。
事情是这样子的,我正在维护一个经历N个同学的项目,维护过程中发现某日志分析平台中,我们的应用偶发很多【无理由】的java.lang.ArrayIndexOutOfBoundsException
,而且在不同的列表页面都偶然蹦出一两个,而且全是在RecycleView.Adapter中出现的。
撸了一遍代码逻辑,很奇怪,除了代码稍微不是很规范之外,重头到尾并未对数据源做任何的删减动作,一切规规矩矩,难道是推送或者是连续两次点击导致打开同一个页面,onNewIntent()
没有处理好,用户手速太快导致数据源不一致?
带着这个疑问,我照着项目中的代码撸了个demo,起先并未注意代码中有个final修饰,经过各种模拟,死活没有得到我想要的答案,贼心不死的我再仔细对比了代码的差异性,一个final引起了我的注意,该不会它才是罪魁祸首吧,越想越觉得有可能,模拟数据变化的情况,果然就是它。
- 问题代码是这样子的
public void setData(View view) {
is = !is;
// 数据源用final修饰
final List<String> strings = new ArrayList<>();
for (int i = 0; i < (is ? 5 : 7); i++) {
strings.add(is ? "原数据" : "替换" + i);
}
if (myAdapter == null) {
// 第一次初始化的时候 数据源指向strings@1(暂时这么叫着)
myAdapter = new MyAdapter(strings);
myAdapter.setOnClickListener(new MyAdapter.OnClickListener() {
@Override
public void onClick(int position) {
// 此时在监听回调的作用域中指向了strings@1
Toast.makeText(MainActivity.this, strings.get(position), Toast.LENGTH_SHORT).show();
}
});
mRcl.setAdapter(myAdapter);
} else {
// 第二次调用该方法时 重新为adapter设置了数据源strings@2
// 第二次调用该方法时 重新为adapter设置了数据源strings@2
// ...
// 重点来了 我们的Adapter从始至终只初始化了一次 而我们点击监听回掉中依然指向的是string@1
// 在拿着string@1去做string@n的事,数组越界、数据不准自然而然就接踵而来
myAdapter.setData(strings);
}
}
复制代码
总结
- 代码风格要规范,像这种如案例中myAdapter、strings如果在页面进入就需使用的可以声明为成员变量
private final List<String> data;private final MyAdapter mMyAdapter;
,可以在声明变量时就初始化,也可以在onCreate之后初始化。像在代码中去对myAdapter的判null处理就完全没有必要了,在数据源发生更改的时候使用data.clear()
、data.addAll()
的方法对数据源进行更改。 - 在类似的回调中,时刻注意我们所持有的引用和我们实际数据源的引用是否是同一个,否侧容易造成这种不可忽视却又不容易查找到的bug。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END