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".