JVM分代收集理论与三种标记算法

分代收集理论与三种标记算法

什么是分代收集理论

目前大部分的JVM,在针对对象进行垃圾收集时,会将对象熬过垃圾收集的次数,视为对象的年龄。依此将对象至少划分为新生代和老年代这两个代。

分代收集理论的理论基础

分代收集理论基于以下三种假说和经验法则。

弱分代假说

绝大多数对象,在第一次垃圾收集时就会被回收,按照经验法则,这个值高达百分之九十八。

强分代假说

熬过越多次收集过程的对象越难以消亡。

跨代引用假说

该假说认为只会存在很少的跨代引用。因为只要经过一些次数的垃圾收集,即使还存在跨代引用,新生代会变成老年代,跨代引用也就自然消失了,所以跨代引用的数量不会多。在对新生代对象进行收集时,由于可能存在老年代对象引用了该对象,那么,需要找到这些老年代对象。根据跨代引用假说,这些跨代引用的数量不会太多,相比于对老年代进行扫描,在新生代建立一个全局数据结构,记录哪一块老年代内存会存在跨代引用,虽然维护这个数据结构,也需要少量的开销。但仍然显得更加合算。

根据分代理论,新生代的对象很大概率一次垃圾收集就会被回收,而老年代中的对象在下一次垃圾收集被回收的概率较小,正是由于这个区别,新生代和老年代的垃圾收集会使用不同的垃圾收集算法和垃圾收集器。

针对各分代收集的名称

Java堆分为新生代和老年代,针对收集对象处于哪一代,一共有以下四种收集方式。

  • 部分收集
    • 新生代收集 Minor GC/Young GC,只收集新生代垃圾对象
    • 老年代收集 (Major GC/Old GC),只收集老年代垃圾对象,目前只有CMS收集器会单独收集老年代对象。需要注意的是,Major GC,目前这个说法有点混淆,有时候专指老年代收集,有时候指的是整堆收集(Full GC)。
    • 混合收集(Mixed GC),收集来自整个新生代以及部分老年代中的垃圾对象。目前只有G1会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

三种垃圾收集算法

有三种基于标记的算法,分别是标记-清除、标记-整理和标记-复制算法。本质上,三种算法都是标记-清除两个阶段,在标记阶段,垃圾收集器将存活的对象进行标记,在回收时只保留这些对象,而将其他对象清除。只是标记-整理和标记-复制算法在清除阶段做了一些改进。三种标记算法本身与分代理论应该没有必然关系,但新生代和老年代各自的特点是选择相应算法的重要指标。

标记清除算法

标记清除算法
是最基础最直接的算法,标记阶段标记出存活的对象,在回收阶段,将其他对象进行清除。由于要清除的对象在内存空间上不连续,需要多次清除内存,并且,清除内存的时间随回收对象的个数增加,所以执行效率低,此外,回收后内存空间不连续。但是和标记复制算法和标记整理算法相比,不需要花费额外的时间进行复制或整理。并且和标记复制算法相比,不需要额外的空间放置复制的对象。

标记复制算法

标记复制算法
将标记为存活的对象复制到一块连续的内存区域,将剩下的内存一次性清除干净。优点是清除后内存空间连续。缺点是需要预留一部分空间用于存放不被清除的对象,降低了内存的使用率。同时,复制对象也有时间上的开销。

标记整理算法

标记整理算法

将标记为存活的对象移动到一端,将剩下的内存一次性清除干净。优点是清除后内存空间连续。缺点是移动对象在时间上有开销。

优点 缺点
标记清除 不需要移动数据 需要多次耗时清除内存、回收后内存空间不连续
标记整理 一次清除一整块内存、回收后内存空间连续 整理时需移动数据,停顿时间长
标记复制 不需要移动数据、停顿时间短、一次清除一整块内存、回收后内存空间连续 需要划出一块内存,复制数据也还是有一点开销

标记复制算法一般会比其它两种算法快十倍左右。当继续存活的对象比较少时,应当使用标记复制算法。在新生代中,根据经验值,98%的对象在第一次GC时就会被回收,所以适合使用标记复制算法。老年代中,对象已经存活了很长时间,这一轮GC中被回收的概率较低,此时,为了避免不断将对象进行复制的开销,应当使用标记清除或标记整理算法。

java8之如何使用函数引用

上一篇文章中以实例讲解如何定义和使用lambda表达式,以及与其它语言相比,lambda表达式在Java中的特殊规范。并且提到,lambda表达式可以进一步简化为函数引用。这篇文章将介绍如何使用函数引用。

函数引用的类型

函数引用分为以下四种:

  • 静态函数,比如Integer类的parseInt函数,可以写作Integer::parseInt
  • 对象级别函数的引用,比如String类的length函数,可以写作String::length
  • 具体实例的函数的引用,比如名称为expensiveTransaction的一个实例的getValue,写作expensiveTransaction::getValue
  • 构造函数的引用

静态函数

比如:

1
Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);

可以写作:

1
Function<String, Integer> stringToInteger = Integer::parseInt;

对象级别函数的引用

1
2
BiPredicate<List<String>, String> contains =
(list, element) -> list.contains(element);

