Interfejs List -> metoda Add -> Barbara

0

Hejo
Mam dlemacik, ostatnio się zastanawiałem nad Interfejsem List i nad operacjami mutowalnymi.
Jeżeli do interfejsu list przypiszemy liste niemutowalną to oczekiwanym zachowaniem metody add będzie rzucenie wyjątku.
No i pytanie czy to łamie zasade liskov? Z jednej strony dokumentacja mówi że metoda add może rzucić wyjątkiem i jest to oczekiwany efekt.
Ale od razu nasuwa mi się pytanie, to po co jest metoda która może rzucać taki wyjątek. Jeżeli ktoś operuje na liście, to co ma zawsze się zabezpieczać przed tym że ktoś może wstawić niemutowalną kolekcje (wiadomo modyfikowanie listy jest bee, ale załóżmy taki przypadek).

2

Oczywiscie ze to lamie Liskov.
Po prostu porzuc java.util i przerzuc sie na Vavr

2

Zasada:

Funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów.

Definicja w dokumentacji:

boolean add​(E e)

Appends the specified element to the end of this list (optional operation).

Lists that support this operation may place limitations on what elements may be added to this list. In particular, some lists will refuse to add null elements, and others will impose restrictions on the type of elements that may be added. List classes should clearly specify in their documentation any restrictions on what elements may be added.

Specified by:
add in interface Collection<E>
Parameters:
e - element to be appended to this list
Returns:
true (as specified by Collection.add(E))
Throws:
UnsupportedOperationException - if the add operation is not supported by this list
ClassCastException - if the class of the specified element prevents it from being added to this list
NullPointerException - if the specified element is null and this list does not permit null elements
IllegalArgumentException - if some property of this element prevents it from being added to this list

No i teraz tak - dokumentacja mówi Ci wprost co powinieneś obsłużyć, żeby mieć pewność, że każdy podtyp będzie elegancko obsłużony. @stivens mówi, że oczywiście łamie Liskov. Ja się nie zgadzam. Dostajesz wprost info co powinieneś zrobić, żeby wszystko śmigało. Nawet masz napisane, że dodawanie jest opcjonalne, tj. istnieje podzbiór podtypów, który nie będzie tego obsługiwał - stąd te wyjątki. To, że nie jest to tak oczywiste jakbyśmy chcieli nie powoduje, że zasada jest złamana. Tutaj bardziej łamiemy KISS, bo nie jest to intuicyjne.
Jeśli Ci to nie pasuje to powinieneś wybrać inny typ jako bazowy, tak aby samo sprawdzanie typów załatwiło Ci zgodność z akcjami, z których chcesz korzystać (o ile taki istnieje). Przerzucanie się na vavr jest jakąś opcją, to jest przyzwoita libka (choć sam nie miałem okazji stosować).

A i jeszcze jedna kwestia mówienie, że papier przyjmie wszystko moim zdaniem trochę nie do końca ma tutaj podstawy - tworzenie biblioteki standardowej nie jest łatwym zadaniem, zwłaszcza kiedy dbasz o kompatybilność wsteczną. Być może można to było zrobić lepiej (patrz -> Scala), być może nie, nie czuję się tutaj w żadnym wypadku upoważniony do oceny na podstawie tego co wyczytałem w necie. Wracając. nie bycie zgodnym z dokumentacją i narzekanie, że coś nie działa to trochę jak składanie mebli z Ikei bez instrukcji i denerwowanie się, że stół jest krzywy i czasami nam kawa spada

0

@Burdzi0: ten interfejs jest po prostu zaprojektowany przeciwko LSP i zadna dokumentacja tego nie zmieni.

Juz widze jak programisci obsluguja wyjatki przy List::add. Raczej nobody cares.

Gdyby List bylo niemutowalne a potem bylo jakies MutableList extends List to wszystko by bylo cacy.

2

@Burdzi0: ten interfejs jest po prostu zaprojektowany przeciwko LSP i zadna dokumentacja tego nie zmieni.

@stivens: udowodnij, że tak jest, bo jak na razie to tylko Twoja opinia xd
Podałem swoje argumenty i podparłem je oficjalną dokumentacją. Albo ja jestem w błędzie, albo Ty, dobrze by było wiedzieć xd

