Привет! Меня зовут Сергей Чеботарёв, я наставник на курсе «Java-разработчик». Многие обучаются программированию полностью самостоятельно, кто-то выбирает курсы и решает практические задачи в рамках программы. И тем и другим важно тренироваться дополнительно. Чтобы помочь с практикой, я приготовил небольшую задачу о выдуманном секретном рукопожатии. 

Давайте напишем программу на Java и вместе разберём эту задачку: вспомним двоичное счисление, простые операции по работе с Map и работу с пользовательским вводом.

Алгоритм

У нас есть небольшое секретное сообщество программистов, которые хотят распознавать друг друга по секретному приветствию. У членов клуба есть желание, чтобы приветствие не было одно на всех (иначе оно быстро перестанет быть секретным), а вычислялось в зависимости от некоего числа, которое будет задавать собеседник.

Для простоты предположим, что числа целые и не превышают 31. При вычислении рукопожатия мы будем рассматривать двоичное представление числа. Каждому биту, начиная с самого младшего, будет соответствовать действие, а старший бит будет разворачивать последовательность действий задом наперёд.

Список действий будет такой:

Бит

Действие

1

подмигнуть

2

моргнуть дважды

4

закрыть глаза

8

подпрыгнуть

16

развернуть последовательность задом наперёд

Для числа 29, например, двоичное представление будет 11101. Значит, последовательность действий — подпрыгнуть, закрыть глаза, подмигнуть.

Давайте напишем программу на Java, которая будет вычислять такое рукопожатие. Для этого воспользуемся версией JDK 17.

Пользовательский ввод

Всякая программа начинается с получения данных от пользователя. Воспользуемся классом Scanner из стандартной библиотеки Java, получим от пользователя строку и преобразуем её в число:

class Handshake {
    static int getNumber() {
       System.out.println("Введите число:");
       final var str = new Scanner(System.in).nextLine(); // считываем строку
       return Integer.parseInt(str); // преобразуем строку в число
    }
}

Заметим, что пользователь совершенно не обязан вводить именно числа, он может ввести что угодно, и ошибочный ввод может спровоцировать выброс исключения.

Давайте предусмотрим это:

class Handshake {
    static Integer getNumber() {
       System.out.println("Введите число:");
       final var str = new Scanner(System.in).nextLine();
       try {
           return Integer.parseInt(str);
       } catch (final NumberFormatException e) { // не удалось распознать число
           return null;
       }
    }
}

Стало гораздо лучше.

Теперь воспользуемся классом Optional, чтобы избежать работы с null-значениями:

class Handshake {
    static Optional<Integer> getNumber() {
        System.out.println("Введите число:");
        final var str = new Scanner(System.in).nextLine();
        try {
            return Optional.of(Integer.parseInt(str));
        } catch (final NumberFormatException e) { // не удалось распознать число
            return Optional.empty();
        }
    }
}

Нам осталось ввести небольшую проверку на то, что число входит в заданный диапазон между 1 и 31. Для этого можно воспользоваться функцией фильтрации из Optional:

class Handshake {
    static Optional<Integer> getNumber() {
        System.out.println("Введите число:");
        final var str = new Scanner(System.in).nextLine();
        try {
            return Optional.of(Integer.parseInt(str))
                    .filter(i -> i > 0 && i < 32);  // нам нужны только такие числа
        } catch (final NumberFormatException e) {
            return Optional.empty();
        }
    }
}

Отлично!

Секреты

Теперь, когда у нас есть число от пользователя, давайте определим данные, на основе которых будет вычисляться рукопожатие. В первую очередь нам понадобится список секретных действий. Лучше всего воспользоваться классом Map из стандартной библиотеки Java:

class Handshake {
    static final Map<Integer, String> SECRETS = Map.of( // строим готовый Map
            1, "подмигнуть",
            2, "дважды моргнуть",
            4, "закрыть глаза",
            8, "подпрыгнуть"
    );
}

Отдельно определим бит, переворачивающий последовательность действий, которые нужно воспроизвести, чтобы быть узнанным:

class Handshake {
    static final int REVERSE_BIT = 16;
}

Вычисление действий

Теперь мы можем сделать собственно вычисления действий из заданного числа. Для этого воспользуемся побитовым умножением &.

Если «умножить» данное число на тот или иной бит, то ненулевой результат будет означать, что соответствующее действие нужно сделать. Если же результат умножения равен 0, то действие делать не нужно. Соответственно, мы можем в цикле перебрать все возможные биты и запомнить те действия, которые нужно сделать:

class Handshake {
    static List<String> getHandshake(final int num) {
        final var actions = new ArrayList<String>();
        for (var i = 1; i < 32; i = i * 2) {
            final var bit = num & i;
            if (bit != 0) {  // раз действие нужно
                actions.add(SECRETS.get(i)); // добавляем его
            }
        }
        return actions;
    }
}

Так как нам понадобится вывести на экран последовательность действий, лучше сразу полученный список объединить через запятую в одну строку с помощью String.join():

class Handshake {
    static String getHandshake(final int num) {
        final var actions = new ArrayList<String>();
        for (var i = 1; i < 32; i = i * 2) {
            final var bit = num & i;
            if (bit != 0) {
                actions.add(SECRETS.get(i));
            }
        }
        return String.join(", ", actions); // объединяем через запятую
    }
}

О нет, мы забыли про разворот! При прохождении цикла по битам нужно запомнить, когда встретится «бит разворота». После цикла, если нужно, мы перепишем последовательность действий в обратном порядке с помощью дополнительного цикла:

class Handshake {
    static String getHandshake(final int num) {
        var reverseActions = false;
        final var actions = new ArrayList<String>();
        for (var i = 1; i < 32; i = i * 2) {
            final var bit = num & i;
            if (bit != 0) {
                if (i == REVERSE_BIT) {
                    reverseActions = true; // будем переворачивать последовательность
                } else {
                    actions.add(SECRETS.get(i));
                }
            }
        }
        if (actions.size() > 0 && reverseActions) {  // если список пустой — переворачивать нечего
            final var rev = new ArrayList<String>();  // сюда запишем перевёрнутую последовательность
            for (var i = actions.size() - 1; i >= 0; i--) {
                rev.add(actions.get(i));
            }
            return String.join(", ", rev);
        } else {
            return String.join(", ", actions);
        }
    }
}

Готовая программа

Теперь соберём полученную функциональность в единую программу:

import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;

public class SecretHandshake {
    static final int REVERSE_BIT = 16;

    static final Map<Integer, String> SECRETS = Map.of(
            1, "подмигнуть",
            2, "дважды моргнуть",
            4, "закрыть глаза",
            8, "подпрыгнуть"
    );

    public static void main(final String[] args) {
        getNumber()  // жизнь без null выглядит вот так:
                .ifPresent(str -> System.out.printf("Действия: %s", getHandshake(str)));
    }

    static Optional<Integer> getNumber() {
        System.out.println("Введите число:");
        final var str = new Scanner(System.in).nextLine();
        try {
            return Optional.of(Integer.parseInt(str))
                    .filter(i -> i > 0 && i < 32);
        } catch (final NumberFormatException e) {
            return Optional.empty();
        }
    }

    static String getHandshake(final int num) {
        var reverseActions = false;
        final var actions = new ArrayList<String>();
        for (var i = 1; i < 32; i = i * 2) {
            final var bit = num & i;
            if (bit != 0) {
                if (i == REVERSE_BIT) {
                    reverseActions = true;
                } else {
                    actions.add(SECRETS.get(i));
                }
            }
        }
        if (actions.size() > 0 && reverseActions) {
            final var rev = new ArrayList<String>();
            for (var i = actions.size() - 1; i >= 0; i--) {
                rev.add(actions.get(i));
            }
            return String.join(", ", rev);
        } else {
            return String.join(", ", actions);
        }
    }
}

Не всё в этой программе идеально, правда? Буду рад вашим предложениям по улучшению — пишите в комментарии!

Пример работы программы

Введите число:

29

Действия: подпрыгнуть, закрыть глаза, подмигнуть

Возможные усложнения задачи

В качестве самостоятельного упражнения можно сделать некоторые улучшения в получившейся программе:

  • Вынести секреты в конфигурационный файл. Это значит, что нужно сделать так, чтобы значения битов не задавались жёстко в коде, а хранились в отдельном файле конфигурации. Это даст возможность легко заменять секреты. Я бы порекомендовал хранить это в .properties или .yaml-файле.

  • Хорошо бы увеличить диапазон принимаемых чисел и задавать максимальное число тоже через конфигурацию. Четырёх действий явно маловато.

  • Написать unit-тесты и найти в программе баги ;)

Комментарии (3)


  1. aleksandy
    21.09.2023 11:02
    +2

    я наставник на курсе «Java-разработчик» ...

    Рановато вам в наставники. Самому бы подучиться.

    Hidden text

    > ... воспользуемся классом Optional ...

    Зачем?

    new Scanner(System.in).nextLine()

    Зачем?

    for (var i = 1; i < 32; i = i * 2)

    Зачем? for (Integer i : SECRETS.keySet()), проверку на обратный порядок сделать отдельно после цикла.
    Для реверса списка тоже специальный метод имеется. Не говоря уже о том, что вместо списка можно было просто двунаправленную очередь использовать.

    actions.size() > 0

    (Челодлань)


  1. b1n4reee
    21.09.2023 11:02

    А почему экзепшн final: final NumberFormatException e?


    1. olku
      21.09.2023 11:02
      +1

      Чтобы игнорируемое непроверяемое исключение было на всякий случай иммутабельным. Вероятно, причина в секретности рукопожатия. Но тогда где private и замена стрингов на массив чаров. Чувствуется некая недосказанность в коде.