可以写作:

1
BiPredicate<List<String>, String> contains = List::contains;

构造函数的引用

下面再举一个构造函数的例子,首先定义一个函数式接口,唯一的方法get返回指定类型的对象。

1
2
3
4
@FunctionalInterface
public interface Supplier<T> {
T get();
}
1
2
Supplier<TantanitReader> constructor = () -> new TantanitReader();
TantanitReader tantanitReader = constructor.get();

上述代码的lambda表达式new并返回一个新的对象,使得constructor变量成为一个构造器的引用。

等价于以下函数引用:

1
2
Supplier<TantanitReader> constructor2 = TantanitReader::new;
TantanitReader tantanitReader2 = constructor2.get();

以上的例子都是没有参数的情况,下面仍然以构造函数为例,介绍有参数的情况:

1
2
3
public TantanitReader(String loginName) {
this.loginName = loginName;
}
1
2
3
4
5
6
7
Function<String,TantanitReader> constructor3 = (loginName) -> new TantanitReader(loginName);
TantanitReader tantanitReader3 = constructor3.apply("jack");

Function<String,TantanitReader> constructor4 = TantanitReader::new;
TantanitReader tantanitReader4 = constructor4.apply("jack");
TantanitReader tantanitReader5 = constructor4.apply("tom");

这时由于函数只有一个参数,可以使用Java自带的Function<T, R>这个函数式接口,它实际起作用的函数如下:

1
R apply(T t);

作用为根据一个参数,返回一个结果。我们由此可以使用constructor4以及相应的函数引用constructor5。

小结

使用函数引用不仅可以简化lambda表达式,而且在语义上可以更加专注于方法名称,也就是要做的事,抽象级别更加接近人类的认知。所以,应当尽可能使用函数引用。

java8之如何使用lambda表达式

上一篇文章中介绍了lambda表达式的语法,引入了lambda表达式的使用场景,以及使用lambda 表达式的好处。我们将在这篇文章中,已实例讲解如何定义和使用lambda表达式,以及与其它语言相比,lambda表达式在Java中的特殊规范。

使用匿名内部类的例子

首先明确一点,在Java8出现之前,lambda表达式能够做到的,使用内部类也能做到,lambda表达式只是简化了编程。
下面的例子是从列表中根据条件挑选出读者。