Juz widze jak programisci obsluguja wyjatki przy List::add. Raczej nobody cares.

Płakanie potem, że coś wybuchło jest tylko i wyłącznie ich winą.

Gdyby List bylo niemutowalne a potem bylo jakies MutableList extends List to wszystko by bylo cacy.

Zrobienie tego w obecnej bibliotece standardowej jest niemożliwe, jeśli chcesz mieć jakąkolwiek kompatybilność wsteczną.

0

Kazdy Pies jest Ssakiem ale nie kazdy Ssak jest Psem.
Kazda MutowalnaLista jest Lista ale nie kazda Lista jest MutowalnaLista.

Takie dziedziczenie jak proponuje biblioteka standardowa nie ma sensu.

3
Burdzi0 napisał(a):

No i teraz tak - dokumentacja mówi Ci wprost co powinieneś obsłużyć, żeby mieć pewność, że każdy podtyp będzie elegancko obsłużony. @stivens mówi, że oczywiście łamie Liskov. Ja się nie zgadzam. Dostajesz wprost info co powinieneś zrobić, żeby wszystko śmigało. Nawet masz napisane, że dodawanie jest opcjonalne, tj. istnieje podzbiór podtypów, który nie będzie tego obsługiwał - stąd te wyjątki. To, że nie jest to tak oczywiste jakbyśmy chcieli nie powoduje, że zasada jest złamana. Tutaj bardziej łamiemy KISS, bo nie jest to intuicyjne.
Jeśli Ci to nie pasuje to powinieneś wybrać inny typ jako bazowy, tak aby samo sprawdzanie typów załatwiło Ci zgodność z akcjami, z których chcesz korzystać (o ile taki istnieje). Przerzucanie się na vavr jest jakąś opcją, to jest przyzwoita libka (choć sam nie miałem okazji stosować).

No ja się nie zgodzę z Tobą, a zgodzę się ze @stivens - to, że ktoś sobie napisze w dokumentacji, albo w komentarzu w kodzie, albo na swoim blogu (będąc Chief Executive Guru Maintainerem czegoś co ma 150k gwiazdek na GH), że to jest big ball of mud by design nie sprawi, że to przestanie być kulą błota i przestanie łamać LSP.

Jak ja lub Ty popełnię coś takiego w swoim kodzie i będę próbował wcisnąć komuś interfejs z paroma implementacjami mówiąc

no, wiesz, wprawdzie AbstractDog ma metodę bark(), ale używaj jej tylko na LivingDog bo DogStatue i DeceasedDog rzucają wyjątkiem

To to cóś wyląduje na śmietniku historii szybciej, niż zdążysz wymówić vavr.

Jeśli jedna z implementacji wali beztrosko wyjątkiem, gdy spróbujesz wywołać na niej metodę z interfejsu lub klasy abstrakcyjnej, to z punktu widzenia LSP to jest po prostu ultra-bajzel i zdecydowanie nie powinno tak być. Nawet jeśli dopiszesz w javadocu tego interfejsiku, że rzucanie wyjątku jest by design.

Żeby to w ogóle miało szansę mieć ręce i nogi, to coś takiego jak MutableList powinno rozszerzać podstawowy interfejs List o mutowalność (prawdopodobnie implementując dodatkowy interfejs który wprowadza mutujące metody). Ewentualnie, mogłyby mieć wspólny interfejs public List<T> add(T element), gdzie MutableList dodawałby element do wewnętrznego stanu zwracając siebie, a ImmutableList zwracałby nową, zapewne również niemutowalną listę - przynajmniej nie rozwalasz zachowania w jednej z implementacji, choć wtedy jedna metoda ma side-effects a druga nie, co też jest słabawe.

4

Ja osobiście uważam ze to raczej złamanie ISP -> interfejs jest zbyt bogaty i stąd implementacje które sobie z nim nie radzą ;)

3

