Dlaczego wolisz niestatyczne klasy wewnętrzne niż statyczne?

37

To pytanie dotyczy tego, czy uczynić zagnieżdżoną klasę w Javie klasą zagnieżdżoną statycznie czy wewnętrznie zagnieżdżoną. Szukałem tutaj i nad przepełnieniem stosu, ale tak naprawdę nie mogłem znaleźć żadnych pytań dotyczących konsekwencji projektowych tej decyzji.

Znalezione przeze mnie pytania dotyczą różnicy między statycznymi i wewnętrznymi klasami zagnieżdżonymi, co jest dla mnie jasne. Jednak nie znalazłem jeszcze przekonującego powodu, aby kiedykolwiek używać statycznej zagnieżdżonej klasy w Javie - z wyjątkiem klas anonimowych, których nie rozważam w przypadku tego pytania.

Oto moje rozumienie efektu używania statycznych klas zagnieżdżonych:

  • Mniej sprzężenia: generalnie uzyskujemy mniej sprzężenia, ponieważ klasa nie ma bezpośredniego dostępu do atrybutów swojej klasy zewnętrznej. Mniej sprzężenia oznacza ogólnie lepszą jakość kodu, łatwiejsze testowanie, refaktoryzację itp.
  • Pojedynczy Class: moduł ładujący klasy nie musi dbać o nową klasę za każdym razem, gdy tworzymy obiekt klasy zewnętrznej. Ciągle otrzymujemy nowe obiekty dla tej samej klasy.

W przypadku klasy wewnętrznej ogólnie uważam, że ludzie uważają dostęp do atrybutów klasy zewnętrznej za profesjonalistę. Zaczynam się różnić pod tym względem z punktu widzenia projektowania, ponieważ ten bezpośredni dostęp oznacza, że ​​mamy wysoki stopień sprzężenia, a jeśli kiedykolwiek chcemy wyodrębnić zagnieżdżoną klasę do oddzielnej klasy najwyższego poziomu, możemy to zrobić dopiero po zasadniczym odwróceniu w statyczną zagnieżdżoną klasę.

Moje pytanie sprowadza się zatem do tego: czy mylę się, zakładając, że dostęp do atrybutów dostępny dla niestatycznych klas wewnętrznych prowadzi do wysokiego sprzężenia, a tym samym do niższej jakości kodu, i wywnioskuję z tego, że (nieanonimowe) klasy zagnieżdżone powinny ogólnie być statycznym?

Innymi słowy: czy istnieje przekonujący powód, dla którego wolałaby zagnieżdżoną klasę wewnętrzną?

Szczery
źródło
2
z samouczka Java : „Użyj niestatycznej klasy zagnieżdżonej (lub klasy wewnętrznej), jeśli potrzebujesz dostępu do niepublicznych pól i metod instancji zamykającej. Użyj statycznej zagnieżdżonej klasy, jeśli nie potrzebujesz tego dostępu”.
komar
5
Dziękuję za cytat z samouczka, ale to po prostu ponownie potwierdza różnicę, a nie dlaczego chcesz uzyskać dostęp do pól instancji.
Frank
jest to raczej ogólna wskazówka, wskazująca, co preferować. Dodałem bardziej szczegółowe wyjaśnienie, w jaki sposób interpretuję to w tej odpowiedzi
gnat
1
Jeśli klasa wewnętrzna nie jest ściśle powiązana z klasą obejmującą, prawdopodobnie nie powinna to być klasa wewnętrzna.
Kevin Krumwiede

Odpowiedzi:

23

Joshua Bloch w punkcie 22 swojej książki „Effective Java Second Edition” mówi, kiedy użyć jakiego rodzaju klasy zagnieżdżonej i dlaczego. Poniżej kilka cytatów:

Jednym z powszechnych zastosowań statycznej klasy członka jest publiczna klasa pomocnicza, przydatna tylko w połączeniu z jej klasą zewnętrzną. Rozważmy na przykład wyliczenie opisujące operacje obsługiwane przez kalkulator. Wyliczenie operacji powinno być publiczną statyczną klasą członka Calculatorklasy. Klienci Calculatormogą wtedy odwoływać się do operacji przy użyciu nazw takich jak Calculator.Operation.PLUSi Calculator.Operation.MINUS.

