Java 8, как я могу реализовать оператор switch, используя потоки?

У меня есть текстовый файл imgui.ini содержащий:

[Debug]
Pos=7,79
Size=507,392
Collapsed=0

[ImGui Demo]
Pos=320,5
Size=550,680
Collapsed=0

Для каждого "элемента" у меня всегда есть Pos, Size а также Collapsed и мне нужно их прочитать.

Я хотел бы использовать, если это возможно, java 8 потоков.

Можно ли смоделировать поведение оператора switch?

    try (Stream<String> stream = Files.lines(Paths.get(context.io.iniFilename))) {

        ...
/*
    switch(string) {

        case "Pos":
            settings.pos = value;
            break;

        case "Size":
            settings.size = value;
            break;

        case "Collapsed":
            settings.collapsed = value;
            break;
    }
*/

    } catch (IOException e) {
    }
}

4 ответа

Решение

Наилучший способ анализа такого файла (без использования выделенных сторонних библиотек) - через API-интерфейс regex и его интерфейсный класс. Scanner, К сожалению, лучшие операции по его реализации через Stream API в настоящее время отсутствуют. А именно, Matcher.results() а также Scanner.findAll(…) еще не там Поэтому, если мы не хотим ждать до Java 9, мы должны создать аналогичные методы для решения, совместимого с Java 8:

public static Stream<MatchResult> findAll(Scanner s, Pattern pattern) {
    return StreamSupport.stream(new Spliterators.AbstractSpliterator<MatchResult>(
            1000, Spliterator.ORDERED|Spliterator.NONNULL) {
        public boolean tryAdvance(Consumer<? super MatchResult> action) {
            if(s.findWithinHorizon(pattern, 0)!=null) {
                action.accept(s.match());
                return true;
            }
            else return false;
        }
    }, false);
}
public static Stream<MatchResult> results(Matcher m) {
    return StreamSupport.stream(new Spliterators.AbstractSpliterator<MatchResult>(
            m.regionEnd()-m.regionStart(), Spliterator.ORDERED|Spliterator.NONNULL) {
        public boolean tryAdvance(Consumer<? super MatchResult> action) {
            if(m.find()) {
                action.accept(m.toMatchResult());
                return true;
            }
            else return false;
        }
    }, false);
}

Использование методов со сходной семантикой позволяет нам заменить их использование стандартными методами API, как только Java 9 выпущена и становится обычным явлением.

Используя эти две операции, вы можете проанализировать ваш файл, используя

Pattern groupPattern=Pattern.compile("\\[(.*?)\\]([^\\[]*)");
Pattern attrPattern=Pattern.compile("(.*?)=(.*)\\v");
Map<String, Map<String, String>> m;
try(Scanner s=new Scanner(Paths.get(context.io.iniFilename))) {
    m = findAll(s, groupPattern).collect(Collectors.toMap(
        gm -> gm.group(1),
        gm -> results(attrPattern.matcher(gm.group(2)))
            .collect(Collectors.toMap(am->am.group(1), am->am.group(2)))));
}

итоговая карта m содержит всю информацию, сопоставляя имена групп с другой картой, содержащей пары ключ / значение, т.е. вы можете напечатать эквивалент .ini использование файла:

m.forEach((group,attr)-> {
    System.out.println("["+group+"]");
    attr.forEach((key,value)->System.out.println(key+"="+value));
});

Сосредоточив внимание на вопросе "есть ли способ имитировать поведение оператора switch", я думаю, что ответ заключается в том, что вы могли бы, приложив немного усилий. Я спрашивал себя об этом пару лет назад и сделал следующее упражнение (а потом никогда больше не использовал его):

private static <T> Predicate<T> testAndConsume(Predicate<T> pred, Consumer<T> cons) {
    return t -> {
        boolean result = pred.test(t);
        if (result) cons.accept(t);
        return result;
    };
}

public static class SwitchConsumer<T> {
    Predicate<T> conditionalConsumer;
    private SwitchConsumer(Predicate<T> pred) {
        conditionalConsumer = pred;
    }

