Sławomir Ciecierski
Sławomir CiecierskiAdministrator systemów w obiektach zagłębionych @ Ministerstwo Obrony Narodowej

Interfejsy w Javie - Referencje metod - odpowiedź

Dowiedz się, co oznacza, że metoda strReverse() jest zgodna z interfejsem funkcyjnym StringFunc() w kontekście referencji metod w interfejsach Javy.
4.12.20207 min
Interfejsy w Javie - Referencje metod - odpowiedź

Na blogu Bulldogjob opublikowałem jak dotąd serię artykułów dotyczących interfejsów w Javie. Niedawno pod artykułem Interfejsy w Javie - Referencje metod pojawiło się pytanie, na które postanowiłem odpowiedzieć obszerniej, niż krótkim komentarzem. Dziękuję za zainteresowanie i przepraszam, że nie jestem w stanie wystarczająco szybko zaspokajać Państwa potrzeby z dziedziny wiedzy o interfejsach w Javie.


Wklejam tutaj pytanie, które padło w komentarzu pod moim artykułem:

"Jest to możliwe gdyż metoda strReverse() jest zgodna z interfejsem funkcyjnym StringFunc()."

Co to znaczy, że jest zgodna? Tylko to, że ma taką samą deklarację i typ zwracany jak ta, zadeklarowana w interfejsie? Tzn, że można tu wstawić dowolną inną metodę przyjmującą i zwracającą String?


Odpowiedź

Z cytatu wynika, że pytanie powstało po przeczytaniu tekstu o referencji do metod statycznych. Opierając się na tym przykładzie odpowiedź brzmi, że wspomniana zgodność polega na zgodności typów parametrów metody i zwracanego metody. Można zaimplementować inne metody przyjmujące i zwracające String. Dodatkowo zastanówmy się, czy aby na pewno nie da się z tym nic zrobić – czy nie można do tego przykładu zaimplementować metod zwracających np. inne typy? Najprostszym przykładem wydaje się być poniższy, chociaż może niezbyt elegancki.

//interfejs funkcyjny 
interface StringFunc {
    Object func(String n); //poprzednio String!!
}
// ta klasa definiuje 5 metod statycznych 
class MyStringOps {
//1 metoda statyczna odwracająca kolejność znaków w łańcuchu
    static String strReverse(String str) {
        String result = "";
        int i;
        for (i = str.length() - 1; i >= 0; i--) result += str.charAt(i);
        return result;
    }
// 2 metoda statyczna zwracająca tekst pisany tylko dużymi literami
    static String myUpperCase (String str){
        return str.toUpperCase();
    }
// 3 metoda statyczna zwracająca tekst pisany tylko małymi literami
    static String myLowerCase(String str){
        return str.toLowerCase();
    }
// 4 metoda statyczna zwracająca dowolny tekst niezależnie od parametru wejściowego
    static String myAlaMaKota(String str){
        return "Ala ma kota";
    }
// 5 metoda statyczna zwracajaca ilość znaków parametru str
    static int myCounter(String str){
        return str.length();
    }
}
class MethodRefDemo {
//ta metoda ma pierwszy parametr, którego typem
//jest interfejs funkcyjny, a zatem można do niej przekazać
//dowolną instancję tego interfejsu, w tym także referencję
//do metody
    static Object stringOp(StringFunc sf, String s) {
        return sf.func(s);
    }
    public static void main(String args[]) {
        String inStr = "Wyrażenia lambda zwiększają możliwości Javy ;)";
        Object outStr;
        Object outLenght;
//tutaj do wywołania metody stringOps() zostaje
//przekazana referencja do metody strReverse
        outStr = stringOp(MyStringOps::strReverse, inStr);
        System.out.println("Początkowy  łańcuch: " + inStr);
        System.out.println("Łańcuch odwrócony: " + outStr);
        outStr = stringOp(MyStringOps::myUpperCase, inStr);
        System.out.println("Całość dużymi literami: " + outStr);
        outStr= stringOp(MyStringOps::myLowerCase, inStr);
        System.out.println("Całość małymi literami: " + outStr);
        outStr = stringOp(MyStringOps::myAlaMaKota, inStr);
        System.out.println("Zupełnie inny tekst - niezależny od parametru wejściowego: "+ outStr);
//tutaj do wywołania metody stringOps() zostaje
//przekazana referencja do metody myCounter
        outLenght = stringOp(MyStringOps::myCounter, inStr);
        System.out.println("Długość tekstu '" +inStr+"'wynosi: "+ outLenght +" znaków.");
    }
}