Hejo
Mam dlemacik, ostatnio się zastanawiałem nad Interfejsem List i nad operacjami mutowalnymi.
Jeżeli do interfejsu list przypiszemy liste niemutowalną to oczekiwanym zachowaniem metody add będzie rzucenie wyjątku.
No i pytanie czy to łamie zasade liskov? Z jednej strony dokumentacja mówi że metoda add może rzucić wyjątkiem i jest to oczekiwany efekt.

Łamie, ponieważ wg. zasady Liskov kod kliencki powinien działać poprawnie dla podtypów, a ten się po prostu wywali. Taką pułapką jest np. Arrays.asList(...), które zwraca niemodyfikowalną listę - nieraz się na to naciąłem.

Ale od razu nasuwa mi się pytanie, to po co jest metoda która może rzucać taki wyjątek. Jeżeli ktoś operuje na liście, to co ma zawsze się zabezpieczać przed tym że ktoś może wstawić niemutowalną kolekcje (wiadomo modyfikowanie listy jest bee, ale załóżmy taki przypadek).

Tak jest niestety w Javie, ale to nie jest argument, że to jest dobre podejście. Są na to 3 niewykluczające się wyjścia:

  1. piszesz testy, dzięki czemu masz wyspecyfikowane w jaki sposób kod jest używany (to zawsze)
  2. używasz lepszych kolekcji, które chronią nie w runtime, ale podczas kompilacji, np. tych z Vavra
  3. wszędzie robisz try/catcha (nie polecam)
1

Niniejszy temat dobitnie pokazuje, jak trudno jest określić, czy SOLID został złamany czy nie :) Tak jak pisałem tutaj:
Wydajność programisty

1
krancki napisał(a):

No i pytanie czy to łamie zasade liskov? Z jednej strony dokumentacja mówi że metoda add może rzucić wyjątkiem i jest to oczekiwany efekt.

To łamie zasadę podstawienia Liskovej i jest głupie. Przy czym od razu zaznaczę, że nie każde złamanie zasady SOLID czy DRY jest głupie - chociaż muszę przyznać, że zdecydowana większość takich przypadków do czegoś co nazywam "świadomym łamaniem zasad" nie należy.

Ale od razu nasuwa mi się pytanie, to po co jest metoda która może rzucać taki wyjątek. Jeżeli ktoś operuje na liście, to co ma zawsze się zabezpieczać przed tym że ktoś może wstawić niemutowalną kolekcje (wiadomo modyfikowanie listy jest bee, ale załóżmy taki przypadek).

Jest dokładnie w drugą stronę. To ludzie implementujący interfejs List powinni się zatroszczyć o to, żeby zachować zasadę podstawienia. Ludzie korzystający z tego interfejsu nie muszą się zabezpieczać bo przyjmują, że ta zasada jest spełniona. W praktyce oznacza to tyle, że nie wypycha się takich "lewych" instancji poza jakiś konkretny scope.

I tak już poza dla tych wszystkich, którzy twierdzą że zasada podstawienia nie jest złamana - taki teścik:

    void clearAndAdd(final List<Object> list) {
        list.clear();
        list.add(new Object());
        
        Assert.assertEquals(1, list.size());
    }

w zależności od implementacji się wykrzaczy - ergo zasada LSP jest złamana.

2

Gdybyście mieli interfejs:

interface Thrower {
  void maybeThrow();
}

którego implementacje w 99.999% przypadków nie rzucają exceptionami to stwierdzilibyście, że implementacja, która faktycznie rzuca wyjątkiem, łamie zasadę LSP?

Jeśli chodzi o hierarchię typów zawierającą zarówno kolekcje mutowalne jak i niemutowalne to jedynym językiem, którego znam (ale w stosunkowo niewielu językach to drążyłem) gdzie to jest sensownie rozwiązane jest Scala gdzie mamy:

0
Wibowit napisał(a):

Gdybyście mieli interfejs:

interface Thrower {
  void maybeThrow();
}

którego implementacje w 99.999% przypadków nie rzucają exceptionami to stwierdzilibyście, że implementacja, która faktycznie rzuca wyjątkiem, łamie zasadę LSP?

Jeśli pozostałe implementacje by design mają nie rzucać wyjątkami, a jedna z nich będzie by design rzucać wyjątkiem bo na niej nie wolno akurat tej metody wywołać, to jak najbardziej tak.

3

