Dlaczego powinienem używać refleksji?

29

Jestem nowy w Javie; poprzez moje badania przeczytałem, że refleksja służy do wywoływania klas i metod oraz do poznania, które metody są wdrożone, czy nie.

Kiedy powinienem używać odbicia i jaka jest różnica między używaniem odbicia i tworzeniem obiektów i metodami wywoływania w tradycyjny sposób?

Hamzah khammash
źródło
10
Przed opublikowaniem przeprowadź swoją część badań. Na StackExchange jest wiele materiałów (jak zauważył @Jalayn) i ogólnie w Internecie na temat refleksji. Sugeruję przeczytanie np. Java Tutorial on Reflection i wrócę po tym, jeśli będziesz mieć więcej konkretnych pytań.
Péter Török,
1
Musi być milion duplikatów.
DeadMG,
3
Więcej niż kilku profesjonalnych programistów odpowiedziało „tak rzadko, jak to możliwe, a może nawet nigdy”.
Ross Patterson

Odpowiedzi:

38
  • Odbicie jest znacznie wolniejsze niż tylko wywoływanie metod według ich nazw, ponieważ musi sprawdzać metadane w kodzie bajtowym zamiast po prostu używać wstępnie skompilowanych adresów i stałych.

  • Odbicie jest również silniejsze: możesz odzyskać definicję elementu protectedlub finalelementu, usunąć ochronę i manipulować nią tak, jakby została uznana za zmienną! Oczywiście podważa to wiele gwarancji, jakie język zwykle zapewnia twoim programom i może być bardzo, bardzo niebezpieczne.

I to właściwie wyjaśnia, kiedy go używać. Zwykle nie. Jeśli chcesz wywołać metodę, po prostu ją wywołaj. Jeśli chcesz zmutować członka, po prostu zadeklaruj, że jest on zmienny, zamiast iść za plecami kompilacji.

Jednym z użytecznych zastosowań refleksji w świecie rzeczywistym jest pisanie frameworka, który musi współdziałać z klasami zdefiniowanymi przez użytkownika, w których autor frameworku nie wie, czym będą członkowie (a nawet klasy). Refleksja pozwala im radzić sobie z dowolną klasą, nie wiedząc o tym wcześniej. Na przykład nie sądzę, że bez refleksji byłoby możliwe napisanie złożonej biblioteki zorientowanej na aspekty.

Jako inny przykład, JUnit użył trywialnego fragmentu refleksji: wylicza wszystkie metody w twojej klasie, zakłada, że ​​wszystkie te wywoływane testXXXsą metodami testowymi i wykonuje tylko te. Ale można to teraz zrobić lepiej za pomocą adnotacji, aw rzeczywistości JUnit 4 w dużej mierze przeniósł się do adnotacji.

Kilian Foth
źródło
6
„Mocniejszy” wymaga opieki. Nie potrzebujesz refleksji, aby uzyskać kompletność Turinga, więc żadne obliczenia nigdy nie wymagają refleksji. Oczywiście Turing complete nie mówi nic o innych rodzajach mocy, takich jak możliwości we / wy i, oczywiście, odbicie.
Steve314,
1
Odbicie niekoniecznie jest „znacznie wolniejsze”. Możesz użyć odbicia raz, aby wygenerować kod bajtowy opakowania bezpośredniego wywołania.
SK-logic,
2
Będziesz wiedział, kiedy będziesz tego potrzebować. Często zastanawiałem się, dlaczego (poza generowaniem języka) jest to potrzebne. Potem nagle to zrobiłem ... Musiałem chodzić po łańcuchach rodziców / dzieci, aby przeglądać / pokekać dane, kiedy dostałem panele od innych deweloperów, aby wpaść do jednego z systemów, które utrzymuję.
Brian Knoblauch,
@ SK-logic: właściwie do generowania kodu bajtowego wcale nie potrzebujesz odbicia (w rzeczywistości odbicie nie zawiera API do manipulowania kodem bajtowym!).
Joachim Sauer
1
@JoachimSauer, oczywiście, ale potrzebujesz interfejsu API refleksji, aby załadować wygenerowany kod bajtowy.
SK-logic
15

Byłem kiedyś taki jak ty, nie wiedziałem wiele o refleksji - wciąż nie - ale użyłem go raz.

Miałem klasę z dwiema klasami wewnętrznymi i każda klasa miała wiele metod.

Musiałem wywołać wszystkie metody z klasy wewnętrznej, a ręczne wywołanie ich byłoby zbyt dużym nakładem pracy.