Jak widać, można zaimplementować różne metody, które są zgodne z omawianym interfejsem. Są to metody: myUpperCase(), myLowerCase(), myAlaMaKota().

Pierwsze dwie zwracają tekst składający się z dużych oraz małych liter parametru wejściowego. Trzecia zwraca określony tekst, zupełnie niezależny od parametru wejściowego. Proszę samodzielnie sprawdzić co się stanie, jeśli np. w metodzie myAlaMaKota() nie będzie podany parametr – czy program skompiluje się?  

Co się jednak dzieje, gdy zwracany typ interfejsu funkcyjnego jest typu nadrzędnego w stosunku zarówno do String jak i innych pożądanych? Jak wygląda metoda stringOp()? W podanym przykładzie zadeklarowanych typem jest Object, a więc najwyżej stojąca klasa w hierarchii.  

Metoda myCounter() zwraca liczbę typu int znaków parametru tej metody. Ale – w tym przypadku zawsze parametrem wejściowym musi być parametr typu String.

Teraz proszę samodzielnie zaimplementować statyczną metodę zwracającą typ danych np. double. Jaki jest typ parametru wejściowego tej metody? Czy taka metoda jest prawidłowa?

    static double myFloat (String str){
        return 3.5;
    }


Proszę zauważyć, że zmienne  outStr i outLenght są typu Object. Można pokusić się o zastosowanie jawnej konwersji zawężającej ręcznie, rzutując do odpowiednich np. mniej zasobożernych typów (o ile takowe nie wykonują się już automatycznie. Przykładem niech będzie – abstrachując od naszego przykładu - np. niejawne automatyczne rozpakowywanie Integer do int). Metoda main wygląda w ten sposób nastepująco: 

    public static void main(String args[]) {
        String inStr = "Wyrażenia lambda zwiększają możliwości Javy ;)";
        String outStr;
        int outLenght;
//tutaj do wywołania metody stringOps() zostaje
//przekazana referencja do metody strReverse
        outStr = (String)stringOp(MyStringOps::strReverse, inStr);
        System.out.println("Początkowy  łańcuch: " + inStr);
        System.out.println("Łańcuch odwrócony: " + outStr);
        outStr = (String)stringOp(MyStringOps::myUpperCase, inStr);
        System.out.println("Całość dużymi literami: " + outStr);
        outStr = (String)stringOp(MyStringOps::myLowerCase, inStr);
        System.out.println("Całość małymi literami: " + outStr);
        outStr = (String)stringOp(MyStringOps::myAlaMaKota, inStr);
        System.out.println("Zupełnie inny tekst - niezależny od parametru wejściowego: " + outStr);
        //tutaj do wywołania metody stringOps() zostaje
//przekazana referencja do metody myCounter
        outLenght = (int) stringOp(MyStringOps::myCounter, inStr);
        System.out.println("Długość tekstu '" + inStr + "' wynosi: " + outLenght + " znaków.");
    }


W ramach ćwiczenia proszę zastosować metodę  myFloat(). Odsyłam również do dalszych części artykułu np. Referencje do metod a typy sparametryzowane i Referencje do konstruktorów

Co jednak, gdy nie chcemy stosować uogólnień klasy Object? A ponadto nie chcemy, by parametr wejściowy był tylko i wyłącznie taki jak zadeklarowany np. typu String.

Zmodyfikujmy kod, który wyglądać będzie w następujący sposób:

//interfejs funkcyjny
//nie ma przeszkód by T i V reprezentowały tę samą klasę
interface AnyFunc<T, V> {
    T func(V n);
}

