用Optional取代null

一、为啥需要Optional?

1 真实场景

  • 通过openai提供的chat completion接口,获取llm返回的数据。
{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "model": "gpt-3.5-turbo-0125",
  "system_fingerprint": "fp_44709d6fcb",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "\n\nHello there, how may I assist you today?",
    },
    "logprobs": null,
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21
  }
}
  • 从上述json格式的字符串中解析出模型生成的回答(answer)、prompt_tokens、completion_tokens、total_tokens。
public interface IModelResponse {
    @Nullable
    String acquireAnswer();

    @Nullable
    Integer acquirePromptTokens();

    @Nullable
    Integer acquireCompletionTokens();

    @Nullable
    default Integer acquireTotalTokens() {
        Integer promptTokens = acquirePromptTokens();
        Integer completionTokens = acquireCompletionTokens();
        if (promptTokens != null && completionTokens != null) {
            return promptTokens + completionTokens;
        } else {
            return null;
        }
    }
}

@Slf4j
@Data
public class GptResponseBody implements Serializable, IModelResponse {
    private static final long serialVersionUID = -22771207425629929L;

    private String id;
    private String object;
    private Long created;
    private String model;

    @JSONField(name = "system_fingerprint")
    private String systemFingerprint;

    private ArrayList<Choice> choices;
    private Usage usage;

    @Nullable
    @Override
    public String acquireAnswer() {
        try {
            return this.choices.get(0).message.content;
        } catch (Exception e) {
            log.error("[GptResponseBody.acquireAnswer] can't acquire answer, response body: {}", JSON.toJSONString(this), e);
            return null;
        }
    }

    @Nullable
    @Override
    public Integer acquirePromptTokens() {
        try {
            return this.usage.promptTokens;
        } catch (Exception e) {
            log.error("[GptResponseBody.acquirePromptTokens] can't acquire prompt tokens, response body: {}", JSON.toJSONString(this), e);
            return null;
        }
    }

    @Nullable
    @Override
    public Integer acquireCompletionTokens() {
        try {
            return this.usage.completionTokens;
        } catch (Exception e) {
            log.error("[GptResponseBody.acquireCompletionTokens] can't acquire completion tokens, response body: {}", JSON.toJSONString(this), e);
            return null;
        }
    }

    @Data
    private static class Choice implements Serializable {
        private static final long serialVersionUID = 5094234217473627289L;

        private Integer index;
        private Message message;
        private Object logprobs;

        @JSONField(name = "finish_reason")
        private String finishReason;

        @Data
        private static class Message implements Serializable {
            private static final long serialVersionUID = 790653659265221164L;

            private String role;
            private String content;
        }
    }

    @Data
    private static class Usage implements Serializable {
        private static final long serialVersionUID = -3782260460289668685L;

        @JSONField(name = "prompt_tokens")
        private Integer promptTokens;

        @JSONField(name = "completion_tokens")
        private Integer completionTokens;

        @JSONField(name = "total_tokens")
        private Integer totalTokens;
    }
}
class GptResponseBodyTest {
    private GptResponseBody gptResponseBody;

    @BeforeEach
    void init() {
        String responseBodyStr = "{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion\",\"created\":1677652288,\"model\":\"gpt-3.5-turbo-0125\",\"system_fingerprint\":\"fp_44709d6fcb\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\\n\\nHello there, how may I assist you today?\"},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":9,\"completion_tokens\":12,\"total_tokens\":21}}";
        gptResponseBody = JSON.parseObject(responseBodyStr, GptResponseBody.class);
    }


    @Test
    void acquireAnswer() {
        assertNotNull(gptResponseBody);
        String answer = gptResponseBody.acquireAnswer();
        assertEquals("\n\nHello there, how may I assist you today?", answer);
    }

    @Test
    void acquirePromptTokens() {
        assertNotNull(gptResponseBody);
        Integer promptTokens = gptResponseBody.acquirePromptTokens();
        assertEquals(9, promptTokens);
    }

    @Test
    void acquireCompletionTokens() {
        assertNotNull(gptResponseBody);
        Integer completionTokens = gptResponseBody.acquireCompletionTokens();
        assertEquals(12, completionTokens);
    }

    @Test
    void acquireTotalTokens() {
        assertNotNull(gptResponseBody);
        Integer totalTokens = gptResponseBody.acquireTotalTokens();
        assertEquals(21, totalTokens);
    }
}

1.1 可以改进的地方

