Java8(7):自制多糖 switch

更新于 2019-10-29  约 29 分钟

背景

JDK 12 和 JDK 13 已经发布了,伴随着许多对 Java 语法的小改进,比如我们非常熟悉的 switch

JDK12 之前

switch (type) {
    case "all":
        System.out.println("列出所有帖子");
        break;
    case "auditing":
        System.out.println("列出审核中的帖子");
        break;
    case "accepted":
        System.out.println("列出审核通过的帖子");
        break;
    case "rejected":
        System.out.println("列出审核不通过的帖子");
        break;
    default:
        System.out.println("参数'type'错误,请检查");
        break;
}

JDK12

switch (type) {
    case "all" -> System.out.println("列出所有帖子");
    case "auditing" -> System.out.println("列出审核中的帖子");
    case "accepted" -> System.out.println("列出审核通过的帖子");
    case "rejected" -> System.out.println("列出审核不通过的帖子");
    default -> System.out.println("参数'type'错误,请检查");
}

JDK13

String value = switch (i) {
    case  0 -> "zero"
    case  1 -> "one"
    case  2 -> "two"
    default -> "many"
};

新特性很美好,但是如今在业界最流行的版本依然是 JDK8,所以想要在生产环境用上这么舒服的 switch,目前看来还是遥遥无期。好在我们还有 Lambda,正所谓 “自己动手,丰衣足食”,我们来试试能不能自己做出一个和 JDK12 & JDK13swtich 类似的东西,给我们平淡的编码生活,加点糖。

实现

对标 JDK12 的 switch

首先,我们定义一个 Switch 类,然后它接收一个泛型参数,就类似与 Java 的 switch 语句,需要先接收一个参数。

public class Switch<T> {

    /**
     * 输入值
     */
    private final T input;

    private Switch(T input) {
        this.input = input;
    }

    public static <T, R> Switch<T, R> on(T value) {
        return new Switch<>(value);
    }
}

通过静态方法 onon(input) 可以理解为在 value 上做 Switch 操作),我们可以构造出一个 Switch 实例。然后,我们定义一个 Predicate,用来表示当前的条件:

public class Switch<T> {

    private Predicate<T> condition;

    public Switch<T> is(T target) {
        // 判断输入值是否和 target 相等
        condition = Predicate.isEqual(target);
        return this;
    }
}

is 方法的作用就是将当前的条件 condition 定义为 判断 Switch 的输入值是否和传入的 target 相等。既然都引入条件了,自然我们可以让用户自己来定义条件:

public Switch<T, R> is(T target) {
    // 判断输入值是否和 target 相等
    return when(Predicate.isEqual(target));
}

public Switch<T, R> when(Predicate<T> condition) {
      // 用户自己设定条件
    this.condition = Objects.requireNonNull(condition);
    return this;
}

接着我们就可以来定义 switch 语句中的 case ... break 功能:

public Switch<T> thenAccept(Consumer<T> action) {
    requireNonNullArgAndCondition(action);

    if (condition.test(input)) {
        action.accept(input);
    }

    return this;
}

void requireNonNullCondition() {
    if (condition == null) {
        throw new IllegalStateException("A condition must be set first");
    }
}

void requireNonNullArgAndCondition(Object arg) {
    Objects.requireNonNull(arg, "Null argument " + arg.getClass().getName());
    requireNonNullCondition();
}

好像有点不对劲?对哦,switch 只能满足一个 case,如果是我们自己来设定各种条件,可能会存在多个条件都满足的情况 —— 那就不是我们预期的 switch 了。所以我们可以定义一个 boolean 标记,用来表示用户设定的多个条件是否存在某一个满足,如果有一个满足了,则该条件之后的链式方法都直接 短路处理

public class Switch<T> {
        
      ...
      
    /**
     * 是否已经存在过满足的条件
     */
    private boolean met;

    public Switch<T, R> is(T target) {
        return when(Predicate.isEqual(target));
    }
    
    public Switch<T, R> when(Predicate<T> condition) {
        // 短路处理
        if (met) { return this; }
    
        this.condition = Objects.requireNonNull(condition);
        return this;
    }