// ta klasa definiuje metody statyczne
class MyAllOps {
// metoda statyczna zwracająca tekst pisany tylko dużymi literami
    static String myUpperCase(String str) {
        return str.toUpperCase();
    }
// metoda statyczna zwracająca dowolny tekst String niezależnie od parametru str
    static String myAlaMaKota(String str) {
        return "Ala ma kota";
    }
//metoda statyczna zwracajaca ilość int znaków parametru str
    static int myCounter(String str) {
        return str.length();
    }
    //metoda statyczna dająca w wyniku podwojoną wartość parametru wejściowego 
    //tego samego typu
    static double myDoubleTimes2(double dbl) {
        return dbl + dbl;
    }
    //metoda statyczna zwracająca wynik klasy Exception niezależnie od danych wejściowych
    static Exception myHorribleException(String str) {
        return new Exception();
    }
}

class MethorRefDemo {

    static <T, V> T myOp(AnyFunc<T, V> sf, V s) {
        return sf.func(s);
    }

    public static void main(String args[]) {
        String inStr = "Wyrażenia lambda zwiększają możliwości Javy ;)";
        String outStr;
        double inDouble = 44.0;
        double outDouble;
        int outInt;
        Exception outExc;

        outStr = myOp(MyAllOps::myUpperCase, inStr);
        System.out.println("Całość dużymi literami: " + outStr);

        outStr = myOp(MyAllOps::myAlaMaKota, inStr);
        System.out.println("Zupełnie inny tekst - niezależny od parametru wejściowego: " + outStr);

        //inny zestaw danych wejściowych i wyjściowych - różne typy
        //parametr wejściowy String, metoda zwraca wartośc typu int
        //przekazana referencja do metody myDoubleTimes2
        outInt = myOp(MyAllOps::myCounter, inStr);
        System.out.println("Długość tekstu '" + inStr + "' wynosi: " + outInt + " znaków.");

        //inny zestaw danych wejściowych i wyjściowych
        //przekazana referencja do metody myDoubleTimes2
        outDouble = myOp(MyAllOps::myDoubleTimes2, inDouble);
        System.out.println("Podwójny parametr wejściowy wynosi: " + outDouble);

        outExc= myOp(MyAllOps::myHorribleException, inStr);
        System.out.println("Nazwa klasy wyjątku: " + outExc.toString());
    }
}


Rozwiązano problem stosowania różnych typów parametrów wejściowych <V> i wyjściowych <T>, posługując się typami generycznymi i niejako przesuwając deklarowanie typów danych, aż do czasu definiowania metod statycznych. Ponieważ posługujemy się interfejsem funkcyjnym, przykład może wydawać się nieco uproszczony.

MyUpperCase(), myAlaMaKota() - parametrem wejściowym i wyjściowym są dane typu String.
myCounter() - parametr wejściowy jest typem klasy String; metoda zwraca liczbę typu int.
myDoubleTimes2() - parametrem wejściowym i wyjściowym sa dane typu double.
myHorribleException() - parametr wejściowy jest typem klasy String; metoda zwraca wartość klasy Exception. Do wydrukowania zastosowano jawnie metodę toString() zupełnie niepotrzebnie, ponieważ metoda println() stosuje ją niejawnie i automatycznie. Jeśli jednak posiadamy własną implementację metody toString(), to wtedy powinniśmy zastosować ją jawnie.

Proszę jeszcze zauważyć, że gdybyśmy zamiast 

    static <T, V> T myOp(AnyFunc<T, V> sf, V s) {
        return sf.func(s);
    }


zadeklarowali np. 

    static <T, V> T myOp(AnyFunc<T, V> sf, T s) {
        return sf.func(s);
    }


Zmuszało by to nas podczas implementacji metod do tego, by typ parametru wejściowego jak i typ zwracanej przez metodę wartości, były tego samego typu, czego chcieliśmy w naszym przypadku uniknąć.

Pozostał jeszcze jeden problem do rozważenia. Skoro potrafimy już zaimplementować wiele różnych metod interfejsu funkcyjnego, o różnych typach danych wejściowych i zwracających dane różnego typu, to należy zastanowić się, czy można zastosować metody o tej samej nazwie a różnych parametrach, stosując np. zmienną (od Java 1.5) ilość parametrów wejściowych  – może w szerszym zakresie, niż podczas stosowania interfejsów funkcyjnych? Przykładem takiej metody jest metoda println(), która może przyjmować różne typy danych, a nawet ich brak. Zainteresowanych odsyłam do szukania wiedzy w internecie posługując się np. kluczem "przeciążanie metod o zmiennej liczbie argumentów".

<p>Loading...</p>