1. 使用 Stream 简化集合操作
2. 使用 Optional 简化判空逻辑
空指针异常(NullPointerExceptions
)是 Java 最常见的异常之一,一直以来都困扰着 Java 程序员。一方面,程序员不得不在代码中写很多null
的检查逻辑,让代码看起来非常臃肿;另一方面,由于其属于运行时异常,是非常难以预判的。
为了预防空指针异常,Google
的Guava
项目率先引入了Optional
类,通过使用检查空值的方式来防止代码污染,受到Guava
项目的启发,随后在Java 8
中也引入了Optional
类。
Optional 类位于java.util
包下,是一个可以为 null
的容器对象,如果值存在则isPresent()
方法会返回 true
,调用 get()
方法会返回该对象,可以有效避免空指针异常。
2.1 创建Optional对象
empty()、of()、ofNullable()
//可以使用静态方法 empty() 创建一个空的 Optional 对象
Optional<String> empty = Optional.empty();
System.out.println(empty); // 输出:Optional.empty
//可以使用静态方法 of() 创建一个非空的 Optional 对象
Optional<String> opt = Optional.of("沉默王二");
System.out.println(opt); // 输出:Optional[沉默王二]
//当然了,传递给 of() 方法的参数必须是非空的,也就是说不能为 null,否则仍然会抛出 NullPointerException。
String name = null;
Optional<String> optnull = Optional.of(name);
//可以使用静态方法 ofNullable() 创建一个即可空又可非空的 Optional 对象
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull); // 输出:Optional.empty
2.2 判断值是否存在
isPresent(); isEmpty()
//可以通过方法 isPresent() 判断一个 Optional 对象是否存在,如果存在,该方法返回 true,否则返回 false——取代了 obj != null 的判断。
Optional<String> opt = Optional.of("沉默王二");
System.out.println(opt.isPresent()); // 输出:true
Optional<String> optOrNull = Optional.ofNullable(null);
System.out.println(opt.isPresent()); // 输出:false
//Java 11 后还可以通过方法 isEmpty() 判断与 isPresent() 相反的结果。
Optional<String> opt = Optional.of("沉默王二");
System.out.println(opt.isPresent()); // 输出:false
Optional<String> optOrNull = Optional.ofNullable(null);
System.out.println(opt.isPresent()); // 输出:true
2.3 非空表达式
ifPresent(),允许我们使用函数式编程的方式执行一些代码,因此,我把它称为非空表达式。如果没有该方法的话,我们通常需要先通过 isPresent() 方法对 Optional 对象进行判空后再执行相应的代码:
Optional<String> optOrNull = Optional.ofNullable(null);
if (optOrNull.isPresent()) {
System.out.println(optOrNull.get().length());
}
有了 ifPresent()
之后,情况就完全不同了,可以直接将 Lambda 表达式传递给该方法,代码更加简洁,更加直观。
Optional<String> opt = Optional.of("沉默王二");
opt.ifPresent(str -> System.out.println(str.length()));
//Java 9 后还可以通过方法 ifPresentOrElse(action, emptyAction) 执行两种结果,非空时执行 action,空时执行 emptyAction。
Optional<String> opt = Optional.of("沉默王二");
opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.println("为空"));
2.4 设置(获取)默认值
有时候,我们在创建(获取) Optional 对象的时候,需要一个默认值,orElse() 和 orElseGet() 方法就派上用场了。
orElse() 方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值得类型一致。
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("沉默王二");
System.out.println(name); // 输出:沉默王二
orElseGet() 方法与 orElse() 方法类似,但参数类型不同。如果 Optional 对象中的值为 null,则执行参数中的函数。
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(()->"沉默王二");
System.out.println(name); // 输出:沉默王二
orElseGet性能更好一点,因为无论Optional 对象中的值是不是为空,orElse都会执行,只是不返回而已。但是通常这里的逻辑不会很复杂,所以影响不大。
2.5 获取值
直观从语义上来看,get() 方法才是最正宗的获取 Optional 对象值的方法,但很遗憾,该方法是有缺陷的,因为假如 Optional 对象的值为 null,该方法会抛出 NoSuchElementException 异常。这完全与我们使用 Optional 类的初衷相悖。
public class GetOptionalDemo {
public static void main(String[] args) {
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull.get());
}
}
建议 orElseGet()
方法获取 Optional 对象的值。
2.6 filter 、map
-
filter() 方法的参数类型为 Predicate(Java 8 新增的一个函数式接口),也就是说可以将一个 Lambda 表达式传递给该方法作为条件,如果表达式的结果为 false,则返回一个 EMPTY 的 Optional 对象,否则返回过滤后的 Optional 对象。
-
map() 方法,该方法可以按照一定的规则将原有 Optional 对象转换为一个新的 Optional 对象,原有的 Optional 对象不会更改。
public class OptionalMapFilterDemo {
public static void main(String[] args) {
String password = "password";
Optional<String> opt = Optional.ofNullable(password);
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
Predicate<String> eq = pwd -> pwd.equals("password");
boolean result = opt.map(String::toLowerCase).filter(len6.and(len10 ).and(eq)).isPresent();
System.out.println(result);
}
}
总结:使用 Optional,不仅可以避免使用 Stream 进行级联调用的空指针问题;更重要的是,它提供了一些实用的方法帮我们避免判空逻辑。
如下是一些例子,演示了如何使用 Optional 来避免空指针,以及如何使用它的 fluent API 简化冗长的 if-else 判空逻辑:
@Test(expected = IllegalArgumentException.class)
public void optional() {
//通过get方法获取Optional中的实际值,很少用到
assertThat(Optional.of(1).get(), is(1));
//通过ofNullable来初始化一个null,通过orElse方法实现Optional中无数据的时候返回一个默认值
assertThat(Optional.ofNullable(null).orElse("A"), is("A"));
//OptionalDouble是基本类型double的Optional对象,isPresent判断有无数据
assertFalse(OptionalDouble.empty().isPresent());
//通过map方法可以对Optional对象进行级联转换,不会出现空指针,转换后还是一个Optional
assertThat(Optional.of(1).map(Math::incrementExact).get(), is(2));
//通过filter实现Optional中数据的过滤,得到一个Optional,然后级联使用orElse提供默认值
assertThat(Optional.of(1).filter(integer -> integer % 2 == 0).orElse(null), is(nullValue()));
//通过orElseThrow实现无数据时抛出异常
Optional.empty().orElseThrow(IllegalArgumentException::new);
}