Zasada LSP jest relacją rodzic-dziecko, a nie grupa-element:

Subtype Requirement: Let {\displaystyle \phi (x)}\phi (x) be a property provable about objects {\displaystyle x}x of type T. Then {\displaystyle \phi (y)}{\displaystyle \phi (y)} should be true for objects {\displaystyle y}y of type S where S is a subtype of T.

W skrócie: jeśli coś da się udowodnić dla nadtypu to powinno zachodzić dla podtypu. To, że 99.999% implementacji zachowuje się w dany sposób nie ma żadnego wpływu na zachodzenie LSP. Analizując typ T i jego podtyp S wszystkie inne podtypy T nie mają znaczenia. Kodowanie do interfejsu polega na kodowaniu do kontraktu zawartego w interfejsie, a nie w 99.999% jego implementacji.

1

Ale to jest wlasnie to o czym mowilem. Ktos napisal kod, ktory najpierw zlamal ISP a w efekcie tego poszlo jeszcze LSP, wiec sobie dopisal do dokumentacji ze niektore implementacje moga rzucic wyjatkiem. No i takim o to sposobem "nie ma problemu bo wszystko udokumentowane".

Tylko ze kazdy szajs mozna udokumentowac.

0
stivens napisał(a):

Ale to jest wlasnie to o czym mowilem. Ktos napisal kod, ktory najpierw zlamal ISP a w efekcie tego poszlo jeszcze LSP, wiec sobie dopisal do dokumentacji ze niektore implementacje moga rzucic wyjatkiem.

Notabene, nie znam zaszłości historycznych także mogę się mylić, ale jeśli metoda j.u.List#add wcześniej nie przewidywała rzucenia wyjątku, a następnie po pojawieniu się niemutowalnych list zaczęła go przewidywać, to strzelam że dodatkowo złamane zostało też OCP. Nawet jeśli uznamy, że dokumentacją możemy "odłamać" LSP.

No i takim o to sposobem "nie ma problemu bo wszystko udokumentowane".

Tylko ze kazdy szajs mozna udokumentowac.

To. Jeśli docsy mają robić za perfumę maskującą dowolny code smell, to praktycznie jakiekolwiek regułki i zasady można wyrzucić do kosza - jako, że każde złamanie można i tak uzasadnić i unieważnić :P

2

Ktos napisal kod, ktory najpierw zlamal ISP a w efekcie tego poszlo jeszcze LSP

Gdyby jedna reguła implikowała drugą to po co mieć dwie reguły? Reguły są ortogonalne jak parzystość i dodatniość.

wiec sobie dopisal do dokumentacji ze niektore implementacje moga rzucic wyjatkiem

List.add od początku było opcjonalne i miało wyspecyfikowane @throws UnsupportedOperationException if the <tt>add</tt> method is not supported by this list. Widać to np na: https://github.com/fanhongtao/JDK/blob/jdk_1.2.1/src/java/util/List.java#L147-L169 Później doszło nawet Throws: NullPointerException - if the specified element is null and this list does not permit null elements.

2

Ja nie napisalem o logicznej implikacji tylko o relacji przyczynowo-skutkowej.

Mogloby byc i od poczatku. Ktos sie zorientowal, ze nie kazda lista bedzie to obslugiwac ale zamiast wywalic metode na etapie definicji to dorzucil dokumentacje.

0

Mogloby byc i od poczatku. Ktos sie zorientowal, ze nie kazda lista bedzie to obslugiwac ale zamiast wywalic metode na etapie definicji to dorzucil dokumentacje.

Obstawiam, że to nie było łatanie po fakcie, tylko zamierzony efekt. Prawdopodobnie jeden człowiek projektował zarówno metodę List.add jak i implementacje, w których metoda add "nie działa":

Taki C# skopiował ten design nawet do generycznych kolekcji mimo, iż MS zdecydował się na zerwanie kompatybilności wstecznej podczas wprowadzania genericsów:
https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.icollection-1.add?view=netcore-3.1

Exceptions
NotSupportedException
The ICollection<T> is read-only.

https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.idictionary-2.add?view=netcore-3.1

Exceptions
NotSupportedException
The IDictionary<TKey,TValue> is read-only.