Używając refleksji, mogłem wywołać wszystkie te metody w zaledwie 2-3 wierszach kodu, zamiast liczby samych metod.

Mahmoud Hossam
źródło
4
Dlaczego głosowanie negatywne?
Mahmoud Hossam,
1
Głosowanie za preambułą
Dmitrij Minkovsky
1
@MahmoudHossam Być może nie jest to najlepsza praktyka, ale twoja odpowiedź ilustruje ważną taktykę, którą można zastosować.
ankush981,
13

Pogrupowałbym zastosowania refleksji na trzy grupy:

  1. Tworzenie dowolnych klas. Na przykład w ramach wstrzykiwania zależności prawdopodobnie deklarujesz, że interfejs ThingDoer jest implementowany przez klasę NetworkThingDoer. Framework znalazłby następnie konstruktora NetworkThingDoer i utworzył go.
  2. Marshalling i unmarshalling do innego formatu. Na przykład odwzorowanie obiektu za pomocą metod pobierających i ustawień zgodnych z konwencją komponentu bean na JSON iz powrotem. Kod tak naprawdę nie zna nazw pól ani metod, po prostu sprawdza klasę.
  3. Zawinięcie klasy w warstwę przekierowania (być może ta lista faktycznie nie jest załadowana, ale tylko wskaźnik do czegoś, co wie, jak pobrać ją z bazy danych) lub całkowite sfałszowanie klasy (jMock utworzy syntetyczną klasę, która implementuje interfejs do celów testowych).
Kevin Peterson
źródło
To najlepsze wytłumaczenie refleksji, jakie znalazłem na StackExchange. Większość odpowiedzi powtarza to, co mówi Java Trail (czyli „możesz uzyskać dostęp do wszystkich tych właściwości”, ale nie dlaczego), podając przykłady robienia rzeczy z refleksją, które są o wiele łatwiejsze bez, lub daje niejasną odpowiedź na temat tego, jak Wiosna używa tego. Ta odpowiedź faktycznie podaje trzy ważne przykłady, których JVM nie może łatwo obejść bez zastanowienia. Dziękuję Ci!
ndm13
3

Odbicie pozwala programowi na pracę z kodem, który może nie być obecny, i robi to w niezawodny sposób.

„Normalny kod” ma takie fragmenty, URLConnection c = nullktóre przez samą swoją obecność powodują, że moduł ładujący klasy ładuje klasę URLConnection w ramach ładowania tej klasy, zgłaszając wyjątek ClassNotFound i kończąc działanie.

Odbicie umożliwia ładowanie klas na podstawie ich nazw w postaci ciągów i testowanie ich pod kątem różnych właściwości (przydatne w wielu wersjach poza kontrolą) przed uruchomieniem rzeczywistych klas, które są od nich zależne. Typowym przykładem jest kod specyficzny dla OS X używany do nadawania programom Java wyglądu natywnego w systemie OS X, które nie są obecne na innych platformach.


źródło
2

Zasadniczo refleksja oznacza użycie kodu programu jako danych.

Dlatego użycie refleksji może być dobrym pomysłem, gdy kod programu jest użytecznym źródłem danych. (Ale są kompromisy, więc nie zawsze może to być dobry pomysł).

Rozważmy na przykład prostą klasę:

public class Foo {
  public int value;
  public string anotherValue;
}

i chcesz z niego wygenerować XML. Możesz napisać kod do wygenerowania XML:

public XmlNode generateXml(Foo foo) {
  XmlElement root = new XmlElement("Foo");
  XmlElement valueElement = new XmlElement("value");
  valueElement.add(new XmlText(Integer.toString(foo.value)));
  root.add(valueElement);
  XmlElement anotherValueElement = new XmlElement("anotherValue");
  anotherValueElement.add(new XmlText(foo.anotherValue));
  root.add(anotherValueElement);
  return root;
}

Ale to dużo kodu z podstawowymi danymi i za każdym razem, gdy zmieniasz klasę, musisz aktualizować kod. Naprawdę możesz opisać, co robi ten kod jako

  • utwórz element XML o nazwie klasy
  • dla każdej właściwości klasy
    • utwórz element XML o nazwie właściwości
    • wstaw wartość właściwości do elementu XML
    • dodaj element XML do katalogu głównego

To jest algorytm, a dane wejściowe algorytmu to klasa: potrzebujemy jego nazwy oraz nazw, typów i wartości jego właściwości. Tu pojawia się refleksja: daje dostęp do tych informacji. Java pozwala sprawdzać typy przy użyciu metod Classklasy.

