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
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
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
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
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
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
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
// 滥用下标,将下标与枚举值的属性特征联系在一起,应当避免这样做
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

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

/*
* 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读者的例子介绍了使用静态工厂方法替代构造器的三个优点,在有些场景,这些优点会很明显,下次遇到了,记得考虑使用静态工厂方法哦!

Spring MVC之使用Freemarker

Freemarker是使用比较广泛的模板,本文介绍如何使用Spring集成Freemarker,并提供完整实例进行演示。代码结构如下:

代码结构

代码结构

定义Freemarker视图解析器

和其它web应用一样,我们可以在WebMvcConfigurerAdapter定义ViewResolver(视图解析器),这里通过子类WebConfig来实现:

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
@Configuration
@EnableWebMvc
@ComponentScan("tantanit.web")
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
}

@Bean
public ViewResolver viewResolver() {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setCache(true);
resolver.setPrefix("/WEB-INF/templates/");
resolver.setSuffix(".ftl");
resolver.setContentType("text/html; charset=UTF-8");
return resolver;
}

@Bean
public FreeMarkerConfigurer getFreemarkerConfig() throws IOException, TemplateException {
FreeMarkerConfigurer result = new FreeMarkerConfigurer();
result.setTemplateLoaderPaths("/");
return result;
}
}

其中,viewResolver定义了一个FreeMarkerViewResolver类型的解析器,并且配置模板路径、后缀,以及页面编码。同时,必须定义一个FreeMarkerConfigurer,至少指定加载路径,由于WebConfig类加了@Configuration注解,这个FreeMarkerConfigurer将会被FreeMarkerView使用以下代码从应用上下文中自动查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
//FreeMarkerView类中相关代码
protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
try {
return BeanFactoryUtils.beanOfTypeIncludingAncestors(
getApplicationContext(), FreeMarkerConfig.class, true, false);
}
catch (NoSuchBeanDefinitionException ex) {
throw new ApplicationContextException(
"Must define a single FreeMarkerConfig bean in this web application context " +
"(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
"This bean may be given any name.", ex);
}
}

要使WebConfig生效,还需要实现AbstractAnnotationConfigDispatcherServletInitializer,以初始化DispatcherServlet,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}

@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}

}

DispatcherServlet是Spring MVC的核心,它负责接收request,并且决定request将转给哪个组件。您可以在我写的这篇文章《Spring MVC 的基础配置》中了解更多内容。
由于RootConfig是处理非web配置的,在这里,我们在RootConfig中排除web目录。

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ComponentScan(basePackages={"tantanit"},
excludeFilters={
@Filter(type=FilterType.CUSTOM, value=WebPackage.class)
})
public class RootConfig {
public static class WebPackage extends RegexPatternTypeFilter {
public WebPackage() {
super(Pattern.compile("tantanit\\.web"));
}
}
}

到这里,Spring与Freemarker的配置就完成了,下面对页面进行渲染。

渲染页面

主页的controller如下:

1
2
3
4
5
6
7
8
@Controller
public class HomeController {
@RequestMapping( value = "/",method = GET)
public String home(Model model) {
return "home";
}

}

home.ftl如下:

1
2
3
<h1>关于谈谈IT</h1>
<p>谈谈IT,是一个专注于计算机技术、互联网、搜索引擎、SEO、优秀软件、网站架设与运营的原创IT科技博客。</p>
<p>欢迎访问<a href="http://tantanit.com">tantanit.com</a></p>

渲染后效果如下:
代码结构

小结

本文介绍如何将Spring和Freemarker进行集成。本文相关的配置,在之前写的这几篇文章中有更详细的介绍:

此外,之前的这两篇文章讲解了Spring与其它几个主流模板的集成:

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