Итак, вчера мы с вами поиграли в джавовский вариант «Интеллектуальное казино против знатоков», и при этом, при всем уважении к хабровчанам, телезрители выиграли! Победителем этого этапа стал Сергей SerCe Целовальников, решивший три задачи. Как и обещали — мы вручаем ему небольшой приз: VIP-билет на JPoint!

Но радости в сторону. Вчера нас с телезрителями обвинили в том, что большинство вчерашних задач были связаны с Java весьма косвенно. Мы принимаем это обвинение, и поэтому сегодня у нас вариант на чистой Java! никаких спрингов, эксэмэлей и паттернов. Это будет настоящее испытание для истинных любителей хардкора!

Под катом — ответы на вчерашний раунд и суперблиц! Против знатоков сегодня играет телезритель из Петербурга Андрей apangin Паньгин.





Но сначала ответы на вчерашние задачи.

Различные аспекты от Николая Гарбузова
Скомпилируется ли следующий аспект AJC компилятором?
Если да — то что он выведет на консоль при компиляции?

public aspect QuizAspect {
    public static int count(int i) {
        return i++;
    }

    before (int n) : execution(public int QuizAspect.count(int)) 
            && args(n) && if(QuizAspect.count(1)>1) {
        System.out.println("QuizAspect " + n);
    }
}


Правильный ответ. Скомпилируется успешно, но ничего не выведет на консоль при компиляции. При этом вызов метода Quiz.count в рантайме приведет к StackOverflowError ошибке!

Можно подумать, что выражение в if() начнет выпоняться при компиляции кода, но это не так.
(не стоит путать аспекты с мета программированием)
Аспект лишь встроит код совета внутрь метода count, добавит if() к этому коду.


Выражения от Владимира Ситникова
В чём подвох удалять Java-комментарии таким выражением? Укажите 3 причины, почему так делать нельзя. (считаем, что исходник написан нормальными символами) —
Pattern.compile("/\\*(?:[^*]|\\*[^/])*\\*/")


Ответ
  1. Если это джавовская строка, а в ней якобы комент — то не будет работать
  2. Если комент заканчивается на две звездочки слэш, то работать не будет
  3. Если комментарий большой, то получим Будет StackOverflowError, т.к. java regexp’ы используют backtracking.


Пофиксить можно двумя способами:
  • Possessive quantifier:
    Pattern.compile("/\\*(?:[^*]|\\*(?!/))*+\\*/")
  • Independent group:
    Pattern.compile("/\\*(?>,[^*]|\\*(?!/))*\\*/")


Спринговые контексты от Николай Алименкова
Есть 2 Spring контекста:

1. a.xml с бином

<util:list id="myList">        
    <value>3</value>        
    <value>4</value>    
</util:list>


2. b.xml с бином

<util:list id="myList">
    <value>6</value>
</util:list>


Что напечатает такой фрагмент кода:

System.out.println(new ClassPathXmlApplicationContext("a.xml", "b.xml").getBean("myList"));


И как можно заставить его бросить ошибку, не изменяя логику работы кода?

Ответ
Выведет 6, а заставить бросить ошибку можно установив allowBeanDefinitionOverriding у контекста в false.


А теперь самая хардкорная задача вчерашнего дня, по мнению президента, премьера и министра обороны элитарного клуба.
OOM от Никиты Сальникова-Тарновского
Ниже приведены 2 программы. Каждая из них пытается аллоцировать суммарно памяти больше размера хипа. Но одна из них выкидывает java.lang.OutOfMemoryError, а вторая нет. Почему?

public class OOM1 {
    private static final int SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.55);

    public static void main(String[] args) {
        {
            byte[] bytes = new byte[SIZE];
            System.out.println(bytes.length);
        }

        byte[] bytes1 = new byte[SIZE];
        System.out.println(bytes1.length);

        System.out.println("I allocated memory successfully");
    }
}

public class OOM2 {
    private static final int SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.35);

    public static void main(String[] args) {
        {
            byte[] bytes = new byte[SIZE];
            System.out.println(bytes.length);
        }

        byte[] bytes1 = new byte[SIZE];
        System.out.println(bytes1.length);

        byte[] bytes2 = new byte[SIZE];
        System.out.println(bytes2.length);

    System.out.println("I allocated memory successfully");
    }
}



Ответ
В обоих случаях javac понимает, что переменная bytes не будет использована после окончание внутреннего блока. Поэтому второй созданный массив, который мы кладем в переменную bytes1, займет тот же слот, что и переменная bytes. Как следствие, после выполнения присваивания bytes1 < — new bytes[SIZE] значение, которое было в переменной bytes становится недоступным и GC может его удалить. Тем самым OOM2 требует всего лишь 70% от хипа, а не 105%.

Update 1
Вот интересный анализ от cheremin на тему этой задачи.

Update 2
Для тех, у кого оба варианта бросают OOM: для воспроизведения бага запускайте вашу JVM с опцией -server

Груви от Баруха Садогурского и Спринг от Жени Борисова
А вот тут нас ждет облом. jbaruch и EvgenyBorisov попросили меня сохранить интригу и не публиковать ответы на их задачи до JPoint. Сказали, чтобы все желающие узнать ответы приходили на их доклады.

Желающие узнать ответы — попробуйте написать им в личку. Может быть они сжалятся над вами!


Суперблиц от Андрея Паньгина
Вот и настало время истинного хардкора! Знатоки поднимают глаза вверх, и на их экранах появляется улыбающийся apangin. Он кагбэ машет всем знатокам и кидает им тысячи воздушных поцелуев.

Вопрос первый
Что не так с этим кодом? Как его исправить?

    public static double[] getRandomVector(int size) {
        double[] vector = new double[size];
        Arrays.parallelSetAll(vector, i -> Math.random());
        return vector;
    }



Вопрос второй
Как такое может быть, что публичный метод работает заметно быстрее идентичного приватного?

public class Modifiers {
    static final Inner inner = new Inner();
 
    static class Inner {
        int x = 1;
 
        int getX1() { return x; }
        int getX2() { return getX1(); }
        int getX3() { return getX2(); }
        int getX4() { return getX3(); }
        int getX5() { return getX4(); }
        int getX6() { return getX5(); }
        int getX7() { return getX6(); }
        int getX8() { return getX7(); }
        int getX9() { return getX8(); }
 
        private int getPrivate() { return getX9(); }
        public int getPublic()   { return getX9(); }
    }
 
    @Benchmark
    public int testGetPrivate() {
        return inner.getPrivate();
    }
 
    @Benchmark
    public int testGetPublic() {
        return inner.getPublic();
    }
}



Вопрос третий
Можно ли в Java создать класс (именно класс, не интерфейс) без единого конструктора, даже приватного?


Такие дела!

Ваши ответы пишите ниже в комментариях под спойлером. У вас есть 12 часов. Первый, кто решит все три задачи, получит специальный приз от Андрея и Одноклассников. Остальные решившие все три задачи получат памятные призы от JUG.ru.

Удачи!

UPDATE:
Победил lany. Его ответы можете найти ниже в каментах! А вот как его награждали:

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


  1. MaximChistov
    17.04.2015 10:15

    Скрытый текст
    Вопрос третий
    Можно ли в Java создать класс (именно класс, не интерфейс) без единого конструктора, даже приватного?


    На хабре целая статья про этот изврат была, нагуглят быстро :)


    1. 23derevo Автор
      17.04.2015 10:16

      deleted


    1. apangin
      17.04.2015 10:35

      Не экземпляр класса создать, а сам класс такой, что Class.getDeclaredConstructors() вернёт для него пустой массив.


  1. lany
    17.04.2015 11:29
    +7

    Скрытый текст
    1. Math.random использует обычный Random, который очень плохо работает в параллельном режиме. Использовать его с parallelSetAll имеет мало смысла: будет высокий контеншн. Нужен ThreadLocalRandom

    3.
    public class NoConstructor {
      private static class Nested {
        private Nested() {}
      }
    
      public static void main(String[] args) throws Exception {
        new Nested();
        System.out.println(Class.forName("NoConstructor$1").getDeclaredConstructors().length);
      }
    }
    
    $ javac NoConstructor.java
    
    $ java NoConstructor
    0

    Работает в javac, не работает в ecj. Я про это писал здесь. Второй вариант — сгенерировать класс на лету, например, с помощью ASM, как это сделал LSD.

    Над вторым ща подумаю.


    1. lany
      17.04.2015 11:36
      +2

      Скрытый текст
      Во втором для приватного метода javac сгенерирует аксессор, который увеличит глубину вложенности вызовов на один. Видимо, максимальная глубина инлайна ограничена 10. Пытаюсь найти опцию JVM, которая это контролирует.


      1. lany
        17.04.2015 11:43
        +4

        Скрытый текст
        Ну да, предположение правильное. С +PrintCompilation +PrintInlining вижу такое:
            210  263       3       org.sample.Modifiers::testGetPrivate (7 bytes)
                                      @ 3   org.sample.Modifiers$Inner::access$000 (5 bytes)
                                        @ 1   org.sample.Modifiers$Inner::getPrivate (5 bytes)
                                          @ 1   org.sample.Modifiers$Inner::getX9 (5 bytes)
                                            @ 1   org.sample.Modifiers$Inner::getX8 (5 bytes)
                                              @ 1   org.sample.Modifiers$Inner::getX7 (5 bytes)
                                                @ 1   org.sample.Modifiers$Inner::getX6 (5 bytes)
                                                  @ 1   org.sample.Modifiers$Inner::getX5 (5 bytes)
                                                    @ 1   org.sample.Modifiers$Inner::getX4 (5 bytes)
                                                      @ 1   org.sample.Modifiers$Inner::getX3 (5 bytes)
                                                        @ 1   org.sample.Modifiers$Inner::getX2 (5 bytes)
                                                          @ 1   org.sample.Modifiers$Inner::getX1 (5 bytes)   inlining too deep

        А вот getPublic пролазит:

            168  261       3       org.sample.Modifiers::testGetPublic (7 bytes)
                                      @ 3   org.sample.Modifiers$Inner::getPublic (5 bytes)
                                        @ 1   org.sample.Modifiers$Inner::getX9 (5 bytes)
                                          @ 1   org.sample.Modifiers$Inner::getX8 (5 bytes)
                                            @ 1   org.sample.Modifiers$Inner::getX7 (5 bytes)
                                              @ 1   org.sample.Modifiers$Inner::getX6 (5 bytes)
                                                @ 1   org.sample.Modifiers$Inner::getX5 (5 bytes)
                                                  @ 1   org.sample.Modifiers$Inner::getX4 (5 bytes)
                                                    @ 1   org.sample.Modifiers$Inner::getX3 (5 bytes)
                                                      @ 1   org.sample.Modifiers$Inner::getX2 (5 bytes)
                                                        @ 1   org.sample.Modifiers$Inner::getX1 (5 bytes)


        1. 23derevo Автор
          17.04.2015 11:48
          +1

          Результат
          И ты становишься победителем!!! Лови нас с Андреем apangin, мы тебе вручим приз на открытии JPoint!


          1. lany
            17.04.2015 11:51
            +1

            Поймаю :D


        1. lany
          17.04.2015 11:51
          +5

          Скрытый текст
          Вот, нашёл. Что-то не сразу нагуглилось -XX:MaxInlineLevel=11 практически уравнивает результаты. getPrivate у меня всё ещё несколько медленнее, но в пределах доверительного интервала.


  1. m08pvv
    17.04.2015 11:31
    +2

    Первая задача
    Не джавист, но первая мысль при взгляде на
    public static double[] getRandomVector(int size) {
            double[] vector = new double[size];
            Arrays.parallelSetAll(vector, i -> Math.random());
            return vector;
        }
    

    о том, что скорее всего Math.random() в java не очень хорош в concurrency, о чём можно почитать в документации и убедиться. Соответсвенно, использовать ThreadLocalRandom


  1. Mercury13
    17.04.2015 11:58

    Мои непричёсанные мысли
    1. Причина в том, что вся эта штука работает, по сути, последовательно (будет затор у Math.random()). Как сделать правильно и параллельно — сказать не могу, потому что не знаю, как сделать несколько датчиков случайных чисел, инициализированных чем-то более-менее независимым. Думаю, даже если сделать код простым последовательным, всё равно будет быстрее.

    2. Точно не в курсе, но, думаю, какое-то хитрое поведение оптимизатора, который понимает, что getPublic() можно свернуть в return x; а getPrivate() — почему-то нет.

    3. На уровне байткода, разумеется, можно. На уровне языка… думаю, что-нибудь наподобие внутреннего private abstract без нестатических данных.


    1. 23derevo Автор
      17.04.2015 12:11

      Ответ
      все три мысли — в правильном направлении! Копайте дальше!


      1. Mercury13
        17.04.2015 16:17

        Я подсмотрел правильные ответы.

        Заголовок спойлера
        По поводу первого — надо смотреть реализацию threadLocalRandom. Сработает-то сработает, но насколько качественны случайные числа, которые оно выдаст?

        Второе — ключи компилятора. В такие дебри не совался.

        А третье — просто не было времени запустить NetBeans и посмотреть.


  1. lany
    17.04.2015 12:15
    +3

    Вот на вчерашней Никитиной задаче я затупил, хотя про слоты всё знал. Ну да, сейчас понял. Достаточно первый пример модифицировать вот так, и он заработает:

    public class OOM1 {
        private static final int SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.55);
    
        public static void main(String[] args) {
            {
                byte[] bytes = new byte[SIZE];
                System.out.println(bytes.length);
            }
            byte[] bytes1 = null;
    
            bytes1 = new byte[SIZE];
            System.out.println(bytes1.length);
    
            System.out.println("I allocated memory successfully");
        }
    }


    Мораль: пойду на доклад к Никите :D


    1. 23derevo Автор
      17.04.2015 13:04

      ты там осторожно, а то упоритесь вместе по хардкору и натворите чего-нибудь не того!


  1. ragesteel
    17.04.2015 13:18
    +1

    Спасибо что теперь хоть по Java, без всяких там Springов Groovyей!

    Мой ответы
    По первой задачи Math.random() синхронизируется внутри, не очень подходит для параллелизации. Видимо нужно задействовать ThreadLocalRandom().

    Во второй задаче — всё просто, вызов private будет обёрнут ещё в access метод, соответственно лишних overhead.

    По третьей — тут уж я не такой знаток. Наверняка можно какой-нибудь библиотекой кодогенерации так сделать. Получить список констркторов, все удалить и сохранить. А вот чтобы из Java. Немного гуглежа и я нашёл обсуждение на StackOveflow — да, javac добавляет конструктор по умолчанию, это положено по спецификации. А при создании java байткода вручную таких ограничений нет, спецификация класс-файла не обязывает иметь конструктор.


    1. apangin
      17.04.2015 14:13

      Комментарии
      1. Не совсем. Никаких synchronized и локов внутри Math.random нет.
      2. Действительно, будет метод-аксессор, но JIT ведь умеет простые методы инлайнить, чтоб не было никакого оверхеда.
      3. Да, всё так.



  1. relgames
    17.04.2015 14:44
    -1

    Заголовок спойлера
    1 — параллельное заполнение массива будет медленным не столько из-за Random, сколько из-за кэша процессора.

    2 — Разница в том, что для private метода генерируется специальный метод access (предполагаю, т.к. внутренний класс хранится в отдельном class файле, иначе никак):

      public int testGetPrivate();
        Code:
           0: getstatic     #2                  // Field inner:LModifiers$Inner;
           3: invokestatic  #3                  // Method Modifiers$Inner.access$000:(LModifiers$Inner;)I
           6: ireturn
    
      public int testGetPublic();
        Code:
           0: getstatic     #2                  // Field inner:LModifiers$Inner;
           3: invokevirtual #4                  // Method Modifiers$Inner.getPublic:()I
           6: ireturn
    .....
    static int access$000(Modifiers$Inner);
        Code:
           0: aload_0
           1: invokespecial #1                  // Method getPrivate:()I
           4: ireturn
    
    


    3 — не хочу даже думать над этим извратом, один вопрос — зачем???


    1. leventov
      17.04.2015 15:06
      +1

      1. Поясните, что вы имеете ввиду


      1. relgames
        17.04.2015 16:23
        +1

        Заголовок спойлера
        1 — я думал сперва про spatial locality (подумал, тут подвох и не может быть задача на просто знание JavaDoc).

        Тесты заставили меня поменять свои взгляды gist.github.com/relgames/840d89280237acd4c385

        Benchmark                                  (size)  Mode  Cnt        Score        Error  Units
        MyBenchmark.randomParallel                    100  avgt    5       20.629 ±      1.920  us/op
        MyBenchmark.randomSequential                  100  avgt    5        2.996 ±      0.569  us/op
        MyBenchmark.threadLocalRandomParallel         100  avgt    5        4.336 ±      0.172  us/op
        MyBenchmark.threadLocalRandomSequential       100  avgt    5        0.351 ±      0.086  us/op
        
        MyBenchmark.randomParallel                   1000  avgt    5      166.651 ±     52.215  us/op
        MyBenchmark.randomSequential                 1000  avgt    5       28.984 ±      7.477  us/op
        MyBenchmark.threadLocalRandomParallel        1000  avgt    5        7.591 ±      0.420  us/op
        MyBenchmark.threadLocalRandomSequential      1000  avgt    5        3.178 ±      0.683  us/op
        
        MyBenchmark.randomParallel                  10000  avgt    5     1542.161 ±    710.309  us/op
        MyBenchmark.randomSequential                10000  avgt    5      286.011 ±     60.862  us/op
        MyBenchmark.threadLocalRandomParallel       10000  avgt    5       18.001 ±      3.008  us/op
        MyBenchmark.threadLocalRandomSequential     10000  avgt    5       33.620 ±      4.221  us/op
        
        MyBenchmark.randomParallel               10000000  avgt    5  1511798.753 ± 477838.982  us/op
        MyBenchmark.randomSequential             10000000  avgt    5   275049.387 ±  45049.956  us/op
        MyBenchmark.threadLocalRandomParallel    10000000  avgt    5    29184.447 ±  34445.026  us/op
        MyBenchmark.threadLocalRandomSequential  10000000  avgt    5    37231.631 ±  15880.473  us/op
        


        На малых значениях последовательное заполнение действительно быстрее параллельного, но не из-за кэша, а из-за расходов на параллельность. При увеличении размера массива параллельная обработка становится быстрее. В целом, это доказывает рассуждения о том, что parallelStream() нужен не везде, а в тех случаях, когда элементов много либо же когда операции долгие.

        Неожиданным открытием было то, что ThreadLocalRandom работает быстрее Random всегда и везде — на больших, маленьких, параллельных и последовательных операциях. Подозреваю, это из-за синхронизации в Random.


        1. leventov
          17.04.2015 17:01
          +1

          Consider using S.parallelStream().operation(F) instead of S.stream().operation(F) when…

          The total time to execute the sequential version exceeds a minimum threshold. These days, the threshold is roughly (within a factor of ten of) 100 microseconds across most platforms.
          gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html

          А также недавнее упоминание на русском:
          — А части ли случаи, когда Stream API проигрывает по перфомансу классическому (Collections) API?

          Куксенко: Как правило, Stream API проигрывает по производительности классическому API просто потому что Stream API подразумевает под собой некоторый оверхед, некоторые действия, которые мы обязаны сделать для того чтобы сконструировать хороший стрим и начать с ним работать. Вопрос: что мы выиграем, сделав эти некоторые затраты? Без параллелизации с точки зрения перфоманса тут нечего выигрывать, за исключением более компактного и лучше читаемого кода. Вопрос параллелизации, об этом тоже много говорилось, и писались различные примеры, шаблоны, графики, когда лучше переходить.

          Сейчас самая осторожная оценка, которая выше нашей внутренней оценки, и которая, в принципе, достаточно разумна, и ее предлагал Даг Ли: если количество параллелизуемой работы в общем случае превышает 100 миллисекунд, то, параллелизуя ее, можно получить выигрыш с вероятностью 99%. Если количество работы меньше 100 миллисекунд, лучше не заморачиваться параллелизацией. По нашим замерам это 10 миллисекунд, но там уже… Это безопасная граница.
          habrahabr.ru/company/jugru/blog/255219

          Кстати, 23derevo, выходит что Walrus либо оговорился (100 микросекунд/миллисекунд), либо вы неправильно расшифровали.

          Неожиданным открытием было то, что ThreadLocalRandom работает быстрее Random всегда и везде — на больших, маленьких, параллельных и последовательных операциях. Подозреваю, это из-за синхронизации в Random.
          Да :)


          1. 23derevo Автор
            17.04.2015 17:09

            Walrus либо оговорился (100 микросекунд/миллисекунд), либо вы неправильно расшифровали.

            Я спрошу его.


          1. 23derevo Автор
            17.04.2015 17:50

            Да, Серега ошибся. Хотя оценка Дага выглядит ну ОЧЕНЬ консервативной.


            1. leventov
              17.04.2015 18:02

              Как раз наоборот, 100 микросекунд это же в 1000 раз меньше, чем 100 миллисекунд. То есть это довольно рискованная, казалось бы, оценка


              1. 23derevo Автор
                17.04.2015 18:13

                тьфу, вечер пятницы :)

                Спросил Андрея тут.


        1. lany
          17.04.2015 17:31

          Скрытый текст
          Результат интересный, хотя в принципе логичный. В ThreadLocalRandom сид хранится в поле класса Thread, которое ещё и помечено как Contended. То есть тут самая обычная запись в память, никакой синхронизации, никаких барьеров и хорошая поддержка со стороны кэша процессора. В обычном Random нет синхронизации, но есть AtomicLong, который обновляется через CAS. Даже в одном потоке это дополнительные расходы.

          С сегодняшнего дня перехожу на ThreadLocalRandom!


          1. 23derevo Автор
            17.04.2015 17:39

            а у тебя так много рандома под контеншеном? ;)

            Math.random() компактнее выглядит, глаз не режет. Вот увидит любой из твоих менее шарящих коллег ThreadLocalRandom и заподозрит подвох. Начнет копать не туда… Тебе оно нада?


            1. lany
              17.04.2015 18:09
              +1

              Math.random() — это вообще плохо. Во-первых, с него можно получить только дробные числа от 0 до 1. Это требуется не очень часто. Чаще нужно Random.nextInt(a, b). А таскать где-то созданный Random на протяжении всего алгоритма может быть не очень удобно. В результате люди либо пишут что-то уродливое вроде (int)(Math.random()*(b-a))+a. В этом случае ThreadLocalRandom.current().nextInt(a, b) гораздо меньше режет глаз.

              По хорошему стохастический алгоритм должен принимать на вход фабрику рэндомов. Тогда можно и стабильного поведения для тестов добиться, и тредлокал использовать, если скорость нужна.


    1. lany
      17.04.2015 15:28
      +1

      Скрытый текст
      2 — это только часть ответа. Вызовите getX8 вместо getX9 из обоих методов, и производительность будет одинаковой.


      1. relgames
        17.04.2015 16:24

        Заголовок спойлера
        Не уверен, но думаю, есть какой-то дефолтный лимит на inline. Лишний метод этот лимит нарушает.


  1. lany
    17.04.2015 15:33
    +8

    Кстати, про regexp — не могу пройти мимо:

    Пофиксить можно двумя способами:

    Possessive quantifier:

    • Pattern.compile("/\\*(?:[^*]|\\*[^/])*+\\*/")

    Independent group:

    • Pattern.compile("/\\*(?>,[^*]|\\*[^/])*\\*/")

    Нет, нельзя это фиксать! Я не хочу видеть ни первый, ни второй код в своём приложении. Надо написать грамматику, хоть antlr, хоть на худой конец javacc. Это очень просто, куча примеров в сети, и главное — легко поддерживать и дополнять новыми фичами.

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


  1. werewolfspb
    18.04.2015 16:31

    Решение первой задачи
    Проблема кода в том, что для большого числа потоков (их количество определяет JDK на основе, видимо, размера массива и прочего) и большого массива может оказаться, что есть contention, т.к. все потоки
    будут использовать общий экземпляр класса Random. Там, конечно, не просто synchronize, а CAS вызовы внутри в nextDouble(),
    но тем не менее, авторы JDK8 предлагают в таких случаях использовать ThreadLocalRandom.
    Просто, чтобы потоки не дрались между собой. Соответственно фикс простой:
        public static double[] getRandomVector(int size) {
            double[] vector = new double[size];
            Arrays.parallelSetAll(vector, i -> ThreadLocalRandom.current().nextDouble());
            return vector;
        }