https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ilist-1.insert?view=netcore-3.1

Exceptions
NotSupportedException
The IList<T> is read-only.

Ale już np dla ISet<T>.add https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iset-1.add?view=netcore-3.1 nie rzuca wyjątkiem.

0

Obstawiam, że to nie było łatanie po fakcie, tylko zamierzony efekt. Prawdopodobnie jeden człowiek projektował zarówno metodę List.add jak i implementacje, w których metoda add "nie działa":

A w jaki sposob jest to sprzeczne? Bo mnie to nawet nie dziwi, ze ktos sam sobie takiego hot-fixa wymyslil :)

Ktos sie zorientowal, ze nie kazda lista bedzie to obslugiwac ale zamiast wywalic metode na etapie definicji to dorzucil dokumentacje.


Kopiowanie glupich rozwiazan przez MS juz mnie bardziej dziwi

0
Wibowit napisał(a):

Zasada LSP jest relacją rodzic-dziecko, a nie grupa-element:

Subtype Requirement: Let {\displaystyle \phi (x)}\phi (x) be a property provable about objects {\displaystyle x}x of type T. Then {\displaystyle \phi (y)}{\displaystyle \phi (y)} should be true for objects {\displaystyle y}y of type S where S is a subtype of T.

W skrócie: jeśli coś da się udowodnić dla nadtypu to powinno zachodzić dla podtypu. To, że 99.999% implementacji zachowuje się w dany sposób nie ma żadnego wpływu na zachodzenie LSP. Analizując typ T i jego podtyp S wszystkie inne podtypy T nie mają znaczenia.

W tym konkretnym przypadku zasada LSP jest złamana. Na początek - rozważmy List.add(index, element) dla standardowej biblioteki Javy.

W Javie istnieje klasa AbstractList której ta metoda add wygląda tak:

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

Natomiast ArrayList (lub choćby) która dziedziczy po AbstractList już tego wyjątku nie rzuca. Więc podtyp ArrayList zachowa się inaczej niż AbstractList. Sam interfejs List nijak się oczywiście ma do LSP ponieważ metoda add(index, element) nie jest nawet zdefiniowana dla interfejsu List. Przy czym przyznaję, że od razu przeskoczyłem do rozmowy o konkretnych implementacjach w Javie, trzeba było to napisać jaśniej.

1

https://docs.oracle.com/javase/8/docs/api/java/util/AbstractList.html#add-E-

Note that this implementation throws an UnsupportedOperationException unless add(int, E) is overridden.

Co możesz udowodnić dla AbstractList.add co nie zachodzi dla podtypów AbstractList?

W ogólności budowanie własnej wizji kontraktu przez analizowanie szczegółów implementacyjnych czegoś co ma jasny kontrakt jest programistycznym rakiem. Dla przykładu programiści Javy polegają na konkretnym porządku iteracji w HashMapach, co jest absurdem. Jeśli przez 10 lat HashMapa iterowała obiekty w takiej a takiej kolejności to jeśli w nowej wersji Javy kolejność iteracji się zmieni to można traktować to jako błąd? Niestety mimo iż oficjalny kontrakt niczego nie gwarantuje to jednak oczekiwania Javowców, którzy nie potrafią kodować do interfejsu sprawia, że de facto Java jest związana na stałe z czymś co hamuje optymalizacje wydajnościowe. Szczegóły od momentu 42:54 do ok 47

Z hashCode zepsuli jeszcze bardziej bo szczegóły implementacyjne wstawili w dokumentację (np dla kolekcji czy stringów) i już nawet z tego powodu nie da się poprawić hashCode'ów. Stąd pomysł nowej metody Object.longHashCode, której implementacje w standardowych typach już nie będą miały w kontrakcie szczegółów implementacyjnych, a wręcz współczynniki hashowania będą się zmieniać co uruchomienie JVMki: https://openjdk.java.net/jeps/8201462

0
Wibowit napisał(a):

https://docs.oracle.com/javase/8/docs/api/java/util/AbstractList.html#add-E-

Note that this implementation throws an UnsupportedOperationException unless add(int, E) is overridden.