定义TantanitReader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class TantanitReader {
private int age;
private String loginName;
private String realName;
private String career;

public TantanitReader() {
}

public TantanitReader(int age, String loginName, String realName, String career) {
this.age = age;
this.loginName = loginName;
this.realName = realName;
this.career = career;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getLoginName() {
return loginName;
}

public void setLoginName(String loginName) {
this.loginName = loginName;
}

public String getRealName() {
return realName;
}

public void setRealName(String realName) {
this.realName = realName;
}

public String getCareer() {
return career;
}

public void setCareer(String career) {
this.career = career;
}

@Override
public String toString() {
return "age:"+this.getAge()+",loginName:"+this.loginName
+",realName:"+this.getRealName()+",career:"+this.getCareer();
}
}

定义判断的接口:

1
2
3
public interface Predicate<T> {
boolean test(T t);
}

定义选择函数:

1
2
3
4
5
6
7
8
9
10
11
12
public class SelectService<T> {
public List<T> select(Collection<T> source, Predicate<T> predicate){
List result = new LinkedList();
for(T element:source){
if (predicate.test(element)) {
result.add(element);
}
}
return result;
}

}

编写测试用的例子,分别选择成年读者和十多岁(包括10岁)的读者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class TantanitReaderPredicateTest {


public static void main(String[] args) {
SelectService tantanitReaderSelectSerive
=new SelectService<TantanitReader>();
List<TantanitReader> source = new LinkedList<>();
source.add( new TantanitReader(10,"jack","张三","学生"));
source.add(new TantanitReader(18,"rose","李四","学生"));
source.add(new TantanitReader(19,"mike","王五","程序员"));
source.add(new TantanitReader(20,"jack","赵六","作家"));

List<TantanitReader> audultReaders
=tantanitReaderSelectSerive.select(source, new Predicate() {
@Override
public boolean test(Object o) {
TantanitReader tantanitReader=(TantanitReader)o;
return tantanitReader.getAge()>=18;
}
});
System.out.println("tantanit.com成年读者名单如下:");
printTantanitReaders(audultReaders);

System.out.println("tantanit.com 十多岁(包含10岁)成员如下:");
List<TantanitReader> teenReaders
=tantanitReaderSelectSerive.select(source, new Predicate() {
@Override
public boolean test(Object o) {
TantanitReader tantanitReader=(TantanitReader)o;
return tantanitReader.getAge()>=10 && tantanitReader.getAge()<=19;
}
});
printTantanitReaders(teenReaders);
}


public static void printTantanitReaders(List<TantanitReader> tantanitReaders) {
for (TantanitReader tantanitReader : tantanitReaders) {
System.out.println(tantanitReader.toString());
}
}


}

执行后,打印结果如下:

1
2
3
4
5
6
7
8
tantanit.com成员读者名单如下:
age:18,loginName:rose,realName:李四,career:学生
age:19,loginName:mike,realName:王五,career:程序员
age:20,loginName:jack,realName:赵六,career:作家
tantanit.com 十多岁(包含10岁)成员如下:
age:10,loginName:jack,realName:张三,career:学生
age:18,loginName:rose,realName:李四,career:学生
age:19,loginName:mike,realName:王五,career:程序员

可以看到,两次选择读者,都需要new Predicate(),并且重写(Override)test方法,而真正的差异其实只在于判断语句:

1
tantanitReader.getAge()>=18

1
tantanitReader.getAge()>=10 && tantanitReader.getAge()<=19

但是在Java8之前,由于没有lambda表达式,只能忍受这种冗余。如何用lambda表达式来简化代码呢?

为了照顾Java开发人员既有的编程习惯,与其它语言不同,Java8在设计lambda表达式的使用机制时,规定仍然需要使用接口,并且要求所使用的接口必须是函数式接口,在这个例子中,我们仍然可以使用:

1
2
3
public interface Predicate<T> {
boolean test(T t);
}

因为这个接口只有一个抽象方法(java8引入了default方法,default方法有具体实现,不算抽象方法),所以它是函数式接口(functional interface)。函数式接口可以加上@FunctionalInterface声明,也可以不加。但是加上之后,编译器在编译阶段就会检查这个接口是否符合函数式接口的定义,所以这里我们定义一个新的接口,并且加上@FunctionalInterface声明:

1
2
3
4
@FunctionalInterface
public interface PredicateFunction<T> {
boolean test(T t);
}

并且给SelectService添加一个以PredicateFunction为参数的方法:

1
2
3
4
5
6
7
8
9
public List<T> select(Collection<T> source, PredicateFunction<T> predicate){
List result = new LinkedList();
for(T element:source){
if (predicate.test(element)) {
result.add(element);
}
}
return result;
}

再修改测试的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TantanitReaderPredicateFunctionTest {

public static void main(String[] args) {
SelectService tantanitReaderSelectSerive
=new SelectService<TantanitReader>();
List<TantanitReader> source = new LinkedList<>();
source.add( new TantanitReader(10,"jack","张三","学生"));
source.add(new TantanitReader(18,"rose","李四","学生"));
source.add(new TantanitReader(19,"mike","王五","程序员"));
source.add(new TantanitReader(20,"jack","赵六","作家"));

PredicateFunction<TantanitReader> predicateFunction
= (TantanitReader tantanitReader) -> tantanitReader.getAge() >= 18;
List<TantanitReader> audultReaders
=tantanitReaderSelectSerive.select(source,predicateFunction);

System.out.println("tantanit.com成员读者名单如下:");
printTantanitReaders(audultReaders);

System.out.println("tantanit.com 十多岁(包含10岁)成员如下:");
PredicateFunction<TantanitReader> predicateFunction2
= (TantanitReader tantanitReader)
-> tantanitReader.getAge()>=10 && tantanitReader.getAge()<=19;
List<TantanitReader> teenReaders
=tantanitReaderSelectSerive.select(source,predicateFunction2);
printTantanitReaders(teenReaders);
}


public static void printTantanitReaders(List<TantanitReader> tantanitReaders) {
for (TantanitReader tantanitReader : tantanitReaders) {
System.out.println(tantanitReader.toString());
}
}

}

下面我们分析一下这段代码是如何生效的:

1
2
3
4
PredicateFunction<TantanitReader> predicateFunction
= (TantanitReader tantanitReader) -> tantanitReader.getAge() >= 18;
List<TantanitReader> audultReaders
=tantanitReaderSelectSerive.select(source,predicateFunction);

这段代码,生成了一个PredicateFunction类型的实例,并且将该实例的引用作为参数传给tantanitReaderSelectSerive的select方法,并且执行select方法。select在执行过程中,调用predicateFunction的test方法,而test方法的内容就是我们传入的lambda表达式,最终按照lambda表达式,选择出读者。

再进一步,一般可以不定义predicateFunction这个变量,而直接将lambda表达式作为参数传给tantanitReaderSelectSerive的select方法,像这样:

1
2
3
4
List<TantanitReader> audultReaders
=tantanitReaderSelectSerive.select(
source,(TantanitReader tantanitReader) -> tantanitReader.getAge() >= 18
);

但是这个例子,实际上会报编译错误,说TantanitReader和tantanitReaderSelectSerive的select方法的定义不匹配,因为select方法使用的是泛型。java8的文档确实是规定了在使用泛型的情况下,不能直接将lambda表达式作为参数,这个挺无语的。如果不使用泛型的,没有这个问题。

小结

下面总结一下如何使用lambda表达式

  1. 首先,定义一个函数式接口(functional interface),并且在接口中定义需要使用的抽象方法。
  2. 编写业务方法,并且以该函数式接口作为参数,并且调用该接口定义的方法,完成业务逻辑。
  3. 调用业务方法,并且将lambda表达式作为参数传入。

如果使用了泛型,最后一步改为先定义一个函数式接口的实例的引用,再作为参数传给业务方法。

此外,lambda表达式还可以继续简化为函数引用,将在后面的文章中讲解。

Java8详解之lambda表达式

Java8中引入了lambda表达式,从行为参数化的角度,在使用时,将行为作为参数,去除包围在外层的不必要的类声明,使代码更加简洁。

lambda表达式的语法

lambda表达式由参数,->,以及函数体三部分组成。其实函数体可以是表达式,也可以是语句。语句应该包含在{}里,而表达式不能。

lambda表达式举例

1
2
3
4
5
6
(List<String> list) -> list.isEmpty() //布尔类型表达式
() -> new Apple(10) //创建一个新对象
(Apple a) -> { System.out.println(a.getWeight()); } //使用一个对象的属性
(String s) -> s.length() //选择或提取一个对象的属性
(int a, int b) -> a * b //组合两个参数
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) //比较两个对象

