java8 之如何使用函数引用

字数622 大约花费3分钟

上一篇文章 中以实例讲解如何定义和使用 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);

更多

java8 之如何使用 lambda 表达式

字数1,747 大约花费8分钟

上一篇文章 中介绍了 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);
}

更多

Java8 详解之 lambda 表达式

字数900 大约花费4分钟

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);
}

更多

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

字数1,730 大约花费7分钟

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 里找到第一次存进去的值。

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

更多

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

字数540 大约花费2分钟

所有的枚举类型都有一个 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; }
}

更多

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

字数1,592 大约花费6分钟

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

  • 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 值,无法直接表示数据的含义。
  • 不可遍历,没有办法在组内遍历所有枚举数据。

更多

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

字数1,455 大约花费6分钟

什么是单例

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

单例类具备哪些要求

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

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

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

更多

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

字数1,386 大约花费5分钟

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

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

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

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

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

更多

Spring MVC 之使用 Freemarker

字数801 大约花费4分钟

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;
}
}

更多