@Nullable
@Override
public String acquireAnswer() {
    try {
        return this.choices.get(0).message.content;
    } catch (Exception e) {
        log.error("[GptResponseBody.acquireAnswer] can't acquire answer, response body: {}", JSON.toJSONString(this), e);
        return null;
    }
}
  • 之所以用try...catch...this.choices.get(0).message.content;包起来,是因为这行代码有npe的风险,并且不想写一堆的if (xxx != null) {...}
  • 实际上,面对这种场景,我们可以用Java8引入的Optional。毕竟,在大多数场景下,用try...catch...是不合适的。(这里比较合适,因为模型输出结构变化的可能性小,所以,上述代码基本不会抛异常。)

1.2 结论:面对null的检查,Java8的Optional是一个不错的选择。

二、怎么用Optional?

  • 在Java中,要使用某种功能,首先要找到提供这种功能的“接口”(不是狭义的interface)。例如,我需要将字符串反序列化为pojo,那么需要找到提供序列化功能的库(fastjson)。然后,找到提供反序列化功能的接口(JSON.parseObject(xxx))【类的方法 / 对象的方法】。
  • 因此,我首先需要一个Optional的对象。

1 怎么创建Optional对象?

1.1 Optional.empty()

Optional<GptResponseBody> bodyOpt = Optional.empty();

1.2 Optional.of(T value)

Optional<GptResponseBody> bodyOpt = Optional.of(this);
  • 不建议使用这个方法。除非百分之百确认value不为null。否则,value为null时,会抛出npe。

1.3 Optional.ofNullable(T value) 【推荐】

Optional<GptResponseBody> bodyOpt = Optional.ofNullable(this);
  • 源码:
public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}
  • 可以把bodyOpt这个Optional对象详细成一个箱子,如果value不会null,那箱子里就有东西,如果为null,那么箱子就是空的。

2 Optional提供了哪些有用的接口?

2.0 目标:改造acquireAnswer方法

@Nullable
@Override
public String acquireAnswer() {
    if (this != null) { // 这行是多余的
        if (this.choices != null) {
            if (this.choices.get(0) != null) {
                if (this.choices.get(0).message != null) {
                    if (this.choices.get(0).message.content != null) {
                        return this.choices.get(0).message.content;
                    }
                }
            }
        }
    }

    return null;
}

2.1 map:将x转换成y

  • 我们拿到了this,如果this不为null,那么就转换为this.choices。
Optional<GptResponseBody> bodyOpt = Optional.of(this);
Optional<ArrayList<Choice>> choicesOpt = bodyOpt.map(body -> body.choices);
  • 因此,我可以将acquireAnswer方法改造为:
@Nullable
@Override
public String acquireAnswer() {
    return Optional.ofNullable(this)
            .map(body -> body.choices)
            .map(choices -> choices.get(0))
            .map(choice -> choice.message)
            .map(message -> message.content)
            .orElse(null);
}

在这里插入图片描述

  • 那我希望为null时,打日志呢?
  • 这么写?(当然不开倒车啦)
if (answer != null) {
    return answer;
} else {
    log.error("[GptResponseBody.acquirePromptTokens] can't acquire prompt tokens, response body: {}", JSON.toJSONString(this));
}
  • 这么写:
return Optional.ofNullable(this)
	           .map(body -> body.choices)
	           .map(choices -> choices.get(0))
	           .map(choice -> choice.message)
	           .map(message -> message.content)
	           .orElseGet(() -> {
	               log.error("[GptResponseBody.acquirePromptTokens] can't acquire prompt tokens, response body: {}", JSON.toJSONString(this));
	               return null;
	           });
  • 在转换过程中,始终在Optional的“箱子”中。
    在这里插入图片描述
  • 例如:xxx.map(body -> body.choices)xxx如果是一个空箱子,那直接返回一个空箱子。
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

2.2 什么时候需要用到flatMap来进行转换?

  • 场景:
public class Student {
    @Getter
    private Optional<Pet> petOpt;