行为参数化

可以看到,lambda表达式着重表达了行为。其实在java8之前,就已经有类似将行为作为参数进行处理的例子:

1
2
3
4
// java.util.Comparator
public interface Comparator<T> {
public int compare(T o1, T o2);
}
1
2
3
4
5
6
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});

在上面的例子中,定义了一个Comparator接口,并且定义了compare方法。List类型的inventory,定义了sort方法,该方法以Comparator作为参数。在使用时,生成一个匿名或不匿名的Comparator对象,该对象实现了compare方法,指定了具体的比较行为,再将Comparator对象作为参数,传给sort方法。实际上的作用,就是将比较行为,作为参数,传给sort方法,这种思想和做法就叫作“行为参数化”。一个使用的方法(比如sort方法)可以对应多种行为,当需要更改行为时,只需要修改表示行为的代码,使用方不必做出修改,增加了代码的健壮性。

匿名内部类

而像上面的例子中,直接new一个Comparator类型对象,而没有定义一个变量保存该对象的引用,叫做使用匿名内部类。在匿名内部类的情况下,由于方法不能重复使用,可以更加明显地看到代码的冗余。因为每次在使用时需要手动new一个对象,并且还要写出compare方法的签名,而真正有用的其实只是compare的方法体。在Java8中,使用lambda表达式,可以简化这些代码。比如上面的sort方法的使用,可以简化为:

1
inventory.sort( (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight() ) );

定义线程的例子:

1
2
3
4
5
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});

可以简化为:

1
Thread t = new Thread(() -> System.out.println("Hello world"));

GUI中点击事件的例子:

1
2
3
4
5
6
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});

可以简化为:

1
2
Button button = new Button("Send");
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));

可以看出,lambda表达式更加简洁,也可以使开发者在读写代码时,可以更加专注在定义的行为上。

小结

这篇文章介绍了lambda表达式的语法,引入lambda表达式的使用场景,以及使用lambda表达式的好处。我们将在下篇文章中讲解如何定义和使用lambda表达式,以及与其它语言相比,lambda表达式在Java中的特殊规范。

高效Java技巧重写equals方法时应当重写hashCode方法

hashCode需满足的条件

  • 当equals方法中涉及的参数没有改变时,hashCode应保持不变
  • 如果根据equals方法,两个对象是相等的,那么这两个对象的hashCode应该一样
  • 两个对象如果不相等,hashCode不强制要求不一样,但是如果能保证不一样,对哈希的效率会比较有帮助

最重要的是第二点,相等的对象必须有相同的hashCode,由于默认的hashCode方法针对每一个对象返回一个固定的随机值(有的实现是根据对象地址返回值,相当于每一个对象对应一个固定的随机值),所以当我们使用equals方法的同时,必须override(重写)hashCode方法,以满足这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TantanitReaderPhone {
private String areaCode;
private String localNumber;

public TantanitReaderPhone(String areaCode, String localNumber) {
this.areaCode = areaCode;
this.localNumber = localNumber;
}

@Override
public int hashCode() {
return super.hashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof TantanitReaderPhone))
return false;
TantanitReaderPhone tantanitReaderPhone = (TantanitReaderPhone)obj;
return areaCode.equals(tantanitReaderPhone.areaCode)
&& localNumber.equals(tantanitReaderPhone.localNumber);
}

public static void main(String[] args) {
Map<TantanitReaderPhone, String> tantanitReaderPhoneStringMap
= new HashMap<>();
tantanitReaderPhoneStringMap.put(
new TantanitReaderPhone("86","13200001234"),"张三"
);
String name=tantanitReaderPhoneStringMap.get(
new TantanitReaderPhone("86","13200001234")
);
if(name==null){
System.out.print("name is null");
}else {
System.out.print(name);
}

}

}

上面的代码是一个手机号码的例子,手机号码由区号(比如中国是86)和本国手机号构成。我们重写了equals方法,但hashCode使用的仍然是父类也就是Object类的方法,可以理解为是一个随机数。在main函数中,我们定义了一个以TantanitReaderPhone为key的hashMap,保存并试图取出一个value值。需要注意的是,我们保存和取出时,使用的是两个不同的对象(两次都是new一个新的对象),但两个对象有着相同的areaCode和localNumber,根据我们重写的equals方法,这两个对象是相等的。但是由于我们没有重写hashCode方法,这两个对象的哈希值不同,所以使用第二个对象无法在hashMap里找到第一次存进去的值。

