s+=" world";
System.out.println(s);
}
}
复制代码
实际上,s+=" world"并没有改变s所指向的对象,而是指向了另一个String类型的对象,该对象的内容为"Hello world"。原来的字符串常量"Hello"还存在内存中,并没有改变。
通常来讲,要创建一个不可变类需要遵循下面五条原则:
-
类中所有的成员变量被private修饰。
-
类中没有写或者修改成员变量的方法,例如:setxxx。只提供构造函数,一次生成,永不改变。
-
确保类中的所有方法不会被子类覆盖,可以通过把类定义为final或者把类中的方法定义为final来达到这个目的。
-
如果一个类成员不是不可变量,那么在成员初始化或者使用get方法获取该成员变量是需要通过clone方法,来确保类的不可变性。
-
如果有必要,可以通过覆盖Object类的equals()方法和hashCode()方法。
由于类的不可变性,在创建对象的时候就需要初始化所有的成员变量,因此最好提供一个带参数的构造函数来初始化这些成员变量。
一个错误示范:
public class ImmutableClass {
private Date d;
public ImmutableClass(Date d){
this.d = d;
}
public void printState(){
System.out.println(d);
}
}
public class Test {
public static void main(String[] args) {
Date d = new Date();
ImmutableClass immu = new ImmutableClass(d);
immu.printState();
d.setMonth(5);
immu.printState();
}
}
复制代码
由于Date的对象的状态是可以被改变的,而ImmutableClass保存了Date类型对象的引用,当被引用的对象的状态改变的时候会导致ImmutableClass对象状态的改变。
正确的实现方式为:
public class ImmutableClass {
private Date d;
public ImmutableClass(Date d){
// this.d = d;
this.d = (Date)d.clone();// 解除了引用关系
}
public void printState(){
System.out.println(d);
}
}
public class Test {
public static void main(String[] args) {
Date d = new Date();
ImmutableClass immu = new ImmutableClass(d);
immu.printState();
d.setMonth(5);
immu.printState();
}
}
复制代码
不可变类的优缺点:
-
优:不可变类具有使用简单、线程安全、节省内存等优点
-
缺:不可变的对象会因为值的不同而产生新的对象,导致无法预料的问题
Question:
对于一些敏感的数据(例如密码),为什么使用字符串数组存储比使用String安全?
在Java中,String是不可变类,被存储在常量字符串池中,从而实现了字符串的共享,减少了内存开支。正因为如此,一旦一个String类型的字符串被创建出来,这个字符串就会存在于常量池中,知道被垃圾回收器回收为止。
因此,即使这个字符串(比如密码)不再被使用,仍然会在内存中存在一段时间(只有垃圾回收器才会回收这块内容,程序员无法直接回收字符串)。此时有权限访问memory dump(存储器转储)的程序都可能会访问到这个字符串,从而把敏感的数据暴露出去,这是一个非常大的安全隐患。
如果使用字符数组,一旦程序不再使用这个数据,程序员可以把字符数组的内容设置为空,此时这个数据在内存中就不存在。
也就是说,跟String相比,使用字符数组,程序员对数据的生命周期有更好的控制,增强安全性。
1.2 “= =”、equals和hashcode
1.2.1 “= =”
“==”运算符用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,
-
要比较两个基本类型的数据或两个引用变量是否相等,只能使用“==”运算符.
-
要比较两个引用变量是否指向同一块内存,可以使用“= =”
-
要比较两个引用变量的对象内容是否相等,无法使用"= ="
1.2.2 equals
Object类中定义的equals(Object)方法是直接使用“”运算符比较的两个对象,所以在没有覆盖equals(Object)方法的情况下,equals(Object)与“”运算符一样,比较的是引用。
相比“==”运算符,因为equals(Object)方法的特殊之处就在于它可以被覆盖,所以可以通过覆盖的方法让它不是比较引用而是比较数据内容。
1.2.3 hashCode
Object类中的hashCode()方法返回对象在内存中地址转换成的一个int值,所以如果没有重写hashCode()方法,那么任何对象的hashCode()方法都是不相等的。
equals方法和hashCode方法的区别?
-
一般来讲,equals方法是给用户调用的,如果需要判断两个对象是否相等,可以重写equals方法,然后在代码中调用,即可判断是否相等
-
对于hashCode()方法,用户一般不会去调用它。多用在hashmap、hashset等需要判断元素是否重复的地方。
一般在覆盖equals方法的同时也要覆盖hashCode()方法,否则会违反Object.hashCode的通用约定,导致该类无法与所有基于hash值的集合类结合正常运行。
1.3 值传递和引用传递
按值传递指的是在方法调用时,传递的参数是实参值的拷贝。按引用传递指的是在方法调用时,传递的参数是实参的引用,也可以理解为实参所对应的内存空间的地址。
public class Test {
public static void testPassParameter(StringBuffer s1,int n){
s1.append(" World");
n = 8;
}
public static void main(String[] args) {
StringBuffer sb = new StringBuffer(“Hello”);
int n = 1;
testPassParameter(sb,n);
System.out.println(sb);
System.out.println(n);
}
}
复制代码
从运行结果看,int作为参数的时候,对形参的修改不会影响实参,对于StringBuffer类型的参数,对形参的修改影响到了实参。可以理解为:基本类型的参数时按值传递,引用类型的参数时引用传递。
实际上,Java语言中的引用传递还是值传递(传递的是地址的值)。
1.4 Java关键字
1.4.1 static
static关键字主要有两个作用:
1、为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。
2、实现某个方法或属性与类而不是对象关联在一起。
也就是说,在不创建对象的情况下就可以通过类来直接使用类的方法或者属性。
static可修饰的元素
-
变量:静态变量,可以跨越代码块访问
-
方法:静态方法,可以跨越代码块访问
-
代码块:静态代码块,只能定义在类定义下,在类被加载时执行
-
内部类:静态内部类,该类定义可以有外部类名引用
-
导入包:静态导入包,导入指定的static变量
加载时机
static,静态,表示随着类的加载而加载,不会重复加载,执行顺序在main方法执行。在JVM内存中,static修饰的变量存在于方法区中。
1.4.2 final
final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承(不能再派生处新的子类)。
- final属性:被final修饰的变量不可变,不可变又两重含义,一是引用不可变,二是对象不可变。final指的是哪种含义?
public class Test {
public static void main(String[] args) {
final StringBuffer sb = new StringBuffer(“Hello”);
sb = new StringBuffer(“Hello World!”);
}
}
复制代码
编译期间错误
public class Test {
public static void main(String[] args) {
final StringBuffer sb = new StringBuffer(“Hello”);
sb.append(" World!");
System.out.println(sb);