    public Switch<T, R> thenAccept(Consumer<T> action) {
        // 短路处理
        if (met) { return this; }

        requireNonNullArgAndCondition(action);
        if (condition.test(input)) {
            action.accept(input);
            // 标记已经存在过满足的条件
            met = true;
        }

        return this;
    }
}

好像还少了点什么?对哦,switch 还有个 default ... break。那我们定义一个 elseAccept 方法,当且仅当之前没有任何一个条件被满足时,调用这个方法:

public void elseAccept(Consumer<T> action) {
    // 之前存在被满足的条件,直接返回
    if (met) { return; }
    
    Objects.requireNonNull(action);
    action.accept(input);
}

OK,我们来写个小 demo 对比感受一下:

// 获得前端传递的某个参数
String type = getType();

// 传统 switch
switch (type) {
    case "all":
        System.out.println("列出所有帖子");
        break;
    case "auditing":
        System.out.println("列出审核中的帖子");
        break;
    case "accepted":
        System.out.println("列出审核通过的帖子");
        break;
    case "rejected":
        System.out.println("列出审核不通过的帖子");
        break;
    default:
        System.out.println("参数'type'错误,请检查");
        break;
}

// 我们的 Switch
Switch.on(type)
      .is("all")
      .thenAccept(t -> System.out.println("列出所有帖子"))
      .is("auditing")
      .thenAccept(t -> System.out.println("列出审核中的帖子"))
      .is("accepted")
      .thenAccept(t -> System.out.println("列出审核通过的帖子"))
      .is("rejected")
      .thenAccept(t -> System.out.println("列出审核不通过的帖子"))
      .elseAccept(t -> System.out.println("参数'type'错误,请检查"));

虽然我们的 Switch 看起来没有 JDK12 的 switch 那样直观,但是比 JDK12 之前的 switch 语句更加简洁了 —— 而且链式调用,配合 Lambda,写起来更舒服了~ 更重要的是,我们都知道 switch 语句支持的类型有限(整数、枚举、字符,字符串),而我们自定义的 Switch,支持任何类型,比如:

Object value = getValue();

Switch.on(value)
      .is(null)
      .thenAccept(v -> System.out.println("value is null"))
      .is(123)
      .thenAccept(v -> System.out.println("value is 123"))
      .is("abc")
      .thenAccept(v -> System.out.println("value is abc"))
      .is(Arrays.asList(1, 2, 3))
      .thenAccept(v -> System.out.println("value is [1, 2, 3]"))
      .elseAccept(v -> System.out.println("Unknown value"));

而且我们还支持自定义条件语句,所以很显然,我们的 Switch 可以用来代替 if-else 语句:

Object value = getValue();

Switch.on(value)
      .is(null)
      .thenAccept(v -> System.out.println("value is null"))
      .when(Integer.class::isInstance)
      .thenAccept(v -> System.out.println("value is Integer"))
      .when(String.class::isInstance)
      .thenAccept(v -> System.out.println("value is String"))
      .when(Boolean.class::isInstance)
      .thenAccept(v -> System.out.println("value is Boolean"))
      .elseAccept(v -> System.out.println("Unknown type of value"));

// 等价的 if-else
if (value == null) {
    System.out.println("value is null");
} else if (value instanceof Integer) {
    System.out.println("value is Integer");
} else if (value instanceof String) {
    System.out.println("value is String");
} else if (value instanceof Boolean) {
    System.out.println("value is Boolean");
} else {
    System.out.println("Unknown type of value");
}

至于哪个更好用和阅读起来更舒服,就 “仁者见仁,智者见智” 了。

对标 JDK13 的 Switch

JDK13 中,赋予了 switch 语句求值的功能 —— 我们也可以很容易的改造我们的 Switch 来支持这个功能。首先,我们对 Switch 进行抽象,并定义 ConsumptionSwitch 作为消费用的 Switch(即上文中实现的 Switch),定义 EvaluationSwitch 作为用于求值的 Switch。 抽象 Switch

public abstract class Switch<T> {

    /**
     * 输入值
     */
    final T input;

    /**
     * 当前的条件
     */
    Predicate<T> condition;

    /**
     * 是否已经存在某个条件被满足
     */
    boolean met;

    Switch(T input) {
        this.input = input;
    }

    /**
     * 在指定的值上使用 Switch,返回用于消费的 Switch 实例
     */
    public static <I> ConsumptionSwitch<I> on(I input) {
        return new ConsumptionSwitch<>(input);
    }

