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

Interfejsy w Javie - Referencje metod

Zobacz, jak w Javie działają referencje do metod statycznych, metod instancyjnych, konstruktorów oraz sprawdź, jak współpracują ze sobą referencje do metod i typy sparametryzowane.
22.10.202010 min
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.

<p>Loading...</p>