JAVA面试题解惑系列(四)——final、finally和finalize的区别

final、finally和finalize的区别是什么?
这是一道再经典不过的面试题了,我们在各个公司的面试题中几乎都能看到它的身影。final、finally和finalize虽然长得像孪生三
兄弟一样,但是它们的含义和用法却是大相径庭。这一次我们就一起来回顾一下这方面的知识。
final关键字
我们首先来说说final。它可以用于以下四个地方:
定义变量,包括静态的和非静态的。
定义方法的参数。
定义方法。
定义类。
我们依次来回顾一下每种情况下final的作用。首先来看第一种情况,如果final修饰的是一个基本类型,就表示这个变量被赋予的值
是不可变的,即它是个常量;如果final修饰的是一个对象,就表示这个变量被赋予的引用是不可变的,这里需要提醒大家注意的是
,不可改变的只是这个变量所保存的引用,并不是这个引用所指向的对象。在第二种情况下,final的含义与第一种情况相同。实际
上对于前两种情况,有一种更贴切的表述final的含义的描述,那就是,如果一个变量或方法参数被final修饰,就表示它只能被赋值
一次,但是JAVA虚拟机为变量设定的默认值不记作一次赋值。
被final修饰的变量必须被初始化。初始化的方式有以下几种:
在定义的时候初始化。
final变量可以在初始化块中初始化,不可以在静态初始化块中初始化。
静态final变量可以在静态初始化块中初始化,不可以在初始化块中初始化。
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. }
54. public class FinalTest {
55. // 在定义时初始化
56. public final int A = 10;
57. public final int B;
58. // 在初始化块中初始化
59. {
60. B = 20;
61. }
62. // 非静态final变量不能在静态初始化块中初始化
63. // public final int C;
64. // static {
65. // C = 30;
66. // }
67. // 静态常量,在定义时初始化
68. public static final int STATIC_D = 40;
69. public static final int STATIC_E;
70. // 静态常量,在静态初始化块中初始化
71. static {
72. STATIC_E = 50;
73. }
74. // 静态变量不能在初始化块中初始化
75. // public static final int STATIC_F;
76. // {
77. // STATIC_F = 60;
78. // }
79. public final int G;
80. // 静态final变量不可以在构造器中初始化
81. // public static final int STATIC_H;
82. // 在构造器中初始化
83. public FinalTest() {
84. G = 70;
85. // 静态final变量不可以在构造器中初始化
86. // STATIC_H = 80;
87. // 给final的变量第二次赋值时,编译会报错
88. // A = 99;
89. // STATIC_D = 99;
90. }
91. // final变量未被初始化,编译时就会报错
92. // public final int I;
93. // 静态final变量未被初始化,编译时就会报错
94. // public static final int STATIC_J;
95. }

我们运行上面的代码之后出了可以发现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. }
20. class ParentClass {
21. public final void TestFinal() {
22. System.out.println("父类--这是一个final方法");
23. }
24. }
25. public class SubClass extends ParentClass {
26. /**
27. * 子类无法重写(override)父类的final方法,否则编译时会报错
28. */
29. // public void TestFinal() {
30. // System.out.println("子类--重写final方法");
31. // }
32.
33. public static void main(String[] args) {
34. SubClass sc = new SubClass();
35. sc.TestFinal();
36. }
37. }
38.

这里需要特殊说明的是,具有private访问权限的方法也可以增加final修饰,但是由于子类无法继承private方法,因此也无法重写
它。编译器在处理private方法时,是按照final方法来对待的,这样可以提高该方法被调用时的效率。不过子类仍然可以定义同父类
中的private方法具有同样结构的方法,但是这并不会产生重写的效果,而且它们之间也不存在必然联系。
最后我们再来回顾一下final用于类的情况。这个大家应该也很熟悉了,因为我们最常用的String类就是final的。由于final类不允
许被继承,编译器在处理时把它的所有方法都当作final的,因此final类比普通类拥有更高的效率。而由关键字abstract定义的抽象
类含有必须由继承自它的子类重载实现的抽象方法,因此无法同时用final和abstract来修饰同一个类。同样的道理,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. }
11. public final class FinalTest {
12. int i = 10;
13. public static void main(String[] args) {
14. FinalTest ft = new FinalTest();
15. ft.i = 99;
16. System.out.println(ft.i);
17. }
18. }
19.
运行上面的代码试试看,结果是99,而不是初始化时的10。
finally语句
接下来我们一起回顾一下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. }
12. public final class FinallyTest {
13. public static void main(String[] args) {
14. try {
15. throw new NullPointerException();
16. } catch (NullPointerException e) {
17. System.out.println("程序抛出了异常");
18. } finally {
19. System.out.println("执行了finally语句块");
20. }
21. }
22. }