这是因为,哈希表的每个分区,只会对应有限的哈希值,并存储这些哈希值对应的对象。所以哈希值不同,首先找不到对应的分区,即使碰巧哈希表有分区同时对应着两个哈希值,由于哈希表往往会进行优化,对哈希值先进行判断,所以不相等的哈希值找不到对应的对象。

所以,当需要根据哈希值进行存储时,应该重写hashCode方法,根据字段值生成对应的hashCode(哈希值)。下面讲解如何计算hash值,并且改写我们上面的例子,重写hashCode方法,再执行以下main函数,看看有什么结果。

如何计算hashCode

  1. 生成一个int类型的变量result,并且初始化一个值,比如17
  2. 对类中每一个重要字段,也就是影响对象的值的字段,也就是equals方法里有比较的字段,进行以下操作:
    a. 计算这个字段的值filedHashValue
    b. 执行 result = 31 * result + filedHashValue; 更新结果

而要如和计算这个字段的值filedHashValue值呢,根据字段类型分为三种情况,一种是基础数据(比如int,boolean,),一种是对象,还有一种是数组。

如何计算每个重要字段的值

如果字段是基础数据,假设数值为f,根据类型

  • boolean,true返回1,false返回0
  • byte, char, short, int返回对应的int值
  • long,返回f^(f>>>32)
  • float,返回Float.floatToIntBits(f)
  • double,执行Double.doubleToLongBits(f)转为long,再返回compute(int)(f^(f>>>32))

而如果字段是对象,如果是null,返回0,否则根据上述【如何计算hashCode】中所述步骤1和2,计算这个对象的hashCode,作为这个字段的值。实际上是一个递归的过程。

而如果字段是数组,则对每一个元素进行计算,得到filedHashValue,并执行result = 31 * result + filedHashValue;

这里补充一下HashMap对象是如何计算哈希值的,JDK中,HashMap类自身已经实现了hashCode方法。一个hashMap其实是entry的数组。
AbstractMap中,hashCode对数组中的每一个entry计算哈希值,并得到所有哈希值的和。

1
2
3
4
5
6
7
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}

而每一个entry按照以下方式计算哈希值(JDK1.7之后的版本)。

1
2
3
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

这个实现非常巧妙,由于entry由key和value生成,所以将两个哈希值求与,既保证具有相同key和value的entry一定具有相同的哈希值,又保证了效率(与操作的效率很高)。

计算hashCode的注意事项

  • 不能包含equals方法中没有的字段,否则会导致相等的对象可能会有不同的哈希值
  • 和equals方法一样,不应当包含可以通过其他字段计算出来的字段
  • 不要尝试减少计算重要字段,虽然这样做,在算hash值时会比较快,但会导致同一个hash值对应过多对象,比如TantanitReaderPhone中如果只使用areaCode字段计算哈希值,那么所有来自中国的手机号都会是同一个hash值,显然哈希表的性能会很低。
  • 如果对象的值不可更改,应当考虑延迟计算并且缓存哈希值,比如JDK中String的值是不可变的(每一次更改值都会生成新的String对象),所以对hash值的计算使用了延迟计算,并且缓存,String的hashCode方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

TantanitReaderPhone的哈希值计算

1
2
3
4
5
6
7
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode.hashCode();
result = 31 * result + localNumber.hashCode();
return result;
}

由于areaCode和localNumber都是用来区分TantanitReaderPhone的重要字段,所以根据这两个字段来计算哈希值。这两个字段都是String类型,直接调用String自带的hashCode方法(areaCode和localNumber假定都不为null)。

将TantanitReaderPhone的hashCode方法按照上述代码进行重写后,再执行main函数,打印出“张三”,成功从hashMap中取出对应的值了。您可以复制一下TantanitReaderPhone的代码,亲自试试。

高效Java技巧之不滥用枚举的下标

所有的枚举类型都有一个ordinal方法,将返回枚举值在枚举类中的位置。

1
2
3
4
5
6
7
// 滥用下标,将下标与枚举值的属性特征联系在一起,应当避免这样做
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}

Ensemble这个枚举表示合奏,枚举值从独奏(SOLO)依次增加直到十重奏(DECTET),numberOfMusicians表示演奏的数目,正好是下标值加1。比如SOLO排在第一个,下标值为0,而演奏数目正好是1。这种方式非常取巧,但是可能给维护带来噩梦。如果给枚举值重新排序,numberOfMusicians方法的返回值就不再正确。如果有一个新的枚举值对应的演奏数目和已有的枚举值一样,比如double quartet(双重四重奏),和OCTET一样,也是8个演奏,就会造成问题。此外,如果有一个演奏数目与已有的并不连续,比如现在想在上面的枚举基础上加一个triple quartet(三重四重奏),由于演奏数目是12,要想增加这个枚举值,就得先加演奏数目为9、10、11的枚举值,而这本来很可能是完全不需要的。

永远不要将与枚举值有关的属性和它的下标相关联,而应该将属性值独立保存。

1
2
3
4
5
6
7
8
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}

实际上,在枚举的定义中,对ordinal这个方法进行了明确说明:

1
大部分编程人员不会用到这个方法,它是被设计来用于基于枚举的数据结构:EnumSet和EnumMap。

