Por que negate () exige uma conversão explícita para o Predicate?

8

Eu tenho uma lista de nomes. Na linha 3, tive que converter o resultado da expressão lambda para Predicate<String>. O livro que estou lendo explica que o elenco é necessário para ajudar o compilador a determinar qual é a interface funcional correspondente.

No entanto, não preciso de um elenco desse tipo na linha a seguir, porque não ligo negate(). Como isso faz alguma diferença? Eu entendo que negate()aqui retorna Predicate<String>, mas a expressão lambda anterior não faz o mesmo?

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
K Man
fonte
1
... o elenco é necessário para ajudar o compilador a determinar qual é a interface funcional correspondente. responde de qualquer maneira. A razão pela qual a próxima linha não precisa é que a inferência é muito mais simples e não é seguida por uma invocação de método na interface inferida. Aprendi usando funções lambda envolvendo genéricos, que é principalmente sobre inferência nesses casos, e declarações explícitas são muito seguras, comoPredicate<String> predicate = str -> str.length() <= 5; names.removeIf(predicate.negate()); names.removeIf(predicate);
Naman
@Nathan A chamada para negate () não forneceu mais contexto, tornando o elenco explícito ainda menos necessário? A expressão lambda poderia ter se referido a uma Função em vez de um Predicado, afinal, mas a chamada subseqüente para negate () remove qualquer espaço para ambiguidade.
K Man
Mas o que seria negate()chamado se não fosse possível inferir o tipo de atributo lambda que faz parte da expressão e, portanto, ainda mais incapaz de estabelecer que a expressão signifique a Predicate<String>.
Naman 22/02
Não estou dizendo que negate () é necessário. Minha última linha claramente funcionou sem ela. No entanto, não vejo como chamar negate () de repente confundisse o compilador quanto ao tipo de interface funcional.
K Man

Respostas:

8

Não é bem só porque você liganegate() . Dê uma olhada nesta versão, que é muito próxima da sua, mas compila:

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

A diferença entre isso e sua versão? É sobre como as expressões lambda obtêm seus tipos (o "tipo de destino").

O que você acha que isso faz?

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

Sua resposta atual é que "ele chama negate()o Predicate<String>dado pela expressão str -> str.length() <= 5". Direita? Mas isso é apenas porque é isso que você pretende que ele faça. O compilador não sabe disso . Por quê? Porque poderia ser qualquer coisa. Minha própria resposta para a pergunta acima pode ser "ela chama negatemeu outro tipo de interface funcional ... (sim, o exemplo será um pouco bizarro)

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

Eu poderia usar a mesma expressão lambda, names.removeIf((str -> str.length() <= 5).negate());mas com o significado str -> str.length() <= 5de ser um SentenceExpressione não um Predicate<String>.

Explicação: (str -> str.length() <= 5).negate()não cria str -> str.length() <= 5a Predicate<String>. E foi por isso que eu disse que poderia ser qualquer coisa, incluindo minha interface funcional acima.

De volta ao Java ... É por isso que as expressões lambda têm o conceito de "tipo de destino", que define a mecânica pela qual uma expressão lambda é entendida pelo compilador como um determinado tipo de interface funcional (ou seja, como você ajuda o compilador a saber que a expressão é Predicate<String>mais do que SentenceExpressionou qualquer outra coisa que poderia ser). Você pode achar útil ler O que se entende por tipo de destino lambda e contexto de tipo de destino em Java? e Java 8: digitação de destino

Um dos contextos nos quais os tipos de destino são inferidos (se você ler as respostas nessas postagens) é o contexto de chamada, em que você passa uma expressão lambda como argumento para um parâmetro de um tipo de interface funcional, e é isso que é aplicável a names.removeIf(((str -> str.length() <= 5)));: é apenas a expressão lambda dada como argumento para um método que usa a Predicate<String>. Isso não se aplica à instrução que não está compilando.

Então, em outras palavras ...

names.removeIf(str -> str.length() <= 5);usa uma expressão lambda em um local onde o tipo de argumento define claramente qual é o tipo de expressão lambda esperado (ou seja, o tipo de destino str -> str.length() <= 5é claramente Predicate<String>).

No entanto, (str -> str.length() <= 5).negate()não é uma expressão lambda, é apenas uma expressão que usa uma expressão lambda. Isso significa que str -> str.length() <= 5, nesse caso, não está no contexto de chamada que determina o tipo de destino da expressão lambda (como no caso de sua última instrução). Sim, o compilador sabe que removeIfprecisa de a Predicate<String>e sabe com certeza que toda a expressão passada para o método deve ser a Predicate<String>, mas não pressupõe que qualquer expressão lambda na expressão de argumento seja a Predicate<String>(mesmo que você a trate) como predicado chamando negate()-o; poderia ter sido qualquer coisa compatível com a expressão lambda).

É por isso que é necessário digitar seu lambda com uma conversão explícita (ou não, como no primeiro contra-exemplo que eu dei).