运行结果说明了finally的作用:
程序抛出了异常
执行了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. }
65. public final class FinallyTest {
66. // 测试return语句
67. public ReturnClass testReturn() {
68. try {
69. return new ReturnClass();
70. } catch (Exception e) {
71. e.printStackTrace();
72. } finally {
73. System.out.println("执行了finally语句");
74. }
75. return null;
76. }
77. // 测试continue语句
78. public void testContinue() {
79. for (int i = 0; i < 3; i++) {
80. try {
81. System.out.println(i);
82. if (i == 1) {
83. continue;
84. }
85. } catch (Exception e) {
86. e.printStackTrace();
87. } finally {
88. System.out.println("执行了finally语句");
89. }
90. }
91. }
92. // 测试break语句
93. public void testBreak() {
94. for (int i = 0; i < 3; i++) {
95. try {
96. System.out.println(i);
97. if (i == 1) {
98. break;
99. }
100. } catch (Exception e) {
101. e.printStackTrace();
102. } finally {
103. System.out.println("执行了finally语句");
104. }
105. }
106. }
107. public static void main(String[] args) {
108. FinallyTest ft = new FinallyTest();
109. // 测试return语句
110. ft.testReturn();
111. System.out.println();
112. // 测试continue语句
113. ft.testContinue();
114. System.out.println();
115. // 测试break语句
116. ft.testBreak();
117. }
118. }
119. class ReturnClass {
120. public ReturnClass() {
121. System.out.println("执行了return语句");
122. }
123. }

上面这段代码的运行结果如下:
执行了return语句
执行了finally语句
0
执行了finally语句
1
执行了finally语句
2
执行了finally语句
0
执行了finally语句
1
执行了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方法
最后,我们再来看看finalize,它是一个方法,属于java.lang.Object类,它的定义如下:
1.        protected void finalize() throws Throwable { }
2. protected void finalize() throws Throwable { }
3.

众所周知,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. }
13. public final class FinallyTest {
14. // 重写finalize()方法
15. protected void finalize() throws Throwable {
16. System.out.println("执行了finalize()方法");
17. }
18. public static void main(String[] args) {
19. FinallyTest ft = new FinallyTest();
20. ft = null;
21. System.gc();
22. }
23. }

运行结果如下:
执行了finalize()方法
程序调用了java.lang.System类的gc()方法,引起GC的执行,GC在清理ft对象时调用了它的finalize()方法,因此才有了上面的输出
结果。调用System.gc()等同于调用下面这行代码:
Java代码
Runtime.getRuntime().gc();
Runtime.getRuntime().gc();
调用它们的作用只是建议垃圾收集器(GC)启动,清理无用的对象释放内存空间,但是GC的启动并不是一定的,这由JAVA虚拟机来决
定。直到JAVA虚拟机停止运行,有些对象的finalize()可能都没有被运行过,那么怎样保证所有对象的这个方法在JAVA虚拟机停止运
行之前一定被调用呢?答案是我们可以调用System类的另一个方法:
1.        public static void runFinalizersOnExit(boolean value) {
2. //other code
3. }

给这个方法传入true就可以保证对象的finalize()方法在JAVA虚拟机停止运行前一定被运行了,不过遗憾的是这个方法是不安全的,
它会导致有用的对象finalize()被误调用,因此已经不被赞成使用了。
由于finalize()属于Object类,因此所有类都有这个方法,Object的任意子类都可以重写(override)该方法,在其中释放系统资源
或者做其它的清理工作,如关闭输入输出流。

