Java 基础:变量 与 字符串
变量
Java中没有初始化的变量是不能直接使用的
局部变量
String msg;
System.out.print(msg);
就会提示错误,我们必须显式的为变量指定一个初值如null。刚开始学Java的时候写出过这样的代码:
Scanner scan = new Scanner(System.in);
String msg;
int x = scan.nextInt();
if (x > 0) {
msg = "positive";
} else if (x < 0) {
msg = "negative";
} else if (x == 0) {
msg = "zero";
}
System.out.print(msg);
乍一看没什么问题,但这个片段放到一般的main方法里是编译不过的。会提示变量msg
可能未初始化。虽然我们在if-else语句中已经把可能x取值都考虑完整了,但是编译器不能进行这样一个智能的检测,如果没有最后的else字句,它任务这个if-else语句中的所有字句都可能不会进入。这样的情况下有没有额外的语句对msg进行初始化,那么在最后输出的时候msg可能就是处于未初始化的状态。
类成员变量
类成员变量和局部变量不同,它们可以不进行显式的初始化而直接使用。因为类实例创建时首先会进行一个零值初始化(数值变量都为0值,引用变量都为null,boolean为false)。因而可以像如下这样使用:
class Box {
public int width;
public int height;
public String name;
public Box() {}
public Box(String name) {this.name = name}
public static void main(String[] args) {
Box b = new Box();
System.out.println(b.width);
System.out.println(b.height);
System.out.println(b.name);
}
}
以上会输出:
0
0
null
成员变量初始化顺序
初始化顺序以声明顺序为准,构造函数后于(不论是实例还是静态的)初始化块执行。基类初始化先于子类进行。一般来说按照这样的规则分析都可以顺利的推断出成员变量初始化的结果。但是有一些奇葩的情况需要注意,如初始化块中对后面才声明的变量进行赋值操作
public class InitVar {
{
x = 3;
}
int x = 2;
public static void main(String[] args) {
InitVar v = new InitVar();
System.out.println(v.x);
}
}
上例中会输出:
2
当我们调换初始化块和声明语句的位置时输出为3
,不过需要注意当初始化块中使用的变量比声明要靠前时,只能对其进行赋值操作而不能进行变量值读取操作。如下情况是不允许的:
public class InitVar {
{
System.out.println(x);
}
int x = 2;
public static void main(String[] args) { }
}
将会报
InitVar.java:5: error: illegal forward reference
System.out.println(x);
^
这样约束是为了防止循环初始化即读取b的值来初始化a但是b又在a后才声明。而声明b的地方又用b来初始化a。等同于下面的情况:
public class InitVar {
int x = y;
int y = x;
public static void main(String[] args) {}
}
字符串
字符串初始化可以使用字面常量形式直接初始化,也可以使用new搞出一个对象来
String a = "a word";
String b = new String("b word");
常量池
字符串变量一个很特殊的地方就是它的字面常量一般会进入常量池中。那么它们是什么时候进入常量池的呢?并不是在执行那条语句的时候,而是包含该语句(含有字面字符串常量)的类文件加载的时候就已经加入常量池了。这些常量在Java类文件中有专门的区段进行存储,可以通过命令行javap -v进行查看(编译后的.class文件),其中的Constant Pool一段就是其中包含的常量值。比如如下的Java代码
public class StringConstant {
public static void main(String[] args) {
String a = "a word";
String b = new String("b word");
System.out.println(a);
System.out.println(b);
}
}
编译后执行javap -v StringConstant.class查看其类文件内容截取如下
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = String #19 // a word
#3 = Class #20 // java/lang/String
#4 = String #21 // b word
main方法的字节码如下:
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String a word
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #4 // String b word
9: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_1
17: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_2
24: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
ldc表示从常量池中取出常量引用放入操作数栈顶,astore_1表示把当前操作数栈顶的值取出并存入本地变量区域的1号位置中,即完成了String a = "a word"
的执行过程。后续的语句是用于new出一个初始的String对象,然后把常量池中的字符串作为构造函数的参数调用构造函数对初始的String对象进行构造。由于invokespecial(用于调用构造函数)和astore_2都要消耗操作数栈上的String对象引用值,因此在new出String对象后调用了dup指令复制了一份在操作数栈上。
由此可见字符常量并非在new时才加入常量池中,而是在类加载时(不过类具有延时加载机制,可能要到第一次真正使用类时才会去解析类文件中的常量并加入常量池)。
String.intern()方法
String 的intern
方法可以手工的把程序中通过拼接得到的字符串加入常量池(直接使用常量初始化或者常量定义的话该字面常量就已经存在于常量池中了)。那么String.intern()方法除了炫技之外有什么其他用途呢?应该是用在可能会产生大量重复字符串对象且这些对象还会长期存在的情况下,比如要在内存中记录100w册的图书以供长期查询,然后其中的出版社名称是通过某种方式动态提取生成(比如从其他的RPC接口反序列化得到的),那么虽然有许多出版社名称是一样的对于hashmap之类的使用不造成丝毫影响,但是反序列化时都是动态new出String对象,就造成了资源的浪费(原本可以使用常量池的一个对象,现在有许多重复对象)。此时应该使用字符串的intern方法进行检测,使用一致的字符串对象。
当然一般常量池所在的空间都比较小,如果大量对一些短生命周期的字符串使用intern操作是不明智的。
另外intern方法在1.7中与1.6中过程是不同的,1.7会将调用intern的对象引用放入常量池(如果当前没有),而1.6则会复制一份并将其拷贝的引用放入(参见深入理解JVM)。
类间常量引用
当一个类引用另一个类中的常量时会把常量值之间复制过来,当做一种优化手段这在C++里也是存在的。如果A依赖B,而B修改了常量值,A没有进行更新编译那么它使用的任然是老的。可以通过如下代码进行说明,设有两个文件Box.java和Limits.java,都在默认包下
Limits.java
public class Limits {
public static final int MAX_WIDTH = 10000;
}
Box.java
public class Box {
public static void main(String[] args) {
int width = 1000;
if (width < Limits.MAX_WIDTH) {
System.out.println("valid");
} else {
System.out.println("invalid");
}
}
}
那么当第一次Limits.java、Box.java编译后,如果再仅仅修改Limits.java调整MAX_WIDTH的值然后运行java Box并不能使结果有任何改变。使用javap看Box.class文件即可,其中100就是原来Limits.MAX_WIDTH的值,以直接量的形式包含在了代码中。
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: sipush 1000
3: istore_1
4: iload_1
5: bipush 100
7: if_icmpge 21
...
虽然当跟新一些部署应用时需要要考虑到,有可能仅仅更新依赖jar主程序中的常量并没有改变,必须重新编译主程序。