    public Student(Pet pet) {
        petOpt = Optional.ofNullable(pet);
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Pet {
    private String name;
}
  • 当出现Optional<Optional<T>>时,就需要flatMap了。(和Stream的flatMap用法类似)
    在这里插入图片描述
  • 使用flatMap:
@Test
public void testStudent1() {
    Student forrest = new Student(new Pet("墨墨"));
    Optional.ofNullable(forrest)
            .flatMap(Student::getPetOpt)
            .map(Pet::getName)
            .ifPresent(System.out::println);
}

在这里插入图片描述

2.3 真遇到了一个“空箱子”,但希望返回一个默认值,咋办?

2.3.1 get() 【不推荐】
  • 从箱子中取内容,一旦是一个空箱子,那么就抛异常:
@Test
public void testStudent2() {
    Student forrest = null;
    Student student = Optional.ofNullable(forrest).get();
}

java.util.NoSuchElementException: No value present
2.3.2 orElse(T other) 【不如orElseGet()】
  • 如果从箱子中取不到东西,那么就返回一个默认值。
@Test
public void testStudent3() {
   Student forrest = null;
   Student student = Optional.ofNullable(forrest).orElse(new Student(new Pet("墨墨")));
   System.out.println(student);
}
  • 但这种方式有一个缺点(猜猜下面new Student(xxx)会执行几次?):
@Test
public void testStudent3() {
   Student forrest = new Student(new Pet("大黄"));
   Student student = Optional.ofNullable(forrest).orElse(new Student(new Pet("墨墨")));
   System.out.println(student);
}

/**
create a student
create a student
Student(petOpt=Optional[Pet(name=大黄)])
*/
  • orElse(T other)类似于单例模式的恶汉式。管他用不用的上,先创建对象再说。但创建对象可能是很消耗资源的事情(CPU、内存等)。因此,最好在需要的时候再创建大对象。
2.3.3 orElseGet(Supplier<? extends T> other) 【推荐】
@Test
public void testStudent4() {
    Student forrest = new Student(new Pet("大黄"));
    Student student = Optional.ofNullable(forrest).orElseGet(() -> new Student(new Pet("墨墨")));
    System.out.println(student);
}

/**
create a student
Student(petOpt=Optional[Pet(name=大黄)]
*/
  • orElseGet(Supplier<? extends T> other)类似于单例模式的懒汉式,需要时再创建。
2.3.4 orElseThrow(Supplier<? extends X> exceptionSupplier) 【推荐】
@Test
public void testStudent5() {
    Student forrest = null;
    Student student = Optional.ofNullable(forrest).orElseThrow(() -> new RuntimeException("没有找到学生"));
    System.out.println(student);
}

/**
java.lang.RuntimeException: 没有找到学生
*/
  • 有时候,要求箱子里必须有东西,否则程序不应该继续运行了,要抛异常。
2.3.5 ifPresent(Consumer<? super T> consumer)
@Test
public void testStudent6(Consumer<? super T> consumer) {
    Student forrest = new Student(new Pet("大黄"));
    Optional.ofNullable(forrest).ifPresent(System.out::println);
}
  • 这种写法相当于:
if (xxx != null) {
	// 对xxx做一些处理(没有返回值)
}

2.4 和Stream类似,Optional也支持filter

@Test
public void testStudent9() {
    Student student = new Student(new Pet("墨墨"), 18);
    Optional.ofNullable(student)
            .filter(s -> s.getAge() == 18)
            .ifPresent(System.out::println);
}
Java中,我们经常需要进行非空判断来避免NullPointException异常的出现。在过去,我们通常会使用if语句来进行判断,但是随着Java 8版本的推出,Optional类和Stream类的引入可以更加简洁和可读地进行代码编写。 Optional类是一个容器对象,它可以容纳一个非空对象或者不包含任何对象(称为“空”)。在使用Optional类时,我们可以利用它提供的isPresent()方法来判断是否包含非空对象,如果是,则可以直接通过get()方法获取该对象。例如: ``` String str = null; Optional<String> optionalStr = Optional.ofNullable(str); if (optionalStr.isPresent()) { System.out.println(optionalStr.get()); } ``` 上述代码可以改写为: ``` String str = null; Optional.ofNullable(str).ifPresent(System.out::println); ``` Stream类是Java 8版本中引入的一个新类,它提供了一套函数式编程的API,可以对集合数据进行流式处理。在使用Stream类时,我们可以使用filter方法进行非空判断。例如: ``` List<String> list = Arrays.asList("a", "b", null, "d"); list.stream().filter(item -> item != null).forEach(System.out::println); ``` 上述代码可以改写为: ``` List<String> list = Arrays.asList("a", "b", null, "d"); list.stream().filter(Objects::nonNull).forEach(System.out::println); ``` 使用Optional类和Stream类可以更加简化和优化代码的逻辑,减少代码冗余,提高代码执行效率和可读性。但是,在实际使用时也需要注意,Optional类和Stream类的使用要适当,过度使用反而会导致代码可读性变差和效率降低。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值