所以,除非你在编写这样的数据结构,最好不好使用ordinal这个方法。

高效Java技巧之尽可能使用枚举代替常量

有些类型的数据是可枚举的,比如一年的春夏秋冬四季、一年的十二个月、一周的七天,以及应用中其它可枚举的数据。有些代码中习惯使用常量来表示这些可枚举的数据,分为以下两种:

  • int常量
  • String常量
1
2
3
4
5
6
public static final int APPLE_FUJI =0;
public static final int APPLE_PIPPIN =1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

使用int常量,具有以下缺点:

  • 没有强制分组,一个文件内可能存在多组数据,影响可读性和使用。
  • int值是编译时常量(compile-time constants),当值改变时,客户端如果没有重新编译,仍然可以正常运行,但是行为却变化了。
  • 不可打印,打印出的int值,无法直接表示数据的含义。
  • 不可遍历,没有办法在组内遍历所有枚举数据。

String常量与int常量类似,虽然在可打印性这方面好于int常量,但却更不值得考虑。因为:

  • 比较String类型的数据值可能导致性能问题。
  • 容易引导调用方使用硬编码(hard-code)字符串,而字符串的拼写错误在编译时是无法发现的,这将导致bug。

所以,应当枚举代替常量表示枚举型数据。

1
2
public enum Apple  { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

Java枚举类型

在C,C++,和C#中,枚举只是一个int常量。而Java枚举类,更加强大。enum这个关键字实际上是一个语法糖,编译器对其解析的结果,实际上是一个继承了Enum的类,并且实现了许多方法。下面的两段代码,第一段是定义了一个枚举。第二段代码是这个枚举的字节码文件,反编译后的一种结果。
以下为源代码:

1
2
3
4
5

public enum TantanitLogoEnum {
singleton
}

以下为反编译后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

/*
* Decompiled with CFR 0_121.
*/
package com.tantanit.effective;

public final class TantanitLogoEnum
extends Enum<TantanitLogoEnum> {
public static final /* enum */ TantanitLogoEnum singleton = new TantanitLogoEnum();
private static final /* synthetic */ TantanitLogoEnum[] $VALUES;

public static TantanitLogoEnum[] values() {
return (TantanitLogoEnum[])$VALUES.clone();
}

public static TantanitLogoEnum valueOf(String name) {
return Enum.valueOf(TantanitLogoEnum.class, name);
}

private TantanitLogoEnum() {
super(string, n);
}

static {
$VALUES = new TantanitLogoEnum[]{singleton};
}
}


它的基本思路是,为每一个枚举常量,通过一个公有静态常量字段,提供唯一的一个实例。枚举类型都是常量,并且没有可访问的构造器。使用枚举的时候,既不能创建新的实例,又不能继承这个枚举,所以,只能使用枚举创建好的这些实例。换句话说,枚举是对实例可控的(instance-controlled)。枚举是广义上的单例。在我的上一篇文章中介绍了实现单例的几种方式,并且说明只有枚举能够防止反射攻击。由于enum是一个语法糖,隐藏了代码的细节。所以枚举在语法级别防止反射攻击。实际上,枚举满足除了延迟加载之外的所有要求:

  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)

由于枚举实际上是一个继承自Enum的类,而Enum实现了readObject方法,在反序列化时会抛出异常,所以可以防止多次反序列化生成多个不同的实例。而由于枚举变量在编译之后实际上是static final,所以可以保证多个线程只会创建一个实例。但是需要注意的是,我们在枚举中自己添加的方法并不是线程安全的,需要自己处理这个问题。更多单例相关的知识,可以查看我的我的上一篇文章

枚举的其它好处

  • 枚举提供了编译时类型检查,如果你定义了一个类型为Apple的参数,可以保证传入的值要么是null,要么是三个有效值中的一个。
  • 两个不同类型的枚举,可以定义相同名称的枚举常量,因为每个枚举类型有自己的命名空间(这个很好理解,每个类都有自己的命名空间)。
  • 可以在枚举中添加常量,或者改变常量的顺序,而不必重新编译使用方的代码。因为枚举将枚举常量导出的那段代码,使得枚举和客户端代码绝缘,枚举的值并不会像上面说的int常量那样编译到客户端中。
  • 可以调用toString方法,很方便地打印枚举实例值。
  • 与int常量相比,枚举类型可以随意地添加方法、字段以及实现接口。高质量地实现了Object类的所有方法,并且实现了Comparable和Serializable接口,对Serializable的实现设计成可以容忍枚举类型的大多数改变。

枚举类,总体来说,性能和int常量相当。只有在加载和初始化的时候会有一些开销,但这在大多数情况下是可以忽略的。

什么时候使用枚举

什么时候需要使用枚举呢?答案是需要使用固定的常量集合任何时候。既包含一年四季,一周七天这种自然枚举,也包含在编译时就知道所有可能值的其它情况,比如加减乘除等运算。枚举值不必是固定的,可以在后面继续添加。

与int常量相比,枚举的优点是很明显的。枚举更具可读性、更安全,并且功能更强大。许多枚举不需要明显的构造器和成员变量,而有些枚举通过为每个枚举值绑定数据,并且为这些数据提供函数,可以添加额外的功能。

