Почему моя лямбда получает недопустимую прямую ссылку, а мой анонимный класс — нет?

Я пытаюсь представить диаграмму перехода состояний, и я хочу сделать это с помощью перечисления Java. Я хорошо знаю, что есть много других способов сделать это с помощью Map<K, V> или, может быть, с помощью статического блока инициализации в моем перечислении. Однако я пытаюсь понять, почему происходит следующее.

Вот (чрезвычайно) упрощенный пример того, что я пытаюсь сделать.


enum RPS0
{
  
    ROCK(SCISSORS),
    PAPER(ROCK),
    SCISSORS(PAPER);
     
    public final RPS0 winsAgainst;
     
    RPS0(final RPS0 winsAgainst)
    {
        this.winsAgainst = winsAgainst;
    }
}

Очевидно, это не удается из-за недопустимой прямой ссылки.

ScratchPad.java:150: error: illegal forward reference
         ROCK(SCISSORS),
              ^

Это нормально, я принимаю это. Попытка вручную вставить SCISSORS потребовала бы, чтобы Java попытался настроить SCISSORS, что затем вызвало бы настройку PAPER, которая затем вызвала бы настройку ROCK, что привело бы к бесконечному циклу. Тогда я легко понимаю, почему эта прямая ссылка неприемлема и запрещена из-за ошибки компилятора.

Итак, я экспериментировал и пытался сделать то же самое с лямбда-выражениями.

enum RPS1
{
    ROCK(() -> SCISSORS),
    PAPER(() -> ROCK),
    SCISSORS(() -> PAPER);
     
    private final Supplier<RPS1> winsAgainst;
     
    RPS1(final Supplier<RPS1> winsAgainst)
    {
        this.winsAgainst = winsAgainst;
    }
     
    public RPS1 winsAgainst()
    {
        return this.winsAgainst.get();
    }
}

Это не удалось с в основном той же ошибкой.

ScratchPad.java:169: error: illegal forward reference
         ROCK(() -> SCISSORS),
                    ^

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

Кстати, я экспериментировал с добавлением фигурных скобок и возвратом к лямбде, но и это не помогло.

Итак, я попытался с анонимным классом.

enum RPS2
{
    ROCK
    {
        public RPS2 winsAgainst()
        {
            return SCISSORS;
        } 
    },
         
    PAPER
    {
        public RPS2 winsAgainst()
        {
            return ROCK;
        }     
    },
         
    SCISSORS
    {
        public RPS2 winsAgainst()
        {
            return PAPER;
        }
    };
         
    public abstract RPS2 winsAgainst();   
}

Как ни странно, это сработало.

System.out.println(RPS2.ROCK.winsAgainst()); //returns "SCISSORS"

Итак, я подумал поискать ответы в Спецификации языка Java для Java 19, но мои поиски ничего не дали. Я попытался выполнить поиск Ctrl + F (без учета регистра) для соответствующих фраз, таких как «Незаконно», «Вперед», «Ссылка», «Перечисление», «Лямбда», «Аноним» и других. Вот некоторые из ссылок, которые я искал. Может быть, я что-то упустил в них, отвечающее на мой вопрос?

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

РЕДАКТИРОВАТЬ - @DidierL указал ссылку на другой пост StackOverflow, посвященный чему-то подобному. Я думаю, что ответ на этот вопрос совпадает с моим. Короче говоря, анонимный класс имеет свой собственный «контекст», а лямбда — нет. Следовательно, когда лямбда пытается получить объявления переменных/методов/и т. д., это будет так же, как если бы вы сделали это встроенным, как в моем примере RPS0 выше.

Это расстраивает, но я думаю, что, как и ответ @Michael, оба ответили на мой вопрос до конца.

РЕДАКТИРОВАТЬ 2. Добавление этого фрагмента для моего обсуждения с @Michael.


      enum RPS4
      {
      
         ROCK
         {
            
            public RPS4 winsAgainst()
            {
            
               return SCISSORS;
            }
         
         },
         
         PAPER
         {
         
            public RPS4 winsAgainst()
            {
            
               return ROCK;
               
            }
            
         },
         
         SCISSORS
         {
         
            public RPS4 winsAgainst()
            {
            
               return PAPER;
            
            }
         
         },
         ;
         
         public final RPS4 winsAgainst;
         
         RPS4()
         {
         
            this.winsAgainst = this.winsAgainst();
         
         }
         
         public abstract RPS4 winsAgainst();
      
      }
   

