java names shadowing and obscuring

本文通过几个简单示例了解一下java局部变量定义的作用域scope,变量名字的遮蔽shadowing和简单名的遮掩obscuring,这2者具体区别下面会示例说明,先说一下规范里定义的2个名词:简单名和限定名。

Simple Name and Qualified Name

java中的name可以被用来引用在程序中声明的实体entity,其有两种形式:简单名和限定名,即Simple NameQualified Name

  1. 简单名是单个的标识符。
  2. 限定名是由一个名字,一个.号,再一个标识符组成。

完全限定名为Fully Qualified Name,缩写即FQN,该名字在整个命名空间中具有唯一性,局部类没有完全限定名。

Every primitive type, named package, top level class, and top level interface has a fully qualified name.

局部变量声明及其初始化

public class VariableNamesScope {    static int x; // 类加载之后 x 被初始化为0    public static void main(String[] args) {        // 类变量 x 已经被局部变量 x 遮蔽(shadowed)了        // int x = x; // 编译错误        int y;        y = 2;        y = y * 3;        System.out.println("y = " + y); // y = 6        int x = (x = 2) * 3, w = x * 4; // 局部变量 x 在使用之前已经被赋值了        System.out.println("x = " + x); // x = 6        System.out.println("w = " + w); // w = 24        System.out.println("VariableNamesScope.x = " + VariableNamesScope.x);    }}

上述代码运行后,结果如下:

y = 6
x = 6
w = 24
VariableNamesScope.x = 0

其中代码int x = x;这句编译错误,因为局部变量x已经遮蔽了类变量x,此时的x并没有被初始化,作为右值进行赋值是错误的。

在某个块中的局部变量声明的作用域,是在该声明所在的块中的剩余部分,从它的初始化语句开始,包括变量声明语句中右侧出现的此变量声明。所以上述代码int x = (x = 2) * 3中的局部变量x在使用之前已经被初始化赋值为2,然后再乘以3,所以可以编译通过。

子作用域中的局部变量

java中变量只在声明的作用域内起作用,如{}代码块声明的变量,for语句中声明的变量,catch子句中声明的变量,以及lambda表达式中声明的变量,都与代码块外部共享变量名,如果外部已经有此变量声明,在这些子作用域内声明同名变量,则会报编译错误。

不过在局部类或者匿名类中,则可以对相同的变量名再次声明,并因此对其外部声明的各种变量名产生了遮蔽效果,不能再使用简单的变量名直接引用,同时内部类类名不能与其外部类类名相同。

public class NameBlockScope {    public static void main(String[] args) {        {            int i = 1;        }        // 上面这句语句中定义的变量只在其所在的代码块中生效,所以下面的语句会报编译错误        // System.out.println(i); // cannot resolve symbol i        for (int i = 0; i < 5; i++) {            // i 只在其声明之后有效,在此for语句之后就立即失效        }        for (int i : new int[]{1, 2, 3, 4, 5}) {            // i 只在其声明之后有效,在此for语句之后就立即失效        }        // lambda 表达式与上面 for 循环一样的道理        new Thread(() -> {            int i = 0;            int NameBlockScope = 2; // 局部变量名遮掩了类名 obscuring            System.out.println(i);            System.out.println(NameBlockScope);        }).start();        int i = 0;        // for (int i = 0; i < 5; i++) {} // i 在for循环外面已经定义,因此报编译错误        new Thread(new Runnable() {            @Override            public void run() {                int i = 1;                int NameBlockScope = 2; // 变量名遮掩 obscuring                System.out.println(i); // 变量名遮蔽 shadowing                System.out.println(NameBlockScope);            }        }).start();        try {            TimeUnit.MILLISECONDS.sleep(10);        // } catch (InterruptedException i) { // 编译错误,变量i在前面已经声明        } catch (InterruptedException e) {            // 不能声明此变量,在catch子句中已经声明了此变量e            // Exception e; // 编译错误            e.printStackTrace();        }        String System = "";        // System.out.println("Hello World"); // 编译错误,System is obscured by the local variable.    }}

Names Shadow and Obscure

从上面的2个例子简单说一下java中变量名字的遮蔽和遮掩:

  1. Shadowing: 在一个闭合的文本范围之内,某些变量的声明在其作用域之内被其他同名的变量声明所遮蔽,这时简单名就不能引用其声明的实体了。
  2. Obscuring: 一个简单名可以被解释为变量名,类型名或者包名,在相同上下文中,变量名优于类型名,类型名优于包名,这就是名字声明的遮掩,如上示例中的变量名NameBlockScopeSystem就产生了obscuring的效果。
  3. Hiding: 一个简单名可以被隐藏,即子类的类变量隐藏(hiding)超类的类变量,通常也简单的说覆写了超类的类变量,子类覆写超类相同签名的方法一般使用重写overrided这个说法,同理子类实例属性会隐藏超类的实例属性,hiding与上述变量名字的shadowingobsucring不同,并不局限于一个闭合的上下文之内。

类型声明被内部类所遮蔽

内部类HashMap<K, V>遮蔽了jdk的工具类java.util.HashMap<K, V>,内部类方法声明遮蔽外部类方法声明shadowing,如下所示:

public class ClassNameShadowing {    public static void main(String[] args) {        HashMap<Integer, String> map = new HashMap<>();        map.put(1, map.getClass().getCanonicalName());        System.out.println(map.get(1));        ClassNameShadowing classNameShadowing = new ClassNameShadowing();        MethodNameShadowing methodNameShadowing = classNameShadowing.new MethodNameShadowing();        methodNameShadowing.print();    }    public void method() {        System.out.println("method in " + getClass().getCanonicalName());    }    public void shadowing() {        System.out.println("shadowing in " + getClass().getCanonicalName());    }    private class MethodNameShadowing {        /**         * 内部类方法名声明遮蔽外部类方法名声明         */        public void shadowing() {            System.out.println("shadowing in " + getClass().getCanonicalName());        }        public void print() {            method();            shadowing();            ClassNameShadowing.this.shadowing();        }    }}class HashMap<K, V> {    private Map<K, V> map = new ConcurrentHashMap<>();    public V put(K key, V value) {        return map.put(key, value);    }    public V get(K key) {        return map.get(key);    }}

实例属性遮蔽示例

关键字this也常用来访问被遮蔽的实例属性xxx,即使用this.xxx的形式,主要用在构造器和setXxx方法中。如下形式:

public class User {    private int id;    private String name;    public User(int id, String name) {        this.id = id;        this.name = name;    }    public int getId() {        return id;    }    public void setId(int id) {        this.id = id;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    }

References

  1. 6.4. Shadowing and Obscuring
  2. Java concepts explained: Overloading, overriding, shadowing, hiding, and obscuring