美文网首页
Java8 优雅简化代码

Java8 优雅简化代码

作者: 雪飘千里 | 来源:发表于2023-03-01 15:36 被阅读0次

1. 使用 Stream 简化集合操作

Java8 Stream流操作总结

2. 使用 Optional 简化判空逻辑

空指针异常(NullPointerExceptions)是 Java 最常见的异常之一,一直以来都困扰着 Java 程序员。一方面,程序员不得不在代码中写很多null的检查逻辑,让代码看起来非常臃肿;另一方面,由于其属于运行时异常,是非常难以预判的。

为了预防空指针异常,GoogleGuava项目率先引入了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);
}
image.png

注: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一样,基本不消耗性能
image.png

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

相关文章

网友评论

      本文标题:Java8 优雅简化代码

      本文链接:https://www.haomeiwen.com/subject/bxxrldtx.html