Effective Java第二版笔记之使用私有构造器或枚举实现单例

什么是单例

单例是指只会初始化一次,因而最多只会有一个实例的类。单例一般用来表示本质上只有一个的组件。比如操作系统中的窗体管理器和文件系统等。

单例类具备哪些要求

在使用单例时,需要考虑以下几点:

  • 访问权限控制,应当使用私有属性或方法生成实例
  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)
  • 是否使用延迟加载,只在需要的时候才生成实例

如果不考虑延迟加载的问题,枚举是实现单例的最佳选择。

下面以一个完整的例子讲解在不使用枚举的情况下,做到以上几点,(除了反射攻击)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class TantanitLogo implements Serializable {
private volatile static TantanitLogo singleton;
private volatile static boolean initialized = false;

private TantanitLogo() {
synchronized (TantanitLogo.class) {
if (initialized == false) {
initialized = true;
} else {
throw new RuntimeException("受到反射攻击!");
}
}

}

public static TantanitLogo getInstance() {
if (singleton == null) {
synchronized (TantanitLogo.class) {
if (singleton == null) {
singleton = new TantanitLogo();
}
}
}
return singleton;

}

private Object readResolve() {
return singleton;
}

public static void otherMethod() {
System.out.println("执行TantanitLogo类的静态方法otherMethod!");
if (TantanitLogo.singleton == null) {
System.out.println("此时singleton为null,未被初始化!");
} else {
System.out.println("此时singleton不为null,已被初始化!");
}
}
}

访问权限控制

例子中成员变量singleton和构造器都是私有类型的,实现了访问权限控制。

解决反射攻击问题(无除枚举外的其它方式)

目前除了使用枚举似乎没有其它方法可以解决反射攻击,以下代码仍然无法避免反射攻击。

1
2
3
4
5
6
7
8
9
10
11
12
private volatile static boolean initialized = false;

private TantanitLogo() {
synchronized (TantanitLogo.class) {
if (initialized == false) {
initialized = true;
} else {
throw new RuntimeException("受到反射攻击!");
}
}

}

这段代码本意是,添加成员变量initialized,用来标识是否生成过实例,在调用构造函数时,如果已经调用过一次,生成过实例,则报错。但如果使用反射的方式先将initialized改为false,再调用私有构造函数,就可以顺利绕过initialized,生成第二个实例,破坏单例性。

解决反序列化问题

TantanitLogo类实现了Serializable接口,可以被序列化和反序列化,为类添加的readResolve方法,可以解决反序列化时生成新的实例的问题。

1
2
3
private Object readResolve() {
return singleton;
}

在TantanitLogoTest类添加以下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 测试反序列化
*/
public static void testDeserialize() throws IOException, ClassNotFoundException {
TantanitLogo tantanitLogo1 = TantanitLogo.getInstance();


FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(tantanitLogo1);
oos.close();
fos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.out"));
TantanitLogo tantanitLogo3 = (TantanitLogo) ois.readObject();
if (tantanitLogo1 == tantanitLogo3) {
System.out.println("tantanitLogo1与tantanitLogo3是同一个实例");
} else {
System.out.println("tantanitLogo1与tantanitLogo3不是同一个实例");
}
}

当TantanitLogo类中有readResolve方法时,ObjectInputStream的readObject方法会调用readResolve方法,所以输出结果为“tantanitLogo1与tantanitLogo3是同一个实例”,当TantanitLogo类中没有readResolve方法时,则输出“tantanitLogo1与tantanitLogo3不是同一个实例”。

线程安全

1
2
3
4
5
6
7
8
9
10
11
public static TantanitLogo getInstance() {
if (singleton == null) {
synchronized (TantanitLogo.class) {
if (singleton == null) {
singleton = new TantanitLogo();
}
}
}
return singleton;

}

上一篇文章中讲解了使用静态工厂方法代替构造器的好处,这里就是使用getInstance方法就是代替构造器,生成实例。而使用synchronized关键字达到线程安全的目的,您可能注意到,我在synchronized代码块外加了singleton == null的条件判断。这是由于只有当singleton为null时才会进行new操作,生成新的实例,所以只在这个时候对代码加同步限制。

延迟加载

以下是TantanitLogo中的另一个静态类方法:

1
2
3
4
5
6
7
8
public static void otherMethod() {
System.out.println("执行TantanitLogo类的静态方法otherMethod!");
if (TantanitLogo.singleton == null) {
System.out.println("此时singleton为null,未被初始化!");
} else {
System.out.println("此时singleton不为null,已被初始化!");
}
}

由于只在TantanitLogo的静态方法getInstance中进行new操作,生成新的实例。所以调用其它静态方法不会生成新的实例。在TantanitLogoTest添加方法进行测试:

1
2
3
4
5
6
/**
* 测试延迟加载
*/
public static void testLazyLoad() {
TantanitLogo.otherMethod();
}

输出结果为

1
2
执行TantanitLogo类的静态方法otherMethod!
此时singleton为null,未被初始化!”

满足延迟加载的要求。