Co możesz udowodnić dla AbstractList.add co nie zachodzi dla podtypów AbstractList?

Tak jak napisałeś - to jest relacja rodzic-dziecko. Czyli konkretne dziecko (ArrayList) ma konkretny konflikt z rodzicem (AbstractList) i w tym momencie LSP jest złamane. W tym konkretnym przypadku:

  • dla dowolnej pary {index, element} AbstractList.add(index, element) zawsze zwraca UnsupportedOperationException
  • dla dowolnej pary {index, element} ArrayList.add(index, element) nigdy nie zwraca UnsupportedOperationException

W ogólności budowanie własnej wizji kontraktu przez analizowanie szczegółów implementacyjnych czegoś co ma jasny kontrakt jest programistycznym rakiem.

Pełna zgoda. Ale kontrakt, a LSP to nie są pojęcia tożsame.

1

Pełna zgoda. Ale kontrakt, a LSP to nie są pojęcia tożsame.

Jeśli LSP mówi o tym, by kodować do implementacji, a nie interfejsów to jest to reguła do wyrzucenia.

2

Nie łamie. LSP mówi o kontrakcie, nie o implementacji, a "sensowność" kontraktu nie ma znaczenia. To nie znaczy, że List.add jest dobrze zaprojektowaną metodą, ale nie jest to argument, że łapie to LSP.

Wibowit napisał(a):

Co możesz udowodnić dla AbstractList.add co nie zachodzi dla podtypów AbstractList?

Bardzo dobre, ale i odrobinkę podchwytliwe pytanie, co można udowodnić dla X i co nie zachodzi dla Y. Ogólnie zasada LSP w literalnym znaczeniu nie ma za dużo sensu. W swojej podstawowej formie mówi ona, że jeżeli da się udowodnić jakieś p dla typu bazowego, to p musi być spełnione dla typu pochodnego, ale zawsze mogę przyjąć, że moim p dla typu bazowego (o ile ma on implementację) jest dokładnie ta implementacja, co wyklucza jakiekolwiek modyfikowanie zachowania w podtypie.

Oczywistym jest, że nie o takie rozumienie LSP chodzi, więc trzeba uważać, jakie p rozważamy, a ten zbiór rozważanych p, to właśnie dobrze określony kontrakt.

0
Wibowit napisał(a):

Pełna zgoda. Ale kontrakt, a LSP to nie są pojęcia tożsame.

Jeśli LSP mówi o tym, by kodować do implementacji, a nie interfejsów to jest to reguła do wyrzucenia.

LSP nie wypowiada się na ten temat kontraktów i nie ma nic wspólnego z kontraktami. Jeśli metoda jest abstrakcyjna to LSP się jej nie tyczy bo takiej metody po prostu nie ma.

Tak jak napisałeś:

W skrócie: jeśli coś da się udowodnić dla nadtypu to powinno zachodzić dla podtypu

W tym konkretnym przypadku w Javie wyszła słabość języka, które łatano klasami AbstractXXX i błędnie to zrealizowano . Natomiast sama zasada LSP broni przed takimi potworkami (zdarzają się częściej niż mogłoby się wydawać):

    class Product {
        private Integer id;
        private String name;

        Integer getId() {
            return id;
        }

        String getName() {
            return name;
        }
    }

    interface ProductSorter {

        /**
         * Returns list of sorted products
         * @param products
         * @return
         */
        Collection<Product> sort(Collection<Product> products);
    }

    class ByNameProductSorter implements ProductSorter {

        /**
         * Returns collection of products sorted by name
         * @param products
         * @return
         */
        @Override
        public Collection<Product> sort(Collection<Product> products) {
            return products.stream()
                    .sorted(Comparator.comparing(Product::getName))
                    .collect(Collectors.toList());
        }
    }

    class ByIdProductSorter extends ByNameProductSorter {

        /**
         * Returns collection of products sorted by id
         * @param products
         * @return
         */

        @Override
        public Collection<Product> sort(Collection<Product> products) {
            return products.stream()
                    .sorted(Comparator.comparing(Product::getId))
                    .collect(Collectors.toList());
        }
    }

