本文通过几个简单示例了解一下java局部变量定义的作用域scope,变量名字的遮蔽shadowing和简单名的遮掩obscuring,这2者具体区别下面会示例说明,先说一下规范里定义的2个名词:简单名和限定名。
Simple Name and Qualified Name
java中的name可以被用来引用在程序中声明的实体entity,其有两种形式:简单名和限定名,即Simple Name和Qualified Name。
- 简单名是单个的标识符。
- 限定名是由一个名字,一个
.号,再一个标识符组成。
完全限定名为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() { 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中变量名字的遮蔽和遮掩:
- Shadowing: 在一个闭合的文本范围之内,某些变量的声明在其作用域之内被其他同名的变量声明所
遮蔽,这时简单名就不能引用其声明的实体了。 - Obscuring: 一个简单名可以被解释为变量名,类型名或者包名,在相同上下文中,变量名优于类型名,类型名优于包名,这就是名字声明的
遮掩,如上示例中的变量名NameBlockScope和System就产生了obscuring的效果。 - Hiding: 一个简单名可以被隐藏,即子类的类变量隐藏(
hiding)超类的类变量,通常也简单的说覆写了超类的类变量,子类覆写超类相同签名的方法一般使用重写overrided这个说法,同理子类实例属性会隐藏超类的实例属性,hiding与上述变量名字的shadowing和obsucring不同,并不局限于一个闭合的上下文之内。
类型声明被内部类所遮蔽
内部类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; } } |