Strumienie .min () i .max () strumienia Java 8: dlaczego to się kompiluje?

215

Uwaga: to pytanie pochodzi z martwego linku, który był poprzednim pytaniem SO, ale tutaj idzie ...

Zobacz ten kod ( uwaga: wiem, że ten kod nie będzie „działał” i że Integer::comparenależy go użyć - właśnie wyodrębniłem go z połączonego pytania ):

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());

Zgodnie z javadoc .min()i .max()argumentem obu powinno być Comparator. Jednak tutaj odniesienia do metod dotyczą metod statycznych Integerklasy.

Dlaczego w ogóle się to kompiluje?

fge
źródło
6
Zauważ, że to nie działa poprawnie, powinno używać Integer::comparezamiast Integer::maxi Integer::min.
Christoffer Hammarström,
@ ChristofferHammarström Wiem o tym; zwróć uwagę, jak powiedziałem przed fragmentem kodu „Wiem, to absurdalne”
fge
3
Nie próbowałem cię poprawić, mówię ogólnie ludziom. Sprawiłeś, że zabrzmiało to tak, jakbyś myślał, że absurdalne jest to, że metody Integernie są metodami Comparator.
Christoffer Hammarström,

Odpowiedzi:

242

Pozwól mi wyjaśnić, co się tutaj dzieje, ponieważ nie jest to oczywiste!

Po pierwsze, Stream.max()akceptuje takie wystąpienie Comparator, aby elementy w strumieniu mogły być porównywane względem siebie w celu znalezienia minimum lub maksimum, w optymalnej kolejności, o którą nie musisz się zbytnio przejmować.

Pytanie zatem, dlaczego jest Integer::maxakceptowane? W końcu to nie jest komparator!

Odpowiedź jest taka, że ​​nowa funkcjonalność lambda działa w Javie 8. Opiera się ona na koncepcji, która jest nieformalnie znana jako interfejsy „pojedynczej metody abstrakcyjnej” lub interfejsy „SAM”. Chodzi o to, że każdy interfejs z jedną metodą abstrakcyjną może być automatycznie implementowany przez dowolną lambda - lub odwołanie do metody - której sygnatura metody jest zgodna z jedną metodą w interfejsie. Zatem badanie Comparatorinterfejsu (wersja prosta):

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

Jeśli metoda szuka a Comparator<Integer>, to zasadniczo szuka tej sygnatury:

int xxx(Integer o1, Integer o2);

Używam „xxx”, ponieważ nazwa metody nie jest używana w celu dopasowania .

Dlatego zarówno Integer.min(int a, int b)i Integer.max(int a, int b)są na tyle blisko, że autoboxing pozwoli to pojawiał się jako Comparator<Integer>w kontekście metody.

David M. Lloyd
źródło
28
lub alternatywnie list.stream().mapToInt(i -> i).max().get().
assylias
13
@assylias Chcesz użyć .getAsInt()zamiast get()chociaż, że masz do czynienia ze związkiem OptionalInt.
skiwi
... kiedy staramy się jedynie dostarczyć niestandardowy komparator do max()funkcji!
Manu343726,
Warto zauważyć, że ten „interfejs SAM” jest tak naprawdę nazywany „interfejsem funkcjonalnym” i że patrząc na Comparatordokumentację możemy zauważyć, że jest ozdobiony adnotacją @FunctionalInterface. Ten dekorator to magia, która pozwala Integer::maxi Integer::minprzekształca się w Comparator.
Chris Kerekes
2
@ChrisKerekes dekorator @FunctionalInterfacesłuży głównie do celów dokumentacyjnych, ponieważ kompilator może to zrobić z dowolnym interfejsem za pomocą jednej abstrakcyjnej metody.
errantlinguist
117

Comparatorjest interfejsem funkcjonalnym i Integer::maxjest zgodny z tym interfejsem (po uwzględnieniu autoboxowania / rozpakowywania). Przyjmuje dwie intwartości i zwraca an int- dokładnie tak, jak można się tego spodziewać Comparator<Integer>(ponownie, mrużąc oczy, aby zignorować różnicę całkowitą / całkowitą).

Jednak nie spodziewałbym się to zrobić dobry uczynek, zważywszy, że Integer.maxnie są zgodne z semantyką o Comparator.compare. I tak naprawdę to w ogóle nie działa. Na przykład wprowadź jedną małą zmianę:

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