Zgadzam się z tym, że przy poprawnie definiowanych kontraktach LSP pozostanie zachowane. Natomiast problemy zaczynają się, gdy chcemy zdefiniować bardzo ogólny kontrakt - wtedy jest bardzo trudno przewidzieć wszystkie możliwe sytuacje.

1
Wibowit napisał(a):

Z hashCode zepsuli jeszcze bardziej bo szczegóły implementacyjne wstawili w dokumentację (np dla kolekcji czy stringów) i już nawet z tego powodu nie da się poprawić hashCode'ów. Stąd pomysł nowej metody Object.longHashCode, której implementacje w standardowych typach już nie będą miały w kontrakcie szczegółów implementacyjnych, a wręcz współczynniki hashowania będą się zmieniać co uruchomienie JVMki: https://openjdk.java.net/jeps/8201462

Sorry za off-topic, ale nie mogę jaki paździerz. Nie wiedziałem o tym.
Otóż największym IMO problemem hashCode jest to, że należy on do Object.
To powinnien być osobny interfejs - podobnie jak Comparator.
Nie ma tak, że dany typ ma jeden idealny hashCode - bo to zależy jakie obiekty wkładamy do np. konkretnego HashMap.
Raz będzie ich mało i będzie zależeć nam na szybkiej implementacji, raz będzie mnóstwo i będziemy chcieli mniej kolizji..
czasem wystarczy w stringu pierwsza litera jako hashCode, czasem pierwsze 3 - a czasem trzeba brać implementację po wszystkim (jak obecnie).
Dla tego sameto typu możemy chcieć miec w jednym programie kilka różnych hashCode implementacji... podobnie jak z Comparator.
To oczywiscie mniej częsty case niż z Comparatorem, bo raczej będzie dotyczył programów, gdzie walczymy o cykle - ale jednak.

Przy okazji osobną wadą obecnego podejścia- implicit hashCode, jest fakt, że wiele osób nie ma pojęcia o istnieniu problemu i mamy potem dziwne bugi na produkcji.
Albo mamy durne kłótnie o implementacje hashCode na etapie tworzenia klasy. Jeśli masz klasę i wszystkie pola .. to nadal nie wiadomo jaki hahsCode jest optymalny, bo to nie od klasy zależy, ale od tego gdzie hashCode będzie używany.

W tym kontekście nowy hashCode, który powtarza największy IMO błąd poprzedniego designu to niezła $#23@#.

1

Jeśli metoda jest abstrakcyjna to LSP się jej nie tyczy bo takiej metody po prostu nie ma.

List.add jest abstrakcyjna. Czy w takim razie LSP jej nie dotyczy? Hmm, to po co cały ten wątek?

0
Wibowit napisał(a):

Jeśli metoda jest abstrakcyjna to LSP się jej nie tyczy bo takiej metody po prostu nie ma.

List.add jest abstrakcyjna. Czy w takim razie LSP jej nie dotyczy? Hmm, to po co cały ten wątek?

Tak jak napisałem wcześniej:

  1. Bardzo niejawnie przeszedłem z rozmowy o interfejsie na temat rozmowy o konkretnych implementacjach
  2. Nie dotyczy. Metoda abstrakcyjna nie jest stricte własnością obiektu, jedynie znacznikiem, że każdy obiekt implementujący coś takiego powinien taką własność mieć.
  3. Wątek jest w celu wyjaśnienia wątpliwości.
0
wartek01 napisał(a):
  1. Nie dotyczy. Metoda abstrakcyjna nie jest stricte własnością obiektu, jedynie znacznikiem, że każdy obiekt implementujący coś takiego powinien taką własność mieć.

Oczywiście, że dotyczy. LSP mówi o kontrakcie, w tym przypadku o sygnaturze metody, konwencji wywołania, parametrach i całej reszcie części technicznej. To jest świetny przykład, bo tutaj kompilator broni programisty przed złamaniem LSP, ale w innych językach tak nie musi być — metoda może nie istnieć, metoda może mieć inną konwencję wywołania i rzuci segfault, metoda może zostać inaczej "zmanglowana" przez kompilator i w efekcie będzie miała inną nazwę symboliczną.

1 użytkowników online, w tym zalogowanych: 0, gości: 1