    /**
     * 在指定的输入值上使用 Switch,返回用于求值的 Switch 实例
     */
    public static <I, O> EvaluationSwitch<I, O> in(I input) {
        return new EvaluationSwitch<>(input);
    }

    /**
     * 判断输入是否和给定的目标相等
     */
    protected Switch<T> is(T target) {
        return when(Predicate.isEqual(target));
    }

    /**
     * 设定输入值需要满足的条件
     */
    protected Switch<T> when(Predicate<T> condition) {
        // 短路处理
        if (met) { return this; }

        this.condition = Objects.requireNonNull(condition);
        return this;
    }
  
      ......
}

用于消费的的 Switch

/**
 * 用于消费的 Switch
 *
 * @param <T> 输入值的类型
 */
public static class ConsumptionSwitch<V> extends Switch<V> {

    ConsumptionSwitch(V value) {
        super(value);
    }

    @Override
    public ConsumptionSwitch<V> is(V target) {
        super.is(target);
        return this;
    }

    @Override
    public ConsumptionSwitch<V> when(Predicate<V> condition) {
        super.when(condition);
        return this;
    }

    /**
     * 满足某个条件时,对输入值进行消费操作
     */
    public ConsumptionSwitch<V> thenAccept(Consumer<V> action) {
        // 短路处理
        if (met) { return this; }

        requireNonNullArgAndCondition(action);

        if (condition.test(input)) {
            action.accept(input);
            // 标记已经存在过满足的条件
            met = true;
        }

        return this;
    }

    /**
     * 不满足任一条件时,对输入值进行消费操作
     */
    public void elseAccept(Consumer<V> action) {
        // 之前存在被满足的条件,直接返回
        if (met) { return; }

        Objects.requireNonNull(action);
        action.accept(input);
    }
}

改造完毕,现在我们可以来实现用于求值的 Switch。首先,定义一个泛化类型的返回值:

/**
 * 用于求值的 Switch
 *
 * @param <I> 输入值的类型
 * @param <O> 输出值的类型
 */
public static class EvaluationSwitch<I, O> extends Switch<I> {
    
    /**
     * 输出
     */
    private O output;

    EvaluationSwitch(I input) {
        super(input);
    }

    @Override
    public EvaluationSwitch<I, O> is(I target) {
        super.is(target);
        return this;
    }

    @Override
    public EvaluationSwitch<I, O> when(Predicate<I> condition) {
        super.when(condition);
        return this;
    }
}

然后加入两个方法,用来满足条件时进行求值和不满足任一条件时求值:

/**
 * 满足某个条件时,进行求值操作
 */
public EvaluationSwitch<I, O> thenGet(O value) {
    if (met) { return this; }

    requireNonNullCondition();

    // 满足条件
    if (condition.test(input)) {
        output = value;
        // 标记已经产生输出值
        met = true;
    }

    return this;
}

/**
 * 不满足任一条件时,进行求值操作
 */
public O elseGet(O value) {
    return met ? output : value;
}

同样,写个 demo 看看效果:

int num = getNum();
// 输入 num 进行求值
String result = Switch.in(num)
                      .is(0).thenGet("zero")
                      .is(1).thenGet("one")
                      .is(2).thenGet("two")
                      .elseGet("many");

System.out.println(result);

然而,编译不过 —— 因为推导不出返回值的类型....... Switch.input(k) 返回的是 EvaluationSwitch<Integer, Object>,而我们需要的是 EvaluationSwitch<Integer, String>。没办法,通过一个方法转换一下吧:

/**
 * 设定当前 EvaluationSwitch 的输出值的类型
 *
 * @param type 输出值的类型
 * @param <R>  指定的输出值类型
 * @return 当前的 EvaluationSwitch 实例
 */
@SuppressWarnings("unchecked")
public <R> EvaluationSwitch<I, R> out(Class<? extends R> type) {
    return (EvaluationSwitch<I, R>) this;
}

即明确我们要返回的类型:

int num = getNum();

String result = Switch.in(num)
                      .out(String.class)
                      .is(0).thenGet("zero")
                      .is(1).thenGet("one")
                      .is(2).thenGet("two")
                      .elseGet("many");

System.out.println(result);

对比下 JDK13:

int num = getNum();

String value = switch (num) {
    case  0 -> "zero"
    case  1 -> "one"
    case  2 -> "two"
    default -> "many"
};

System.out.println(value);

除了要明确下返回值的类型,二者功能一致;虽然没有 JDK13 简洁,但是我们的 Switch 看起来也非常的直观。而且我们可以引入函数来进一步增强我们的 Switch 的求值功能:

/**
 * 满足某个条件时,使用 Function 进行求值操作,当前 Switch 实例的输入值会作为 Function 的输入
 */
public EvaluationSwitch<I, O> thenApply(Function<I, O> mapper) {
    if (met) { return this; }

    requireNonNullArgAndCondition(mapper);

    if (condition.test(input)) {
        output = mapper.apply(input);
        met = true;
    }

    return this;
}

/**
 * 不满足任一条件时,使用 Function 进行求值操作,当前 Switch 实例的输入值会作为 Function 的输入
 */
public O elseApply(Function<I, O> mapper) {
    Objects.requireNonNull(mapper);

    return met ? output : mapper.apply(input);
}

/**
 * 满足某个条件时,使用 Supplier 进行求值操作
 */
public EvaluationSwitch<I, O> thenSupply(Supplier<O> supplier) {
    if (met) { return this; }

    requireNonNullArgAndCondition(supplier);

    if (condition.test(input)) {
        output = supplier.get();
        met = true;
    }

    return this;
}

/**
 * 不满足任一条件时,使用 Supplier 进行求值操作
 */
public O elseSupply(Supplier<O> supplier) {
    Objects.requireNonNull(supplier);

    return met ? output : supplier.get();
}

写个 demo:

ScheduleTypeEnum scheduleType = getScheduleType();

// 使用 if-else
LocalDateTime ptTime;
if (scheduleType == BY_DAY) {
    ptTime = LocalDateTime.now().minusDays(1);
} else if (scheduleType == BY_HOUR) {
    ptTime = LocalDateTime.now().minusHours(1);
} else if (scheduleType == BY_MINUTE) {
    ptTime = LocalDateTime.now().minusMinutes(1);
} else {
    ptTime = LocalDateTime.now().minusSeconds(1);
}

// 使用 Java8 switch
LocalDateTime ptTime;
switch (scheduleType) {
    case BY_DAY:
        ptTime = LocalDateTime.now().minusDays(1);
        break;
    case BY_HOUR:
        ptTime = LocalDateTime.now().minusHours(1);
        break;
    case BY_MINUTE:
        ptTime = LocalDateTime.now().minusMinutes(1);
        break;
    default:
        ptTime = LocalDateTime.now().minusMinutes(1);
        break;
}

// 使用本文的求值 Switch
LocalDateTime ptTime = Switch.input(scheduleType)
                             .output(LocalDateTime.class)
                             .is(BY_DAY)
                             .thenSupply(() -> LocalDateTime.now().minusDays(1))
                             .is(BY_HOUR)
                             .thenSupply(() -> LocalDateTime.now().minusHours(1))
                             .is(BY_MINUTE)
                             .thenSupply(() -> LocalDateTime.now().minusMinutes(1))
                             .elseSupply(() -> LocalDateTime.now().minusSeconds(1));

之所以这里使用 thenSupply 而不是直接使用 thenGet,是因为使用函数可以 惰性求值

最终的 Switch 代码可见:Switch.java

扩展

isIn 操作

is 用来判断 输入 是否和某个 特定值 相等,那如果需要判断 输入 是否在某个 一群值 中呢?很简单,同样基于 when 方法:

/**
 * 判断输入是否存在给定的一群值中
 *
 * @param values 给定的一群值
 * @return 当前 Switch 实例
 */
protected Switch<T> isIn(T... values) {
    Objects.requireNonNull(values);

    return when(e -> {
        for (T value : values) {
            if (Objects.equals(e, value)) {
                return true;
            }
        }
        
        return false;
    });
}

大家还有什么改进和想法呢,欢迎评论交流~

阅读 1.3k更新于 2019-10-29

推荐阅读
Java Follower
用户专栏

Java 平台是博大精深的。作为一名 Java 的追随者,聊聊各种有趣或者有坑的 Java 技术。

65 人关注
23 篇文章
专栏主页
目录