您可以以每次启动调用一个测试函数的方法,对以上几个特征分别进行测试。如果您觉得哪个特征在您的应用场景中不重要,也可以很容易地进行简化。

使用枚举类实现单例

1
2
3
public enum TantanitLogoEnum {
singleton
}

使用只有一个元素的枚举类可以很方便地实现单例,并且满足除了延迟加载之外的所有要求:

  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)

下篇文章,将会对Enum类的工作原理进行解析,并解释为什么枚举具有这些优势。

Effective Java第二版笔记之考虑使用静态工厂方法代替构造器

创建对象最直接的想法就是通过new调用构造器,其实大多数情况下应当通过自己写一个静态公有方法,返回类的实例,比如下面这个方法:

1
2
3
  public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}

和直接使用构造器相比,使用静态工厂方法具有以下优点:

  • 静态工厂方法可以根据用途自己定义名称(不必与类相同),可读性更强
  • 静态工厂方法可能不用在每次调用时都创建新对象
  • 静态工厂方法可以返回声明类型的子类型的实例

下面分别阐述这三个优点。

静态工厂方法可以根据用途自己定义名称

构造器的方法名只能使用类名,如果有多个构造器,只能通过参数类型甚至顺序来区分,这样可读性非常差,而且不容易记,调用的时候很容易出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TantanitReader {
private int age;
private String loginName;
private String realName;
private String career;

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getLoginName() {
return loginName;
}

public void setLoginName(String loginName) {
this.loginName = loginName;
}

public String getRealName() {
return realName;
}

public void setRealName(String realName) {
this.realName = realName;
}

public String getCareer() {
return career;
}

public void setCareer(String career) {
this.career = career;
}
}

比如在上面的例子中,类TantanitReader表示我的官方博客tantanit.com的读者信息,其中age表示年龄,loginName表示登录名,realName表示真实姓名,career表示职业,除了age是int类型之外,其它几个都是字符串类型。

假设现在需要根据年龄和登录名,创建一个读者,使用构造器的话,代码如下:

1
2
3
4
public TantanitReader(int age, String loginName) {
this.age = age;
this.loginName = loginName;
}

假设又有一个场景,需要根据年龄(int类型)和真实姓名(String类型)创建读者,如果使用构造器的话,参数类型和上一个构造器相同,要解决这个问题呢?机智如你,一定想到通过对调参数顺序这个取巧(而不优雅)的方法,来规避这个问题。

1
2
3
4
public TantanitReader(String realName, int age) {
this.realName = realName;
this.age = age;
}

如果一个类要通过构造器创建读者,就需要根据这两个构造器的参数顺序记住各自的用法,是不是很容易记错啊。就算记住了也要查文档或者看看TantanitReader这个类的源文件,再确认一遍吧。

好了,又来了一个场景,需要根据年龄(int类型)和职业创建读者,如果使用构造器的话,相信聪明如你,也是没有办法了。

所以,应当使用静态工厂方法来代替构造器。下面的代码很好地解决了这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static TantanitReader getByAgeAndLoginName(int age,String loginName){
TantanitReader tantanitReader=new TantanitReader();
tantanitReader.setAge(age);
tantanitReader.setLoginName(loginName);
return tantanitReader;
}

public static TantanitReader getByAgeAndRealName(int age,String realName){
TantanitReader tantanitReader=new TantanitReader();
tantanitReader.setAge(age);
tantanitReader.setRealName(realName);
return tantanitReader;
}

public static TantanitReader getByAgeAndCareer(int age,String career){
TantanitReader tantanitReader=new TantanitReader();
tantanitReader.setAge(age);
tantanitReader.setCareer(career);
return tantanitReader;
}

三个方法使用不同的名称,并且在名称中暗示了参数的顺序,这样,在调用的时候,就不容易出错,也不用再查看文档或构造器的源码了。

静态工厂方法可能不用在每次调用时都创建新对象

使用静态工厂方法,在有些使用场景下,可以重复使用一个提前生成的对象,或者从缓存中获取一个对象,而不用创建一个新的对象。文章开头的例子中的Boolean.valueOf(boolean)方法,调用时就不用创建新的对象。这样节省了内存开销,也提高了性能。同时,和每次都new一个新的对象,都是不同的对象相比,在这种重复使用的场景中,每次返回的对象都是严格意义相同的对象,可以做到对象级别的控制。这种控制对单例和不可实例化的使用场景很有用,并且可以放心的使用==代替equals方法,以提高性能。比如枚举类就使用了这样的技术。

静态工厂方法可以返回声明类型的子类型的实例

构造函数,只能返回该类的实例,不能返回该类的子类的实例,静态工厂方法不受这个限制,因此可以很好地使用Java语言的多态性。在方法声明返回值类型为父类型,甚至接口类型,而返回子类型或接口的某个类型的具体实现。这样,将来想返回其它子类型或接口的其它实现时,只要直接修改方法体,不用改方法的声明,从而不会影响调用方的使用。

小结

本文使用谈谈IT读者的例子介绍了使用静态工厂方法替代构造器的三个优点,在有些场景,这些优点会很明显,下次遇到了,记得考虑使用静态工厂方法哦!

© 2022 谈谈IT All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero