[Java8 New Features] The most complete Optional practical tutorial in history, it's amazing!

Table of contents

1. Pre-basis

2. What is Optional

2.1 Theoretical Expansion

3. Why use Optional

3.1 Detailed Explanation of Russian Matryoshka Empty Judgment

4. Basic knowledge of Optional

4.1 Thoughts on API

5. How to use Optional correctly in work

5.1 orElseThrow

5.2 filter

5.3 orElse and orElseGet

5.4 map and flatMap

5.5 Project actual combat

combat one

Combat II

Actual Combat 3 Simplify if.else

Actual Combat 4 Solve the checkStyle problem

Combat Five Optional Improves Code Readability

Actual Combat Six Bold Refactoring of Code

Actual Combat Seven Abandoning Trinocular Operation

6. Optional operation summary

Seven, Optional wrong use


1. Pre-basis

The source code of the Optional class is widely used:
1. Four major functional interfaces
2. Lambda expressions

2. What is Optional

1. Java 8 has added a new class - Optional
2. Optional is a container for placing values ​​that may be empty, and it can handle null reasonably and gracefully.
3. The essence of Optional is that it stores a real value inside. When constructing, it directly judges whether its value is empty. 4. The
java.util.Optional<T> class is essentially a container, and the value of the container can be It is empty to indicate that a value does not exist, and it can be non-empty to indicate that a value exists.
5. The Optional class (java.util.Optional) is a container class that represents the existence or non-existence of a value. In the past, null was used to indicate the absence of a value. Now Optional can better express this concept. And can avoid null pointer exception.

2.1 Theoretical Expansion

  Monad is a programming mode for dealing with side effects. Simply put, it encapsulates some operations that may cause side effects and executes them in a specific scope to control their impact on the program. In functional programming, the Monad pattern is often used to handle some side effects, such as IO operations, exception handling, state management, etc.
Optional is a very typical Monad implementation in Java. Its main function is to avoid null pointer exceptions and encapsulate objects that may be empty, and provide a series of functional operations, such as , , and other methods, to make the code map()more filter()robust flatMap()and Elegant and safe. Just like we usually perform null judgment on null values, Optional provides a more beautiful and convenient way to avoid deep nested null judgments, and at the same time increase the readability and maintainability of the code.
  For functional programming and Monad mode, this method is very important, because as the size of the program increases, there will be more and more side effects. At this time, it becomes especially important to avoid the impact of side effects on the program. By using the Monad pattern and container types like Optional, we can better control side effects and make programs more stable and reliable.

3. Why use Optional

1. If it is used to solve the common NullPointerException in the program. But in the actual development process, many people use Optional with a little knowledge, and codes like if (userOpt.isPresent()){...} can be seen everywhere. If this is the case, I would rather see an honest null judgment, so forcing the use of Optional increases the complexity of the code.
2. This is a clear warning to prompt developers to pay attention to null values ​​here.
3. Non-explicit null judgment, when the Russian-style matryoshka null judgment occurs, the code processing is more elegant.
4. Using Optional can sometimes filter some attributes very conveniently, and its methods can be called in chains, and the methods can be combined with each other, so that we can complete complex logic with a small amount of code.
5. Prevent null pointer (NPE), simplify if...else... judgment, reduce code cyclomatic complexity
6. The reason why Optional can solve the problem of NPE is because it clearly tells us that it does not need to be judged null. It is like a road sign at a crossroads, clearly telling you where to go
7. A long time ago, in order to avoid NPE, we would write a lot of codes like if (obj != null) {}, sometimes forget to write, it may An NPE occurs, causing an online fault. In the Java technology stack, if someone's code has NPE, it is very likely to be laughed at. This exception is considered by many people to be a low-level error. The emergence of Optional can make it easier for everyone to avoid the probability of being ridiculed because of low-level mistakes.
8. The first is to change our traditional way of judging nulls (actually, it wraps up a layer for us, and writes the code for judging nulls for us), and uses functional programming and declarative programming to check and process basic data . The second is that declarative programming is more friendly to people who read the code.

3.1 Detailed Explanation of Russian Matryoshka Empty Judgment

  Manual if (obj!=null) judgment is the most versatile and reliable, but I am afraid of the Russian matryoshka-style if judgment.
An example of a situation:
In order to obtain: Province (Province) → City (Ctiy) → District (District) → Street (Street) → Road Name (Name)
As a "rigorous and conscientious" back-end development engineer, if manually Null pointer protection, we will inevitably write like this:

public String getStreetName( Province province ) {
    if( province != null ) {
        City city = province.getCity();
        if( city != null ) {
            District district = city.getDistrict();
            if( district != null ) {
                Street street = district.getStreet();
                if( street != null ) {
                    return street.getName();
                }
            }
        }
    }
    return "未找到该道路名";
}
为了获取到链条最终端的目的值,直接链式取值必定有问题,因为中间只要某一个环节的对象为 null,则代码一定会炸,并且抛出 NullPointerException异常,然而俄罗斯套娃式的 if判空实在有点心累。
Optional接口本质是个容器,你可以将你可能为 null的变量交由它进行托管,这样我们就不用显式对原变量进行 null值检测,防止出现各种空指针异常。
Optional语法专治上面的俄罗斯套娃式 if 判空,因此上面的代码可以重构如下:

public String getStreetName( Province province ) {
    return Optional.ofNullable( province )
            .map( i -> i.getCity() )
            .map( i -> i.getDistrict() )
            .map( i -> i.getStreet() )
            .map( i -> i.getName() )
            .orElse( "未找到该道路名" );
}

pretty! The nested if/else is judged to be empty!
Explain the execution process:
ofNullable(province): It constructs an Optional instance in a smart packaging way, whether the province is null or not. If it is null, return a singleton empty Optional object; if not null, return an Optional wrapper object
map(xxx): This function mainly performs value conversion, if the value of the previous step is not null, then call the specific method in the brackets Perform value conversion; otherwise, directly return the singleton Optional wrapper object
orElse(xxx) in the previous step: It is well understood that it is called when the value conversion of a certain step above is terminated, and a final default value is given

4. Basic knowledge of Optional

Common methods of the Optional class:

Optional.of(T t) : Creates an Optional instance.

Optional.empty() : Create an empty Optional instance.

Optional.ofNullable(T t): If t is not null, create an Optional instance, otherwise create an empty instance.

isPresent() : Determine whether a value is contained.

orElse(T t) : If the calling object contains a value, return that value, otherwise return t.

orElseGet(Supplier s) : If the calling object contains a value, return that value, otherwise return the value obtained by s.

map(Function f): If there is a value to process it, and return the processed Optional, otherwise return Optional.empty().

flatMap(Function mapper): Similar to map, the return value must be Optional.

4.1 Thoughts on API

1.of(T value)
If something exists, then it naturally has the value of existence. When we are running, we don't want to hide NullPointerException.
Instead, report immediately, in which case the Of function is used. But I have to admit that such scenes are really rare. I have only used this function in writing junit test cases.

2. get()
Intuitive From the semantic point of view, the get() method is the most authentic method to obtain the value of the Optional object,
but unfortunately, this method is flawed, because if the value of the Optional object is null, the method will A NoSuchElementException is thrown. This completely defeats the purpose of using the Optional class in the first place.

5. How to use Optional correctly in work

5.1 orElseThrow

The orElseThrow() method does not return a default value when it encounters a value that does not exist, but throws an exception.

public void validateRequest(String requestId) {
    Optional.ofNullable(requestId)
            .orElseThrow(() -> new IllegalArgumentException("请求编号不能为空"));
    // 执行后续操作
}
Optional<User> optionalUser = Optional.ofNullable(null);
User user = optionalUser.orElseThrow(() -> new RuntimeException("用户不存在"));

// 传入 null 参数,获取一个 Optional 对象,并使用 orElseThrow 方法
    try {
        Optional optional2 = Optional.ofNullable(null);
        Object object2 = optional2.orElseThrow(() -> {
                    System.out.println("执行逻辑,然后抛出异常");
                    return new RuntimeException("抛出异常");
                }
        );
        System.out.println("输出的值为:" + object2);
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }

5.2 filter

Receives a functional interface, and returns an Optional object if it conforms to the interface, otherwise returns an empty Optional object.
For example, we need to filter out people whose age range is between 25 and 35 years old. Before Java8, we need to create a method as follows to detect whether each person's age range is between 25 and 35 years old.

public boolean filterPerson(Peron person){
    boolean isInRange = false;
    if(person != null && person.getAge() >= 25 && person.getAge() <= 35){
        isInRange =  true;
    }
    return isInRange;
}

public boolean filterPersonByOptional(Peron person){
     return Optional.ofNullable(person)
       .map(Peron::getAge)
       .filter(p -> p >= 25)
       .filter(p -> p <= 35)
       .isPresent();
}
使用Optional看上去就清爽多了,这里,map()仅仅是将一个值转换为另一个值,并且这个操作并不会改变原来的值。

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

5.3 orElse and orElseGet

Conclusion: when optional.isPresent() == false, there is no difference between orElse() and orElseGet();
and when optional.isPresent() == true, orElse will always call the subsequent function whether you need it or not.

If the method is not purely computational, use Optional's orElse(T);
if it interacts with the database or calls remotely, you should use orElseGet(Supplier).
It is recommended to use orElseGet. It is forbidden to use orElse when there are some complex operations, remote calls, disk io and other expensive actions.
Reason: orElse will still execute when value is not empty.

public class GetValueDemo {
    public static String getDefaultName() {
        System.out.println("Getting Default Name");
        return "binghe";
    }

    public static void main(String[] args) {
/*        String text = null;
        System.out.println("Using orElseGet:");
        String defaultText = Optional.ofNullable(text).orElseGet(GetValueDemo::getDefaultName);
        assertEquals("binghe", defaultText);
        System.out.println("Using orElse:");
        defaultText = Optional.ofNullable(text).orElse(GetValueDemo.getDefaultName());
        assertEquals("binghe", defaultText);*/

        // TODO: 2023/5/13 重点示例
        String name = "binghe001";

        System.out.println("Using orElseGet:");
        String defaultName = Optional.ofNullable(name).orElseGet(GetValueDemo::getDefaultName);
        assertEquals("binghe001", defaultName);

        System.out.println("Using orElse:");
        defaultName = Optional.ofNullable(name).orElse(getDefaultName());
        assertEquals("binghe001", defaultName);
    }  
}    

The running results are shown below.
Using orElseGet:
Using orElse:
Getting default name...
You can see that when the orElseGet() method is used, the getDefaultName() method is not executed because the Optional contains a value, and when using orElse, it is executed as usual. So you can see that when the value exists, orElse creates one more object than orElseGet. If there is network interaction when creating an object, the overhead of system resources will be relatively large, which is something we need to pay attention to.

5.4 map and flatMap

        String len = null;
        Integer integer = Optional.ofNullable(len)
                .map(s -> s.length())
                .orElse(0);
        System.out.println("integer = " + integer);


        Person person = new Person("evan", 18);
        Optional.ofNullable(person)
                .map(p -> p.getName())
                .orElse("");

        Optional.ofNullable(person)
                .flatMap(p -> Optional.ofNullable(p.getName()))
                .orElse("");

Note: The method getName returns an Optional object. If we use map, we need to call the get() method again, but we don't need to use flatMap().

5.5 Project actual combat

combat one

public class OptionalExample {
    /**
     * 测试的 main 方法
     */
    public static void main(String[] args) {
        // 创建一个测试的用户集合
        List<User> userList = new ArrayList<>();

        // 创建几个测试用户
        User user1 = new User("abc");
        User user2 = new User("efg");
        User user3 = null;

        // 将用户加入集合
        userList.add(user1);
        userList.add(user2);
        userList.add(user3);

        // 创建用于存储姓名的集合
        List<String> nameList = new ArrayList();
        List<User> nameList03 = new ArrayList();
        List<String> nameList04 = new ArrayList();
        // 循环用户列表获取用户信息,值获取不为空且用户以 a 开头的姓名,
        // 如果不符合条件就设置默认值,最后将符合条件的用户姓名加入姓名集合
/*
        for (User user : userList) {
            nameList.add(Optional.ofNullable(user).map(User::getName).filter(value -> value.startsWith("a")).orElse("未填写"));
        }
*/

        // 输出名字集合中的值
/*        System.out.println("通过 Optional 过滤的集合输出:");
        System.out.println("nameList.size() = " + nameList.size());
        nameList.stream().forEach(System.out::println);*/


/*        Optional.ofNullable(userList)
                .ifPresent(u -> {
                    for (User user : u) {
                        nameList04.add(Optional.ofNullable(user).map(User::getName).filter(f -> f.startsWith("e")).orElse("无名"));
                    }
                });*/

        Optional.ofNullable(userList)
                .ifPresent(u -> {
                   u.forEach(m->{
                       Optional<String> stringOptional = Optional.ofNullable(m).map(User::getName).filter(f -> f.startsWith("a"));
                       stringOptional.ifPresent(nameList04::add);
                   });
                });
        System.out.println("nameList04.size() = " + nameList04.size());
        nameList04.forEach(System.err::println);


        Optional.ofNullable(userList).ifPresent(nameList03::addAll);
        System.out.println("nameList03.size() = " + nameList03.size());
        nameList03.stream().forEach(System.err::println);

    }

}

Combat II

以前写法
public String getCity(User user)  throws Exception{
        if(user!=null){
            if(user.getAddress()!=null){
                Address address = user.getAddress();
                if(address.getCity()!=null){
                    return address.getCity();
                }
            }
        }
        throw new Excpetion("取值错误"); 
    }


    public String getCity(User user) throws Exception{
    return Optional.ofNullable(user)
                   .map(u-> u.getAddress())
                   .map(a->a.getCity())
                   .orElseThrow(()->new Exception("取指错误"));
}

Actual Combat 3 Simplify if.else

以前写法
public User getUser(User user) throws Exception{
    if(user!=null){
        String name = user.getName();
        if("zhangsan".equals(name)){
            return user;
        }
    }else{
        user = new User();
        user.setName("zhangsan");
        return user;
    }
}

java8写法
public User getUser(User user) {
    return Optional.ofNullable(user)
                   .filter(u->"zhangsan".equals(u.getName()))
                   .orElseGet(()-> {
                        User user1 = new User();
                        user1.setName("zhangsan");
                        return user1;
                   });
}

Actual Combat 4 Solve the checkStyle problem


BaseMasterSlaveServersConfig smssc = new BaseMasterSlaveServersConfig();
if (clientName != null) {
    smssc.setClientName(clientName);
}
if (idleConnectionTimeout != null) {
    smssc.setIdleConnectionTimeout(idleConnectionTimeout);
}
if (connectTimeout != null) {
    smssc.setConnectTimeout(connectTimeout);
}
if (timeout != null) {
    smssc.setTimeout(timeout);
}
if (retryAttempts != null) {
    smssc.setRetryAttempts(retryAttempts);
}
if (retryInterval != null) {
    smssc.setRetryInterval(retryInterval);
}
if (reconnectionTimeout != null) {
    smssc.setReconnectionTimeout(reconnectionTimeout);
}
if (password != null) {
    smssc.setPassword(password);
}
if (failedAttempts != null) {
    smssc.setFailedAttempts(failedAttempts);
}
// ...后面还有很多这种判断,一个if就是一个分支,会增长圈复杂度


改造后:
Optional.ofNullable(clientName).ifPresent(smssc::setClientName);
Optional.ofNullable(idleConnectionTimeout).ifPresent(smssc::setIdleConnectionTimeout);
Optional.ofNullable(connectTimeout).ifPresent(smssc::setConnectTimeout);
Optional.ofNullable(timeout).ifPresent(smssc::setTimeout);
Optional.ofNullable(retryAttempts).ifPresent(smssc::setRetryAttempts);
Optional.ofNullable(retryInterval).ifPresent(smssc::setRetryInterval);
Optional.ofNullable(reconnectionTimeout).ifPresent(smssc::setReconnectionTimeout);
// ...缩减为一行,不但减少了圈复杂度,而且减少了行数

Combat Five Optional Improves Code Readability

Traditional operation:

public class ReadExample {
    //    举个栗子:你拿到了用户提交的新密码,你要判断用户的新密码是否符合设置密码的规则,比如长度要超过八位数,然后你要对用户的密码进行加密。
    private static String newPSWD = "12345679";
    public static void main(String[] args) throws Exception {
        // 简单的清理
        newPSWD = ObjectUtil.isEmpty(newPSWD) ? "" : newPSWD.trim();
        // 是否符合密码策略
        if (newPSWD.length() <= 8) throw new Exception("Password rules are not met: \n" + newPSWD);
        // 加密
        //将 MD5 值转换为 16 进制字符串
        try {
            final MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(newPSWD.getBytes(StandardCharsets.UTF_8));
            newPSWD = new BigInteger(1, md5.digest()).toString(16);
        } catch (
                NoSuchAlgorithmException e) {
            System.out.println("Encryption failed");
        }
        System.out.println("We saved a new password for the user: \n" + newPSWD);
    }
}

Optimized version:

优化一:
public class BetterReadExample {
    //    举个栗子:你拿到了用户提交的新密码,你要判断用户的新密码是否符合设置密码的规则,比如长度要超过八位数,然后你要对用户的密码进行加密。
    private static String newPSWD = "888888888";
    public static void main(String[] args) throws Exception {

        Function<String, String> md = (o) -> {
            try {
                final MessageDigest md5;
                md5 = MessageDigest.getInstance("MD5");
                md5.update(o.getBytes(StandardCharsets.UTF_8));
                return new BigInteger(1, md5.digest()).toString(16);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("Encryption failed");
            }
        };


        String digestpwd;
        digestpwd = Optional.ofNullable(newPSWD)
                            .map(String::trim)
                            .filter(f -> f.length() > 8)
                            .map(md)
                            .orElseThrow(() -> new RuntimeException("Incorrect saving new password"));
        System.err.println("digestpwd = " + digestpwd);

    }
}

优化二:
/**
 *增加可读性
 */
public class BetterReadExample02 {
    //    举个栗子:你拿到了用户提交的新密码,你要判断用户的新密码是否符合设置密码的规则,比如长度要超过八位数,然后你要对用户的密码进行加密。
    private static String newPSWD = "888888888";

    //清除
    private static String clean(String s){
        return s.trim();
    }

    private static boolean filterPw(String s){
        return s.length()>8;
    }

    private static RuntimeException myREx() {
        return new RuntimeException("Incorrect saving new password");
    }

    public static void main(String[] args) throws Exception {
        //项目实战中,把main方法里面的代码再抽出一个独立方法
        Function<String, String> md = (o) -> {
            try {
                final MessageDigest md5;
                md5 = MessageDigest.getInstance("MD5");
                md5.update(o.getBytes(StandardCharsets.UTF_8));
                return new BigInteger(1, md5.digest()).toString(16);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("Encryption failed");
            }
        };

        String digestpwd;
        digestpwd = Optional.ofNullable(newPSWD)
                            .map(BetterReadExample02::clean)
                            .filter(BetterReadExample02::filterPw)
                            .map(md)
                            .orElseThrow(BetterReadExample02::myREx);
        System.err.println("digestpwd = " + digestpwd);

    }
}

Actual Combat Six Bold Refactoring of Code

//1. map 示例
if ( hero != null){
   return "hero : " + hero.getName() + " is fire...";
 } else { 
   return "angela";
 }
 //重构成
 String heroName = hero
 .map(this::printHeroName)
 .orElseGet(this::getDefaultName);

public void printHeroName(Hero dog){
   return  "hero : " + hero.getName() + " is fire...";
}
public void getDefaultName(){
   return "angela";
}

//2. filter示例
Hero hero = fetchHero();
if(hero != null && hero.hasBlueBuff()){
  hero.fire();
}

//重构成
Optional<Hero> optionalHero = fetchHero();
optionalHero
 .filter(Hero::hasBlueBuff)
 .ifPresent(this::fire);

Actual Combat Seven Abandoning Trinocular Operation

//第一种判空
if (Objects.notNull(taskNode.getFinishTime())) {
  taskInfoVo.set(taskNode.getFinishTime().getTime());
}
//第二种判空 保留builder模式
TaskInfoVo
.builder()
.finishTime(taskNode.getFinishTime() == null ? null : taskNode.getFinishTime().getTime())
.build()));

//第三种判空
public Result<TaskInfoVo> getTaskInfo(String taskId){
  TaskNode taskNode = taskExecutor.getByTaskId(String taskId);
  //返回任务视图
  TaskInfoVo taskInfoVo = TaskInfoVo
                      .builder()
                      .taskName(taskNode.getName())
                      .finishTime(Optional.ofNullable(taskNode.getFinishTime()).map(date ->date.getTime()).orElse(null))
             .user(taskNode.getUser())
                     .memo(taskNode.getMemo())
                     .build()));;

  return Result.ok(taskInfoVo);
}

6. Optional operation summary

The reason why NPE is annoying is that as long as NPE occurs, we can solve it. But once it occurs, it is already an afterthought, and there may have been an online failure. But in the Java language, NPE is very easy to appear. Optional provides a template method to effectively and efficiently avoid NPE.

Next, let’s summarize the above usage:
Optional is a wrapper class, immutable, and not serializable. There is
no public constructor, and you need to use the of and ofNullable methods to create it.
An empty Optional is a singleton, and it all refers to Optional.EMPTY
. To get the value of Optional, you can use get, orElse, orElseGet, orElseThrow

In addition, there are some practical suggestions:
before using the get method, the isPresent check must be used. But before using isPresent, first think about whether orElse, orElseGet and other methods can be used instead.
orElse and orElseGet, orElseGet is preferred. This is lazy calculation.
Optional should not be used as a parameter or class attribute, but can be used as a return value. Try to
extract the function parameters of map and filter as a separate method, so as to maintain chain calls and
not assign null to Optional. Although Optional supports null values, do not explicitly pass null to Optional and
try to avoid using Optional.get()
. When the result is not sure whether it is null, and the result needs to be processed in the next step, use Optional;
try as much as possible in classes and collections Do not use Optional as a basic element;
try not to pass Optional in method parameters;
do not use Optional as a parameter of a Java Bean Setter method
because Optional is not serializable and reduces readability.
Do not use Optional as the type of the Java Bean instance field
for the same reason as above.

Seven, Optional wrong use

1. Use in POJO

public class User {
    private int age;
    private String name;
    private Optional<String> address;
}

This way of writing will bring troubles to serialization. Optional itself does not implement serialization, and the existing JSON serialization framework does not provide support for this.

2. It is used in the injected attributes.
It is estimated that fewer people will use this writing method, but it does not rule out those with brains.

public class CommonService {
    private Optional<UserService> userService;
    public User getUser(String name) {
        return userService.ifPresent(u -> u.findByName(name));
    }
}

First of all, dependency injection is mostly under the framework of spring, and it is very convenient to use @Autowired directly. But if the above writing method is used, if the userService set fails, the program should terminate and report an exception, not silently, so that it seems that there is no problem.

3. Directly use isPresent() to perform if check
This directly refers to the above example. There is no difference between using if to judge and writing before 1.8. Instead, the return value is wrapped with a layer of Optional, which increases the complexity of the code and does not bring any real benefits. In fact, isPresent() is generally used at the end of stream processing to determine whether the conditions are met.

list.stream()
    .filer(x -> Objects.equals(x,param))
    .findFirst()
    .isPresent()

4. Using Optional in method parameters
Before we use something, we have to understand what problem this thing was born to solve. To put it bluntly, Optional is to express nullability. If the method parameter can be empty, why not overload it? Including the use of constructors as well. The overloaded business expression is clearer and more intuitive.

//don't write method like this
public void getUser(long uid,Optional<Type> userType);

//use Overload
public void getUser(long uid) {
    getUser(uid,null);
}
public void getUser(long uid,UserType userType) {
    //doing something
}   

5. Directly using Optional.get
Optional will not help you make any empty judgments or exception handling. If you use Optional.get() directly in the code, it is as dangerous as not making any empty judgments. This may appear in the so-called rush to go online, rush to deliver, and you are not very familiar with Optional, so you can use it directly. Let me say one more thing here, and some people may ask: Party A/business is in a hurry, and there are so many demands, how can I give him time to optimize? Because I have encountered it in real work, but the two are not contradictory, because the difference in the number of lines of code is not big, as long as I keep learning, it is something that comes at hand.

If it is helpful to you, please like it, hehe!!

Guess you like

Origin blog.csdn.net/quanzhan_King/article/details/130717621