(五)——传了值还是传了引用?

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. }
52. public class ParamTest {
53. // 初始值为0
54. protected int num = 0;
55. // 为方法参数重新赋值
56. public void change(int i) {
57. i = 5;
58. }
59. // 为方法参数重新赋值
60. public void change(ParamTest t) {
61. ParamTest tmp = new ParamTest();
62. tmp.num = 9;
63. t = tmp;
64. }
65. // 改变方法参数的值
66. public void add(int i) {
67. i += 10;
68. }
69. // 改变方法参数属性的值
70. public void add(ParamTest pt) {
71. pt.num += 20;
72. }
73. public static void main(String[] args) {
74. ParamTest t = new ParamTest();
75. System.out.println("参数--基本类型");
76. System.out.println("原有的值:" + t.num);
77. // 为基本类型参数重新赋值
78. t.change(t.num);
79. System.out.println("赋值之后:" + t.num);
80. // 为引用型参数重新赋值
81. t.change(t);
82. System.out.println("运算之后:" + t.num);
83. System.out.println();
84. t = new ParamTest();
85. System.out.println("参数--引用类型");
86. System.out.println("原有的值:" + t.num);
87. // 改变基本类型参数的值
88. t.add(t.num);
89. System.out.println("赋引用后:" + t.num);
90. // 改变引用类型参数所指向对象的属性值
91. t.add(t);
92. System.out.println("改属性后:" + t.num);
93. }
94. }

这段代码的运行结果如下:
参数--基本类型
原有的值:0
赋值之后:0
运算之后:0
参数--引用类型
原有的值:0
赋引用后:0
改属性后:20
从上面这个直观的结果中我们很容易得出如下结论:
对于基本类型,在方法体内对方法参数进行重新赋值,并不会改变原有变量的值。
对于引用类型,在方法体内对方法参数进行重新赋予引用,并不会改变原有变量所持有的引用。
方法体内对参数进行运算,不影响原有变量的值。
方法体内对参数所指向对象的属性进行运算,将改变原有变量所指向对象的属性值。
上面总结出来的不过是我们所看到的表面现象。那么,为什么会出现这样的现象呢?这就要说到值传递和引用传递的概念了。这个问题向来是颇有争议的。
大家都知道,在JAVA中变量有以下两种:
基本类型变量,包括char、byte、short、int、long、float、double、boolean。
引用类型变量,包括类、接口、数组(基本类型数组和对象数组)。
当基本类型的变量被当作参数传递给方法时,JAVA虚拟机所做的工作是把这个值拷贝了一份,然后把拷贝后的值传递到了方法的内部。因此在上面的例子中,我们回头来看看这个方法:
1.        public void change(int i) {
2. i = 5;
3. }
4. // 为方法参数重新赋值
5. public void change(int i) {
6. i = 5;
7. }

在这个方法被调用时,变量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. }
31. public class BooleanTest {
32. // 布尔型值
33. boolean bool = true;
34. // 为布尔型参数重新赋值
35. public void change(boolean b) {
36. b = false;
37. }
38. // 对布尔型参数进行运算
39. public void calculate(boolean b) {
40. b = b && false;
41. // 为了方便对比,将运算结果输出
42. System.out.println("b运算后的值:" + b);
43. }
44. public static void main(String[] args) {
45. BooleanTest t = new BooleanTest();
46. System.out.println("参数--布尔型");
47. System.out.println("原有的值:" + t.bool);
48. // 为布尔型参数重新赋值
49. t.change(t.bool);
50. System.out.println("赋值之后:" + t.bool);
51. // 改变布尔型参数的值
52. t.calculate(t.bool);
53. System.out.println("运算之后:" + t.bool);
54. }
55. }

输出结果如下:
参数--布尔型
原有的值:true
赋值之后:true
b运算后的值:false
运算之后:true
那么当引用型变量被当作参数传递给方法时JAVA虚拟机又是怎样处理的呢?同样,它会拷贝一份这个变量所持有的引用,然后把它传递给JAVA虚拟机为方法创建的局部变量,从而这两个变量指向了同一个对象。在篇首所举的示例中,ParamTest类型变量t和局部变量pt在JAVA虚拟机中是以如下的方式存储的:

有一种说法是当一个对象或引用类型变量被当作参数传递时,也是值传递,这个值就是对象的引用,因此JAVA中只有值传递,没有引用传递。还有一种说法是引用可以看作是对象的别名,当对象被当作参数传递给方法时,传递的是对象的引用,因此是引用传递。这两种观点各有支持者,但是前一种观点被绝大多数人所接受,其中有《Core
Java》一书的作者,以及JAVA的创造者James Gosling,而《Thinking in Java》一书的作者Bruce
Eckel则站在了中立的立场上。
我个人认为值传递中的值指的是基本类型的数值,即使对于布尔型,虽然它的表现形式为true和false,但是在栈中,它仍然是以数值形式保存的,即0表示false,其它数值表示true。而引用是我们用来操作对象的工具,它包含了对象在堆中保存地址的信息。即使在被作为参数传递给方法时,实际上传递的是它的拷贝,但那仍是引用。因此,用引用传递来区别与值传递,概念上更加清晰。
最后我们得出如下的结论:
基本类型和基本类型变量被当作参数传递给方法时,是值传递。在方法实体中,无法给原变量重新赋值,也无法改变它的值。
对象和引用型变量被当作参数传递给方法时,在方法实体中,无法给原变量重新赋值,但是可以改变它所指向对象的属性。至于到底它是值传递还是引用传递,这并不重要,重要的是我们要清楚当一个引用被作为参数传递给一个方法时,在这个方法体内会发生什么。

(六)——字符串(String)杂谈

上一次我们已经一起回顾了面试题中常考的到底创建了几个String对象的相关知识,这一次我们以几个常见面试题为引子,来回顾一
下String对象相关的其它一些方面。
String的length()方法和数组的length属性
String类有length()方法吗?数组有length()方法吗?
String类当然有length()方法了,看看String类的源码就知道了,这是这个方法的定义:
1.        public int length() {
2. return count;
3. }

String的长度实际上就是它的属性--char型数组value的长度。数组是没有length()方法的,大家知道,在JAVA中,数组也被作为对
象来处理,它的方法都继承自Object类。数组有一个属性length,这也是它唯一的属性,对于所有类型的数组都是这样。
中文汉字在char中的保存
一个中文汉字能保存在一个char类型里吗?
请看下面的例子:
1.        public class ChineseTest {
2. public static void main(String[] args) {
3. // 将一个中文汉字赋值给一个char变量
4. char a = '中';
5. char b = '文';
6. char c = '测';
7. char d = '试';
8. char e = '成';
9. char f = '功';
10. System.out.print(a);
11. System.out.print(b);
12. System.out.print(c);
13. System.out.print(d);
14. System.out.print(e);
15. System.out.print(f);
16. }
17. }
18. public class ChineseTest {
19. public static void main(String[] args) {
20. // 将一个中文汉字赋值给一个char变量
21. char a = '中';
22. char b = '文';
23. char c = '测';
24. char d = '试';
25. char e = '成';
26. char f = '功';
27. System.out.print(a);
28. System.out.print(b);
29. System.out.print(c);
30. System.out.print(d);
31. System.out.print(e);
32. System.out.print(f);
33. }
34. }

编译没有报错,运行结果:
中文测试成功
答案就不用说了。为什么一个中文汉字可以保存在一个char变量里呢?因为在JAVA中,一个char是2个字节(byte),而一个中文汉
字是一个字符,也是2个字节。而英文字母都是一个字节的,因此它也能保存到一个byte里,一个中文汉字却不能。请看:
1.        public class ChineseTest {
2. public static void main(String[] args) {
3. // 将一个英文字母赋值给一个byte变量
4. byte a = 'a';
5. // 将一个中文汉字赋值给一个byte变量时,编译会报错
6. // byte b = '中';
7.
8. System.out.println("byte a = " + a);
9. // System.out.println("byte b = "+b);
10. }
11. }
12. public class ChineseTest {
13. public static void main(String[] args) {
14. // 将一个英文字母赋值给一个byte变量
15. byte a = 'a';
16. // 将一个中文汉字赋值给一个byte变量时,编译会报错
17. // byte b = '中';
18. System.out.println("byte a = " + a);
19. // System.out.println("byte b = "+b);
20. }
21. }

运行结果:
byte a = 97
正如大家所看到的那样,我们实际上是把字符'a'对应的ASCII码值赋值给了byte型变量a。
让我们回过头来看看最初的例子,能不能将a、b、c、d、e、f拼接在一起一次输出呢?让我们试试看:
1.        public class ChineseTest {
2. public static void main(String[] args) {
3. // 将一个中文汉字赋值给一个char变量
4. char a = '中';
5. char b = '文';
6. char c = '测';
7. char d = '试';
8. char e = '成';
9. char f = '功';
10. System.out.print(a + b + c + d + e + f);
11. }
12. }
13. public class ChineseTest {
14. public static void main(String[] args) {
15. // 将一个中文汉字赋值给一个char变量
16. char a = '中';
17. char b = '文';
18. char c = '测';
19. char d = '试';
20. char e = '成';
21. char f = '功';
22. System.out.print(a + b + c + d + e + f);
23. }
24. }

运行结果:
156035
这显然不是我们想要的结果。只所以会这样是因为我们误用了“+”运算符,当它被用于字符串和字符串之间,或者字符串和其他类
型变量之间时,它产生的效果是字符串的拼接;但当它被用于字符和字符之间时,效果等同于用于数字和数字之间,是一种算术运算
。因此我们得到的“156035”是'中'、'文'、'测'、'试'、'成'、'功'这六个汉字分别对应的数值算术相加后的结果。
字符串的反转输出
这也是面试题中常考的一道。我们就以一个包含了全部26个英文字母,同时又具有完整含义的最短句子作为例子来完成解答。先来看
一下这个句子:

引用
A quick brown fox jumps over the lazy dog.(一只轻巧的棕色狐狸从那条懒狗身上跳了过去。)

最常用的方式就是反向取出每个位置的字符,然后依次将它们输出到控制台:
1.        public class StringReverse {
2. public static void main(String[] args) {
3. // 原始字符串
4. String s = "A quick brown fox jumps over the lazy dog.";
5. System.out.println("原始的字符串:" + s);
6.
7. System.out.print("反转后字符串:");
8. for (int i = s.length(); i > 0; i--) {
9. System.out.print(s.charAt(i - 1));
10. }
11.
12. // 也可以转换成数组后再反转,不过有点多此一举
13. char[] data = s.toCharArray();
14. System.out.println();
15. System.out.print("反转后字符串:");
16. for (int i = data.length; i > 0; i--) {
17. System.out.print(data[i - 1]);
18. }
19. }
20. }
21. public class StringReverse {
22. public static void main(String[] args) {
23. // 原始字符串
24. String s = "A quick brown fox jumps over the lazy dog.";
25. System.out.println("原始的字符串:" + s);
26. System.out.print("反转后字符串:");
27. for (int i = s.length(); i > 0; i--) {
28. System.out.print(s.charAt(i - 1));
29. }
30. // 也可以转换成数组后再反转,不过有点多此一举
31. char[] data = s.toCharArray();
32. System.out.println();
33. System.out.print("反转后字符串:");
34. for (int i = data.length; i > 0; i--) {
35. System.out.print(data[i - 1]);
36. }
37. }
38. }

运行结果:
原始的字符串:A quick brown fox jumps over the lazy dog.
反转后字符串:.god yzal eht revo spmuj xof nworb kciuq A
反转后字符串:.god yzal eht revo spmuj xof nworb kciuq A
以上两种方式虽然常用,但却不是最简单的方式,更简单的是使用现有的方法:
1.        public class StringReverse {
2. public static void main(String[] args) {
3. // 原始字符串
4. String s = "A quick brown fox jumps over the lazy dog.";
5. System.out.println("原始的字符串:" + s);
6.
7. System.out.print("反转后字符串:");
8. StringBuffer buff = new StringBuffer(s);
9. // java.lang.StringBuffer类的reverse()方法可以将字符串反转
10. System.out.println(buff.reverse().toString());
11. }
12. }
13. public class StringReverse {
14. public static void main(String[] args) {
15. // 原始字符串
16. String s = "A quick brown fox jumps over the lazy dog.";
17. System.out.println("原始的字符串:" + s);
18. System.out.print("反转后字符串:");
19. StringBuffer buff = new StringBuffer(s);
20. // java.lang.StringBuffer类的reverse()方法可以将字符串反转
21. System.out.println(buff.reverse().toString());
22. }
23. }

运行结果:
原始的字符串:A quick brown fox jumps over the lazy dog.
反转后字符串:.god yzal eht revo spmuj xof nworb kciuq A

按字节截取含有中文汉字的字符串
要求实现一个按字节截取字符串的方法,比如对于字符串"我ZWR爱JAVA",截取它的前四位字节应该是"我ZW",而不是"我ZWR",同时
要保证不会出现截取了半个汉字的情况。
英文字母和中文汉字在不同的编码格式下,所占用的字节数也是不同的,我们可以通过下面的例子来看看在一些常见的编码格式下,
一个英文字母和一个中文汉字分别占用多少字节。
1.        import java.io.UnsupportedEncodingException;
2.
3. public class EncodeTest {
4. /**
5. * 打印字符串在指定编码下的字节数和编码名称到控制台
6. *
7. * @param s
8. * 字符串
9. * @param encodingName
10. * 编码格式
11. */
12. public static void printByteLength(String s, String
encodingName) {
13. System.out.print("字节数:");
14. try {
15. System.out.print(s.getBytes(encodingName).length);
16. } catch (UnsupportedEncodingException e) {
17. e.printStackTrace();
18. }
19. System.out.println(";编码:" + encodingName);
20. }
21.
22. public static void main(String[] args) {
23. String en = "A";
24. String ch = "人";
25.
26. // 计算一个英文字母在各种编码下的字节数
27. System.out.println("英文字母:" + en);
28. EncodeTest.printByteLength(en, "GB2312");
29. EncodeTest.printByteLength(en, "GBK");
30. EncodeTest.printByteLength(en, "GB18030");
31. EncodeTest.printByteLength(en, "ISO-8859-1");
32. EncodeTest.printByteLength(en, "UTF-8");
33. EncodeTest.printByteLength(en, "UTF-16");
34. EncodeTest.printByteLength(en, "UTF-16BE");
35. EncodeTest.printByteLength(en, "UTF-16LE");
36.
37. System.out.println();
38.
39. // 计算一个中文汉字在各种编码下的字节数
40. System.out.println("中文汉字:" + ch);
41. EncodeTest.printByteLength(ch, "GB2312");
42. EncodeTest.printByteLength(ch, "GBK");
43. EncodeTest.printByteLength(ch, "GB18030");
44. EncodeTest.printByteLength(ch, "ISO-8859-1");
45. EncodeTest.printByteLength(ch, "UTF-8");
46. EncodeTest.printByteLength(ch, "UTF-16");
47. EncodeTest.printByteLength(ch, "UTF-16BE");
48. EncodeTest.printByteLength(ch, "UTF-16LE");
49. }
50. }
51. import java.io.UnsupportedEncodingException;
52. public class EncodeTest {
53. /**
54. * 打印字符串在指定编码下的字节数和编码名称到控制台
55. *
56. * @param s
57. * 字符串
58. * @param encodingName
59. * 编码格式
60. */
61. public static void printByteLength(String s, String encodingName) {
62. System.out.print("字节数:");
63. try {
64. System.out.print(s.getBytes(encodingName).length);
65. } catch (UnsupportedEncodingException e) {
66. e.printStackTrace();
67. }
68. System.out.println(";编码:" + encodingName);
69. }
70. public static void main(String[] args) {
71. String en = "A";
72. String ch = "人";
73. // 计算一个英文字母在各种编码下的字节数
74. System.out.println("英文字母:" + en);
75. EncodeTest.printByteLength(en, "GB2312");
76. EncodeTest.printByteLength(en, "GBK");
77. EncodeTest.printByteLength(en, "GB18030");
78. EncodeTest.printByteLength(en, "ISO-8859-1");
79. EncodeTest.printByteLength(en, "UTF-8");
80. EncodeTest.printByteLength(en, "UTF-16");
81. EncodeTest.printByteLength(en, "UTF-16BE");
82. EncodeTest.printByteLength(en, "UTF-16LE");
83. System.out.println();
84. // 计算一个中文汉字在各种编码下的字节数
85. System.out.println("中文汉字:" + ch);
86. EncodeTest.printByteLength(ch, "GB2312");
87. EncodeTest.printByteLength(ch, "GBK");
88. EncodeTest.printByteLength(ch, "GB18030");
89. EncodeTest.printByteLength(ch, "ISO-8859-1");
90. EncodeTest.printByteLength(ch, "UTF-8");
91. EncodeTest.printByteLength(ch, "UTF-16");
92. EncodeTest.printByteLength(ch, "UTF-16BE");
93. EncodeTest.printByteLength(ch, "UTF-16LE");
94. }
95. }

运行结果如下:
英文字母:A
字节数:1;编码:GB2312
字节数:1;编码:GBK
字节数:1;编码:GB18030
字节数:1;编码:ISO-8859-1
字节数:1;编码:UTF-8
字节数:4;编码:UTF-16
字节数:2;编码:UTF-16BE
字节数:2;编码:UTF-16LE
中文汉字:人
字节数:2;编码:GB2312
字节数:2;编码:GBK
字节数:2;编码:GB18030
字节数:1;编码:ISO-8859-1
字节数:3;编码:UTF-8
字节数:4;编码:UTF-16
字节数:2;编码:UTF-16BE
字节数:2;编码:UTF-16LE

UTF-16BE和UTF-16LE是UNICODE编码家族的两个成员。UNICODE标准定义了UTF-8、UTF-16、UTF-32三种编码格式,共有UTF-8、UTF-16
、UTF-16BE、UTF-16LE、UTF-32、UTF-32BE、UTF-32LE七种编码方案。JAVA所采用的编码方案是UTF-16BE。从上例的运行结果中我们
可以看出,GB2312、GBK、GB18030三种编码格式都可以满足题目的要求。下面我们就以GBK编码为例来进行解答。
如果我们直接按照字节截取会出现什么情况呢?我们来测试一下:
1.        import java.io.UnsupportedEncodingException;
2.
3. public class CutString {
4. public static void main(String[] args) throws
UnsupportedEncodingException {
5. String s = "我ZWR爱JAVA";
6. // 获取GBK编码下的字节数据
7. byte[] data = s.getBytes("GBK");
8. byte[] tmp = new byte[6];
9. // 将data数组的前六个字节拷贝到tmp数组中
10. System.arraycopy(data, 0, tmp, 0, 6);
11. // 将截取到的前六个字节以字符串形式输出到控制台
12. s = new String(tmp);
13. System.out.println(s);
14. }
15. }
16. import java.io.UnsupportedEncodingException;
17.
18. public class CutString {
19. public static void main(String[] args) throws
UnsupportedEncodingException {
20. String s = "我ZWR爱JAVA";
21. // 获取GBK编码下的字节数据
22. byte[] data = s.getBytes("GBK");
23. byte[] tmp = new byte[6];
24. // 将data数组的前六个字节拷贝到tmp数组中
25. System.arraycopy(data, 0, tmp, 0, 6);
26. // 将截取到的前六个字节以字符串形式输出到控制台
27. s = new String(tmp);
28. System.out.println(s);
29. }
30. }
31.