    public static <C> SwitchConsumer<C> inCase(Predicate<C> pred, Consumer<C> cons) {
        return new SwitchConsumer<>(testAndConsume(pred, cons));
    }

    public SwitchConsumer<T> elseIf(Predicate<T> pred, Consumer<T> cons) {
        return new SwitchConsumer<>(conditionalConsumer.or(testAndConsume(pred,cons)));
    }

    public Consumer<T> elseDefault(Consumer<T> cons) {
        return testAndConsume(conditionalConsumer.negate(),cons)::test;   // ::test converts Predicate to Consumer
    }
}

testAndConsume составляет Predicate и Consumer, создавая Predicate который возвращает то же значение, но вызывает Consumer как побочный эффект, если значение истинно. Это становится основой для каждого "случая" в "переключателе". Каждый "случай" связан Predicate.or(), который обеспечивает короткое замыкание "else-if" природы коммутатора. Наконец, составленный Predicate превращается в Consumer добавляя ::test к Predicate,

Применяя его к вашему фрагменту кода, он выглядит так:

    Stream.of("Pos=320,5", "Size=550,680", "Collapsed=0")
            .map(s -> s.split("="))
            .forEach(SwitchConsumer.<String[]>
                    inCase(arr -> "Pos".equals(arr[0]), arr -> settings.pos = arr[1])
                    .elseIf(arr -> "Size".equals(arr[0]), arr -> settings.size = arr[1])
                    .elseIf(arr -> "Collapsed".equals(arr[0]), arr -> settings.collapsed = arr[1])
                    .elseDefault(arr -> {}));

Это примерно так же, как переключение без фактического переключения в Consumer тело.

Попытка:

try {
        Path file = Paths.get("G:\\tmp", "img.ini");
        Stream<String> lines = Files.lines(file);

        lines.filter(line->{
            if("pos".equalsIgnoreCase(line.split("=")[0])){
                //process pos line here
                System.out.println("pos"+line);
                return false;
            }
            return true;
        }).filter(line->{
            System.out.println("2"+line);
            if("Collapsed".equalsIgnoreCase(line.split("=")[0])){
                //process Collapsed line here
                System.out.println("Collapsed"+line);
                return false;
            }
            return true;
        }).filter(line->{
            System.out.println("3"+line);
            if("Size".equalsIgnoreCase(line.split("=")[0])){
                //process Size line here
                System.out.println("Size"+line);
                return false;
            }
            return true;
        }).forEach(line->{
            //settings = new Settings();
        });;
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

Еще один способ прочитать ваш файл конфигурации:

public class Main {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("D:\\Development\\workspace\\Application\\src\\main\\resources\\init.txt");
        String content = new String(Files.readAllBytes(path));

        Map<String, Config> configMap = Stream.of(content.split("\\n\\r"))
            .map(config -> Arrays.asList(config.split("\\r")))
            .collect(HashMap<String, Config>::new, (map, list) -> {
                String header = list.get(0);
                String pos = list.get(1);
                String size = list.get(2);
                String collapsed = list.get(3);
                map.put(header, new Config(pos.substring(pos.indexOf("=") + 1), size.substring(size.indexOf("=") + 1), collapsed.substring(collapsed.indexOf("=") + 1)));
            }, (m, u) -> {});

        System.out.println(configMap);
    }
}

class Config {
    public String pos;
    public String size;
    public String collapsed;

    public Config(String pos, String size, String collapsed) {
        this.pos = pos;
        this.size = size;
        this.collapsed = collapsed;
    }

    @Override
    public String toString() {
        return "Config{" +  "pos='" + pos + '\'' + ", size='" + size + '\'' + 
               ", collapsed='" + collapsed + '\'' + '}';
    }
}

Результатом будет карта:

{
    [Debug]=Config{pos='7,79', size='507,392', collapsed='0'}, 
    [ImGui Demo]=Config{pos='320,5', size='550,680', collapsed='0'}
}
Другие вопросы по тегам