注:Optional 判空的生成应用见 Java程序中空值/异常最佳实践
3. Map接口 Map接口 putIfAbsent / computeIfAbsent/computeIfPresent
3.1 putIfAbsent
如果给定的key不存在(或者key对应的value为null),关联给定的key和给定的value,并返回null;如果存在,返回当前值(不会把value放进去);
使用场景:如果我们要变更某个key的值,我们又不知道key是否存在的情况下,而又不希望增加key的情况使用。
demo:
// 创建一个 HashMap
HashMap<Integer, String> sites = new HashMap<>();
// 往 HashMap 添加一些元素
sites.put(1, "Google");
sites.put(2, "Runoob");
sites.put(3, "Taobao");
System.out.println("sites HashMap: " + sites);
// HashMap 不存在该key
sites.putIfAbsent(4, "Weibo");
// HashMap 中存在 Key
sites.putIfAbsent(2, "Wiki");
System.out.println("Updated Languages: " + sites);
//结果为
sites HashMap: {1=Google, 2=Runoob, 3=Taobao}
Updated sites HashMap: {1=Google, 2=Runoob, 3=Taobao, 4=Weibo}
3.2 computeIfAbsent
如果给定的 key 不存在(或者 key 对应的 value 为 null),就去计算 mappingFunction 的值;
如果 mappingFunction 的值不为 null,就把 key = value 放进去;
如果 mappingFunction 的值为 null,就不会记录该映射关系,返回值为 null;
如果计算 mappingFunction 的值的过程出现异常,再次抛出异常,不记录映射关系,返回 null;
如果存在该 key,返回的是新的 value,就算 key 对应的 value 部位 null,返回 null;(HashMap 返回的是旧 value)
default V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
map.computeIfAbsent(key, k -> new Value(f(k)));
map.computeIfAbsent(key, k -> new HashSet<V>()).add(v);
demo:
public void test() {
Map<String, List<String>> map = new HashMap<>();
// 如果key不存在,则创建新list并放入数据;key存在,则直接往list放入数据
map.computeIfAbsent("fruit", k -> new ArrayList<>()).add("apple");
map.computeIfAbsent("fruit", k -> new ArrayList<>()).add("orange");
map.computeIfAbsent("language", k -> new ArrayList<>()).add("english");
// 遍历
map.forEach((k, v) -> System.out.println(k + " " + v));
}
map.computeIfAbsent("fruit", k -> new ArrayList<>()).add("apple");
这一句的意思是
key为fruit的键值对是否存在
1、如果存在,则给当前操作的List集合添加 apple
2、如果不存在,则新创建List结构,将apple添加到List集合,再存入Map集合
putIfAbsent 和 computeIfAbsent
- 都是在 key 不存在的时候才会建立 key 和 value 的映射关系;
- putIfAbset 不论传入的 value 是否为空,都会建立映射(并不适合所有子类,例如 HashTable),而computeIfAbsent 方法,当存入value为空时,不做任何操作
- putIfAbsent返回的是旧value,当key不存在时返回null;computeIfAbsent返回的都是新的value,即使computeIfAbsent在传入的value为null时,不会新建映射关系,但返回的也是null;
3.3 computeIfPresent
key 存在并且不为空,计算 remappingFunction 的值 value;
如果 value 不为空,保存指定 key 和 value 的映射关系;
如果 value 为 null,remove(key);
如果计算 value 的过程抛出了异常,computeIfPresent 方法中会再次抛出,key 和其对应的值不会改变
default V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
demo:
// 创建一个 HashMap
HashMap<String, Integer> prices = new HashMap<>();
// 往HashMap中添加映射关系
prices.put("Shoes", 200);
prices.put("Bag", 300);
prices.put("Pant", 150);
System.out.println("HashMap: " + prices);
// 重新计算鞋加上10%的增值税后的价值
int shoesPrice = prices.computeIfPresent("Shoes", (key, value) -> value + value * 10/100);
System.out.println("Price of Shoes after VAT: " + shoesPrice);
// 输出更新后的HashMap
System.out.println("Updated HashMap: " + prices);
执行以上程序输出结果为:
HashMap: {Pant=150, Bag=300, Shoes=200}
Price of Shoes after VAT: 220
Updated HashMap: {Pant=150, Bag=300, Shoes=220}}
computeIfPresent 和 computeIfAbsent
-
这两个方法正好相反,computeIfPresent 在key存在时,才会用新的value替换oldValue
-
当传入的key存在,并且传入的value为null时,computeIfPresent 会remove(key),把传入的key对应的映射关系移除;而computeIfAbsent不论何时都不会remove();
-
computeIfPresent 只有在key存在,并且传入的value不为空的时候,返回值是value,其他情况都是返回null;
computeIfAbsent 只有在key不存在,并且传入的value不为null的时候才会返回value,其他情况都返回null;
4. 函数式接口 Lambda 表达式
函数式接口是一种只有单一抽象方法的接口,使用 @FunctionalInterface 来描述,可以隐式地转换成 Lambda 表达式。使用 Lambda 表达式来实现函数式接口,不需要提供类名和方法定义,通过一行代码提供函数式接口的实例,就可以让函数成为程序中的头等公民,可以像普通数据一样作为参数传递,而不是作为一个固定的类中的固定方法。
那,函数式接口到底是什么样的呢?java.util.function 包中定义了各种函数式接口。
比如,用于提供数据的 Supplier 接口,就只有一个 get 抽象方法,没有任何入参、有一个返回值:
@FunctionalInterfacepublic interface Supplier {
/** * Gets a result. * * @return a result */
T get();
}
我们可以使用 Lambda 表达式或方法引用,来得到 Supplier 接口的实例:
//使用Lambda表达式提供Supplier接口实现,返回OK字符串
Supplier stringSupplier = ()->"OK";
//使用方法引用提供Supplier接口实现,返回空字符串
Supplier supplier = String::new;
4.1 四大基本函数式接口
Function<T, R>
接受一个入参T,返回R类型对象,使用apply方法获取方法执行的内容
R apply(T t);
demo1:
//Function接口是输入一个数据,计算后输出一个数据。我们先把字符串转换为大写,然后通过andThen组合另一个Function实现字符串拼接
Function<String, String> upperCase = String::toUpperCase;
Function<String, String> duplicate = s -> s.concat(s);
upperCase.andThen(duplicate).apply("test");
demo2:
User user = new User(88, "bb");
String name = uft.apply(user);
System.out.println(name);
/**
* Function<T, R> lambda写法
*/
private static Function<User, String> uft = u -> u.getName();
Consumer<T>
接受一个参数T,没有返回值,使用accept方法对参数执行操作
oid accept(T t);
demo1:
//Consumer接口是消费一个数据。我们通过andThen方法组合调用两个Consumer,输出两行abcdefg
Consumer<String> println = System.out::println;
println.andThen(println).accept("abcdefg");
demo2:
User user = new User(88, "bb");
uc.accept(user);
/**
* Consumer<T> lambda写法
*/
private static Consumer<User> uc = u -> System.out.println(u.getName());
Supplier<T>
没有入参,返回T类型结果,使用get方法获取返回结果
T get();
demo1:
//Supplier是提供一个数据的接口。这里我们实现获取一个随机数
Supplier<Integer> random = ()->ThreadLocalRandom.current().nextInt();
System.out.println(random.get());
demo2:
User user1 = us.get();
System.out.println(user1.getName());
/**
* Supplier<T> lambda写法
*/
private static Supplier<User> us = () -> new User(1, "us");
Predicate<T>
接受一个入参,返回结果为true或者false,使用test方法进行测试并返回测试结果
boolean test(T t);
demo1:
//Predicate接口是输入一个参数,返回布尔值。我们通过and方法组合两个Predicate条件,判断是否值大于0并且是偶数
Predicate<Integer> positiveNumber = i -> i > 0;
Predicate<Integer> evenNumber = i -> i % 2 == 0;
assertTrue(positiveNumber.and(evenNumber).test(2));
demo2:
boolean test = up.test(user);
System.out.println(test);
/**
* Predicate<T>
*/
private static Predicate<User> up = u -> !u.getName().isEmpty();
4.2 自定义函数式接口
参见 Java 函数式编程实例
5 实体转换工具Mapstruct
MapStruct是一个代码生成器,简化了不同的Java Bean之间映射的处理,所以映射指的就是从一个实体变化成一个实体。例如我们在实际开发中,DAO层的实体和一些数据传输对象(DTO),大部分属性都是相同的,只有少部分的不同,通过mapStruct,可以让不同实体之间的转换变的简单。我们只需要按照约定的方式进行配置即可。
1.BeanUtils原理:反射,是在运行阶段,至于反射为什么慢,后续我了解再补充;
2.MapStruct原理:在编译阶段生产get、set方法,就跟我们自己写get、set一样,基本不消耗性能;
MapStruct的优点
-
相比于手动get、set,无需手写转换工具类,减轻大量的体力活
-
效率高
核心的转换逻辑并不是通过反射实现,而是通过编译时自动生成基于getter/setter 转换实现类; -
性能高
基于简单的get、set操作,效率达到最佳 -
编译时类型安全
只能映射相同名称或带映射标记的属性; -
编译时产生错误报告
如果映射不完整或映射不正确则会在编译时抛出异常,代码将无法正常运行; -
能明确查看转换的细节
编译生成的class对象可以看到详细的转换过程,方便快速定位转换过程中的问题。
5.1 入门
- 引入jar
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.1.Final</version>
</dependency>
- 实体类
实体类是开发过程少不了的,就算是用工具生成肯定也是要有的;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String name;
private int age;
private GenderEnum gender;
private Double height;
private Date birthday;
}
public enum GenderEnum {
Male("1", "男"),
Female("0", "女");
private String code;
private String name;
public String getCode() {
return this.code;
}
public String getName() {
return this.name;
}
GenderEnum(String code, String name) {
this.code = code;
this.name = name;
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StudentVO {
private String name;
private int age;
private String gender;
private Double height;
private String birthday;
}
- 创建转换Mapper
需要手写的部分就是这个Mapper的接口,编译完成后会自动生成相应的实现类
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mapping(source = "gender.name", target = "gender")
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
StudentVO student2StudentVO(Student student);
}
- test demo
public class Test {
public static void main(String[] args) {
Student student = Student.builder().name("小明").age(6).gender(GenderEnum.Male).height(121.1).birthday(new Date()).build();
System.out.println(student);
//这行代码便是实际要用的代码
StudentVO studentVO = StudentMapper.INSTANCE.student2StudentVO(student);
System.out.println(studentVO);
}
}
mapper可以进行字段映射,改变字段类型,指定格式化的方式,包括一些日期的默认处理。
也可以手动指定格式化的方法:
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mapping(source = "gender", target = "gender")
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
StudentVO student2StudentVO(Student student);
default String getGenderName(GenderEnum gender) {
return gender.getName();
}
}
5.2 List 转换
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mapping(source = "gender.name", target = "gender")
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
StudentVO student2StudentVO(Student student);
List<StudentVO> students2StudentVOs(List<Student> studentList);
}
public static void main(String[] args) {
Student student = Student.builder().name("小明").age(6).gender(GenderEnum.Male).height(121.1).birthday(new Date()).build();
List<Student> list = new ArrayList<>();
list.add(student);
List<StudentVO> result = StudentMapper.INSTANCE.students2StudentVOs(list);
System.out.println(result);
}
5.3 多对象转换到一个对象
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String name;
private int age;
private GenderEnum gender;
private Double height;
private Date birthday;
}
@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class Course {
private String courseName;
private int sortNo;
private long id;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StudentVO {
private String name;
private int age;
private String gender;
private Double height;
private String birthday;
private String course;
}
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mapping(source = "student.gender.name", target = "gender")
@Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "course.courseName", target = "course")
StudentVO studentAndCourse2StudentVO(Student student, Course course);
}
public class Test {
public static void main(String[] args) {
Student student = Student.builder().name("小明").age(6).gender(GenderEnum.Male).height(121.1).birthday(new Date()).build();
Course course = Course.builder().id(1L).courseName("语文").build();
StudentVO studentVO = StudentMapper.INSTANCE.studentAndCourse2StudentVO(student, course);
System.out.println(studentVO);
}
}
5.4 默认值
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mapping(source = "student.gender.name", target = "gender")
@Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "course.courseName", target = "course")
@Mapping(target = "name", source = "student.name", defaultValue = "张三")
StudentVO studentAndCourse2StudentVO(Student student, Course course);
}
5.5 性能
- 1.BeanUtils原理:反射,是在运行阶段。
- 2.MapStruct原理:在编译阶段生产get、set方法,就跟我们自己写get、set一样,基本不消耗性能

优先推荐使用MapStruct,其次是org.springframework.beans中的 BeanUtils;
网友评论