¿Por qué negate () requiere un lanzamiento explícito para predicar?


8

Tengo una lista de nombres. En la línea 3, tuve que enviar el resultado de la expresión lambda a Predicate<String>. El libro que estoy leyendo explica que el reparto es necesario para ayudar al compilador a determinar cuál es la interfaz funcional correspondiente.

Sin embargo, no necesito tal reparto en la siguiente línea porque no llamo negate(). ¿Cómo hace esto alguna diferencia? Entiendo que negate()aquí regresa Predicate<String>, pero ¿la expresión lambda anterior no hace lo mismo?

List<String> names = new ArrayList<>();
//add various names here
names.removeIf(((Predicate<String>) str -> str.length() <= 5).negate()); //cast required
names.removeIf(((str -> str.length() <= 5))); //compiles without cast

1
... el reparto es necesario para ayudar al compilador a determinar cuál es la interfaz funcional coincidente. responde de todos modos. La razón por la cual la siguiente línea no lo necesita es que la inferencia es mucho más simple y no es seguida por una invocación de método en la interfaz inferida. Aprendí a usar funciones lambda que involucran genéricos, que se trata principalmente de inferencia en tales casos y las declaraciones explícitas son mucho más seguras, comoPredicate<String> predicate = str -> str.length() <= 5; names.removeIf(predicate.negate()); names.removeIf(predicate);
Naman

@Nathan ¿No debería la llamada a negar () haber proporcionado aún más contexto, haciendo que el reparto explícito sea aún menos necesario? La expresión lambda podría haberse referido a una función en lugar de un predicado, después de todo, pero la llamada posterior a negate () elimina cualquier margen de ambigüedad.
K Man

Pero, ¿a qué se negate()recurriría si no es capaz de inferir el tipo del atributo lambda que forma parte de la expresión y, por lo tanto, no puede establecer que la expresión significa a Predicate<String>?
Naman

No estoy diciendo que negate () sea necesario. Mi última línea claramente funcionó sin ella. Sin embargo, no veo cómo llamar a negate () podría confundir repentinamente al compilador en cuanto al tipo de interfaz funcional.
K Man

Respuestas:


8

No es solo porque llamasnegate() . Eche un vistazo a esta versión, que es muy parecida a la suya pero que compila:

Predicate<String> predicate = str -> str.length() <= 5;
names.removeIf(predicate.negate());

¿La diferencia entre esto y tu versión? Se trata de cómo las expresiones lambda obtienen sus tipos (el "tipo de destino").

¿Qué crees que hace esto?

(str -> str.length() <= 5).negate()?

Su respuesta actual es que "invoca negate()lo Predicate<String>dado por la expresión str -> str.length() <= 5". ¿Derecha? Pero eso es solo porque esto es lo que querías que hiciera. El compilador no lo sabe . ¿Por qué? Porque podría ser cualquier cosa. Mi propia respuesta a la pregunta anterior podría ser "llama negatea mi otro tipo de interfaz funcional ... (sí, el ejemplo será un poco extraño)

interface SentenceExpression {
    boolean checkGrammar();
    default SentenceExpression negate() {
        return ArtificialIntelligence.contradict(explainSentence());
    };
}

Podría usar la misma expresión lambda names.removeIf((str -> str.length() <= 5).negate());pero que significa str -> str.length() <= 5ser SentenceExpressionmás que un Predicate<String>.

Explicación: (str -> str.length() <= 5).negate()no hace str -> str.length() <= 5a Predicate<String>. Y es por eso que dije que podría ser cualquier cosa, incluida mi interfaz funcional anterior.

De vuelta a Java ... Esta es la razón por la cual las expresiones lambda tienen el concepto de "tipo de destino", que define la mecánica por la cual el compilador entiende una expresión lambda como un tipo de interfaz funcional dado (es decir, cómo ayuda al compilador a saber que la expresión es en Predicate<String>lugar de SentenceExpressiono cualquier otra cosa que podría ser). Puede resultarle útil leer ¿Qué se entiende por tipo de destino lambda y contexto de tipo de destino en Java? y Java 8: mecanografía de destino

Uno de los contextos en los que se infieren los tipos de destino (si lee las respuestas en esas publicaciones) es el contexto de invocación, donde pasa una expresión lambda como argumento para un parámetro de un tipo de interfaz funcional, y eso es lo que es aplicable a names.removeIf(((str -> str.length() <= 5)));: es solo la expresión lambda dada como argumento de un método que toma a Predicate<String>. Esto no se aplica a la declaración que no se está compilando.

Entonces, en otras palabras ...

names.removeIf(str -> str.length() <= 5);utiliza una expresión lambda en un lugar donde el tipo de argumento define claramente cuál es el tipo de expresión lambda que se espera que sea (es decir, el tipo de destino str -> str.length() <= 5es claramente Predicate<String>).

Sin embargo, (str -> str.length() <= 5).negate()no es una expresión lambda, es solo una expresión que utiliza una expresión lambda. Esto quiere decir que str -> str.length() <= 5en este caso no está en el contexto de invocación que determina el tipo de destino de la expresión lambda (como en el caso de su última declaración). Sí, el compilador sabe que removeIfnecesita un Predicate<String>, y sabe con certeza que toda la expresión pasada al método tiene que ser un Predicate<String>, pero no asumiría que cualquier expresión lambda en la expresión de argumento sería un Predicate<String>(incluso si lo trata como predicado al invocarlo negate(); podría haber sido cualquier cosa que sea compatible con la expresión lambda).

Es por eso que es necesario escribir su lambda con un molde explícito (o de lo contrario, como en el primer contraejemplo que di).


