高薪程序员&面试题精讲系列17之Java代码中如果有太多if语句该如何优化?

2021/11/12 9:40:23

本文主要是介绍高薪程序员&面试题精讲系列17之Java代码中如果有太多if语句该如何优化?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一. 今日题目

我们在面试时,面试官会从各个方面考察我们到底有没有参与过项目,既会考察我们对某个技能的掌握和理解情况,也会考察我们的编程习惯和技巧。比如,有这么一道面试题:

如果你的项目代码中,有大量的if/else语句,你有什么优化解决方案?

二. 题目剖析

我们在平时开发写的代码中,if-else判断语句基本是必不可少的。当我们只有一两层判断语句嵌套的时候其实还好,但是当我们过度地、不必要地使用 if...else语句,就会对代码的可读性、可扩展性造成负面影响。另外如果判断语句越来越多,后期进行项目维护也会比较困难,对于后面接手项目的人来说,也是一个很头疼的问题。

所以去除代码中过多的if...else语句,就是对程序员软件重构、设计模式、面向对象设计、架构模式、数据结构等多方面技术综合运用能力的反映了。所以我们的代码中要合理使用 if...else,既不能没有,也不能过度。这些对某个技术的综合、合理地运用,都需要程我们序员在工作中不断的摸索总结。

这也是这道面试题的考察目的!

三. 存在问题

1. 示例代码

壹哥 先给各位展示如下一段代码,请回忆一下,在你之前的项目中,有没有如下风格的代码,在一个类或方法中,有大量的if...else if...else..的代码。

if (condition1) {
    doSomeThing1();
} else if (condition2) {
    doSomeThing2();
} else if (condition3) {
    doSomeThing3(); 
} else if (condition4) {
    doSomeThing4();
} else {
    doSomeThing5(); 
}...

这样的代码,就是if与else的堆叠,功能当然可以实现,就是代码看起来总归是怪怪的。据说某大厂中,某些复杂的业务就是这样if...else...嵌套来嵌套去的。对于后面项目维护改造来说,这样的代码看起来真的是一个让人头疼恶心的“屎山屑海”。

2. 问题原因

那么为什么会出现上面这样的代码呢?其实出现这种情况的原因有很多,比如

  • 设计不够完善;
  • 需求考虑不完善
  • 开发人员变动等。

其实基本上就是前期迭代懒得优化,来一个需求,就加一个if,再来一个需求,就再加一个if。久而久之,就成了if...else叠罗汉了。

四. 解决方案

那么我们该如何解决代码中过多的if...else语句呢?可以考虑采用以下解决方案。

  1. 适时的return;
  2. 用switch或三目运算符替换;
  3. 策略模式:多态、函数式编程、枚举;
  4. 使用Optional;
  5. 使用Assert;
  6. 表驱动模式;
  7. 职责链模式;
  8. 注解驱动;
  9. 事件驱动;
  10. 有限状态机

1. 适时的return

比如我们有以下的代码:

if (condition) {
	// do something
} else {   
	return xxx;
}

其实上面的代码完全可以按如下方式优化:

if (!condition) {
	return xxx;
}

// do something

我们先判断!condition,将return语句放在前面,这样就可以去掉else语句了,相对于上面的语句代码清晰了不少。

2. 用switch或三目运算符替换

对符合特定条件的if语句,可以考虑选择switch或三目运算符进行替换,这里不做详细展示描述。

3. 策略模式

3.1 策略模式概念

策略模式作为一种软件设计模式,指对象有某个行为,但在不同的场景中,该行为有不同的实现算法。在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改,这种类型的设计模式属于行为型模式。策略模式由以下3种角色组成:

  1. 抽象策略角色(Strategy):策略类,通常由一个接口或者抽象类实现。
  2. 具体策略角色(ConcreteStrategy):包装了相关的算法和行为。
  3. 环境角色(Context):持有一个策略类的引用,最终给客户端调用。

3.2 策略模式使用场景

  1. 针对同一类型的问题有多种处理方式,仅仅是具体的处理方法有差别时。
  2. 出现同一抽象类有多个子类,而又需要使用 if-else 或者 switch-case 来选择具体子类时。

3.3 策略模式具体案例

比如有这么一个开发场景,我们要根据不同的性别执行不同的操作,类似的这种场景是很常见的。通常的实现如下:

if ("man".equals(strategy)) {   
	// 执行男人相关操作 
} else if ("woman".equals(strategy)) {   
	// 执行女人相关操作
} else if ("other".equals(strategy)) {   
	// 执行其他操作
}

对于以上的代码,我们可以采用以下策略进行优化。

3.3.1 以多态的策略优化模式

首先定义一个接口类Strategy。

public interface Strategy {

    void run() throws Exception;

}

然后写若干个实现类:

//男人的策略实现类
@Slf4j
public class ManStrategy implements Strategy {

