学习内容来自于mooc中华东师范大学课程Java核心技术。
常量设计和常量池:
常量设计
常量:不会修改的变量
但是Java没有关键字constant
按照之前关键词对变量的修饰:
- 不能修改,final
- 不会修改/只读/只要一份,static
- 方便访问,public
结合一下,Java常量的关键词即为 public static final 或 public final static。
这样就保证了定义的变量只有一份且不会被修改。
设计常量时,变量名字建议全大写,并用连字符( _ )相连。如UPPER_BOUND。
public class Constants {
public final static double PI_NUMBER = 3.1415926;
public static final String DEFAULT_COUNTRY = "China";
public static void main(String[] args) {
System.out.println(Constants.PI_NUMBER); // 也属于静态变量
System.out.println(Constants.DEFAULT_COUNTRY);
}
}
还有一种特殊情况,接口中定义的变量默认都是常量。
哪怕没有用public static final修饰,也会默认认为如此。
而且如果给接口中定义的变量加关键字private,会出现编译无法通过的错误。
并且如果实现了接口的类尝试修改常量,也会报错。
The final field Animal.color cannot be assigned
public interface Animal {
String color = "blue";
public void eat();
public void move();
}
public class Cat implements Animal {
public void eat() {
System.out.println("Cat can eat!");
}
public void move() {
System.out.println("Cat can move!");
}
public static void main(String[] args) {
Cat cat = new Cat();
cat.color = "White"; // error,The final field Animal.color cannot be assigned
}
}
常量池
先看一个例子:
public class IntegerTest {
public static void main(String[] args) {
Integer n1 = 127;
Integer n2 = 127;
System.out.println(n1 == n2);
Integer n3 = 128;
Integer n4 = 128;
System.out.println(n3 == n4);
Integer n5 = new Integer(127);
System.out.println(n1 == n5);
}
}
输出结果:
true
false
false
这是为什么呢?
Java为很多基本类型的包装类都建立了常量池。
常量池:相同的值只存储一份,节省内存,共享访问。
基本类型的包装类与其对应的常量池为:
基本类型 | 包装类 | 常量池 |
---|---|---|
bool | Boolean | true/false |
byte | Byte | -128-127 |
short | Short | -128-127 |
int | Integer | -128-127 |
long | Long | -128-127 |
char | Character | 0-127 |
float | Float | 无 |
double | Double | 无 |
所以,刚刚的例子中n1和n2指向了常量池中的同一个对象。而n3和n4申请的值不在常量池中,也就是各自有一个Integer类的对象。同理,n5是new了一个对象,不适用常量池中中的对象。
public class CacheTest {
public static void main(String[] args) {
Boolean b1 = true;
Boolean b2 = true;
System.out.println("Boolean Test: " + String.valueOf(b1 == b2));
Byte b3 = 127;
Byte b4 = 127;
System.out.println("Byte Test: " + String.valueOf(b3 == b4));
Character c1 = 127;
Character c2 = 127;
System.out.println("Character Test: " + String.valueOf(c1 == c2));
Short s1 = -128;
Short s2 = -128;
System.out.println("Short Test: " + String.valueOf(s1 == s2));
Long l1 = -128L;
Long l2 = -128L;
System.out.println("Long Test: " + String.valueOf(l1 == l2));
Float f1 = 0.5f;
Float f2 = 0.5f;
System.out.println("Float Test: " + String.valueOf(f1 == f2));
}
}
输出结果:
Boolean Test: true
Byte Test: true
Character Test: true
Short Test: true
Long Test: true
Float Test: false
字符串常量都建立了常量池缓存机制
String s1 = "abc";
String s2 = "abc";
String s3 = "ab" + "c"; // 都是常量,编译器会优化
String s4 = "a" + "b" + "c";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1 == s4);
输出结果都为true。
常量池的作用很明显,就是为了节约内存。
基本类型的包装类和字符串有两种创建方式:
- 常量式创建,放在栈内存,将被常量化。如 String s1 = “abc”;
- new对象进行创建,放在堆内存,不会常量化。如 Intege n = new Integer(10);
这两种创建方式创建的对象存放的位置不同。
栈内存容量小但是速度快,堆内存容量大但是速度慢。
看下面两个例子。
public static void main(String[] args) {
int i1 = 10;
Integer i2 = 10; // 10装入常量池,i2指向它
System.out.println(i1 == i2); // true
// 基本类型和包装类进行比较,包装类自动拆箱变为基本类型
Integer i3 = new Integer(10);
System.out.println(i1 == i3); // true
// 基本类型和包装类进行比较,包装类自动拆箱变为基本类型
System.out.println(i2 == i3); // false
// 对象比较,比较其地址
// i2在常量池,存在栈内存中;i3是new出的对象,存在堆内存中
Integer i4 = new Integer(5);
Integer i5 = new Integer(5);
System.out.println(i1 == (i4+i5)); // true
System.out.println(i2 == (i4+i5)); // true
System.out.println(i3 == (i4+i5)); // true
// + 操作将会使i4+i5自动拆箱为基本类型并得出结果10
// 基本类型10和对象比较,会使对象自动拆箱为基本类型
System.out.println(i4 == i5); // false
// 堆内存中的两个不同对象
Integer i6 = i4 + i5;
// + 操作会使i4+i5自动拆箱为基本类型并得出结果10,之后该对象指向常量池中的10
System.out.println(i1 == i6); // true
// 基本类型和包装类进行比较,包装类自动拆箱变为基本类型
System.out.println(i2 == i6); // true
// 都指向常量池
System.out.println(i3 == i6); // false
// i6在常量池,存在栈内存中;i3是new出的对象,存在堆内存中
}
String s0 = "abcdef";
String s1 = "def";
String s2 = "abc" + s1; // 涉及到变量,编译器不优化
String s3 = "abc" + "def"; // 都是常量,编译器优化成abcdef
String s4 = "abc" + new String ("def"); // 涉及到new对象,编译器不优化
System.out.println(s0 == s3); // true
System.out.println(s2 == s3); // false
System.out.println(s2 == s4); // false
System.out.println(s3 == s4); // false
不可变对象和字符串:
不可变对象
不可变对象
- 一旦创建,这个对象(状态、值)不能被更改了
- 其内在成员变量的值不能修改
- 包含八个基本的包装类,以及String、BigInteger和BigDecimal
例子:
String a = new String("abc");
String b = a;
System.out.println(b); // abc
a = "def";
System.out.println(b); // abc
输出的两次b都为 abc。
要注意,String是一个不可变对象,它定义的字符串是不可被修改的。
所以,第一句话执行时,在内存中申请了一个值为 abc 的空间(堆内存),对象a指向这块内存。
第二句话执行时,创建了一个对象b指向a指向的空间。
第四句话执行时,在内存中申请了一个值为 def 的空间(常量池,栈内存),对象a更改指向到这块内存,而对象b保持不变。
还有一个例子:
public static void change(String b){
b = "def";
}
a = new String("abc");
change(a);
System.out.println(a); // abc
在这个例子中,a首先在内存中申请了一个值为 abc 的空间(堆内存),即对象a指向这块内存。
进入change方法后,实参a把自己的指针传给了形参b,之后对象b在内存中申请了一个值为 def 的空间(常量池,栈内存),并指向这块内存。但是a仍然指向原来的空间。
显然,不可变对象不可改变,如果需要新的内容,只能clone或者new一个对象进行修改。
可以通过以下方法保证这个不可变性:
- 所有的属性都是final和private的
- 不提供setter方法
- 类是final的,或者所有方法都是final的
不可变对象的优点:
- 只读,线程安全
- 并发读,提高性能
- 可以重复使用
缺点:
- 制造垃圾,浪费空间
因为相对不可变对象进行修改,就会开辟新的空间,旧的对象会被搁置,直到垃圾回收。
Java字符串
字符串使用频率极高,是一种典型的不可变对象。
之所以是不可变的,可以通过 String 类的底层源码看出:
- 在源码中,String 类的字符串内容存储在常量数组
private final char value[]
,因为关键字 final 的原因,该数组不可被更改 - 不过这个不可修改指的是字符数组 value 不可以指向新的地址,但是单个字符内容是可以变化的
String定义方法一般有两种:
- String a = “abc”;
- String b = new String(“def”);
字符串内容比较:equals方法。
是否指向同一个对象:指针比较 =。
由于String不可修改,因而Java有提供其他的字符串类。
StringBuffer/StringBuilder可修改字符串类,可以通过append方法进行修改。
StringBuffer和StringBuilder的对象都是可变对象。
- StringBuffer,同步、线程安全、修改快速
- StringBuilder,不同步、线程不安全、修改极快
看下面的例子:
public static void main(String[] args){
int n = 50000;
Calendar t1 = Calendar.getInstance();
String a = new String("");
for (int i = 0; i < n; i++)
a = a + i + ",";
System.out.println(Calendar.getInstance().getTimeInMillis() - t1.getTimeInMillis());
Calendar t2 = Calendar.getInstance();
StringBuffer b = new StringBuffer("");
for (int i = 0; i < n; i++){
b.append(i);
b.append(",");
}
System.out.println(Calendar.getInstance().getTimeInMillis() - t2.getTimeInMillis());
Calendar t3 = Calendar.getInstance();
StringBuilder c = new StringBuilder("");
for (int i = 0; i < n; i++){
c.append(i);
c.append(",");
}
System.out.println(Calendar.getInstance().getTimeInMillis() - t3.getTimeInMillis());
}
输出结果:
4154
18
5
可以看出它们之间效率的差距。
所以,如果程序中有大量的字符串加减操作,建议使用StringBuffer/StringBuilder类。
public static void changeValue(int a) {
a = 10;
}
public static void changeValue(String s1) {
s1 = "def";
}
public static void changeValue(StringBuffer s2) {
s2.append("def");
}
public static void changeValue(StringBuilder s1) {
s1.append("def");
}
public static void main(String[] args) {
int a = 5;
String b = "abc";
StringBuffer c = new StringBuffer("abc");
changeValue(a);
changeValue(b);
changeValue(c);
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
输出结果:
5
abc
abcdef
可见c最后变为abcdef,因为方法调用时c把指针传给s2,这是一个可变对象,然后直接在对象内容上进行了修改,所以a指向的内容相应也修改。