JAVA面试题解惑系列(一)——类的初始化顺序
关键字: java 面试题 初始化
作者:臧圩人(zangweiren)
网址:http://zangweiren.javaeye.com
>>>转载请注明出处!<<<
大家在去参加面试的时候,经常会遇到这样的考题:给你两个类的代码,它们之间是继承的关系,每个类里只有构造器方法和一些变量,构造器里可能还有一段代码对变量值进行了某种运算,另外还有一些将变量值输出到控制台的代码,然后让我们判断输出的结果。这实际上是在考查我们对于继承情况下类的初始化顺序的了解。
我们大家都知道,对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)>构造器。我们也可以通过下面的测试代码来验证这一点:
1. public class InitialOrderTest {
2.
3. // 静态变量
4. public static String staticField = "静态变量";
5. // 变量
6. public String field = "变量";
7.
8. // 静态初始化块
9. static {
10. System.out.println(staticField);
11. System.out.println("静态初始化块");
12. }
13.
14. // 初始化块
15. {
16. System.out.println(field);
17. System.out.println("初始化块");
18. }
19.
20. // 构造器
21. public InitialOrderTest() {
22. System.out.println("构造器");
23. }
24.
25. public static void main(String[] args) {
26. new InitialOrderTest();
27. }
28. }
public class InitialOrderTest {
// 静态变量
public static String staticField = "静态变量";
// 变量
public String field = "变量";
// 静态初始化块
static {
System.out.println(staticField);
System.out.println("静态初始化块");
}
// 初始化块
{
System.out.println(field);
System.out.println("初始化块");
}
// 构造器
public InitialOrderTest() {
System.out.println("构造器");
}
public static void main(String[] args) {
new InitialOrderTest();
}
}
运行以上代码,我们会得到如下的输出结果:
1. 静态变量
2. 静态初始化块
3. 变量
4. 初始化块
5. 构造器
这与上文中说的完全符合。那么对于继承情况下又会怎样呢?我们仍然以一段测试代码来获取最终结果:
1. class Parent {
2. // 静态变量
3. public static String p_StaticField = "父类--静态变量";
4. // 变量
5. public String p_Field = "父类--变量";
6.
7. // 静态初始化块
8. static {
9. System.out.println(p_StaticField);
10. System.out.println("父类--静态初始化块");
11. }
12.
13. // 初始化块
14. {
15. System.out.println(p_Field);
16. System.out.println("父类--初始化块");
17. }
18.
19. // 构造器
20. public Parent() {
21. System.out.println("父类--构造器");
22. }
23. }
24.
25. public class SubClass extends Parent {
26. // 静态变量
27. public static String s_StaticField = "子类--静态变量";
28. // 变量
29. public String s_Field = "子类--变量";
30. // 静态初始化块
31. static {
32. System.out.println(s_StaticField);
33. System.out.println("子类--静态初始化块");
34. }
35. // 初始化块
36. {
37. System.out.println(s_Field);
38. System.out.println("子类--初始化块");
39. }
40.
41. // 构造器
42. public SubClass() {
43. System.out.println("子类--构造器");
44. }
45.
46. // 程序入口
47. public static void main(String[] args) {
48. new SubClass();
49. }
50. }
class Parent {
// 静态变量
public static String p_StaticField = "父类--静态变量";
// 变量
public String p_Field = "父类--变量";
// 静态初始化块
static {
System.out.println(p_StaticField);
System.out.println("父类--静态初始化块");
}
// 初始化块
{
System.out.println(p_Field);
System.out.println("父类--初始化块");
}
// 构造器
public Parent() {
System.out.println("父类--构造器");
}
}
public class SubClass extends Parent {
// 静态变量
public static String s_StaticField = "子类--静态变量";
// 变量
public String s_Field = "子类--变量";
// 静态初始化块
static {
System.out.println(s_StaticField);
System.out.println("子类--静态初始化块");
}
// 初始化块
{
System.out.println(s_Field);
System.out.println("子类--初始化块");
}
// 构造器
public SubClass() {
System.out.println("子类--构造器");
}
// 程序入口
public static void main(String[] args) {
new SubClass();
}
}
运行一下上面的代码,结果马上呈现在我们的眼前:
1. 父类--静态变量
2. 父类--静态初始化块
3. 子类--静态变量
4. 子类--静态初始化块
5. 父类--变量
6. 父类--初始化块
7. 父类--构造器
8. 子类--变量
9. 子类--初始化块
10. 子类--构造器
现在,结果已经不言自明了。大家可能会注意到一点,那就是,并不是父类完全初始化完毕后才进行子类的初始化,实际上子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了。
那么对于静态变量和静态初始化块之间、变量和初始化块之间的先后顺序又是怎样呢?是否静态变量总是先于静态初始化块,变量总是先于初始化块就被初始化了呢?实际上这取决于它们在类中出现的先后顺序。我们以静态变量和静态初始化块为例来进行说明。
同样,我们还是写一个类来进行测试:
1. public class TestOrder {
2. // 静态变量
3. public static TestA a = new TestA();
4.
5. // 静态初始化块
6. static {
7. System.out.println("静态初始化块");
8. }
9.
10. // 静态变量
11. public static TestB b = new TestB();
12.
13. public static void main(String[] args) {
14. new TestOrder();
15. }
16. }
17.
18. class TestA {
19. public TestA() {
20. System.out.println("Test--A");
21. }
22. }
23.
24. class TestB {
25. public TestB() {
26. System.out.println("Test--B");
27. }
28. }
public class TestOrder {
// 静态变量
public static TestA a = new TestA();
// 静态初始化块
static {
System.out.println("静态初始化块");
}
// 静态变量
public static TestB b = new TestB();
public static void main(String[] args) {
new TestOrder();
}
}
class TestA {
public TestA() {
System.out.println("Test--A");
}
}
class TestB {
public TestB() {
System.out.println("Test--B");
}
}
运行上面的代码,会得到如下的结果:
1. Test--A
2. 静态初始化块
3. Test--B
大家可以随意改变变量a、变量b以及静态初始化块的前后位置,就会发现输出结果随着它们在类中出现的前后顺序而改变,这就说明静态变量和静态初始化块是依照他们在类中的定义顺序进行初始化的。同样,变量和初始化块也遵循这个规律。
了解了继承情况下类的初始化顺序之后,如何判断最终输出结果就迎刃而解了。
总结:
静态代码为什么先于非静态代码这是因为静态代码是在类加载完毕后执行的,而加载类的顺序是先父类后子类,所以静态代码的执行是先执行父类的,然后执行子类的。对于非静态变量以及实例初始化块都是在构造函数里的代码执行前执行。所以静态代码是在类加载后执行,而实例代码是在构造函数执行前执行。但是当我们显示控制类加载的时候情况有点变化,显示加载可以有关两种方法:
第一种:利用forName方法
当我们查API文档就会发现forName方法有两种形式。分别如下:
public static Class<?> forName(String className)
throws ClassNotFoundException
public static Class<?> forName(String name,
boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
第二个方法值得注意的就是第二个参数boolean initialize,如果我们把这个参数设置为false,那么当我们加载完类后就不会执行静态代码和静态的初始化动作。只有当我们new一个对象的时候才会初始化。而第三个参数是用来指明类的加载器的。
如果查看java.lang.Class类的源代码,上述两种方法最终都会调用Class类中的私有的native方法forName0(),此方法的声明如下:
private static native Class forName0(String name, boolean init , ClassLoader loader)
throws ClassNotFoundException;
所以当我们调用Class.forName(name )时,其实是在方法内部调用了:
forName0(name, true, ClassLoader.getCallerClassLoader());
当我们调用Class.forName(name, initialize, loader )的时候,实际上此方法内部调用了:
forName0(name, initialize, loader);
第二种:利用Class对象获取的ClassLoader装载。
此方法也是在实例化时才执行静态代码的执行。
综上所述可以总结如下:
1 对于隐式的加载(new一个对象和调用类的静态方法),静态代码是在类加载后立刻执行,而对于显示加载(第一种是用java.lang.Class的forName(String str)方法,第二种是用java.lang.ClassLoader的loadClass())就如同我上面所说,加载过程是可以由我们来控制的。
2 实例化代码执行是载构造函数执行之前,涉及到继承时,父类的构造函数执行之前执行父类里的实例化代码,子类的构造函数执行之前执行子类的实例化代码。所以这样可以保证子类中用到的变量都是已经经过父类初始化的,从而保证了初始化的正确性。
呵呵,这些是我学习J2SE的时候总结的,今天和大家分享。
JAVA面试题解惑系列(二)——到底创建了几个String对象?
关键字: java 面试题 string 创建几个对象
作者:臧圩人(zangweiren)
网址:http://zangweiren.javaeye.com
>>>转载请注明出处!<<<
我们首先来看一段代码:
1. String str=new String("abc");
String str=new String("abc");
紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢?相信大家对这道题并不陌生,答案也是众所周知的,2个。接下来我们就从这道题展开,一起回顾一下与创建String对象相关的一些JAVA知识。
我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:
1. public String(String original) {
2. //other code ...
3. }
public String(String original) {
//other code ...
}
大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:
1. 使用new创建对象。
2. 调用Class类的newInstance方法,利用反射机制创建对象。
我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。
这种方式是String特有的,并且它与new的方式存在很大区别。
1. String str="abc";
String str="abc";
毫无疑问,这行代码创建了一个String对象。
1. String a="abc";
2. String b="abc";
String a="abc";
String b="abc";
那这里呢?答案还是一个。
1. String a="ab"+"cd";
String a="ab"+"cd";
再看看这里呢?答案仍是一个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。
对于第三个例子:
1. String a="ab"+"cd";
String a="ab"+"cd";
由于常量的值在编译的时候就被确定了。在这里,"ab"和"cd"都是常量,因此变量a的值在编译时就可以确定。这行代码编译后的效果等同于:
1. String a="abcd";
String a="abcd";
因此这里只创建了一个对象"abcd",并且它被保存在字符串池里了。
现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况:
1. 如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。
2. 如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。
下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池:
1. public class StringTest {
2. public static void main(String[] args) {
3. String a = "ab";// 创建了一个对象,并加入字符串池中
4. System.out.println("String a = \"ab\";");
5. String b = "cd";// 创建了一个对象,并加入字符串池中
6. System.out.println("String b = \"cd\";");
7. String c = "abcd";// 创建了一个对象,并加入字符串池中
8.
9. String d = "ab" + "cd";
10. // 如果d和c指向了同一个对象,则说明d也被加入了字符串池
11. if (d == c) {
12. System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中");
13. }
14. // 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池
15. else {
16. System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中");
17. }
18.
19. String e = a + "cd";
20. // 如果e和c指向了同一个对象,则说明e也被加入了字符串池
21. if (e == c) {
22. System.out.println(" a +\"cd\" 创建的对象 \"加入了\" 字符串池中");
23. }
24. // 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池
25. else {
26. System.out.println(" a +\"cd\" 创建的对象 \"没加入\" 字符串池中");
27. }
28.
29. String f = "ab" + b;
30. // 如果f和c指向了同一个对象,则说明f也被加入了字符串池
31. if (f == c) {
32. System.out.println("\"ab\"+ b 创建的对象 \"加入了\" 字符串池中");
33. }
34. // 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池
35. else {
36. System.out.println("\"ab\"+ b 创建的对象 \"没加入\" 字符串池中");
37. }
38.
39. String g = a + b;
40. // 如果g和c指向了同一个对象,则说明g也被加入了字符串池
41. if (g == c) {
42. System.out.println(" a + b 创建的对象 \"加入了\" 字符串池中");
43. }
44. // 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池
45. else {
46. System.out.println(" a + b 创建的对象 \"没加入\" 字符串池中");
47. }
48. }
49. }
public class StringTest {
public static void main(String[] args) {
String a = "ab";// 创建了一个对象,并加入字符串池中
System.out.println("String a = \"ab\";");
String b = "cd";// 创建了一个对象,并加入字符串池中
System.out.println("String b = \"cd\";");
String c = "abcd";// 创建了一个对象,并加入字符串池中
String d = "ab" + "cd";
// 如果d和c指向了同一个对象,则说明d也被加入了字符串池
if (d == c) {
System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中");
}
// 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池
else {
System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中");
}
String e = a + "cd";
// 如果e和c指向了同一个对象,则说明e也被加入了字符串池
if (e == c) {
System.out.println(" a +\"cd\" 创建的对象 \"加入了\" 字符串池中");
}
// 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池
else {
System.out.println(" a +\"cd\" 创建的对象 \"没加入\" 字符串池中");
}
String f = "ab" + b;
// 如果f和c指向了同一个对象,则说明f也被加入了字符串池
if (f == c) {
System.out.println("\"ab\"+ b 创建的对象 \"加入了\" 字符串池中");
}
// 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池
else {
System.out.println("\"ab\"+ b 创建的对象 \"没加入\" 字符串池中");
}
String g = a + b;
// 如果g和c指向了同一个对象,则说明g也被加入了字符串池
if (g == c) {
System.out.println(" a + b 创建的对象 \"加入了\" 字符串池中");
}
// 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池
else {
System.out.println(" a + b 创建的对象 \"没加入\" 字符串池中");
}
}
}
运行结果如下:
1. String a = "ab";
2. String b = "cd";
3. "ab"+"cd" 创建的对象 "加入了" 字符串池中
4. a +"cd" 创建的对象 "没加入" 字符串池中
5. "ab"+ b 创建的对象 "没加入" 字符串池中
6. a + b 创建的对象 "没加入" 字符串池中
从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。
但是有一种情况需要引起我们的注意。请看下面的代码:
1. public class StringStaticTest {
2. // 常量A
3. public static final String A = "ab";
4.
5. // 常量B
6. public static final String B = "cd";
7.
8. public static void main(String[] args) {
9. // 将两个常量用+连接对s进行初始化
10. String s = A + B;
11. String t = "abcd";
12. if (s == t) {
13. System.out.println("s等于t,它们是同一个对象");
14. } else {
15. System.out.println("s不等于t,它们不是同一个对象");
16. }
17. }
18. }
public class StringStaticTest {
// 常量A
public static final String A = "ab";
// 常量B
public static final String B = "cd";
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
}
这段代码的运行结果如下:
· s等于t,它们是同一个对象
这又是为什么呢?原因是这样的,对于常量来讲,它的值是固定的,因此在编译期就能被确定了,而变量的值只有到运行时才能被确定,因为这个变量可以被不同的方法调用,从而可能引起值的改变。在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:
1. String s=A+B;
String s=A+B;
等同于:
1. String s="ab"+"cd";
String s="ab"+"cd";
我对上面的例子稍加改变看看会出现什么情况:
1. public class StringStaticTest {
2. // 常量A
3. public static final String A;
4.
5. // 常量B
6. public static final String B;
7.
8. static {
9. A = "ab";
10. B = "cd";
11. }
12.
13. public static void main(String[] args) {
14. // 将两个常量用+连接对s进行初始化
15. String s = A + B;
16. String t = "abcd";
17. if (s == t) {
18. System.out.println("s等于t,它们是同一个对象");
19. } else {
20. System.out.println("s不等于t,它们不是同一个对象");
21. }
22. }
23. }
public class StringStaticTest {
// 常量A
public static final String A;
// 常量B
public static final String B;
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
}
它的运行结果是这样:
· s不等于t,它们不是同一个对象
只是做了一点改动,结果就和刚刚的例子恰好相反。我们再来分析一下。A和B虽然被定义为常量(只能被赋值一次),但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。
由于字符串池中对象的共享能够带来效率的提高,因此我们提倡大家用引号包含文本的方式来创建String对象,实际上这也是我们在编程中常采用的。
接下来我们再来看看intern()方法,它的定义如下:
1. public native String intern();
public native String intern();
这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。
我们来看这段代码:
1. public class StringInternTest {
2. public static void main(String[] args) {
3. // 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
4. String a = new String(new char[] { 'a', 'b', 'c', 'd' });
5. String b = a.intern();
6. if (b == a) {
7. System.out.println("b被加入了字符串池中,没有新建对象");
8. } else {
9. System.out.println("b没被加入字符串池中,新建了对象");
10. }
11. }
12. }
public class StringInternTest {
public static void main(String[] args) {
// 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
String a = new String(new char[] { 'a', 'b', 'c', 'd' });
String b = a.intern();
if (b == a) {
System.out.println("b被加入了字符串池中,没有新建对象");
} else {
System.out.println("b没被加入字符串池中,新建了对象");
}
}
}
运行结果:
1. b没被加入字符串池中,新建了对象
如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。
最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别:
· 栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
· 堆(heap):用于存储对象。
我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。
当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。
说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。
总结:
让小弟来小结一下这篇文章所说的事情吧
1.堆和栈的概念。
2.编译器也是很机械化的,只能一行行的读代码,所以,在有的时候看似为静态的值,其实编译器是看不出来的,它也只能当机立断得判断。
3.看完后感觉在lz所说的字符串池中存储的其实也是基本数据类型char的数组或者说序列char[],在String类中将其包装。在vb还是c#中我忘了,就区分了这两种字符串的概念,分为基本类型string和对象类型String(注意大小写)。
4.在这里完全能感受到创作一个完整、封闭的语言是多么的缜密于细致,其实这也取决于当时Gosling或其开发团队的想法,当然作为这么优秀的语言,我们确实应该学习得更加细致。
JAVA面试题解惑系列(三)——变量(属性)的覆盖
关键字: java 面试题 继承 变量的覆盖 属性
作者:臧圩人(zangweiren)
网址:http://zangweiren.javaeye.com
>>>转载请注明出处!<<<
我们来看看这么一道题:
1. class ParentClass {
2. public int i = 10;
3. }
4.
5. public class SubClass extends ParentClass {
6. public int i = 30;
7.
8. public static void main(String[] args) {
9. ParentClass parentClass = new SubClass();
10. SubClass subClass = new SubClass();
11. System.out.println(parentClass.i + subClass.i);
12. }
13. }
class ParentClass {
public int i = 10;
}
public class SubClass extends ParentClass {
public int i = 30;
public static void main(String[] args) {
ParentClass parentClass = new SubClass();
SubClass subClass = new SubClass();
System.out.println(parentClass.i + subClass.i);
}
}
控制台的输出结果是多少呢?20?40?还是60?
变量,或者叫做类的属性,在继承的情况下,如果父类和子类存在同名的变量会出现什么情况呢?这就是这道题要考查的知识点——变量(属性)的覆盖。
这个问题虽然简单,但是情况却比较复杂。因为我们不仅要考虑变量、静态变量和常量三种情况,还要考虑private、friendly(即不加访问修饰符)、protected和public四种访问权限下对属性的不同影响。
我们先从普通变量说起。依照我们的惯例,先来看一段代码:
1. class ParentClass {
2. private String privateField = "父类变量--private";
3.
4. /* friendly */String friendlyField = "父类变量--friendly";
5.
6. protected String protectedField = "父类变量--protected";
7.
8. public String publicField = "父类变量--public";
9.
10. // private的变量无法直接访问,因此我们给他增加了一个访问方法
11. public String getPrivateFieldValue() {
12. return privateField;
13. }
14. }
15.
16. public class SubClass extends ParentClass {
17. private String privateField = "子类变量--private";
18.
19. /* friendly */String friendlyField = "子类变量--friendly";
20.
21. protected String protectedField = "子类变量--protected";
22.
23. public String publicField = "子类变量--public";
24.
25. // private的变量无法直接访问,因此我们给他增加了一个访问方法
26. public String getPrivateFieldValue() {
27. return privateField;
28. }
29.
30. public static void main(String[] args) {
31. // 为了便于查阅,我们统一按照private、friendly、protected、public的顺序
32. // 输出下列三种情况中变量的值
33.
34. // ParentClass类型,ParentClass对象
35. ParentClass parentClass = new ParentClass();
36. System.out.println("ParentClass parentClass = new ParentClass();");
37. System.out.println(parentClass.getPrivateFieldValue());
38. System.out.println(parentClass.friendlyField);
39. System.out.println(parentClass.protectedField);
40. System.out.println(parentClass.publicField);
41.
42. System.out.println();
43.
44. // ParentClass类型,SubClass对象
45. ParentClass subClass = new SubClass();
46. System.out.println("ParentClass subClass = new SubClass();");
47. System.out.println(subClass.getPrivateFieldValue());
48. System.out.println(subClass.friendlyField);
49. System.out.println(subClass.protectedField);
50. System.out.println(subClass.publicField);
51.
52. System.out.println();
53.
54. // SubClass类型,SubClass对象
55. SubClass subClazz = new SubClass();
56. System.out.println("SubClass subClazz = new SubClass();");
57. System.out.println(subClazz.getPrivateFieldValue());
58. System.out.println(subClazz.friendlyField);
59. System.out.println(subClazz.protectedField);
60. System.out.println(subClazz.publicField);
61. }
62. }
class ParentClass {
private String privateField = "父类变量--private";
/* friendly */String friendlyField = "父类变量--friendly";
protected String protectedField = "父类变量--protected";
public String publicField = "父类变量--public";
// private的变量无法直接访问,因此我们给他增加了一个访问方法
public String getPrivateFieldValue() {
return privateField;
}
}
public class SubClass extends ParentClass {
private String privateField = "子类变量--private";
/* friendly */String friendlyField = "子类变量--friendly";
protected String protectedField = "子类变量--protected";
public String publicField = "子类变量--public";
// private的变量无法直接访问,因此我们给他增加了一个访问方法
public String getPrivateFieldValue() {
return privateField;
}
public static void main(String[] args) {
// 为了便于查阅,我们统一按照private、friendly、protected、public的顺序
// 输出下列三种情况中变量的值
// ParentClass类型,ParentClass对象
ParentClass parentClass = new ParentClass();
System.out.println("ParentClass parentClass = new ParentClass();");
System.out.println(parentClass.getPrivateFieldValue());
System.out.println(parentClass.friendlyField);
System.out.println(parentClass.protectedField);
System.out.println(parentClass.publicField);
System.out.println();
// ParentClass类型,SubClass对象
ParentClass subClass = new SubClass();
System.out.println("ParentClass subClass = new SubClass();");
System.out.println(subClass.getPrivateFieldValue());
System.out.println(subClass.friendlyField);
System.out.println(subClass.protectedField);
System.out.println(subClass.publicField);
System.out.println();
// SubClass类型,SubClass对象
SubClass subClazz = new SubClass();
System.out.println("SubClass subClazz = new SubClass();");
System.out.println(subClazz.getPrivateFieldValue());
System.out.println(subClazz.friendlyField);
System.out.println(subClazz.protectedField);
System.out.println(subClazz.publicField);
}
}
这段代码的运行结果如下:
1. ParentClass parentClass = new ParentClass();
2. 父类变量--private
3. 父类变量--friendly
4. 父类变量--protected
5. 父类变量--public
6.
7. ParentClass subClass = new SubClass();
8. 子类变量--private
9. 父类变量--friendly
10. 父类变量--protected
11. 父类变量--public
12.
13. SubClass subClazz = new SubClass();
14. 子类变量--private
15. 子类变量--friendly
16. 子类变量--protected
17. 子类变量--public
从上面的结果中可以看出,private的变量与其它三种访问权限变量的不同,这是由于方法的重写(override)而引起的。关于重写知识的回顾留给以后的章节,这里我们来看一下其它三种访问权限下变量的覆盖情况。
分析上面的输出结果就会发现,变量的值取决于我们定义的变量的类型,而不是创建的对象的类型。
在上面的例子中,同名的变量访问权限也是相同的,那么对于名称相同但是访问权限不同的变量,情况又会怎样呢?事实胜于雄辩,我们继续来做测试。由于private变量的特殊性,在接下来的实验中我们都把它排除在外,不予考虑。
由于上面的例子已经说明了,当变量类型是父类(ParentClass)时,不管我们创建的对象是父类(ParentClass)的还是子类(SubClass)的,都不存在属性覆盖的问题,因此接下来我们也只考虑变量类型和创建对象都是子类(SubClass)的情况。
1. class ParentClass {
2. /* friendly */String field = "父类变量";
3. }
4.
5. public class SubClass extends ParentClass {
6. protected String field = "子类变量";
7.
8. public static void main(String[] args) {
9. SubClass subClass = new SubClass();
10. System.out.println(subClass.field);
11. }
12. }
class ParentClass {
/* friendly */String field = "父类变量";
}
public class SubClass extends ParentClass {
protected String field = "子类变量";
public static void main(String[] args) {
SubClass subClass = new SubClass();
System.out.println(subClass.field);
}
}
运行结果:
· 子类变量
1. class ParentClass {
2. public String field = "父类变量";
3. }
4.
5. public class SubClass extends ParentClass {
6. protected String field = "子类变量";
7.
8. public static void main(String[] args) {
9. SubClass subClass = new SubClass();
10. System.out.println(subClass.field);
11. }
12. }
class ParentClass {
public String field = "父类变量";
}
public class SubClass extends ParentClass {
protected String field = "子类变量";
public static void main(String[] args) {
SubClass subClass = new SubClass();
System.out.println(subClass.field);
}
}
运行结果:
· 子类变量
上面两段不同的代码,输出结果确是相同的。事实上,我们可以将父类和子类属性前的访问修饰符在friendly、protected和public之间任意切换,得到的结果都是相同的。也就是说访问修饰符并不影响属性的覆盖,关于这一点大家可以自行编写测试代码验证。
对于静态变量和常量又会怎样呢?我们继续来看:
1. class ParentClass {
2. public static String staticField = "父类静态变量";
3.
4. public final String finalField = "父类常量";
5.
6. public static final String staticFinalField = "父类静态常量";
7. }
8.
9. public class SubClass extends ParentClass {
10. public static String staticField = "子类静态变量";
11.
12. public final String finalField = "子类常量";
13.
14. public static final String staticFinalField = "子类静态常量";
15.
16. public static void main(String[] args) {
17. SubClass subClass = new SubClass();
18. System.out.println(SubClass.staticField);
19. System.out.println(subClass.finalField);
20. System.out.println(SubClass.staticFinalField);
21. }
22. }
class ParentClass {
public static String staticField = "父类静态变量";
public final String finalField = "父类常量";
public static final String staticFinalField = "父类静态常量";
}
public class SubClass extends ParentClass {
public static String staticField = "子类静态变量";
public final String finalField = "子类常量";
public static final String staticFinalField = "子类静态常量";
public static void main(String[] args) {
SubClass subClass = new SubClass();
System.out.println(SubClass.staticField);
System.out.println(subClass.finalField);
System.out.println(SubClass.staticFinalField);
}
}
运行结果如下:
1. 子类静态变量
2. 子类常量
3. 子类静态常量
虽然上面的结果中包含“子类静态变量”和“子类静态常量”,但这并不表示父类的“静态变量”和“静态常量”可以被子类覆盖,因为它们都是属于类,而不属于对象。
上面的例子中,我们一直用对象来对变量(属性)的覆盖做测试,如果是基本类型的变量,结果是否会相同呢?答案是肯定的,这里我们就不再一一举例说明了。
最后,我们来做个总结。通过以上测试,可以得出一下结论:
1. 由于private变量受访问权限的限制,它不能被覆盖。
2. 属性的值取父类还是子类并不取决于我们创建对象的类型,而是取决于我们定义的变量的类型。
3. friendly、protected和public修饰符并不影响属性的覆盖。
4. 静态变量和静态常量属于类,不属于对象,因此它们不能被覆盖。
5. 常量可以被覆盖。
6. 对于基本类型和对象,它们适用同样的覆盖规律。
我们再回到篇首的那道题,我想大家都已经知道答案了,输出结果应该是40。
JAVA面试题解惑系列(四)——final、finally和finalize的区别
关键字: java 面试题 final finally finalize
作者:臧圩人(zangweiren)
网址:http://zangweiren.javaeye.com
>>>转载请注明出处!<<<
final、finally和finalize的区别是什么?
这是一道再经典不过的面试题了,我们在各个公司的面试题中几乎都能看到它的身影。final、finally和finalize虽然长得像孪生三兄弟一样,但是它们的含义和用法却是大相径庭。这一次我们就一起来回顾一下这方面的知识。
我们首先来说说final。它可以用于以下四个地方:
1. 定义变量,包括静态的和非静态的。
2. 定义方法的参数。
3. 定义方法。
4. 定义类。
我们依次来回顾一下每种情况下final的作用。首先来看第一种情况,如果final修饰的是一个基本类型,就表示这个变量被赋予的值是不可变的,即它是个常量;如果final修饰的是一个对象,就表示这个变量被赋予的引用是不可变的,这里需要提醒大家注意的是,不可改变的只是这个变量所保存的引用,并不是这个引用所指向的对象。在第二种情况下,final的含义与第一种情况相同。实际上对于前两种情况,有一种更贴切的表述final的含义的描述,那就是,如果一个变量或方法参数被final修饰,就表示它只能被赋值一次,但是JAVA虚拟机为变量设定的默认值不记作一次赋值。
被final修饰的变量必须被初始化。初始化的方式有以下几种:
1. 在定义的时候初始化。
2. final变量可以在初始化块中初始化,不可以在静态初始化块中初始化。
3. 静态final变量可以在静态初始化块中初始化,不可以在初始化块中初始化。
4. final变量还可以在类的构造器中初始化,但是静态final变量不可以。
通过下面的代码可以验证以上的观点:
1. public class FinalTest {
2. // 在定义时初始化
3. public final int A = 10;
4.
5. public final int B;
6. // 在初始化块中初始化
7. {
8. B = 20;
9. }
10.
11. // 非静态final变量不能在静态初始化块中初始化
12. // public final int C;
13. // static {
14. // C = 30;
15. // }
16.
17. // 静态常量,在定义时初始化
18. public static final int STATIC_D = 40;
19.
20. public static final int STATIC_E;
21. // 静态常量,在静态初始化块中初始化
22. static {
23. STATIC_E = 50;
24. }
25.
26. // 静态变量不能在初始化块中初始化
27. // public static final int STATIC_F;
28. // {
29. // STATIC_F = 60;
30. // }
31.
32. public final int G;
33.
34. // 静态final变量不可以在构造器中初始化
35. // public static final int STATIC_H;
36.
37. // 在构造器中初始化
38. public FinalTest() {
39. G = 70;
40. // 静态final变量不可以在构造器中初始化
41. // STATIC_H = 80;
42.
43. // 给final的变量第二次赋值时,编译会报错
44. // A = 99;
45. // STATIC_D = 99;
46. }
47.
48. // final变量未被初始化,编译时就会报错
49. // public final int I;
50.
51. // 静态final变量未被初始化,编译时就会报错
52. // public static final int STATIC_J;
53. }
public class FinalTest {
// 在定义时初始化
public final int A = 10;
public final int B;
// 在初始化块中初始化
{
B = 20;
}
// 非静态final变量不能在静态初始化块中初始化
// public final int C;
// static {
// C = 30;
// }
// 静态常量,在定义时初始化
public static final int STATIC_D = 40;
public static final int STATIC_E;
// 静态常量,在静态初始化块中初始化
static {
STATIC_E = 50;
}
// 静态变量不能在初始化块中初始化
// public static final int STATIC_F;
// {
// STATIC_F = 60;
// }
public final int G;
// 静态final变量不可以在构造器中初始化
// public static final int STATIC_H;
// 在构造器中初始化
public FinalTest() {
G = 70;
// 静态final变量不可以在构造器中初始化
// STATIC_H = 80;
// 给final的变量第二次赋值时,编译会报错
// A = 99;
// STATIC_D = 99;
}
// final变量未被初始化,编译时就会报错
// public final int I;
// 静态final变量未被初始化,编译时就会报错
// public static final int STATIC_J;
}
我们运行上面的代码之后出了可以发现final变量(常量)和静态final变量(静态常量)未被初始化时,编译会报错。
用final修饰的变量(常量)比非final的变量(普通变量)拥有更高的效率,因此我们在实际编程中应该尽可能多的用常量来代替普通变量,这也是一个很好的编程习惯。
当final用来定义一个方法时,会有什么效果呢?正如大家所知,它表示这个方法不可以被子类重写,但是它这不影响它被子类继承。我们写段代码来验证一下:
1. class ParentClass {
2. public final void TestFinal() {
3. System.out.println("父类--这是一个final方法");
4. }
5. }
6.
7. public class SubClass extends ParentClass {
8. /**
9. * 子类无法重写(override)父类的final方法,否则编译时会报错
10. */
11. // public void TestFinal() {
12. // System.out.println("子类--重写final方法");
13. // }
14.
15. public static void main(String[] args) {
16. SubClass sc = new SubClass();
17. sc.TestFinal();
18. }
19. }
class ParentClass {
public final void TestFinal() {
System.out.println("父类--这是一个final方法");
}
}
public class SubClass extends ParentClass {
/**
* 子类无法重写(override)父类的final方法,否则编译时会报错
*/
// public void TestFinal() {
// System.out.println("子类--重写final方法");
// }
public static void main(String[] args) {
SubClass sc = new SubClass();
sc.TestFinal();
}
}
这里需要特殊说明的是,具有private访问权限的方法也可以增加final修饰,但是由于子类无法继承private方法,因此也无法重写它。编译器在处理private方法时,是按照final方法来对待的,这样可以提高该方法被调用时的效率。不过子类仍然可以定义同父类中的private方法具有同样结构的方法,但是这并不会产生重写的效果,而且它们之间也不存在必然联系。
最后我们再来回顾一下final用于类的情况。这个大家应该也很熟悉了,因为我们最常用的String类就是final的。由于final类不允许被继承,编译器在处理时把它的所有方法都当作final的,因此final类比普通类拥有更高的效率。final的类的所有方法都不能被重写,但这并不表示final的类的属性(变量)值也是不可改变的,要想做到final类的属性值不可改变,必须给它增加final修饰,请看下面的例子:
1. public final class FinalTest {
2.
3. int i = 10;
4.
5. public static void main(String[] args) {
6. FinalTest ft = new FinalTest();
7. ft.i = 99;
8. System.out.println(ft.i);
9. }
10. }
public final class FinalTest {
int i = 10;
public static void main(String[] args) {
FinalTest ft = new FinalTest();
ft.i = 99;
System.out.println(ft.i);
}
}
运行上面的代码试试看,结果是99,而不是初始化时的10。
接下来我们一起回顾一下finally的用法。这个就比较简单了,它只能用在try/catch语句中,并且附带着一个语句块,表示这段语句最终总是被执行。请看下面的代码:
1. public final class FinallyTest {
2. public static void main(String[] args) {
3. try {
4. throw new NullPointerException();
5. } catch (NullPointerException e) {
6. System.out.println("程序抛出了异常");
7. } finally {
8. System.out.println("执行了finally语句块");
9. }
10. }
11. }
public final class FinallyTest {
public static void main(String[] args) {
try {
throw new NullPointerException();
} catch (NullPointerException e) {
System.out.println("程序抛出了异常");
} finally {
System.out.println("执行了finally语句块");
}
}
}
运行结果说明了finally的作用:
1. 程序抛出了异常
2. 执行了finally语句块
请大家注意,捕获程序抛出的异常之后,既不加处理,也不继续向上抛出异常,并不是良好的编程习惯,它掩盖了程序执行中发生的错误,这里只是方便演示,请不要学习。
那么,有没有一种情况使finally语句块得不到执行呢?大家可能想到了return、continue、break这三个可以打乱代码顺序执行语句的规律。那我们就来试试看,这三个语句是否能影响finally语句块的执行:
1. public final class FinallyTest {
2.
3. // 测试return语句
4. public ReturnClass testReturn() {
5. try {
6. return new ReturnClass();
7. } catch (Exception e) {
8. e.printStackTrace();
9. } finally {
10. System.out.println("执行了finally语句");
11. }
12. return null;
13. }
14.
15. // 测试continue语句
16. public void testContinue() {
17. for (int i = 0; i < 3; i++) {
18. try {
19. System.out.println(i);
20. if (i == 1) {
21. continue;
22. }
23. } catch (Exception e) {
24. e.printStackTrace();
25. } finally {
26. System.out.println("执行了finally语句");
27. }
28. }
29. }
30.
31. // 测试break语句
32. public void testBreak() {
33. for (int i = 0; i < 3; i++) {
34. try {
35. System.out.println(i);
36. if (i == 1) {
37. break;
38. }
39. } catch (Exception e) {
40. e.printStackTrace();
41. } finally {
42. System.out.println("执行了finally语句");
43. }
44. }
45. }
46.
47. public static void main(String[] args) {
48. FinallyTest ft = new FinallyTest();
49. // 测试return语句
50. ft.testReturn();
51. System.out.println();
52. // 测试continue语句
53. ft.testContinue();
54. System.out.println();
55. // 测试break语句
56. ft.testBreak();
57. }
58. }
59.
60. class ReturnClass {
61. public ReturnClass() {
62. System.out.println("执行了return语句");
63. }
64. }
public final class FinallyTest {
// 测试return语句
public ReturnClass testReturn() {
try {
return new ReturnClass();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("执行了finally语句");
}
return null;
}
// 测试continue语句
public void testContinue() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(i);
if (i == 1) {
continue;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("执行了finally语句");
}
}
}
// 测试break语句
public void testBreak() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(i);
if (i == 1) {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("执行了finally语句");
}
}
}
public static void main(String[] args) {
FinallyTest ft = new FinallyTest();
// 测试return语句
ft.testReturn();
System.out.println();
// 测试continue语句
ft.testContinue();
System.out.println();
// 测试break语句
ft.testBreak();
}
}
class ReturnClass {
public ReturnClass() {
System.out.println("执行了return语句");
}
}
上面这段代码的运行结果如下:
1. 执行了return语句
2. 执行了finally语句
3.
4. 0
5. 执行了finally语句
6. 1
7. 执行了finally语句
8. 2
9. 执行了finally语句
10.
11. 0
12. 执行了finally语句
13. 1
14. 执行了finally语句
很明显,return、continue和break都没能阻止finally语句块的执行。从输出的结果来看,return语句似乎在finally语句块之前执行了,事实真的如此吗?我们来想想看,return语句的作用是什么呢?是退出当前的方法,并将值或对象返回。如果finally语句块是在return语句之后执行的,那么return语句被执行后就已经退出当前方法了,finally语句块又如何能被执行呢?因此,正确的执行顺序应该是这样的:编译器在编译return new ReturnClass();时,将它分成了两个步骤,new ReturnClass()和return,前一个创建对象的语句是在finally语句块之前被执行的,而后一个return语句是在finally语句块之后执行的,也就是说finally语句块是在程序退出方法之前被执行的。同样,finally语句块是在循环被跳过(continue)和中断(break)之前被执行的。
最后,我们再来看看finalize,它是一个方法,属于java.lang.Object类,它的定义如下:
1. protected void finalize() throws Throwable { }
protected void finalize() throws Throwable { }
众所周知,finalize()方法是GC(garbage collector)运行机制的一部分,关于GC的知识我们将在后续的章节中来回顾。
在此我们只说说finalize()方法的作用是什么呢?
finalize()方法是在GC清理它所从属的对象时被调用的,如果执行它的过程中抛出了无法捕获的异常(uncaught exception),GC将终止对改对象的清理,并且该异常会被忽略;直到下一次GC开始清理这个对象时,它的finalize()会被再次调用。
请看下面的示例:
1. public final class FinallyTest {
2. // 重写finalize()方法
3. protected void finalize() throws Throwable {
4. System.out.println("执行了finalize()方法");
5. }
6.
7. public static void main(String[] args) {
8. FinallyTest ft = new FinallyTest();
9. ft = null;
10. System.gc();
11. }
12. }
public final class FinallyTest {
// 重写finalize()方法
protected void finalize() throws Throwable {
System.out.println("执行了finalize()方法");
}
public static void main(String[] args) {
FinallyTest ft = new FinallyTest();
ft = null;
System.gc();
}
}
运行结果如下:
· 执行了finalize()方法
程序调用了java.lang.System类的gc()方法,引起GC的执行,GC在清理ft对象时调用了它的finalize()方法,因此才有了上面的输出结果。调用System.gc()等同于调用下面这行代码:
1. Runtime.getRuntime().gc();
Runtime.getRuntime().gc();
调用它们的作用只是建议垃圾收集器(GC)启动,清理无用的对象释放内存空间,但是GC的启动并不是一定的,这由JAVA虚拟机来决定。直到JAVA虚拟机停止运行,有些对象的finalize()可能都没有被运行过,那么怎样保证所有对象的这个方法在JAVA虚拟机停止运行之前一定被调用呢?答案是我们可以调用System类的另一个方法:
1. public static void runFinalizersOnExit(boolean value) {
2. //other code
3. }
public static void runFinalizersOnExit(boolean value) {
//other code
}
给这个方法传入true就可以保证对象的finalize()方法在JAVA虚拟机停止运行前一定被运行了,不过遗憾的是这个方法是不安全的,它会导致有用的对象finalize()被误调用,因此已经不被赞成使用了。
由于finalize()属于Object类,因此所有类都有这个方法,Object的任意子类都可以重写(override)该方法,在其中释放系统资源或者做其它的清理工作,如关闭输入输出流。
通过以上知识的回顾,我想大家对于final、finally、finalize的用法区别已经很清楚了。
好文,一直在关注你的这个系列。
提一点:
如果在finally 语句块里有return语句,那么方法return得到的结果是什么呢?
1. package test;
2.
3. public class ParentClassTest {
4. protected String field1 = "parent field 1";
5.
6. protected void method1() {
7. System.out.println("ParentClass method 1");
8. }
9.
10. public static void main(String[] args) {
11. ParentClassTest test = new SubClassTest();
12. test.method1();
13. System.out.println(test.field1);
14. }
15. }
16.
17. class SubClassTest extends ParentClassTest {
18. protected String field1 = "sub field 1";
19.
20. public void method1() {
21. System.out.println("subclass method 1");
22. }
23. }
24.
25. class FinallyTest {
26.
27. public Object testReturn() {
28. try {
29. return new ReturnClass();
30. }
31. catch (Exception e) {
32. e.printStackTrace();
33. }
34. finally {
35. System.out.println("执行了finally语句");
36. return new AnotherReturnClass();
37. }
38. //return new ReturnClass2();
39. }
40.
41. public static void main(String[] args) {
42. FinallyTest ft = new FinallyTest();
43. // 测试return语句
44. Object rtc = ft.testReturn();
45. System.out.print(rtc.getClass());
46. }
47. }
48.
49. class ReturnClass {
50. public ReturnClass() {
51. System.out.println("执行了return语句");
52. }
53. }
54.
55. class AnotherReturnClass {
56. public AnotherReturnClass() {
57. System.out.println("执行了 finally 语句块里的 return语句");
58. }
59. }
package test;
public class ParentClassTest {
protected String field1 = "parent field 1";
protected void method1() {
System.out.println("ParentClass method 1");
}
public static void main(String[] args) {
ParentClassTest test = new SubClassTest();
test.method1();
System.out.println(test.field1);
}
}
class SubClassTest extends ParentClassTest {
protected String field1 = "sub field 1";
public void method1() {
System.out.println("subclass method 1");
}
}
class FinallyTest {
public Object testReturn() {
try {
return new ReturnClass();
}
catch (Exception e) {
e.printStackTrace();
}
finally {
System.out.println("执行了finally语句");
return new AnotherReturnClass();
}
//return new ReturnClass2();
}
public static void main(String[] args) {
FinallyTest ft = new FinallyTest();
// 测试return语句
Object rtc = ft.testReturn();
System.out.print(rtc.getClass());
}
}
class ReturnClass {
public ReturnClass() {
System.out.println("执行了return语句");
}
}
class AnotherReturnClass {
public AnotherReturnClass() {
System.out.println("执行了 finally 语句块里的 return语句");
}
}
输出结果:
执行了return语句
执行了finally语句
执行了 finally 语句块里的 return语句
class test.AnotherReturnClass
也就是说,finally内部的return 结果会覆盖之前执行return的结果。这个行似乎很容易引起歧义:明明我的return语句被执行了,怎么结果不对呢?
不过这个return语句使得编译器给出了一个警告:finally block does not complete normally.
JAVA面试题解惑系列(五)——传了值还是传了引用?
关键字: java 面试题 值传递 引用传递
作者:臧圩人(zangweiren)
网址:http://zangweiren.javaeye.com
>>>转载请注明出处!<<<
JAVA中的传递都是值传递吗?有没有引用传递呢?
在回答这两个问题前,让我们首先来看一段代码:
1. public class ParamTest {
2. // 初始值为0
3. protected int num = 0;
4.
5. // 为方法参数重新赋值
6. public void change(int i) {
7. i = 5;
8. }
9.
10. // 为方法参数重新赋值
11. public void change(ParamTest t) {
12. ParamTest tmp = new ParamTest();
13. tmp.num = 9;
14. t = tmp;
15. }
16.
17. // 改变方法参数的值
18. public void add(int i) {
19. i += 10;
20. }
21.
22. // 改变方法参数属性的值
23. public void add(ParamTest pt) {
24. pt.num += 20;
25. }
26.
27. public static void main(String[] args) {
28. ParamTest t = new ParamTest();
29.
30. System.out.println("参数--基本类型");
31. System.out.println("原有的值:" + t.num);
32. // 为基本类型参数重新赋值
33. t.change(t.num);
34. System.out.println("赋值之后:" + t.num);
35. // 为引用型参数重新赋值
36. t.change(t);
37. System.out.println("运算之后:" + t.num);
38.
39. System.out.println();
40.
41. t = new ParamTest();
42. System.out.println("参数--引用类型");
43. System.out.println("原有的值:" + t.num);
44. // 改变基本类型参数的值
45. t.add(t.num);
46. System.out.println("赋引用后:" + t.num);
47. // 改变引用类型参数所指向对象的属性值
48. t.add(t);
49. System.out.println("改属性后:" + t.num);
50. }
51. }
public class ParamTest {
// 初始值为0
protected int num = 0;
// 为方法参数重新赋值
public void change(int i) {
i = 5;
}
// 为方法参数重新赋值
public void change(ParamTest t) {
ParamTest tmp = new ParamTest();
tmp.num = 9;
t = tmp;
}
// 改变方法参数的值
public void add(int i) {
i += 10;
}
// 改变方法参数属性的值
public void add(ParamTest pt) {
pt.num += 20;
}
public static void main(String[] args) {
ParamTest t = new ParamTest();
System.out.println("参数--基本类型");
System.out.println("原有的值:" + t.num);
// 为基本类型参数重新赋值
t.change(t.num);
System.out.println("赋值之后:" + t.num);
// 为引用型参数重新赋值
t.change(t);
System.out.println("运算之后:" + t.num);
System.out.println();
t = new ParamTest();
System.out.println("参数--引用类型");
System.out.println("原有的值:" + t.num);
// 改变基本类型参数的值
t.add(t.num);
System.out.println("赋引用后:" + t.num);
// 改变引用类型参数所指向对象的属性值
t.add(t);
System.out.println("改属性后:" + t.num);
}
}
这段代码的运行结果如下:
1. 参数--基本类型
2. 原有的值:0
3. 赋值之后:0
4. 运算之后:0
5.
6. 参数--引用类型
7. 原有的值:0
8. 赋引用后:0
9. 改属性后:20
从上面这个直观的结果中我们很容易得出如下结论:
1. 对于基本类型,在方法体内对方法参数进行重新赋值,并不会改变原有变量的值。
2. 对于引用类型,在方法体内对方法参数进行重新赋予引用,并不会改变原有变量所持有的引用。
3. 方法体内对参数进行运算,不影响原有变量的值。
4. 方法体内对参数所指向对象的属性进行运算,将改变原有变量所指向对象的属性值。
上面总结出来的不过是我们所看到的表面现象。那么,为什么会出现这样的现象呢?这就要说到值传递和引用传递的概念了。这个问题向来是颇有争议的。
大家都知道,在JAVA中变量有以下两种:
1. 基本类型变量,包括char、byte、short、int、long、float、double、boolean。
2. 引用类型变量,包括类、接口、数组(基本类型数组和对象数组)。
当基本类型的变量被当作参数传递给方法时,JAVA虚拟机所做的工作是把这个值拷贝了一份,然后把拷贝后的值传递到了方法的内部。因此在上面的例子中,我们回头来看看这个方法:
1. // 为方法参数重新赋值
2. public void change(int i) {
3. i = 5;
4. }
// 为方法参数重新赋值
public void change(int i) {
i = 5;
}
在这个方法被调用时,变量i和ParamTest型对象t的属性num具有相同的值,却是两个不同变量。变量i是由JAVA虚拟机创建的作用域在change(int i)方法内的局部变量,在这个方法执行完毕后,它的生命周期就结束了。在JAVA虚拟机中,它们是以类似如下的方式存储的:
很明显,在基本类型被作为参数传递给方式时,是值传递,在整个过程中根本没有牵扯到引用这个概念。这也是大家所公认的。对于布尔型变量当然也是如此,请看下面的例子:
1. public class BooleanTest {
2. // 布尔型值
3. boolean bool = true;
4.
5. // 为布尔型参数重新赋值
6. public void change(boolean b) {
7. b = false;
8. }
9.
10. // 对布尔型参数进行运算
11. public void calculate(boolean b) {
12. b = b && false;
13. // 为了方便对比,将运算结果输出
14. System.out.println("b运算后的值:" + b);
15. }
16.
17. public static void main(String[] args) {
18. BooleanTest t = new BooleanTest();
19.
20. System.out.println("参数--布尔型");
21. System.out.println("原有的值:" + t.bool);
22. // 为布尔型参数重新赋值
23. t.change(t.bool);
24. System.out.println("赋值之后:" + t.bool);
25.
26. // 改变布尔型参数的值
27. t.calculate(t.bool);
28. System.out.println("运算之后:" + t.bool);
29. }
30. }
public class BooleanTest {
// 布尔型值
boolean bool = true;
// 为布尔型参数重新赋值
public void change(boolean b) {
b = false;
}
// 对布尔型参数进行运算
public void calculate(boolean b) {
b = b && false;
// 为了方便对比,将运算结果输出
System.out.println("b运算后的值:" + b);
}
public static void main(String[] args) {
BooleanTest t = new BooleanTest();
System.out.println("参数--布尔型");
System.out.println("原有的值:" + t.bool);
// 为布尔型参数重新赋值
t.change(t.bool);
System.out.println("赋值之后:" + t.bool);
// 改变布尔型参数的值
t.calculate(t.bool);
System.out.println("运算之后:" + t.bool);
}
}
输出结果如下:
1. 参数--布尔型
2. 原有的值:true
3. 赋值之后:true
4. b运算后的值:false
5. 运算之后:true
那么当引用型变量被当作参数传递给方法时JAVA虚拟机又是怎样处理的呢?同样,它会拷贝一份这个变量所持有的引用,然后把它传递给JAVA虚拟机为方法创建的局部变量,从而这两个变量指向了同一个对象。在篇首所举的示例中,ParamTest类型变量t和局部变量pt在JAVA虚拟机中是以如下的方式存储的:
有一种说法是当一个对象或引用类型变量被当作参数传递时,也是值传递,这个值就是对象的引用,因此JAVA中只有值传递,没有引用传递。这种说法显然是混淆了值和引用的概念。
值传递中的值指的是基本类型的数值,即使对于布尔型,虽然它的表现形式为true和false,但是在栈中,它仍然是以数值形式保存的,即0表示false,其它数值表示true。而引用是我们用来操作对象的工具,它包含了对象在堆中保存地址的信息。即使在被作为参数传递给方法时,实际上传递的是它的拷贝,但那仍是引用。
最后我们得出如下的结论:
1. 基本类型和基本类型变量被当作参数传递给方法时,是值传递。在方法实体中,无法给原变量重新赋值,也无法改变它的值。
2. 对象和引用型变量被当作参数传递给方法时,是引用传递。在方法实体中,无法给原变量重新赋值,但是可以改变它所指向对象的属性。
什么叫引用?只因为这个变量的值和其它的不一样.
首先理解:都是变量
int i;
ArrayList b;
i和b都是变量.
但i是基本变量,也叫原始变量.
其它的就叫引用变量,因为它的值是一个内存地址值.引用对象的.但记住:它们都是有一个值的!i是一个数字,而b是一个内存地址值(简单的说是一个十六进制的值).除了基本变量之外的变量都是引用变量.Vector a;这里的a也是一个变量.它也是有值的,它的值是一个十六进制的值.
变量的赋值:
int i=10;
int j=i;
//这里把i的值10给了j,所以j的值也是10
ArrayList b=new ArrayList();
ArrayList c=b;
//首先,b是一个引用变量,它的"值":是一个内存地址值!!! new ArrayList()要分配一段内存保存它们,怎么样找到这段内存?那就是通过b里的值了.b的值就是new ArrayList()所占内存的首地址.然后c也是一个引用变量,它的值(地址值)和b是一样的.也就是new ArrayList()所占内存的首地址.所以当通过b或者c进行操作时,它们都是操作同一个对象的.
在方法调用的时候,方法的参数实际也就是一个变量.如果是基本类型变量的时候,假设有方法method(int aa);
int j=10;
method(j);
这里边,int aa实际也是定义了一个变量,调用的时候把j的值:10也给了aa.所以aa也是10,改变了aa的值并不会改变j的值.
如果是引用变量的时候,假设有方法methodA(ArrayList aa);
ArrayList b = new ArrayList();
methodA(b);
//方法定义了变量aa,调用的时候把b的值(地址值!!!!!)给了aa,所以aa与b有一样的值(地址值!!!!),在方法里通过aa去操作的时候,b所引用的对象也就被改变了,因为它们引用同一个对象.
纸 a = new 银行帐户();//开一个银行帐户,返回一个卡号给你,写在你的纸a里边.
用一张纸(引用变量),把你的银行卡号写在上边,然后调用我的时候,我用另外一张纸(引用变量---方法的形数),把你的号码抄过来.然后我通过这个卡号,去到银行找到你的帐号,给你存点钱.
然后你用你的纸(引用变量)上的卡号 <没变,还是那个卡号>再去查询银行帐号的时候就会发现了多了一些钱了.....
不知道LZ有没仔细看我的bolg
LZ给出一个确切地“值传递”和“引用传递”的准确定义,或者说其区分准则是什么?这样讨论起来才好说,要不然很难说明白。这种概念性讨论最基本的就是先弄清楚定义,要不然很容易鸡同鸭讲。
所谓参数传递是将实参的“值”传递给形参
不知道LZ怎么理解“值”、“数值”、“引用”
JAVA中是分堆和栈的,对象是在堆中生成,其引用存在栈中,也就是说引用其实是一个变量,存储的是“地址”(在Java中称为引用),很类似C++中的指针,莫非“地址”(引用)就不是“值”了??不知从何说起。从这个意义上说,java中传递引用时,实际上是将实参(存的是地址)的值拷贝给形参,也就是值复制。
在C++中的“引用传递”,是将变量a作为实参传递时,实际传递的是a的地址。所以才称为引用。在C++中当变量的地址&a作为实参传递时,并不能说是引用传递,而是值传递(这点应该没什么疑问吧?)。
继续支持你的这个系列。
说说我对值传递和引用传递的看法:
首先我认为,大家对Java传递参数的行为是清楚的,这个争论只是一个语义上的争论。
也就是我们是否需要区分值传递和应用传递呢?或者说这样的区分有没有意义?是否合理?
博主认为存在引用传递的关键点在于,传递的对象地址值,本质上它是一个引用,无论它是否被copy过。
认为只有值传递的关键点在于,传递的对象地址值,它是一个值的copy,这个值代表的意义无所谓。
引用是c++里的概念,由于java跟c++是有一定关系的,这里把引用迁移过来,如果合理未尝不可。
c++中关于引用的解释一般喜欢说是看作“别名”,我查了几本书,大部分提到引用并不会分配内存空间,也有一本书提到,某些编译器会分配存储空间来存储被引用对象的地址。
那么还是回到语义上来,c++里的这个引用,语义上是“别名”的意思,我的理解是,一组指向同一个对象的别名应该只存储一份内存地址。当然具体实现可能会把引用当做一个不可变的指针来处理(每个别名都存储自己的对象地址)。但是请注意,我们应该关注于它的语义,即:它没有任何值的copy,即使是一个地址,只是另外一个名字而已。
但是java里面没有这样的概念,所有的地址传递其行为是值的传递方式,语义上统一成值传递更为清晰,我们只需要考虑这个值具体是什么,无非两种,要么是基本类型值,要么是个地址。
所以我认为这个“引用”的概念放到java中并不合适。只有值传递的说法更合理。