Jednym z powszechnych zastosowań niestatycznej klasy elementu jest zdefiniowanie adaptera, który pozwala na postrzeganie instancji klasy zewnętrznej jako instancji klasy niepowiązanej. Na przykład implementacje Mapinterfejsu zazwyczaj korzystają nonstatic klas członkowskie do wdrożenia ich poglądy zbiórki , które są zwracane przez Map„s keySet, entrySeti valuesmetod. Podobnie implementacje interfejsów kolekcji, takie jak Seti List, zwykle używają niestatycznych klas elementów do implementacji swoich iteratorów:

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

Jeśli deklarujesz klasę członkowską, która nie wymaga dostępu do załączającej instancji, zawsze umieszczaj staticmodyfikator w jej deklaracji, czyniąc ją statyczną, a nie niestatyczną klasą członkowską.

erakityna
źródło
1
Nie jestem do końca pewien, jak / czy ten adapter zmienia widok MySetinstancji klasy zewnętrznej ? Mógłbym wyobrazić sobie coś w rodzaju typu zależnego od ścieżki, tj. myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), Ale z drugiej strony w Javie nie jest to faktycznie możliwe, ponieważ klasa byłaby taka sama dla każdego.
Frank
@Frank Jest to dość typowa klasa adaptera - pozwala oglądać zestaw jako strumień jego elementów (Iterator).
user253751
@immibis dzięki, jestem w pełni świadomy tego, co robi iterator / adapter, ale to pytanie dotyczyło sposobu jego implementacji.
Frank
@Frank Mówiłem, jak to zmienia widok zewnętrznej instancji. Właśnie to robi adapter - zmienia sposób wyświetlania niektórych obiektów. W tym przypadku tym obiektem jest instancja zewnętrzna.
user253751,
10

Masz rację zakładając, że dostęp do atrybutów dostępny dla niestatycznych klas wewnętrznych prowadzi do wysokiego sprzężenia, a tym samym do niższej jakości kodu, a (nieanonimowe i nielokalne ) klasy wewnętrzne powinny zasadniczo być statyczne.

Implikacje projektowe decyzji o uczynieniu klasy wewnętrznej niestatyczną są określone w Java Puzzlers , Puzzle 90 (pogrubiona czcionka w poniższym cytacie jest moja):

Ilekroć piszesz klasę członkowską, zadaj sobie pytanie: Czy ta klasa naprawdę potrzebuje zamykającej instancji? Jeśli odpowiedź brzmi „nie”, zrób to static. Zajęcia wewnętrzne są czasem przydatne, ale mogą łatwo wprowadzić komplikacje, które utrudniają zrozumienie programu. Mają złożone interakcje z rodzajami (Puzzle 89), refleksjami (Puzzle 80) i dziedziczeniem (ta łamigłówka) . Jeśli zadeklarujesz Inner1się static, problem zniknie. Jeśli również zadeklarować Inner2się static, można rzeczywiście zrozumieć, co program robi: bonus ładne rzeczywiście.

Podsumowując, rzadko jest właściwe, aby jedna klasa była zarówno klasą wewnętrzną, jak i podklasą innej. Mówiąc bardziej ogólnie, rzadko właściwe jest rozszerzenie klasy wewnętrznej; jeśli musisz, zastanów się długo nad załączającą instancją. Ponadto wolą staticklasy zagnieżdżone od innych niż static. Większość klas członków może i powinna być zadeklarowana static.

Jeśli jesteś zainteresowany, bardziej szczegółowa analiza Puzzle 90 znajduje się w tej odpowiedzi na Stack Overflow .


Warto zauważyć, że powyższa jest zasadniczo rozszerzoną wersją wskazówek zawartych w samouczku klas i obiektów Java :