names.removeIf (((str -> str.length () <= 5))); claramente compila sin problema. Todavía no entiendo cómo llamar a negate (), que devuelve un Predicate, de repente hace que el compilador dude de que toda la expresión se refiera a un Predicate.
K Man

@KMan Creo que necesita más detalles sobre cómo se escriben las expresiones lambda. Editaré y agregaré un enlace.
ernest_k

Ya he leído bastante sobre las expresiones lambda. Si la expresión completa es o no una expresión lambda es irrelevante, ¿no es así? ¿Negate () no devuelve Predicate <String> en mi código anterior? Podría estar equivocado, pero no creo que lo esté.
K Man

1
@KMan ¿ Negate () no devuelve Predicate <String> en mi código anterior? .. No, no lo hace hasta que se lo solicita,Predicate<String> que es lo que el compilador no ha podido inferir en primer lugar debido al uso de negatesí mismo como se explica en esta respuesta.
Naman

@KMan que he editado, creo que debes releer la respuesta. En resumen, como Naman está diciendo, que se reduce a la comprensión de que (str -> str.length() <= 5).negate()no lo hace str -> str.length() <= 5una Predicate<String>, incluso si eso es lo que pretende.
ernest_k

4

No sé por qué esto tiene que ser tan confuso. Esto se puede explicar con 2 razones, la OMI.

  • Las expresiones lambda son expresiones poli .

Dejaré que descubras qué significa esto y cuáles son las JLSpalabras que lo rodean. Pero en esencia, estos son como genéricos:

static class Me<T> {
    T t...
}

¿Cuál es el tipo Taquí? Bueno, eso depende. Si lo haces :

Me<Integer> me = new Me<>(); // it's Integer
Me<String>  m2 = new Me<>(); // it's String

Se dice que las expresiones poli dependen del contexto de donde se usan. Las expresiones lambda son iguales. Tomemos la expresión lambda de forma aislada aquí:

(String str) -> str.length() <= 5

cuando lo miras, ¿qué es esto? Bueno es un Predicate<String>? Pero puede ser A Function<String, Boolean>? O puede ser incluso MyTransformer<String, Boolean>, donde:

 interface MyTransformer<String, Boolean> {
     Boolean transform(String in){
         // do something here with "in"
     } 
 } 

Las opciones son infinitas.

  • En teoría, .negate()llamar directamente podría ser una opción.

Desde 10_000 millas arriba, está en lo correcto: está proporcionando eso str -> str.length() <= 5a un removeIfmétodo, que solo acepta a Predicate. No hay más removeIfmétodos, por lo que el compilador debería ser capaz de "hacer lo correcto" cuando lo proporcione (str -> str.length() <= 5).negate().

Entonces, ¿cómo es que esto no funciona? Comencemos con tu comentario:

¿No debería la llamada a negate () haber proporcionado aún más contexto, haciendo que el reparto explícito sea aún menos necesario?

Parece que aquí es donde comienza el problema principal, simplemente no es así como javacfunciona. No puede tomar todo str -> str.length() <= 5).negate(), decirse a sí mismo que esto es un Predicate<String>(ya que lo está utilizando como argumento para removeIf) y luego descomponer aún más la parte sin .negate()y ver si eso Predicate<String>también es un . javacactúa a la inversa, necesita conocer el objetivo para poder saber si es legal llamar negateo no.

También debe hacer una distinción clara entre expresiones poli y expresiones , en general. str -> str.length() <= 5).negate()es una expresión, str -> str.length() <= 5es una expresión poli.

Puede haber idiomas donde las cosas se hacen de manera diferente y donde esto es posible, javacsimplemente no es ese tipo.


1

En el siguiente ejemplo

      names.removeIf(str -> str.length() <= 5); //compiles without cast

La expresión devuelve verdadero. Si en el siguiente ejemplo sin el elenco, trueno sabe nada sobre el métodonegate()

Por otra parte,

   names.removeIf(((Predicate<String>) str -> str.length() <= 5).negate()); //cast required

La expresión se convierte en Predicate<String>para decirle al compilador dónde encontrar el método negate. Entonces es el método negate()que realmente realiza la evacuación de la siguiente manera:

   (s)->!test(s) where s is the string argument

Tenga en cuenta que podría lograr el mismo resultado sin el elenco de la siguiente manera:

    names.removeIf(str->!str.length <= 5) 
      // or
    name.removeIf(str->str.length > 5)

1

Sin entrar demasiado en las cosas, la conclusión es que la lambda str -> str.length() <= 5no es necesariamente una, Predicate<String>como explicó Eugene .

Dicho esto, negate()es una función miembro de Predicate<T>y el compilador no puede usar la llamada a negate()para ayudar a inferir el tipo con el que se debe interpretar el lambda. Incluso si lo intentara, podría haber problemas porque si varias clases posibles tuvieran negate()funciones, no sabría cuál elegir.

Imagina que eres el compilador.

  • Ya ves str -> str.length() <= 5. Usted sabe que es lambda que toma una cadena y devuelve un booleano, pero no sabe específicamente qué tipo representa.
  • A continuación, verá la referencia del miembro .negate()pero porque no conoce el tipo de expresión a la izquierda del "." debe informar un error ya que no puede usar la llamada para negar para ayudar a inferir el tipo.

Si se hubiera negado como una función estática, las cosas funcionarían. Por ejemplo:

public class PredicateUtils {
    public static <T> Predicate<T> negate(Predicate<T> p) {
        return p.negate();
}

te permitiría escribir

names.removeIf(PredicateUtils.negate(str -> str.length() <= 5)); //compiles without cast

Debido a que la elección de la función de negación no es ambigua, el compilador sabe que necesita interpretar str -> str.length() <= 5como Predicate<T>ay puede forzar el tipo correctamente.

Espero que esto ayude.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.