Интересный эксперимент. jenkov.com/tutorials/java/lambda-expressions.html говорится: «Лямбда-выражения Java могут использоваться только в том случае, если тип, с которым они сопоставляются, представляет собой интерфейс с одним методом». Таким образом, похоже, что то место, где вы пытались применить лямбду, не подходит для ее применения.

Zack Macomber 10.01.2023 17:31

@ZackMacomber Спасибо за ваш ответ. Хотя я не уверен, что вы правы. Разве интерфейс, с которым я сравниваю, не должен быть моим Supplier<RPS1>?

davidalayachew 10.01.2023 17:32

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

Michael 10.01.2023 17:33

@Майкл, я вижу твои правки. Спасибо за изменения. Я составил простой маркированный список поисковых запросов, которые пытался выполнить. Это должно удовлетворить краткость, позволяя людям оказывать более информированную/направленную поддержку. Пожалуйста, отредактируйте мое редактирование, если вы считаете, что оно должно быть другим.

davidalayachew 10.01.2023 17:43

Отвечает ли это на ваш вопрос? Доступ к значению перечисления того же класса внутри объявления перечисления в лямбда-выражении не компилируется

Didier L 10.01.2023 18:00

@DidierL О, я думаю, ты нашел это, Дидье. Я читаю дальше, но это может быть

davidalayachew 10.01.2023 18:02

@DidierL Думаю, ты прав. Оба наших вопроса терпят неудачу по причине, указанной в ответе. Я отредактировал свой ответ, включив в него ссылку на этот вопрос и простое объяснение. Спасибо вам за помощь. stackoverflow.com/a/55166692/10118965

davidalayachew 10.01.2023 18:21
Как сделать движок для футбольного матча? (простой вариант)
Как сделать движок для футбольного матча? (простой вариант)
Футбол. Для многих людей, живущих на земле, эта игра - больше, чем просто спорт. И эти люди всегда мечтают стать футболистом или менеджером. Но, к...
Знайте свои исключения!
Знайте свои исключения!
В Java исключение - это событие, возникающее во время выполнения программы, которое нарушает нормальный ход выполнения инструкций программы. Когда...
Лучшая компания по разработке спортивных приложений
Лучшая компания по разработке спортивных приложений
Ищете лучшую компанию по разработке спортивных приложений? Этот список, несомненно, облегчит вашу работу!
Blibli Automation Journey - Как захватить сетевой трафик с помощью утилиты HAR в Selenium 4
Blibli Automation Journey - Как захватить сетевой трафик с помощью утилиты HAR в Selenium 4
Если вы являетесь веб-разработчиком или тестировщиком, вы можете быть знакомы с Selenium, популярным инструментом для автоматизации работы...
Фото ️🔁 Radek Jedynak 🔃 on ️🔁 Unsplash 🔃
Фото ️🔁 Radek Jedynak 🔃 on ️🔁 Unsplash 🔃
Что такое Java 8 Streams API? Java 8 Stream API
Деревья поиска (Алгоритм4 Заметки к учебнику)
Деревья поиска (Алгоритм4 Заметки к учебнику)
(1) Двоичные деревья поиска: среднее lgN, наихудшее N для вставки и поиска.
5
7
92
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Я не эксперт, поэтому могу ошибаться, но это мое понимание.

Недопустимая прямая ссылка означает, что вы пытаетесь использовать переменную до того, как она была определена. Это как сказать "i = 10; int i;".

Первые два фактически используют переданную переменную. RPS0 и RPS1 присваивают неизвестную переменную SCISSORS полю, ожидающему переменную RPS0/RPS1. Это должен быть ожидаемый результат.

Тогда для анонимного класса он должен делать что-то другое. Java должна переупорядочивать определения, чтобы сначала определить экземпляры RPS2, а затем создавать их экземпляры.

