Java 8(7):自家製多糖switch


背景
JDK 12とJDK 13はすでに発表されており、Java文法の小さな改善を伴っています.例えば、私たちがよく知っているswitchなどです.
JDK 12より前
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"
};

新しい特性は素晴らしいが、現在業界で最も流行しているバージョンは依然としてJDK 8なので、生産環境でこんなに快適なswitchを使いたいのは、まだ遠いようだ.幸いなことに、私たちにはLambdaがあります.「自分で手を出して、衣食を豊かにする」ということです.私たちは自分でJDK12 & JDK13swtichに似たようなものを作って、私たちに平板なコード生活をして、砂糖を加えることができますか.
インプリメンテーション
JDK 12のswitch
まず、Switchクラスを定義し、Javaのswitch文と同様に汎用パラメータを受信します.まずパラメータを受信する必要があります.
public class Switch {

    /**
     *    
     */
    private final T input;

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

    public static  Switch on(T input) {
        return new Switch<>(input);
    }
}

静的方法on(on(input)はinput上でSwitchの動作を行うと理解できる)により、Switchの例を構築することができる.次に、現在の条件を表すPredicateを定義します.
public class Switch {

    private Predicate condition;

    public Switch is(T target) {
        //          target   
        condition = Predicate.isEqual(target);
        return this;
    }
}
isメソッドの役割は、現在の条件conditionをSwitchの入力値が入力されたtargetと等しいか否かを判断することである.条件が導入された以上、ユーザー自身で条件を定義することができます.
public Switch is(T target) {
    //          target   
    return when(Predicate.isEqual(target));
}

public Switch when(Predicate condition) {
      //         
    this.condition = Objects.requireNonNull(condition);
    return this;
}

次に、switch文のcase ... break機能を定義します.
public Switch thenAccept(Consumer 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は1つのcaseしか満たしていません.もし私たちが自分でいろいろな条件を設定したら、複数の条件が満たされる可能性があります.それは私たちが予想していたswitchではありません.したがって、ユーザが設定した複数の条件にいずれかが満たされているかどうかを示すbooleanタグを定義することができ、1つが満たされている場合、その条件の後のチェーンメソッドは直接短絡処理される.
public class Switch {
        
      ...
      
    /**
     *             
     */
    private boolean met;

    public Switch is(T target) {
        return when(Predicate.isEqual(target));
    }
    
    public Switch when(Predicate condition) {
        //     
        if (met) { return this; }
    
        this.condition = Objects.requireNonNull(condition);
        return this;
    }

    public Switch thenAccept(Consumer action) {
        //     
        if (met) { return this; }

        requireNonNullArgAndCondition(action);
        if (condition.test(input)) {
            action.accept(input);
            //             
            met = true;
        }

        return this;
    }
}

まだ何か足りないようですか?そうですよ.switchdefault ... breakがあります.では、elseAcceptメソッドを定義し、これまで条件が満たされていなかった場合にのみ、このメソッドを呼び出します.
public void elseAccept(Consumer 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はJDK 12のswitchほど直感的ではないように見えますが、JDK 12以前の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");
}

どちらが使いやすく、読みやすいかについては、「仁者見仁、智者見智」です.
JDK 13のスイッチを合わせる
JDK 13には、switch文の評価機能が付与されています.私たちのSwitchを簡単に改造してこの機能をサポートすることもできます.まず、Switchを抽象化し、ConsumptionSwitchを消費用のSwitch(すなわち、上述したSwitch)とし、EvaluationSwitchを評価のためのSwitchとして定義する.抽象Switch:
public abstract class Switch {

    /**
     *    
     */
    final T input;

    /**
     *      
     */
    Predicate condition;

    /**
     *              
     */
    boolean met;

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

    /**
     *            Switch,        Switch   
     */
    public static  ConsumptionSwitch on(I input) {
        return new ConsumptionSwitch<>(input);
    }

    /**
     *            Switch,        Switch   
     */
    public static  EvaluationSwitch input(I input) {
        return new EvaluationSwitch<>(input);
    }

    /**
     *               
     */
    protected Switch is(T target) {
        return when(Predicate.isEqual(target));
    }

    /**
     *             
     */
    protected Switch when(Predicate condition) {
        //     
        if (met) { return this; }

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

消費に使用されるSwitch:
/**
 *       Switch
 *
 * @param        
 */
public static class ConsumptionSwitch extends Switch {

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

    @Override
    public ConsumptionSwitch is(I target) {
        super.is(target);
        return this;
    }

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

    /**
     *        ,          
     */
    public ConsumptionSwitch thenAccept(Consumer action) {
        //     
        if (met) { return this; }

        requireNonNullArgAndCondition(action);

        if (condition.test(input)) {
            action.accept(input);
            //             
            met = true;
        }

        return this;
    }

    /**
     *         ,          
     */
    public void elseAccept(Consumer action) {
        //           ,    
        if (met) { return; }

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

改造が完了し、値を求めるためのSwitchを実現することができます.まず、汎化タイプの戻り値を定義します.
/**
 *       Switch
 *
 * @param        
 * @param        
 */
public static class EvaluationSwitch extends Switch {
    
    /**
     *   
     */
    private O output;

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

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

    @Override
    public EvaluationSwitch when(Predicate condition) {
        super.when(condition);
        return this;
    }
}

次に、条件を満たすときに評価する方法と、いずれかの条件を満たさないときに評価する方法を2つ追加します.
/**
 *        ,      
 */
public EvaluationSwitch 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();

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

System.out.println(result);

しかし、コンパイルは--戻り値のタイプが導出されないため.......Switch.input(k)EvaluationSwitchを返し、私たちが必要とするのはEvaluationSwitchである.仕方がないので、一つの方法で変換してみましょう.
/**
 *      EvaluationSwitch        
 *
 * @param type       
 * @param           
 * @return     EvaluationSwitch   
 */
@SuppressWarnings("unchecked")
public  EvaluationSwitch output(Class extends R> type) {
    return (EvaluationSwitch) this;
}

つまり、私たちが返すタイプを明確にします.
int num = getNum();

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

System.out.println(result);

比較するとJDK 13:
int num = getNum();

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

System.out.println(value);

明示的に値を返すタイプを除いて、両者の機能は一致している.JDK 13は簡潔ではありませんが、私たちのSwitchも非常に直感的に見えます.さらに、Switchの評価機能をさらに強化するために関数を導入することができます.
/**
 *        ,   Function       ,   Switch           Function    
 */
public EvaluationSwitch thenApply(Function 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 mapper) {
    Objects.requireNonNull(mapper);

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

/**
 *        ,   Supplier       
 */
public EvaluationSwitch thenSupply(Supplier supplier) {
    if (met) { return this; }

    requireNonNullArgAndCondition(supplier);

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

    return this;
}

/**
 *         ,   Supplier       
 */
public O elseSupply(Supplier 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
拡張
in操作isは、入力が特定の値と等しいかどうかを判断するために使用されます.入力が特定の値のグループにあるかどうかを判断する必要がある場合は、次のようにします.whenメソッドに基づいて簡単です.
/**
 *                
 *
 * @param values       
 * @return    Switch   
 */
protected Switch in(T... values) {
    Objects.requireNonNull(values);

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

皆さんは何か改善やアイデアがありますか?コメント交流を歓迎します~