.Min () e .max () do Java 8 stream: por que isso compila?

215

Nota: esta pergunta se origina de um link morto que era uma pergunta anterior do SO, mas aqui vai ...

Veja este código ( nota: eu sei que esse código não "funcionará" e que Integer::comparedeve ser usado - eu apenas o extraí da pergunta vinculada ):

final ArrayList <Integer> list 
    = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

System.out.println(list.stream().max(Integer::max).get());
System.out.println(list.stream().min(Integer::min).get());

De acordo com o javadoc de .min()e .max(), o argumento de ambos deve ser a Comparator. No entanto, aqui as referências de método são para métodos estáticos da Integerclasse.

Então, por que isso é compilado?

fge
fonte
6
Observe que ele não funciona corretamente, deve estar usando em Integer::comparevez de Integer::maxe Integer::min.
Christoffer Hammarström
@ ChristofferHammarström Eu sei disso; note como eu disse antes que o extrato de código "Eu sei, é absurdo"
fge
3
Eu não estava tentando corrigir você, estou dizendo às pessoas em geral. Você fez parecer como se pensasse que a parte que é absurda é que os métodos de Integernão são métodos de Comparator.
Christoffer Hammarström

Respostas:

242

Deixe-me explicar o que está acontecendo aqui, porque não é óbvio!

Primeiro, Stream.max()aceita uma instância de Comparatorpara que os itens no fluxo possam ser comparados entre si para encontrar o mínimo ou o máximo, em uma ordem ideal com a qual você não precisa se preocupar muito.

Portanto, a questão é, obviamente, por que é Integer::maxaceito? Afinal, não é um comparador!

A resposta está na maneira como a nova funcionalidade lambda funciona no Java 8. Ela se baseia em um conceito que é conhecido informalmente como interfaces de "método abstrato único" ou interfaces "SAM". A idéia é que qualquer interface com um método abstrato possa ser implementada automaticamente por qualquer lambda - ou referência de método - cuja assinatura de método corresponda ao método único na interface. Então, examinando a Comparatorinterface (versão simples):

public Comparator<T> {
    T compare(T o1, T o2);
}

Se um método está procurando por um Comparator<Integer>, então está basicamente procurando por esta assinatura:

int xxx(Integer o1, Integer o2);

Eu uso "xxx" porque o nome do método não é usado para fins de correspondência .

Portanto, ambos Integer.min(int a, int b)e Integer.max(int a, int b)estão próximos o suficiente para que a caixa automática permita que isso apareça como Comparator<Integer>em um contexto de método.

David M. Lloyd
fonte
28
ou alternativamente: list.stream().mapToInt(i -> i).max().get().
assylias 21/03
13
@assylias Você deseja usar em .getAsInt()vez de get(), como você está lidando com um OptionalInt.
skiwi
... quando o que estamos apenas tentando é fornecer um comparador personalizado para uma max()função!
Manu343726
Vale a pena notar que essa "Interface SAM" é na verdade chamada de "Interface Funcional" e que, observando a Comparatordocumentação, podemos ver que ela é decorada com a anotação @FunctionalInterface. Este decorador é a mágica que permite Integer::maxe Integer::minpode ser convertida em a Comparator.
precisa
2
O @ChrisKerekes, o decorador, @FunctionalInterfaceé principalmente para fins de documentação, pois o compilador pode fazer isso felizmente com qualquer interface com um único método abstrato.
precisa saber é o seguinte
117

Comparatoré uma interface funcional e Integer::maxestá em conformidade com essa interface (depois de considerar o autoboxing / unboxing). Ele pega dois intvalores e retorna um int- exatamente como você esperaria Comparator<Integer>(novamente, apertando os olhos para ignorar a diferença Inteiro / int).

No entanto, eu não esperaria que ele fizesse a coisa certa, já que Integer.maxisso não está de acordo com a semântica de Comparator.compare. E, de fato, realmente não funciona em geral. Por exemplo, faça uma pequena alteração:

for (int i = 1; i <= 20; i++)
    list.add(-i);

... e agora o maxvalor é -20 e o minvalor é -1.

Em vez disso, as duas chamadas devem usar Integer::compare:

System.out.println(list.stream().max(Integer::compare).get());
System.out.println(list.stream().min(Integer::compare).get());
Jon Skeet
fonte
1
Eu sei sobre as interfaces funcionais; e sei por que isso dá resultados errados. Eu só quero saber como na terra o compilador não grite comigo
fge
6
@gee: Bem, foi sobre o unboxing que você não sabia? (Eu não olhei em exatamente essa parte.) Um Comparator<Integer>teria int compare(Integer, Integer)... não é incompreensível que o Java permite uma referência método de int max(int, int)converter a isso ...
Jon Skeet
7
@gee: O compilador deve conhecer a semântica de Integer::max? Da perspectiva dele, você passou por uma função que atendeu às suas especificações, é tudo o que pode realmente continuar.
Mark Peters
6
@fge: Em particular, se você entende parte do que está acontecendo, mas está intrigado com um aspecto específico, vale a pena deixar isso claro na pergunta para evitar que as pessoas desperdiçam seu tempo explicando os bits que você já conhece.
Jon Skeet
20
Eu acho que o problema subjacente é o tipo de assinatura Comparator.compare. Deve retornar um enumde {LessThan, GreaterThan, Equal}, não um int. Dessa forma, a interface funcional não corresponderia e você obteria um erro de compilação. IOW: a assinatura de tipo de Comparator.comparenão captura adequadamente a semântica do que significa comparar dois objetos e, portanto, outras interfaces que não têm absolutamente nada a ver com a comparação de objetos acidentalmente têm a mesma assinatura de tipo.
Jörg W Mittag 22/03
19

Isso funciona porque Integer::minresolve uma implementação da Comparator<Integer>interface.

A referência do método Integer::minresolve Integer.min(int a, int b), resolve e IntBinaryOperator, presumivelmente, a autobox ocorre em algum lugar, tornando-o a BinaryOperator<Integer>.

E os min()resp max()métodos da Stream<Integer>pedir a Comparator<Integer>interface a ser implementado.
Agora isso resolve para o método único Integer compareTo(Integer o1, Integer o2). Qual é do tipo BinaryOperator<Integer>.

E assim a mágica aconteceu porque os dois métodos são a BinaryOperator<Integer>.

skiwi
fonte
Não é totalmente correto dizer que Integer::minimplementa Comparable. Não é um tipo que pode implementar qualquer coisa. Mas é avaliado em um objeto que implementa Comparable.
Lii
1
@ Lii Obrigado, eu consertei agora.
skiwi
Comparator<Integer>é uma interface de método único abstrato (também conhecida como "funcional") e Integer::mincumpre seu contrato, portanto o lambda pode ser interpretado como este. Não sei como você vê o BinaryOperator entrando em jogo aqui (ou IntBinaryOperator, também) - não há relação de subtipagem entre isso e o Comparator.
Paŭlo Ebermann 26/10
2

Além das informações fornecidas por David M. Lloyd, pode-se acrescentar que o mecanismo que permite isso é chamado de digitação de destino .

A idéia é que o tipo que o compilador atribui a expressões lambda ou referências a métodos não depende apenas da expressão em si, mas também de onde é usado.

O destino de uma expressão é a variável à qual seu resultado é atribuído ou o parâmetro ao qual seu resultado é passado.

As expressões lambda e as referências de método recebem um tipo que corresponde ao tipo de seu destino, se esse tipo puder ser encontrado.

Consulte a seção Inferência de tipo no Tutorial Java para obter mais informações.

Lii
fonte
1

Eu tive um erro com uma matriz obtendo o máximo e o mínimo, então minha solução foi:

int max = Arrays.stream(arrayWithInts).max().getAsInt();
int min = Arrays.stream(arrayWithInts).min().getAsInt();
JPRLCol
fonte