Użyj niestatycznej klasy zagnieżdżonej (lub klasy wewnętrznej), jeśli potrzebujesz dostępu do niepublicznych pól i metod instancji obejmującej. Użyj statycznej zagnieżdżonej klasy, jeśli nie potrzebujesz tego dostępu.

Tak więc odpowiedź na pytanie, które zadałeś innymi słowami w samouczku, jest jedynym przekonującym powodem zastosowania niestatyczności jest, gdy wymagany jest dostęp do niepublicznych pól i metod instancji obejmującej .

Sformułowanie samouczka jest dość szerokie (może to być powód, dla którego Java Puzzlers próbują go wzmocnić i zawęzić). W szczególności bezpośredni dostęp do otaczających pól instancji nigdy nie był wymagany w moim doświadczeniu - w tym sensie, że alternatywne sposoby, takie jak przekazywanie ich jako parametrów konstruktora / metody, zawsze były łatwiejsze do debugowania i obsługi.


Ogólnie rzecz biorąc, moje (dość bolesne) spotkania z debugowaniem wewnętrznych klas bezpośrednio uzyskujących dostęp do pól otaczającej instancji wywarły silne wrażenie, że ta praktyka przypomina użycie stanu globalnego wraz ze znanymi złymi z nim związanymi.

Oczywiście Java sprawia, że ​​uszkodzenie takiej „quasi-globalnej” jest zawarte w klasie zamkniętej, ale kiedy musiałem debugować konkretną klasę wewnętrzną, czułem, że taka pomoc zespołu nie pomogła zmniejszyć bólu: wciąż miałem pamiętać o „obcym” semantyce i szczegółach zamiast w pełni koncentrować się na analizie konkretnego kłopotliwego obiektu.


Dla zachowania kompletności mogą zdarzyć się przypadki, w których powyższe rozumowanie nie ma zastosowania. Na przykład, zgodnie z moim odczytaniem map.keySet javadocs , ta funkcja sugeruje ścisłe sprzężenie, w wyniku czego unieważnia argumenty przeciwko klasom niestatycznym:

Zwraca Setwidok kluczy zawartych na tej mapie. Zestaw jest wspierany przez mapę, więc zmiany na mapie są odzwierciedlane w zestawie i odwrotnie ...

Nie to, że powyższe w jakiś sposób ułatwiłoby utrzymanie, testowanie i debugowanie zaangażowanego kodu, ale przynajmniej pozwoliło by argumentować, że komplikacja pasuje / jest uzasadniona zamierzoną funkcjonalnością.

komar
źródło
Dzielę się swoimi doświadczeniami na temat problemów spowodowanych tym wysokim sprzężeniem, ale nie zgadzam się z użyciem instrukcji wymaganej przez samouczek . Mówisz sam, że tak naprawdę nigdy nie wymagałeś dostępu do pola, więc niestety to również nie w pełni odpowiada na moje pytanie.
Frank
@Frank dobrze, jak widzisz, moja interpretacja wskazówek do samouczków (które wydają się być obsługiwane przez Java Puzzlers) jest jak, używaj klas niestatycznych tylko wtedy, gdy możesz udowodnić, że jest to wymagane, a w szczególności udowodnić, że alternatywne sposoby są gorzej . Warto zauważyć, że z mojego doświadczenia wynika, że ​​alternatywne sposoby zawsze były lepsze
komnata
O to chodzi. Jeśli zawsze jest lepiej, to dlaczego zawracamy sobie tym głowę?
Frank
@Frank inna odpowiedź zawiera przekonujące przykłady, że nie zawsze tak jest. Według mojego odczytania map.keySet javadocs ta funkcja sugeruje ścisłe sprzężenie, w wyniku czego unieważnia argumenty przeciwko klasom niestatycznym „Zestaw jest wspierany przez mapę, więc zmiany w mapie są odzwierciedlane w zestawie i odwrotnie ... ”
komar
1

Niektóre nieporozumienia:

Mniej sprzężenia: generalnie uzyskujemy mniej sprzężenia, ponieważ klasa [statyczna wewnętrzna] nie może bezpośrednio uzyskać dostępu do atrybutów swojej klasy zewnętrznej.