... a teraz maxwartość wynosi -20, a minwartość wynosi -1.

Zamiast tego oba połączenia powinny używać Integer::compare:

System.out.println(list.stream().max(Integer::compare).get());
System.out.println(list.stream().min(Integer::compare).get());
Jon Skeet
źródło
1
Wiem o interfejsach funkcjonalnych; i wiem, dlaczego daje złe wyniki. Zastanawiam się tylko, jak, u licha, kompilator nie krzyczał na mnie
feb
6
@fge: Cóż, to było rozpakowanie, o którym nie wiedziałeś? (Nie przyjrzałem się dokładnie tej części.) Comparator<Integer>Miałbym int compare(Integer, Integer)... to nie zadziwiające, że Java pozwala int max(int, int)na konwersję referencji metod do tego ...
Jon Skeet
7
@fge: Czy kompilator powinien znać semantykę Integer::max? Z jego perspektywy przekazałeś funkcję spełniającą jej specyfikację, to wszystko, co naprawdę może trwać.
Mark Peters
6
@fge: W szczególności, jeśli rozumiesz część tego, co się dzieje, ale intryguje Cię jeden konkretny aspekt, warto to wyjaśnić w pytaniu, aby ludzie nie marnowali czasu na wyjaśnianie fragmentów, które już znasz.
Jon Skeet
20
Myślę, że podstawowym problemem jest podpis typu Comparator.compare. Należy zwracać enumo {LessThan, GreaterThan, Equal}, a nie int. W ten sposób funkcjonalny interfejs nie byłby właściwie zgodny i pojawiałby się błąd kompilacji. IOW: podpis typu Comparator.comparenie odpowiednio przechwytuje semantykę tego, co oznacza porównywanie dwóch obiektów, a zatem inne interfejsy, które absolutnie nie mają nic wspólnego z porównywaniem obiektów, przypadkowo mają ten sam podpis.
Jörg W Mittag
19

Działa to, ponieważ Integer::minrozwiązuje się do implementacji Comparator<Integer>interfejsu.

Odniesienie do metody resolves Integer::minto Integer.min(int a, int b), resolved to IntBinaryOperatori prawdopodobnie autoboxing występuje gdzieś, co czyni ją a BinaryOperator<Integer>.

I min()odpowiednio max()metody Stream<Integer>zapytaj Comparator<Integer>interfejsu, który ma zostać zaimplementowany.
Teraz rozwiązuje to jedną metodę Integer compareTo(Integer o1, Integer o2). Który jest typu BinaryOperator<Integer>.

I tak magia się wydarzyła, ponieważ obie metody są BinaryOperator<Integer>.

skiwi
źródło
Nie jest do końca poprawne stwierdzenie, że to Integer::minimplementuje Comparable. To nie jest typ, który może zaimplementować wszystko. Ale jest przetwarzany w obiekt, który implementuje Comparable.
Lii
1
@Lii Dzięki, właśnie to naprawiłem.
skiwi
Comparator<Integer>jest interfejsem z pojedynczą abstrakcyjną metodą („funkcjonalną”) i Integer::minspełnia swój kontrakt, więc lambda może być interpretowana w ten sposób. Nie wiem, jak widzisz BinaryOperator wchodzący tutaj (lub IntBinaryOperator) - nie ma związku między tym a Komparatoriem.
Paŭlo Ebermann
2

Oprócz informacji podanych przez Davida M. Lloyda można dodać, że mechanizm, który na to pozwala, nazywa się typowaniem celu .

Chodzi o to, że typ, który kompilator przypisuje do wyrażeń lambda lub odwołań do metody, nie zależy tylko od samego wyrażenia, ale także od miejsca jego użycia.

Celem wyrażenia jest zmienna, do której przypisany jest jego wynik lub parametr, do którego przekazywany jest wynik.

Wyrażeniom lambda i referencjom do metod przypisywany jest typ, który odpowiada typowi ich celu, jeśli taki typ można znaleźć.

Aby uzyskać więcej informacji, zobacz sekcję Wnioskowanie typu w samouczku Java.

Lii
źródło
1

Miałem błąd z uzyskaniem przez tablicę maksimum i min, więc moim rozwiązaniem było:

int max = Arrays.stream(arrayWithInts).max().getAsInt();
int min = Arrays.stream(arrayWithInts).min().getAsInt();
JPRLCol
źródło