ernest_k
fonte
names.removeIf (((str -> str.length () <= 5))); compila claramente sem problemas. Ainda não entendi como chamar negate (), que retorna um Predicado, de repente faz com que o compilador duvide que toda a expressão se refira a um Predicado.
K Man
@KMan Acho que você precisa de mais detalhes sobre como as expressões lambda são digitadas. Vou editar e adicionar um link.
ernest_k 22/02
Eu já li bastante sobre expressões lambda. Se a expressão inteira é ou não uma expressão lambda é irrelevante, não é? Negate () não retorna Predicate <> no meu código acima? Eu posso estar enganado, mas acho que não.
K Man
1
@KMan negate () não retorna Predicate <> no meu código acima? .. Não, até que seja chamado umPredicate<String> que é o que o compilador não conseguiu inferir em primeiro lugar devido ao uso de negatesi mesmo, conforme explicado nesta resposta.
Naman 22/02
@ KMan que editei - acho que você precisa reler a resposta. Em suma, como Naman está dizendo, tudo se resume a entender que (str -> str.length() <= 5).negate()não produz str -> str.length() <= 5um Predicate<String>, mesmo que seja isso que você pretende.
ernest_k 22/02
4

Não sei por que isso deve ser tão confuso. Isso pode ser explicado por 2 razões, IMO.

  • Expressões lambda são expressões polis .

Vou deixar você descobrir o que isso significa e quais JLSsão as palavras ao seu redor. Mas, em essência, são como genéricos:

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

qual é o tipo Taqui? Bem, isto depende. Se você fizer :

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

Dizem-se que expressões poli dependem do contexto de onde são usadas. As expressões lambda são as mesmas. Vamos considerar a expressão lambda isoladamente aqui:

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

quando você olha para isso, o que é isso? Bem, é um Predicate<String>? Mas pode ser A Function<String, Boolean>? Ou pode ser par MyTransformer<String, Boolean>, onde:

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

As escolhas são infinitas.

  • Em teoria, .negate()chamado diretamente poderia ser uma opção.

A partir das 10_000 milhas acima, você está correto: você está fornecendo isso str -> str.length() <= 5a um removeIfmétodo, que aceita apenas a Predicate. Não há mais removeIfmétodos, portanto, o compilador deve ser capaz de "fazer a coisa correta" quando você fornecer isso (str -> str.length() <= 5).negate().

Então, como é que isso não funciona? Vamos começar com o seu comentário:

A chamada para negate () não deveria fornecer ainda mais contexto, tornando a conversão explícita ainda menos necessária?

Parece que é aqui que o principal problema começa, simplesmente não é assim que javacfunciona. Não pode pegar o todo str -> str.length() <= 5).negate(), diga a si mesmo que esse é um Predicate<String>(já que você o está usando como argumento removeIf) e depois decomponha ainda mais a parte sem .negate()e veja se isso Predicate<String>também é. javacage de maneira inversa, precisa conhecer o alvo para saber se é legal ligar negateou não.

Além disso, você precisa fazer uma distinção clara entre expressões poli e expressões , em geral. str -> str.length() <= 5).negate()é uma expressão, str -> str.length() <= 5é uma expressão poli.

Pode haver idiomas em que as coisas são feitas de maneira diferente e onde isso é possível, javacsimplesmente não é desse tipo.

Eugene
fonte
1

No exemplo a seguir

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

a expressão retorna verdadeira. Se no exemplo a seguir sem o elenco, truenão souber nada sobre o métodonegate()

Por outro lado,

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

A expressão é convertida em Predicate<String>para informar ao compilador onde encontrar o método negate. Então é o método negate()que realmente faz a avaliação pelo seguinte:

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

Observe que você pode obter o mesmo resultado sem o elenco da seguinte maneira:

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

Sem aprofundar as coisas, o ponto principal é que o lambda str -> str.length() <= 5não é necessariamente um, Predicate<String>como Eugene explicou.

Dito isso, negate()é uma função membro do Predicate<T>e o compilador não pode usar a chamada negate()para ajudar a inferir o tipo em que o lambda deve ser interpretado. Mesmo que tentasse, poderia haver problemas, porque se várias classes possíveis tivessem negate()funções, não saberia qual escolher.

Imagine que você é o compilador.

  • Você vê str -> str.length() <= 5. Você sabe que é lambda que pega uma String e retorna um booleano, mas não sabe especificamente qual o tipo que ela representa.
  • Em seguida, você verá a referência do membro, .negate()mas porque não sabe o tipo da expressão à esquerda do "." você precisa relatar um erro, pois não pode usar a chamada para negar para ajudar a inferir o tipo.

Se o negado tivesse sido implementado como uma função estática, as coisas funcionariam. Por exemplo:

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

permitiria escrever

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

Como a escolha da função negar é inequívoca, o compilador sabe que precisa ser interpretado str -> str.length() <= 5como Predicate<T>ae pode coagir o tipo corretamente.

Eu espero que isso ajude.

ajz
fonte