    @Override
    public void run() throws Exception {
        // 快速男人的逻辑
        log.debug("执行男人相关的逻辑...");
    }

}

//女人的策略实现类
@Slf4j
public class WomanStrategy implements Strategy {

    @Override
    public void run() throws Exception {
        // 快速女人的逻辑
        log.debug("执行女人相关的逻辑...");
    }

}

//其他人的策略实现类
@Slf4j
public class OtherStrategy implements Strategy {

    @Override
    public void run() throws Exception {
        // 快速其他的逻辑
        log.debug("执行其他相关的逻辑...");
    }

}

具体使用简单示例:

public class StrategyTest {

    public static void main(String[] args) {
        try {
            Strategy strategy = initMap("man");
            strategy.run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //初始化Map,获取某个性别策略
    private static Strategy initMap(String key) {
        //使用简单示例
        HashMap<String, Strategy> map = new HashMap<>();
        map.put("man", new ManStrategy());
        map.put("woman", new WomanStrategy());
        map.put("other", new OtherStrategy());
        return map.get(key);
    }

}

该方案的缺点:

这种优化方案有一个弊端:为了能够快速拿到对应的实现策略,需要一个map对象来保存实现策略,当添加一个新策略的时候,还需要手动添加到这个map中,容易被忽略

3.3.2 以函数式编程的策略优化模式

我们可以对上面的代码进行稍作修改,采用Java8里的函数式编程方法来简化代码。

首先定义一个interface接口。

public interface Strategy {

    void run() throws Exception;
}

然后使用函数式编程进行代码的操作。

@Slf4j
public class StrategyTest {

    public static void main(String[] args) {
        //使用简单示例
        HashMap<String, Strategy> map = new HashMap<>();
        map.put("man", () -> log.debug("执行男人相关的逻辑..."));
        map.put("woman", () -> log.debug("执行女人相关的逻辑..."));
        map.put("other", () -> log.debug("执行其他人相关的逻辑..."));

        try {
            map.get("woman").run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

3.3.3 以枚举的策略优化模式

定义一个枚举策略类。

@Slf4j
public enum Strategy {

    //男人状态
    MAN(0) {
        @Override
        void run() {
            //执行男人相关操作
            log.debug("执行男人相关的逻辑");
        }
    },
    //女人状态
    WOMAN(1) {
        @Override
        void run() {
            //执行女人相关操作
            log.debug("执行女人相关的逻辑");
        }
    },
    //其他状态
    OTHER(2) {
        @Override
        void run() {
            //执行其他相关操作
            log.debug("执行其他相关的逻辑");
        }
    };

    abstract void run();

    public int statusCode;

    Strategy(int statusCode) {
        this.statusCode = statusCode;
    }

}

简单使用示例:

public static void main(String[] args) {
        try {
            //简单使用示例
            String param = String.valueOf(Strategy.WOMAN);
            Strategy strategy = Strategy.valueOf(param);
            strategy.run();
        } catch (Exception e) {
            e.printStackTrace();
        }
}

3.4 策略模式的优点

  1. 策略模式的 Strategy 类(或接口等)为 Context 定义了一系列的可供重用的算法或行为,继承有助于析取出这些算法中的公共功能。
  2. 使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。
  3. 简化了单元测试。因为每个算法都有自己的具体的实现类,可以通过自己的接口单独测试。

3.5 策略模式的缺点

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
  2. 策略模式造成很多的策略类,每个具体策略类都会产生一个新类。

4. Optional

Java 代码中的一部分 if...else 是由非空检查导致的。因此,降低这部分带来的 if...else 也就能降低整体的 if...else 的个数。

Java 从 8 开始引入了 Optional 类,用于表示可能为空的对象。这个类提供了很多方法,用于相关的操作,可以用于消除 if...else,开源框架 Guava 和 Scala 语言也提供了类似的功能。该方式适用于有较多非空判断的 if...else语句中。

示例代码:

String str = "Hello World!";

if (str != null) {
    System.out.println(str);
} else {
    System.out.println("Null");
}

利用Optional优化之后:

Optional<String> optional = Optional.of("Hello World!");
optional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));

5. Assert 模式

assert的用法像是一种"契约式编程"。顾名思义,如果程序的运行不满足某种特定条件或者输入未遵守某个约定,则程序将会终止执行。面对异常情况的处理,我们通常会使用if来做逻辑处理,以实现程序的健壮性。面对异常情况,if的做法显的更温柔,而assert则简单粗暴。但使用if语句会造成很大的累赘,如下所示:

if(假设成立){
    程序正常运行;
}else{
    报错&&终止程序!(避免由程序运行引起更大的错误)  
}

异常的发生毕竟是少数案例,如果处处使用if语句来做判断处理,那么就会有N多个 if 语句,甚至会出现一个if 语句的括号从文件头到文件尾。在大多数情况下,意外的发生只是属于偶然性事件,所以这里有了assert断言,如下所示:

//断言1结果为true,则继续往下执行
assert true;
System.out.println("断言1没有问题,Let`s Go!");

try{
    //断言2结果为false,程序终止
    assert false : "断言失败,此表达式的信息将会在抛出异常的时候输出!";
    System.out.println("断言2出现问题,Stop the World!");
}catch (AssertionError err){
    System.out.println(err.getMessage());
}

6. 表驱动模式

对于逻辑表达模式固定的 if...else 代码,可以通过某种映射关系,将逻辑表达式用表格的方式表示,也就是从表里查询信息,来找到某个输入所对应的处理逻辑函数,使用这个处理函数进行运算。该方式适用于逻辑表达模式固定的 if...else语句。

例如下面的代码:

if (param.equals(value1)) {
    doAction1(someParams);
}else if (param.equals(value2)) {
    doAction2(someParams);
}else if (param.equals(value3)) {
    doAction3(someParams);
}

可以利用Java 8 的 Lambda 和 Functional Interface来进行重构。

//这里泛型 ? 是为了方便演示,实际开发时可替换为我们需要的真正类型
Map<?, Function<?> action> actionMappings = new HashMap<>(); 
// When init
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
 
// 省略 null 判断
actionMappings.get(param).apply(someParams);

7. 职责链模式

当 if...else 中的条件表达式灵活多变,无法将条件中的数据抽象为表格并用统一的方式进行判断时,这时可以把对条件的判断权交给每个功能组件,并用方法链的形式将这些组件串联起来,形成完整的功能。该方式适用于条件表达式灵活多变,没有统一的形式。

职责链的模式在开源框架的 Filter、Interceptor 功能的实现中可以见到很多。下面看一下通用的使用模式:

public void handle(request) {
    if (handlerA.canHandle(request)) {
        handlerA.handleRequest(request);
    } else if (handlerB.canHandle(request)) {
        handlerB.handleRequest(request);
    } else if (handlerC.canHandle(request)) {
        handlerC.handleRequest(request);
    }
}

重构代码:

public void handle(request) {
  handlerA.handleRequest(request);
}
 
public abstract class Handler {
    
  protected Handler next;
    
  public abstract void handleRequest(Request request);
    
  public void setNext(Handler next) { this.next = next; }
}
 
public class HandlerA extends Handler {
  public void handleRequest(Request request) {
    if (canHandle(request)) doHandle(request);
    else if (next != null) next.handleRequest(request);
  }
}

8. 注解驱动

通过 Java 注解(或其它语言的类似机制) 定义执行某个方法的条件。在程序执行时,通过对比入参与注解中定义的条件是否匹配,再决定是否调用此方法。具体实现时,可以采用表驱动或职责链的方式实现。适用于条件分支很多,对程序扩展性和易用性均有较高要求的场景。通常是某个系统中经常遇到新需求的核心功能。

9. 事件驱动

通过关联不同的事件类型和对应的处理机制,来实现复杂的逻辑,同时达到解耦的目的。从理论角度讲,事件驱动可以看做是表驱动的一种,但从实践角度讲,事件驱动和前面提到的表驱动有多处不同。具体来说:

  1. 表驱动通常是一对一的关系,事件驱动通常是一对多;
  2. 表驱动中,触发和执行通常是强依赖;事件驱动中,触发和执行是弱依赖。

正是上述两者不同,导致了两者适用场景的不同。具体来说,事件驱动可用于如订单支付完成触发库存、物流、积分等功能。

10. 有限状态机

有限状态机通常被称为状态机(无限状态机这个概念可以忽略)。先引用维基百科上的定义:

有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限状态以及在这些状态**之间的转移和动作等行为的数学模型。

其实,状态机也可以看做是表驱动的一种,其实就是当前状态和事件两者组合与处理函数的一种对应关系。当然,处理成功之后还会有一个状态转移处理。

虽然现在互联网后端服务都在强调无状态,但这并不意味着不能使用状态机这种设计。其实,在很多场景中,如协议栈、订单处理等功能中,状态机有这其天然的优势。因为这些场景中天然存在着状态和状态的流转。

五. 总结

这道题目考察的其实是我们的编程习惯和思维,要求我们不仅要完成功能,还要保证代码风格的优美,只要我们开发时对代码细节多思考,这些目标还是容易实现的。最后 壹哥 再给各位总结一下精简代码中大量if语句的方式。

  1. 适时的return;
  2. 用switch或三目运算符替换;
  3. 策略模式:多态、函数式编程、枚举;
  4. 使用Optional;
  5. Assert;
  6. 表驱动模式;
  7. 职责链模式;
  8. 注解驱动;
  9. 事件驱动;
  10. 有限状态机

请各位在项目中结合具体需求,合理选择某种或某些策略使用。



这篇关于高薪程序员&面试题精讲系列17之Java代码中如果有太多if语句该如何优化?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程