Спасибо за ваш ответ. Я вижу, что вы говорите. Я понимаю, что акт использования значения перечисления сохраняется до фактического вызова метода. Меня смущает, почему то же самое не происходит с лямбдой? Лямбда еще не должна использовать значение перечисления. Даже что-то вроде ROCK(() -> {return RPS1.SCISSORS;}), все равно не работает.

davidalayachew 10.01.2023 17:45
Ответ принят как подходящий

Я считаю, что это связано с тем, что JLS называет «определенным назначением».

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

  1. Какой-то другой класс впервые обращается к классу перечисления, например. RPS0.ROCK.winsAgainst()
  2. "clinit" (вызывается статический инициализатор)
  3. Константы перечисления инициализируются в порядке объявления
    1. Эквивалент public static final RPS0 ROCK = new RPS0(SCISSORS);
    2. Эквивалент public static final RPS0 PAPER = new RPS0(ROCK);
    3. Эквивалент public static final RPS0 SCISSORS = new RPS0(PAPER);
  4. Теперь класс enum загружен
  5. Выражение RPS0.ROCK оценивается для этого ссылочного класса в #1.
  6. winsAgainst вызывается в этом экземпляре.

Причина, по которой это не работает в строке ROCK, заключается в том, что SCISSORS не является «определенно назначенным». Зная порядок инициализации, мы видим, что это еще хуже. Мало того, что он точно не назначен, он определенно не назначен. Присвоение SCISSORS (3.3) происходит после ROCK (3.1).

SCISSORS будет нулевым, когда ROCK попытается получить к нему доступ, если компилятор разрешит это. Он назначается позже.

Мы можем убедиться в этом сами, если введем некоторую косвенность. Проблема определенного присваивания больше не существует, потому что наши конструкторы не ссылаются на поля напрямую. Компилятор не проверяет какое-либо определенное присваивание в выражении конструктора. Конструктор использует результат вызова метода, а не поле.

Все, что мы сделали, это обманули компилятор, заставив его разрешить что-то, что может потерпеть неудачу.

enum RPS0
{
    ROCK(scissors()),
    PAPER(rock()),
    SCISSORS(paper());

    public final RPS0 winsAgainst;

    RPS0(final RPS0 winsAgainst)
    {
        this.winsAgainst = Objects.requireNonNull(winsAgainst); // boom
    }
    
    
    
    private static RPS0 scissors() {
        return RPS0.SCISSORS;
    }

    private static RPS0 rock() {
        return RPS0.ROCK;
    }

    private static RPS0 paper() {
        return RPS0.PAPER;
    }
}

Случай лямбды почти идентичен. Значение до сих пор точно не присвоено. Рассмотрим случай, когда конструктор перечисления вызывает get для Supplier. Вернитесь к порядку инициализации выше. В приведенном ниже примере ROCK будет пытаться получить доступ к SCISSORS до того, как он будет инициализирован, и это потенциальная ошибка, от которой компилятор пытается вас защитить.

enum RPS1
{
    ROCK(() -> SCISSORS), // compiler error
    PAPER(() -> ROCK),
    SCISSORS(() -> PAPER);
     
    private final Supplier<RPS1> winsAgainst;
     
    RPS1(final Supplier<RPS1> winsAgainst)
    {
        RPS1.get(); // doesn't compile, but would be null if it did
    }
}

На самом деле это раздражает, потому что вы знаете, что не используете Поставщика таким образом, и это единственный раз, когда он может быть еще не назначен.

Причина, по которой абстрактный класс работает, заключается в том, что цель выражения в конструкторе теперь полностью отсутствует. Опять же, вернемся к порядку инициализации. Вы должны увидеть это для всего, что вызывает winsAgainst, например. в примере № 1 этот вызов (№ 6) обязательно происходит после того, как все константы перечисления уже инициализированы (№ 3). Компилятор может гарантировать, что этот доступ безопасен.

Объединив две вещи, которые мы знаем — что мы можем использовать косвенность, чтобы остановить жалобы компилятора на отсутствие определенного присваивания, и что Supplier может лениво предоставлять значение — мы можем создать альтернативное решение:

enum RPS0
{
    ROCK(RPS0::scissors), // i.e. () -> scissors()
    PAPER(RPS0::rock),
    SCISSORS(RPS0::paper);

    public final Supplier<RPS0> winsAgainst;

    RPS0(Supplier<RPS0> winsAgainst) {
        this.winsAgainst = winsAgainst;
    }
    
    public RPS0 winsAgainst() {
        return winsAgainst.get();
    }


    // Private indirection methods
    private static RPS0 scissors() {
        return RPS0.SCISSORS;
    }

    private static RPS0 rock() {
        return RPS0.ROCK;
    }

    private static RPS0 paper() {
        return RPS0.PAPER;
    }
}

Это доказуемо безопасно, при условии, что конструктор никогда не вызывает Supplier.get (включая любые методы, которые вызывает сам конструктор).

Благодарю за ваш ответ. Я не знаю, подходит ли слово «связывание», но оно определенно указывает мне правильное направление. Чтобы убедиться, что я понимаю, похоже, вы говорите, что реальная проблема здесь заключается в том, что Java пытается «привязать» раньше для лямбда-выражений, чем для анонимных классов, что приводит к возникновению этой проблемы?

davidalayachew 10.01.2023 17:54

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

davidalayachew 10.01.2023 18:09

Кроме того, кто-то в комментариях порекомендовал эту ссылку -- stackoverflow.com/questions/55162147/… -- я привожу ее на случай, если вы сможете понять ее больше, чем я. Я все еще пытаюсь понять это, но похоже, что ваш ответ и их ответ подразумевают одно и то же.

davidalayachew 10.01.2023 18:11

«На самом деле это не имеет прямого отношения к лямбда-выражениям или анонимным классам» - я думаю, что это напрямую связано с «классами». В тот момент, когда вы используете ДРУГОЙ (будь то анонимный или любой другой) класс, недопустимая прямая ссылка исчезает. Насколько мне известно, лямбда-выражения не являются классами.

Zack Macomber 10.01.2023 18:18

@ZackMacomber Я понимаю, что ты говоришь. В другом ответе на другой вопрос возникла концепция «контекста», в которой говорится, что JLS говорит, что контекст для лямбда отличается от контекста анонимного класса. Вот ответ -- stackoverflow.com/a/55166692/10118965

davidalayachew 10.01.2023 18:25

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

davidalayachew 10.01.2023 18:26

@Michael Глупый я, я должен был запустить его, прежде чем прокомментировать. Теперь я понимаю, что вы имеете в виду. Спасибо.

davidalayachew 10.01.2023 18:29

@Майкл И я вижу твое последнее редактирование. Это многое проясняет, спасибо. Короче говоря, лямбда-выражения, методы и любые другие формы косвенности просто не будут работать, потому что все они проходят в поле до его инициализации. Вы продемонстрировали, как мы можем обмануть компилятор, чтобы он не улавливал опасность в том, что мы делаем это, но это не отменяет того факта, что все, что мы делаем, — это не позволяем компилятору спасти нас от чего-то плохого. На самом деле единственный способ добиться того, чего я хочу, — это использовать анонимный класс, потому что он ничего не оценивает до вызова метода. Да?

davidalayachew 10.01.2023 18:35

@Michael Спасибо за ваше терпение, помогающее мне понять это. Область видимости и то, как выбираются переменные, — это одна из вещей, которые я никогда не понимал в Java так хорошо. Мне потребовалось много времени, чтобы понять, что такое лямбда-выражения и как они не могут ссылаться на поля, пока они не станут фактически окончательными. Довольно сложно исследовать этот аспект Java, так как большая часть этого происходит под капотом и отчасти зависит от того, знаете ли вы и остаетесь на «Счастливом пути».

davidalayachew 10.01.2023 18:46

@davidalayachew Я пересмотрел свой ответ. Я думаю, что сейчас лучше. Я собираюсь очистить свои комментарии выше, но дайте мне знать, если это все еще неясно.

