一、为啥需要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) ;
}
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) ;
}
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) ;
}
有时候,要求箱子里必须有东西,否则程序不应该继续运行了,要抛异常。
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 ) {
}
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 ) ;
}