Kilka innych przypadków użycia:

  • zdefiniuj adresy URL na serwerze sieciowym na podstawie nazw metod klasy, a parametry adresów URL na podstawie argumentów metody
  • przekonwertować strukturę klasy na definicję typu GraphQL
  • wywołuje każdą metodę klasy, której nazwa zaczyna się od „test” jako test jednostkowy

Jednak pełne odzwierciedlenie oznacza nie tylko spojrzenie na istniejący kod (który sam w sobie jest znany jako „introspekcja”), ale także modyfikowanie lub generowanie kodu. W tym celu istnieją dwa znaczące przypadki użycia w Javie: serwery proxy i makiety.

Powiedzmy, że masz interfejs:

public interface Froobnicator {
  void froobnicateFruits(List<Fruit> fruits);
  void froobnicateFuel(Fuel fuel);
  // lots of other things to froobnicate
}

i masz implementację, która robi coś interesującego:

public class PowerFroobnicator implements Froobnicator {
  // awesome implementations
}

W rzeczywistości masz też drugą implementację:

public class EnergySaverFroobnicator implements Froobnicator {
  // efficient implementations
}

Teraz potrzebujesz także danych wyjściowych dziennika; po prostu chcesz komunikat dziennika za każdym razem, gdy wywoływana jest metoda. Możesz jawnie dodać dane wyjściowe dziennika do każdej metody, ale byłoby to irytujące i musiałbyś to zrobić dwa razy; raz na każde wdrożenie. (A więc jeszcze więcej, jeśli dodasz więcej implementacji).

Zamiast tego możesz napisać proxy:

public class LoggingFroobnicator implements Froobnicator {
  private Logger logger;
  private Froobnicator inner;

  // constructor that sets those two

  public void froobnicateFruits(List<Fruit> fruits) {
    logger.logDebug("froobnicateFruits called");
    inner.froobnicateFruits(fruits);
  }

  public void froobnicateFuel(Fuel fuel) {
    logger.logDebug("froobnicateFuel( called");
    inner.froobnicateFuel(fuel);
  }
  // lots of other things to froobnicate
}

Ponownie jednak istnieje powtarzalny wzorzec, który można opisać algorytmem:

  • proxy rejestratora to klasa implementująca interfejs
  • ma konstruktor, który przyjmuje inną implementację interfejsu i rejestrator
  • dla każdej metody w interfejsie
    • implementacja rejestruje komunikat „$ nazwa metody o nazwie”
    • a następnie wywołuje tę samą metodę na interfejsie wewnętrznym, przekazując wszystkie argumenty

a wejściem tego algorytmu jest definicja interfejsu.

Refleksja pozwala zdefiniować nową klasę za pomocą tego algorytmu. Java pozwala to zrobić przy użyciu metod java.lang.reflect.Proxyklasy, a istnieją biblioteki, które dają jeszcze więcej mocy.

Jakie są wady refleksji?

  • Twój kod staje się trudniejszy do zrozumienia. Masz jeden poziom abstrakcji, który jest dalej usuwany z konkretnych efektów twojego kodu.
  • Kod staje się trudniejszy do debugowania. Zwłaszcza w bibliotekach generujących kod wykonywany kod może nie być kodem, który napisałeś, ale kodem, który wygenerowałeś, a debugger może nie być w stanie pokazać Ci tego kodu (lub pozwolić ci umieszczać punkty przerwania).
  • Twój kod staje się wolniejszy. Dynamiczne czytanie informacji o typie i uzyskiwanie dostępu do pól za pomocą ich uchwytów środowiska wykonawczego zamiast dostępu do kodowania jest wolniejsze. Dynamiczne generowanie kodu może złagodzić ten efekt, kosztem jeszcze trudniejszego debugowania.
  • Twój kod może stać się bardziej kruchy. Dynamiczny dostęp do odbicia nie jest sprawdzany przez kompilator, ale generuje błędy w czasie wykonywania.
Sebastian Redl
źródło
1

Odbicie może automatycznie utrzymywać synchronizację części twojego programu, gdzie wcześniej musiałbyś ręcznie zaktualizować swój program, aby używał nowych interfejsów.

DeadMG
źródło
5
Cena, którą płacisz w tym przypadku, polega na tym, że tracisz sprawdzanie typu przez kompilator i bezpieczeństwo refaktora w IDE. To kompromis, którego nie jestem skłonny zrobić.
Barend