输出结果:
我ZWR?
在截取前六个字节时,第二个汉字“爱”被截取了一半,导致它无法正常显示了,这样显然是有问题的。
我们不能直接使用String类的substring(int beginIndex, int
endIndex)方法,因为它是按字符截取的。'我'和'Z'都被作为一个字
符来看待,length都是1。实际上我们只要能区分开中文汉字和英文字母,这个问题就迎刃而解了,而它们的区别就是,中文汉字是
两个字节,英文字母是一个字节。
1.        import java.io.UnsupportedEncodingException;
2.
3. public class CutString {
4.
5. /**
6. * 判断是否是一个中文汉字
7. *
8. * @param c
9. * 字符
10. * @return true表示是中文汉字,false表示是英文字母
11. * @throws UnsupportedEncodingException
12. * 使用了JAVA不支持的编码格式
13. */
14. public static boolean isChineseChar(char c)
15. throws UnsupportedEncodingException {
16. // 如果字节数大于1,是汉字
17. // 以这种方式区别英文字母和中文汉字并不是十分严谨,但在这个题目中,这样判断已经足够了
18. return String.valueOf(c).getBytes("GBK").length > 1;
19. }
20.
21. /**
22. * 按字节截取字符串
23. *
24. * @param orignal
25. * 原始字符串
26. * @param count
27. * 截取位数
28. * @return 截取后的字符串
29. * @throws UnsupportedEncodingException
30. * 使用了JAVA不支持的编码格式
31. */
32. public static String substring(String orignal, int count)
33. throws UnsupportedEncodingException {
34. // 原始字符不为null,也不是空字符串
35. if (orignal != null && !"".equals(orignal)) {
36. // 将原始字符串转换为GBK编码格式
37. orignal = new String(orignal.getBytes(), "GBK");
38. // 要截取的字节数大于0,且小于原始字符串的字节数
39. if (count > 0 && count <
orignal.getBytes("GBK").length) {
40. StringBuffer buff = new StringBuffer();
41. char c;
42. for (int i = 0; i < count; i++) {
43. // charAt(int index)也是按照字符来分解字符串的
44. c = orignal.charAt(i);
45. buff.append(c);
46. if (CutString.isChineseChar(c)) {
47. // 遇到中文汉字,截取字节总数减1
48. --count;
49. }
50. }
51. return buff.toString();
52. }
53. }
54. return orignal;
55. }
56.
57. public static void main(String[] args) {
58. // 原始字符串
59. String s = "我ZWR爱JAVA";
60. System.out.println("原始字符串:" + s);
61. try {
62. System.out.println("截取前1位:" +
CutString.substring(s, 1));
63. System.out.println("截取前2位:" +
CutString.substring(s, 2));
64. System.out.println("截取前4位:" +
CutString.substring(s, 4));
65. System.out.println("截取前6位:" +
CutString.substring(s, 6));
66. } catch (UnsupportedEncodingException e) {
67. e.printStackTrace();
68. }
69. }
70. }
71. import java.io.UnsupportedEncodingException;
72. public class CutString {
73. /**
74. * 判断是否是一个中文汉字
75. *
76. * @param c
77. * 字符
78. * @return true表示是中文汉字,false表示是英文字母
79. * @throws UnsupportedEncodingException
80. * 使用了JAVA不支持的编码格式
81. */
82. public static boolean isChineseChar(char c)
83. throws UnsupportedEncodingException {
84. // 如果字节数大于1,是汉字
85. // 以这种方式区别英文字母和中文汉字并不是十分严谨,但在这个题目中,这样判断已经足够了
86. return String.valueOf(c).getBytes("GBK").length > 1;
87. }
88. /**
89. * 按字节截取字符串
90. *
91. * @param orignal
92. * 原始字符串
93. * @param count
94. * 截取位数
95. * @return 截取后的字符串
96. * @throws UnsupportedEncodingException
97. * 使用了JAVA不支持的编码格式
98. */
99. public static String substring(String orignal, int count)
100. throws UnsupportedEncodingException {
101. // 原始字符不为null,也不是空字符串
102. if (orignal != null && !"".equals(orignal)) {
103. // 将原始字符串转换为GBK编码格式
104. orignal = new String(orignal.getBytes(), "GBK");
105. // 要截取的字节数大于0,且小于原始字符串的字节数
106. if (count > 0 && count < orignal.getBytes("GBK").length) {
107. StringBuffer buff = new StringBuffer();
108. char c;
109. for (int i = 0; i < count; i++) {
110. // charAt(int index)也是按照字符来分解字符串的
111. c = orignal.charAt(i);
112. buff.append(c);
113. if (CutString.isChineseChar(c)) {
114. // 遇到中文汉字,截取字节总数减1
115. --count;
116. }
117. }
118. return buff.toString();
119. }
120. }
121. return orignal;
122. }
123. public static void main(String[] args) {
124. // 原始字符串
125. String s = "我ZWR爱JAVA";
126. System.out.println("原始字符串:" + s);
127. try {
128. System.out.println("截取前1位:" + CutString.substring(s, 1));
129. System.out.println("截取前2位:" + CutString.substring(s, 2));
130. System.out.println("截取前4位:" + CutString.substring(s, 4));
131. System.out.println("截取前6位:" + CutString.substring(s, 6));
132. } catch (UnsupportedEncodingException e) {
133. e.printStackTrace();
134. }
135. }
136. }

运行结果:
原始字符串:我ZWR爱JAVA
截取前1位:我
截取前2位:我
截取前4位:我ZW
截取前6位:我ZWR爱
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值