Michael 10.01.2023 19:46

@Майкл Это отличный ответ, спасибо за это. Это многое проясняет. Как ни странно, ваш вопрос привел меня к другому — той части, где вы сказали «Раздражает, правда», разве мы не так же уязвимы для этого через анонимные классы, как мы были бы с Supplier<RPS1>? Я отредактировал свой вопрос, пожалуйста, посмотрите, что я имею в виду. Это RPS4.

davidalayachew 10.01.2023 20:08

@davidalayachew Да, RPS4 все еще не работает. Измените инициализатор поля на this.winsAgainst = Objects.requireNonNull(this.winsAgainst()); Он взорвется. Однако стоит отметить, что поле здесь не добавляет никакого значения. Вы можете просто звонить winsAgainst() каждый раз, когда будете обращаться к полю, и тогда все заработает.

Michael 10.01.2023 21:02

@Michael О, абсолютно, я намеренно туплю, чтобы показать, что предупреждение - довольно короткий забор. Несмотря на это, с этим редактированием, я думаю, вы затронули проблему в самом сердце - ребята из JDK дали более сильный набор ошибок компилятора лямбда-выражениям, чем они сделали для анонимных классов, с намерением защитить нас, ссылаясь на определенное назначение и ссылки JLS Я отправил. Тем не менее, оба решения по-прежнему склонны к косвенности и повреждению. И из-за всего этого, идти так, как вы, с Supplier (или моим анонимным классом) является «самым безопасным» решением, которое все еще разрешено. Большое спасибо!

davidalayachew 10.01.2023 21:09

И, чтобы было ясно, эти «защиты» делают все возможное, чтобы защитить людей от того, чтобы они не попали в ту же выбоину. Но, конечно, они не могут (или не хотят) сделать проверку на 100% исчерпывающей для всех возможностей. И в результате это все еще можно сломать, как вы показали с нулем. Тем не менее, они приложили больше усилий, чтобы защитить нас с помощью лямбда-выражений, что объясняет расхождения. Не говоря уже о том, что у лямбда-выражений больше возможностей для злоупотреблений, а проблемы с анонимными классами в то время были хорошо известны. Если все это правда, то теперь я все понимаю.

davidalayachew 10.01.2023 21:12

Еще раз спасибо, что нашли время, чтобы провести меня через это. Благодаря вашим и всем остальным усилиям я чувствую, что понимаю всю шкалу «почему», а не только «что».

davidalayachew 10.01.2023 21:13

Не совсем ответ, но как только вы замените первое лямбда-выражение анонимным классом, оно начнет работать.

enum RPS1 {
    ROCK(new Supplier<>() {
        @Override
        public RPS1 get() {
            return SCISSORS;
        }
    }),
        
    PAPER(() -> ROCK),
    SCISSORS(() -> PAPER);

    private final Supplier<RPS1> winsAgainst;

    RPS1(final Supplier<RPS1> winsAgainst) {
        this.winsAgainst = winsAgainst;
    }

    public RPS1 winsAgainst() {
        return this.winsAgainst.get();
    }
}

Если это то, с чем вы собираетесь работать, вам не нужен поставщик. Вы можете использовать анонимный класс для первого. Это работает только потому, что камень-ножницы-бумага имеет циклическую связь, но для сложной модели перехода состояний я подозреваю, что этот метод «нарушения» циклических отношений либо чрезвычайно утомителен, либо физически невозможен.

Michael 10.01.2023 17:53

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

davidalayachew 10.01.2023 17:57

Https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.3

Основываясь на правилах, похоже, что анонимный класс считается приемлемым, потому что это «другой класс».

В примере есть несколько комментариев, которые говорят «хорошо — происходит в другом классе».

Благодарю за ваш ответ. Я вроде понимаю. Я предполагаю, что следующий вопрос: почему лямбда-тело не считается другим классом? Разве целью лямбды не является предоставление функции, область действия которой является ее собственной, но с некоторыми полями/методами/и т. д., заимствованными из области, в которой она находится? Возможно, я ошибаюсь.

davidalayachew 10.01.2023 18:05

Другие вопросы по теме