之前在翻译学习EOPL过程中回顾以前的代码时发现一个让人后背发凉的危险,一种极其稀有、可是一旦出现就难以发现并或许形成非常大影响的bug,本文就记录下这个问题。
问题场景
下面来看一段常见的示例程序:
public class DemoActivity extends Activity {
public static final String TAG = DemoActivity.class.getSimpleName();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_test).setOnClickListener(new BaseClickListener() {
@Override
protected void onClicked(View v) {
Log.i(TAG, "onClicked: clicked");
}
});
}
}
这是一个避免快速点击的示例,这个程序中类DemoActivity
包含了一个BaseClickListener
的匿名内部类,然后这个类中引用了外部类的声明的变量TAG
(好像是这样)。但事实上,BaseClickListener
的完成是这样的:
public abstract class BaseClickListener implements View.OnClickListener {
public static final String TAG = BaseClickListener.class.getSimpleName();
private long lastClickTime = 0L;
@Override
public final void onClick(View v) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastClickTime >= 500) {
onClicked(v);
} else {
Log.d(TAG, "onClick: click to fast");
}
lastClickTime = currentTime;
}
protected abstract void onClicked(View v);
}
因此在DemoActivity
中匿名内部类引用的其实是BaseClickListener
类中声明的TAG
。这儿发生了变量隐瞒的现象,并且仍是一种不行见的、“隐式”的隐瞒。这种隐瞒的存在使得外部文件的修改能够直接影响其时文件的语义。
试想,这两个类归于不同的模块,甚至不同的项目,运用BaseClickListener
的人甚至有或许不会去看源码、甚至拿不到源码,而编写BaseClickListener
的人更不会考虑DemoActivity
的完成。一开始BaseClickListener
类中或许没有TAG
变量,DemoActivity
中对TAG
的引用指向DemoActivity.TAG
,后来BaseClickListener
引入了这个变量,这时候,不管是DemoActivity
的开发者仍是BaseClickListener
的开发者都不知道这儿出了问题,而DemoActivity
的程序语义已经发生了改变。
上述示例程序中问题规划仅仅是打印日志,所以影响规划有限。但假设换成其他的变量,比如常见的id
、rootView
、name
、content
、currentTime
、count
等等等等,那么命运好的情况下直接编译不过,命运欠好的情况程序崩溃甚至数据污染,并且这个问题需要通过盯梢变量引用才能发现,光肉眼看还或许看不出来。
我还没有在实际项目中遇到过这个问题,但假设真的遇到了,那我或许要跟我的同事“友尽”了,因此将其列为“友尽”级bug。
问题剖析
上面问题发生原因如下:
- 这是“隐式”发生的隐瞒,仍是无意识的隐瞒,且隐瞒跨越多文件;
- 变量/常量/办法声明界说与运用跨多文件,且引用选用简化办法,并非彻底办法;
首要简明谈谈隐瞒(shadow)。(之所以简明,是由于这个概念结合作用域、上下文来讲比较简单讲透,这个另外在写文章总结)
隐瞒在现在大部分流行言语里都或多或少出现一点,除了上面展示的,还有例如下面javascript程序中典型的隐瞒:
function outer(x){
function inner(x){
{
let x = 3
console.log("x1:" + x)// x = 3
}
console.log("x2:" + x)// x = 2
}
console.log("x3:" + x)// x = 1
inner(x + 1)
}
outer(1)
这种隐瞒还相对安全,由于是可见规划内发生的显式隐瞒。可是Java的类作用域与词法作用域结合使得能够发生隐式的隐瞒,只需出现内部类,不管是成员域仍是办法甚至局部变量都有或许发生这类隐瞒。这是言语规划中的意外,关于言语运用者来说难以避免这类问题。
一点点主张
- 最差的主张:避免运用内部类;
- 次好的主张:关于类的成员域以及办法的访问选用全限定名,比如:
OuterClass.this.name
- 较好的主张:外部类可运用简写来引用成员域或办法,内部类则选用全名。