Interfejsy w Javie - Referencje metod
W moim pierwszym artykule mówiłem o podstawowych koncepcjach związanych z interfejsami Javy, w kolejnym o podstawowych koncepcjach związanych z wyrażeniami lambda i ich zastosowaniu w interfejsach funkcyjnych. Dziś skupię się na referencjach metod.
Referencje metod
Jak wspomniano, referencje do metod pozwalają odwoływać się do metod bez ich wykonywania. Możliwość ta jest związana z wyrażeniami lambda, gdyż także wymaga kontekstu typu docelowego będącego interfejsem funkcyjnym. W momencie przetwarzania także referencja do metody tworzy instancję interfejsu funkcyjnego. Dostępnych jest kilka różnych rodzajów referencji do metod. Poniżej omówione będą referencje do metod statycznych, referencje do metod instancyjnych, referencje do metod a typy sparametryzowane, a także referencje do konstruktorów.
Referencje do metod statycznych
Składnia używana do do tworzenia referencji do metod statycznych wygląda tak:
NazwaKlasy::nazwaMetody
Dwa dwukropki stanowią separator dodany do Javy w JDK 8 specjalnie w tym celu. Tak utworzona referencja do metody może być używana wszędzie tam, gdzie będzie zgodna ze swym typem docelowym.
//interfejs funkcyjny reprezentujący operacje na łańcuchach
interface StringFunc {
String func(String n);
}
// ta klasa definiuje metodę statyczną o nazwie strReverse()
class MyStringOps {
//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;
}
}
class MethodRefDemo {
//ta metoda ma pierwszy parametr, którego typem
//jest interfeks funkcyjny, a zatem można do niej przekazać
//dowolną instancję tego interfejsu, w tym także referencję
//do metody
static String 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 ;)";
String outStr;
//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);
}
}
Wyniki:
Początkowy łańcuch: Wyrażenia lambda zwiększają możliwości Javy ;)
Łańcuch odwrócony: ); yvaJ icśowilżom ąjazskęiwz adbmal aineżaryW
W przykładzie szczególną uwagę należy zwrócić na wiersz:
outStr = stringOp(MyStringOps::strReverse, inStr);
W tej instrukcji referencja do statycznej metody strReverse()
, zadeklarowanej w klasie MyStringOp
, zostaje przekazana jako pierwszy argument wywołania metody stringOp
. Jest to możliwe gdyż metoda strReverse()
jest zgodna z interfejsem funkcyjnym StringFunc
. A zatem wyrażenie MyStringOps::strReverse
zostaje przekształcone na referencję do obiektu, w którym metoda strReverse
stanowić będzie implementację metody func()
interfejsu funkcyjnego StringFunc
.
Referencje do metod instancyjnych
Ogólna postać wyrażenia pozwalającego na pobranie referencji do metody instancyjnej określonego obiektu wygląda następująco:
referencjaObiektu::nazwaMetody
i jest bardzo podobna do składni używanej do pobierania referencji do metod statycznych. Różni się jedynie tym, że nazwa klasy została zastapiona zmienna referencyjną. Poprzedni program został zmodyfikowany tak by używać referencji do metody instancyjnej:
//interfejs funkcyjny reprezentujący operacje na łańcuchach
interface StringFunc {
String func(String n);
}
// w tym przypadku klasa definiuje metodę instancyjną o nazwie
//strReverse
class MyStringOps {
String strReverse(String str) {
String result = "";
int i;
for (i = str.length() - 1; i >= 0; i--) result += str.charAt(i);
return result;
}
}
class MethodRefDemo2 {
//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 String 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 :)";
String outStr;
//tworzy obiekt klasy MyStringOps
MyStringOps strOps = new MyStringOps();
//teraz do metody stringOp() przekazywna jest referencja
//do instancyjnej metody strReversje
outStr = stringOp(strOps::strReverse, inStr);
System.out.println("Początkowy łańcuch: " + inStr);
System.out.println("Łańcuch odwrócony: " + outStr);
}
}
Program generuje podobne wyniki do poprzedniego:
Początkowy łańcuch: Wyrażenia lambda zwiększają możliwości Javy :)
Łańcuch odwrócony: ): yvaJ icśowilżom ąjazskęiwz adbmal aineżaryW
Metoda strReverse
klasy MyStringOps
jest metodą instancyjną. W metodzie main programu tworzona jest instancja klasy MyStringOps
. Następnie instancja ta zostaje użyta do tworzenia referencji do metody strReverse
i przekazania jej w wywołaniu metody stringOp
, jak pokazano poniżej:
outStr = stringOp(strOps::strReverse, inStr);
Istnieje także możliwość obsługi sytuacji, w których chcemy wskazać jedną metodę instancyjną, która ma być używaną nie w jednym, lecz z dowolnymi obiektami danej klasy. W takim przypadku referencję do metody należy utworzyć w następujący sposób:
NazwaKlasy::nazwaMetodyInstancyjnej
W tym wyrażeniu zamiast konkretnego obiektu podawana jest nazwa klasy, choć w drugiej części wyrażenia używana jest nazwa metody instancyjnej. W takim przypadku pierwszy parametr interfejsu funkcyjnego odpowiada obiektowi, na rzecz którego metoda ma być wywołana, a drugi parametr (jeśli istnieje) odpowiada parametrowi metody.
Poniżej przedstawiony został przykład takiej referencji. Program definiuje metodę counter()
, która zlicza obiekty w tablicy spełniające warunek zdefiniowany przez metodę func()
interfejsu funkcyjnego MyFunc
. W tym przypadku zliczane są instancje klasy HighTemp
.
// Program przedstawia zastosowanie referencji do metody
// instancyjnej wywoływanej na rzecz różnych obiektów.
// Interfejs funkcyjny, którego metoda ma dwa parametry
// typu referencyjnego i zwraca wynik typu boolean.
interface MyFunc<T> {
boolean func(T v1, T v2);
}
// Klasa przechowująca informację o wysokości temperatury
// w ciągu dnia.
class HighTemp {
private int hTemp;
HighTemp(int ht) { hTemp = ht; }
// Zwraca true, jeśli obiekt HighTemp, na rzecz którego metoda
// została wywołana, ma tę samą temperaturę co przekazany
// obiekt ht2.
boolean sameTemp(HighTemp ht2) {
return hTemp == ht2.hTemp;
}
// Zwraca true, jeśli obiekt HighTemp, na rzecz którego metoda
// została wywołana, ma niższą temperaturę niż przekazany
// obiekt ht2.
boolean lessThanTemp(HighTemp ht2) {
return hTemp < ht2.hTemp;
}
}
class InstanceMethWithObjectRefDemo {
// Metoda zwraca liczbę obiektów spełniających
// kryterium przekazane jako parametr typu MyFunc.
static <T> int counter(T[] vals, MyFunc<T> f, T v) {
int count = 0;
for(int i=0; i < vals.length; i++)
if(f.func(vals[i], v)) count++;
return count;
}
public static void main(String args[])
{
int count;
// Tworzy tablicę obiektów HighTemp.
HighTemp[] weekDayHighs = { new HighTemp(30), new HighTemp(27),
new HighTemp(32), new HighTemp(30),
new HighTemp(30), new HighTemp(33),
new HighTemp(26), new HighTemp(25) };
// Wywołanie metody counter() operującej na tablicy
// obiektów HighTemp. Warto zwrócić uwagę na referencję do
// metody instancyjnej sameTemp(), przekazywaną jako
// drugi argument wywołania.
count = counter(weekDayHighs, HighTemp::sameTemp,
new HighTemp(30));
System.out.println(count + " dni miały temperaturę 30 stopni.");
// Utworzenie i użycie kolejnej tablicy obiektów HighTemp.
HighTemp[] weekDayHighs2 = { new HighTemp(16), new HighTemp(6),
new HighTemp(17), new HighTemp(10),
new HighTemp(11), new HighTemp(10),
new HighTemp(-1), new HighTemp(9) };
count = counter(weekDayHighs2, HighTemp::sameTemp,
new HighTemp(10));
System.out.println(count + " dni miało temperaturę 10 stopni.");
// Zastosowanie metody lessThanTemp() do znalezienia liczby dni,
// w których temperatura była niższa od zadanej.
count = counter(weekDayHighs, HighTemp::lessThanTemp,
new HighTemp(30));
System.out.println(count + " dni miały temperaturę niższą od 30 stopni.");
count = counter(weekDayHighs2, HighTemp::lessThanTemp,
new HighTemp(10));
System.out.println(count + " dni miały temperaturę niższą od 10 stopni.");
}
}
Wyniki:
3 dni miały temperaturę 30 stopni.
2 dni miało temperaturę 10 stopni.
3 dni miały temperaturę niższą od 30 stopni.
3 dni miały temperaturę niższą od 10 stopni.
W powyższym programie należy zwrócić uwagę na fakt, że klasa HighTemp
ma dwie metody instancyjne: sameTemp()
oraz lessTemp()
. Pierwsza z nich zwraca wartość true
, jeśli w obu obiektach HighTemp
jest zapisana taka sama temperatura. Z kolei druga metoda druga metoda zwraca wartość true
, jeśli temperatura zapisana w obiekcie wywołującym jest niższa od temperatury w obiekcie przekazanym w wywołaniu.
Każda z tych metod ma ma parametr typu HighTemp
i każda zwraca wynik typu boolean. A zatem każda z nich jest zgodna z interfejsem funkcyjnym MyFunc
, gdyż obiekt wywołujący może zostać odwzorowany na pierwszy parametr metody func(), a argument – na jej drugi parametr. Dzięki temu, kiedy do wywołania metody counter()
zostanie przekazane wywołanie
HighTemp:: someTemp
zostanie utworzona instancja interfejsu funkcyjnego MyFunc
, w której typ pierwszego parametru będzie odpowiadał typowi obiektu użytego do wywołania metody instancyjnej, czyli HighTemp
. Także drugi parametr będzie będzie typu HighTemp
, gdyż tego typu jest parametr metody sameTemp()
. To samo dotyczy metody lessThanTemp()
.
Istnieje także możliwość odwołania się do wersji metody zdefiniowanej w klasie nadrzędnej. W tym celu należy użyć wyrażenia korzystającego ze słowa kluczowego super:
super::nazwa
gdzie nazwa jest nazwą metody. Odwołanie może mieć także poniższą postać:
nazwaTypu.super::nazwa
gdzie nazwaTypu
to nazwa klasy zewnętrznej lub interfejsu nadrzędnego.
Referencje do metod a typy sparametryzowane
Referencji do metod można także używać wraz z klasami i metodami sparametryzowanymi np.
// Program przedstawia użycie referencji do metod do
// wywoływania metody sparametryzowanej zadeklarowanej
// w niesparametryzowanej klasie.
// Interfejs funkcyjny operujący na tablicy i wartości,
// który zwraca wynik typu int.
interface MyFunc<T> {
int func(T[] vals, T v);
}
// Ta klasa definiuje metodę o nazwie countMatching(), która
// zwraca liczbę elementów tablicy, które są równe przekazanej
// wartości. Należy zwrócić uwagę, że metoda countMatching()
// jest sparametryzowana, natomiast sama klasa MyArrayOps nie jest.
class MyArrayOps {
static <T> int countMatching(T[] vals, T v) {
int count = 0;
for(int i=0; i < vals.length; i++)
if(vals[i] == v) count++;
return count;
}
}
class GenericMethodRefDemo {
// Typem pierwszego parametru tej metody jest interfejs funkcyjny
// MyFunc. Pozostałe dwa parametry pozwalają na przekazanie tablicy
// i wartości typu T.
static <T> int myOp(MyFunc<T> f, T[] vals, T v) {
return f.func(vals, v);
}
public static void main(String args[])
{
Integer[] vals = { 1, 2, 3, 4, 2 ,3, 4, 4, 5 };
String[] strs = { "Jeden", "Dwa", "Trzy", "Dwa" };
int count;
count = myOp(MyArrayOps::<Integer>countMatching, vals, 4);
System.out.println("Tablica vals zawiera " + count + " wartości 4.");
count = myOp(MyArrayOps::<String>countMatching, strs, "Dwa");
System.out.println("Tablica strs zawiera " + count + " łańcuchy \"Dwa\".");
}
}
Wykonanie tego programu zwróci następujące wyniki
Tablica vals zawiera 3 warości 4.
Tablica strs zawiera 2 łańcuchów "Dwa".
W przykładzie MyArrayOps
jest normalną, niesparametryzowaną klasą, zawierajaca sparametryzowaną metodę statyczną countMatching()
. Metoda ta zwraca liczbę elementów tablicy, które są równe przekazanej wartości. Należy tu zwrócić uwagę na sposób, w jaki został podany argument typu metody. Przykładowo pierwsze odwołanie do tej metody wygląda tak:
count = myOp(MyArrayOps::<Integer>countMatching, vals, 4);
W przypadku pobieranie referencji do metody sparametryzowanej jej argument typu podawany jest za klasą, przed symbolem ::
.
Referencje do konstruktorów
Ogólna postać wyrażenia pozwalającego na pobranie referencji wygląda w następujący sposób:
NazwaKlasy::new
Taką referencję można zapisać w dowolnej zmiennej referencyjnej, której typem jest interfejs funkcyjny definiujący metodę zgodną z konstruktorem, na przykład:
// Program demonstrujący tworzenie i stosowanie
// referencji do konstruktorów.
// MyFunc jest interfejsem funkcyjnym, którego metoda
// zwraca referencję typu MyClass.
interface MyFunc {
MyClass func(int n);
}
class MyClass {
private int val;
// Ten konstruktor wymaga przekazania jednego argumentu.
MyClass(int v) { val = v; }
// To jest konstruktor domyślny.
MyClass() { val = 0; }
// ...
int getVal() { return val; };
}
class ConstructorRefDemo {
public static void main(String args[])
{
// Ta instrukcja tworzy referencję do konstruktora
// klasy MyClass. Ponieważ metoda func() interfejsu
// MyFunc ma jeden parametr, zatem w tym przypadku
// słowo new odwołuje się do konstruktora klasy MyClass,
// który ma jeden parametr, a nie do konstruktora
// domyślnego.
MyFunc myClassCons = MyClass::new;
// Tworzy instancję klasy MyClass, wywołując konstruktor
// za pomocą utworzonej wcześniej referencji.
MyClass mc = myClassCons.func(100);
// Używa utworzonej wcześniej instancji klasy MyClass.
System.out.println("Zapisana wartość to: " + mc.getVal( ));
}
}
Program ten wygeneruje następujacy wynik:
Zapisana wartość to: 100
W programie należy zwrócić uwagę na metodę func(
), która zwraca referencje typu MyClass
i ma jeden parametr typu int
. Klasa MyClass
definiuje dwa konstruktory, jeden z parametrem typu int
, a drugi jest domyślnym konstruktorem bezargumentowym. Podczas stosowania instrukcji
MyFunc myClassCons = MyClass::new;
zwraca referencję do konstruktora klasy MyClass
. Ponieważ metoda func()
interfejsu MyFunc
ma jeden parametr typu int
, zatem w tym przypadku zostanie zwrócona referencja do konstruktora MyClass(int v)
.
W podobny sposób tworzone są referencje do konstruktorów klas sparametryzowanych. Rozwiązanie to działa tak samo jak pobieranie referencji do metod klas sparametryzowanych.
Predefiniowane interfejsy funkcyjne
W wielu przypadkach definiowanie interfejsów nie jest konieczne gdyż dostępny jest pakiet java.util.function
z kilkoma predefiniowanymi interfejsami np.
Przykłady:
UnaryOperator<T>
, który wykonuje jednoargumentową operację na obiekcie typu T i zwraca wynik, który jest także obiektem typu T. Interfejs udostępnia metodę o nazwie apply()
.
BinaryOperator<T>
wykonuje operację na dwóch obiektach typu T i zwraca wynik, który także jest typu T. Interfejs ten udostępnia metodę o nazwie apply()
.
Function<T,R>
wykonuje operację na obiekcie typu T i zwraca wynik będący obiektem typu R. Interfejs ten udostępnia metodę o nazwie apply()
.
W takim przypadku kod do obliczania silni wygląda następująco:
// Zastosowanie wbudowanego interfejsu funkcyjnego Function.
// Importuje interfejs Function.
import java.util.function.Function;
class UseFunctionInterfaceDemo {
public static void main(String args[])
{
// To blokowe wyrażenie lambda wyznacza silnię liczby całkowitej.
// W tym rozwiązaniu został zastosowany budowany
// interfejs funkcyjny Function.
Function<Integer, Integer> factorial = (n) -> {
int result = 1;
for(int i=1; i <= n; i++)
result = i * result;
return result;
};
System.out.println("Silnia liczby 3 wynosi " + factorial.apply(3));
System.out.println("Silnia liczby 5 wynosi " + factorial.apply(5));
}
}
Podsumowanie
Zachęcam do zapoznawania się z dokumentacją nowszych wersji – wręcz wyrobienia w sobie nawyku efektywnego poruszania się w strukturze wiedzy, podczas rozwiązywania mniej i bardziej skomplikowanych zadań; znajdowanie własnych oraz studiowanie istniejących rozwiązań. Ze względu na szeroko dostępna literaturę, nadal zalecam edukację rozpoczynać od wersji Java 8.