IIRC - Właściwie to może. (Oczywiście, jeśli są to pola obiektowe, nie będzie miał zewnętrznego obiektu do oglądania. Niemniej jednak, jeśli otrzyma taki obiekt z dowolnego miejsca, wówczas może uzyskać bezpośredni dostęp do tych pól).

Klasa pojedyncza: moduł ładujący klasę nie musi dbać o nową klasę za każdym razem, gdy tworzymy obiekt klasy zewnętrznej. Po prostu ciągle otrzymujemy nowe obiekty dla tej samej klasy.

Nie jestem do końca pewien, co masz na myśli. Program ładujący klasy jest zaangażowany tylko dwa razy, raz dla każdej klasy (gdy klasa jest załadowana).


Wędrowanie następuje:

W Javaland wydaje się, że trochę boimy się tworzyć klasy. Myślę, że to musi być znacząca koncepcja. Zazwyczaj należy do własnego pliku. Potrzebuje znacznej płyty grzewczej. Potrzebuje uwagi do swojego projektu.

W porównaniu z innymi językami - np. Scala, a może C ++: Nie ma absolutnie żadnego dramatu tworzącego malutki uchwyt (klasa, struktura, cokolwiek) w każdym momencie, gdy dwa bity danych należą do siebie. Elastyczne reguły dostępu pomagają ci, odcinając płytę kotłową, pozwalając ci fizycznie zbliżyć powiązany kod. Zmniejsza to „dystans umysłu” między dwiema klasami.

Możemy rozwiać wątpliwości projektowe dotyczące bezpośredniego dostępu, zachowując prywatność klasy wewnętrznej.

(... Gdybyś zapytał „dlaczego nie-statyczna publiczna klasa wewnętrzna?” To bardzo dobre pytanie - nie sądzę, żebym kiedykolwiek znalazł uzasadniony przypadek dla nich).

Możesz z nich korzystać w dowolnym momencie, co pomaga w odczytywaniu kodu i unikaniu naruszeń OSUSZANIA i płyty kotłowej. Nie mam gotowego przykładu, ponieważ z mojego doświadczenia nie zdarza się to często, ale się zdarza.


Statyczne klasy wewnętrzne są nadal dobrym podejściem domyślnym , btw. Niestatyczne ma generowane dodatkowe ukryte pole, przez które klasa wewnętrzna odnosi się do zewnętrznego.

Iain
źródło
0

Można go użyć do utworzenia wzoru fabrycznego. Wzorzec fabryczny jest często używany, gdy tworzone obiekty wymagają dodatkowych parametrów konstruktora do działania, ale są nużące, aby zapewnić za każdym razem:

Rozważać:

public class QueryFactory {
    @Inject private Database database;

    public Query create(String sql) {
        return new Query(database, sql);
    }
}

public class Query {
    private final Database database;
    private final String sql;

    public Query(Database database, String sql) {
        this.database = database;
        this.sql = sql;
    } 

    public List performQuery() {
        return database.query(sql);
    }
}

Użyty jako:

Query q = queryFactory.create("SELECT * FROM employees");

q.performQuery();

To samo można osiągnąć dzięki niestatycznej klasie wewnętrznej:

public class QueryFactory {
    @Inject private Database database;

    public class Query {
        private final String sql;

        public Query(String sql) {
            this.sql = sql;
        }

        public List performQuery() {
            return database.query(sql);
        }
    }
}

Użyj jako:

Query q = queryFactory.new Query("SELECT * FROM employees");

q.performQuery();

Fabryki skorzystają z konkretnie nazwanych metod, jak create, createFromSQLlub createFromTemplate. To samo można zrobić tutaj również w postaci wielu klas wewnętrznych:

public class QueryFactory {

    public class SQL { ... }
    public class Native { ... }
    public class Builder { ... }

}

Potrzebne jest mniej przekazywania parametrów z klasy fabrycznej do skonstruowanej, ale czytelność może nieco ucierpieć.

Porównać:

Queries.Native q = queries.new Native("select * from employees");

vs

Query q = queryFactory.createNative("select * from employees");
john16384
źródło