luchob/softuni-sep-2023

Method overriding

Closed this issue · 3 comments

thrako commented

Здравей,

На много странен проблем се натъкнах. Ето линк към демо в GitHub. Имам следната йерархия.
interface Person -> abstract class BasePerson -> abstract class Company -> class BulgarianLLC

//imports skipped for brevity
@Entity
public abstract class Company extends BasePerson {

    @OneToMany(targetEntity = BasePerson.class)
    @Fetch(FetchMode.SUBSELECT)
    protected Set<BasePerson> representatives = new HashSet<>();

    //other fields skipped for brevity
    
    public abstract <T extends Company> T addRepresentative(BasePerson representative);

    public abstract <T extends Company> T addAllRepresentatives(List<BasePerson> representatives);
    
    //other methods skipped for brevity

В BulgarianLLC се опитвам да имплементирам addAllRepresentatives по следния начин:

//imports skipped for brevity
@Entity
public class BulgarianLLC extends Company {

    //compiles without any problems
    @Override
    public BulgarianLLC addRepresentative(BasePerson representative) {

        this.representatives.add(representative);
        return this;
    }
    
    //error: Method does not override method from its superclass
    @Override    

    //error: 'addAllRepresentatives(List<BasePerson>)' in 'dev.thrako.autodocs.model.person.company.BulgarianLLC' 
    //clashes with 'addAllRepresentatives(List<BasePerson>)' in 'dev.thrako.autodocs.model.person.company.Company'; 
    //both methods have same erasure, yet neither overrides the other
    public BulgarianLLC addAllRepresentatives(List<BasePerson> representatives) {

        this.representatives.addAll(representatives);
        return this;
    }

Пробвах с List<Person>, List<BasePerson>, List<Object>, Collection<Person>, Collection<BasePerson>, Collection<Object> както в абстрактния, така и в конкретния клас. Единственият начин по който се компилира е ако в конкретния клас (BulgarianLLC) използвам List или Collection без да специфицирам от какви обекти, тогава получавам само warning: Raw use of parameterized class 'List'.

В други случаи използвам същата конструкция, както съм показал с метода за добавяне на един представител и няма никакви проблеми. Единствено в този, в който параметърът е Collection/List. Предполагам, че е нещо свързано с generics в Java, четох отговори на подобни въпроси в StackOverflow, но така и не разбрах защо методът в конретния клас не override-ва абстрактния метод като са с една и съща сигнатура, единствено return type-ът им е различен, но BulgarianLLC extend-ва BasePerson, а и ако това беше проблемът, то същият щеше да е наличен и в другите подобни методи.

Ще се радвам, ако ти имаш идея какво се случва, да ме осветлиш.

П.П. Вече към края се сетих, че можеше да направя демо, което да кача в GitHub. Ще го направя допълнително.
EDIT: Добавил съм линк към демо в началото на текста.
EDIT 2: Проблемът изглежда се решава, ако в абстрактния метод сложа Company като return type, а в конкретния си използвам BulgarianLLC. Все пак не разбирам защо работи така.

Поздрави,
Траян

thrako commented

Добре, отиваме в твоя код. Там имаме:

public abstract T addAllRepresentatives(List representatives);

и в подкласа:

public BulgarianLLC addAllRepresentatives(List representatives)

Сега големия въпрос? Овъррайдват ли се тези два метода? Отговорът е тук: https://docs.oracle.com/javase/tutorial/java/IandI/override.html

An instance method in a subclass with the same signature (name, plus the number and the type of its parameters) and return type as an instance method in the superclass overrides the superclass's method.
...
An overriding method can also return a subtype of the type returned by the overridden method. This subtype is called a covariant return type.

Според последните две изречения от тази дефиниция, двата метода не се овъррайдват, въпреки че erasure-a им е еднакъв. Ако ретърн типа беше Company, както във единия от вариантите ти (public abstract Company), то биха се овъррайднали. Но не и public abstract , защото BulgarianLLC не е събтайп на .

Добре, донякъде картинката се изясни, но не съм съвсем.

Продължавам да не разбирам, как така причината е че BulgarianLLC не е събтайп на <T extends Company>, защото другият метод, с единично добавяне, работи:

public abstract class Company extends BasePerson {
    ...
    public abstract <T extends Company> T addRepresentative(BasePerson representative);
    ...
}

се override-ва от:

public class BulgarianLLC extends Company{
   ... 
   @Override
   public BulgarianLLC addRepresentative(BasePerson representative) {...}
   ...
}

Както е видно и тук BulgarianLLC не е събтайп на <T extends Company>.
Да, дава предупреждение: Unchecked overriding: return type requires unchecked conversion. Found 'BulgarianLLC', required 'T', но дори и самото предупреждение показва, че тук се случва overriding.

Започвам да си мисля, че според Java List<BasePerson> като параметър в абстрактния метод не е covariance на List<BasePerson> като параметър в конкретния метод, което вече би било мнооого странно.

EDIT: Разбира се, че това не е вярно, защото като сменим return type на абстрактния метод да връща Company, кодът се компилира. Също така, ако в конкретните класове се използва непараметезиран List, също се компилира. Явно Java може да се оправи с използването на generics в return type ИЛИ в parameters, но не и с двете.

Поздрави,
Траян

thrako commented

Въпросът ми е само от любопитство, не е пречка за работата ми по проекта, така че, освен ако на теб също не ти е любопитно, не се чувствай ангажиран да ми отговаряш. Така или иначе, изпозвам Company като return type, което вероятно и изначално беше по-доброто решение. Сега ме терзаят други проблеми с дизайна, но ще се помъча още малко преди да се изкуша да попитам :-)

luchob commented

Абе ще взема да си изтрия отговора отгоре, тъй като има някои грешни моменти :-)

Най-накратко положението е следното. В класа имаш:

public abstract <T extends Company> T addRepresentative(BasePerson representative);

Наследеното е:

@Override BulgarianLLC addRepresentative(BasePerson representative);

Компилаторът трябва да гарантира, че твоя код работи винаги. Обаче, в случая това не е възможно, защото например:

BulgarianJSC jsc = company.addRepresentative(...)

може и да не работи, няма гаранция, не е type safe. Оттам идва и WARNING-а. Но, това би могло да се компилира.
Компилаторът прави това в т.нар "raw mode", в който влизаш нормално например ако махнеш <> от List.
Само че raw mode-a работи в режим всичко или нищо. Т.е. за него

void (List xs, List y) е като void (List xs, List y)

И така, Company метода става raw. Вместо:

public abstract <T extends Company> T addRepresentative(BasePerson representative);

става за него:

public abstract Company addRepresentative(BasePerson representative);

Което е "компилируемо" и затова може да се справи.

Аналогично нещо става и с:

public abstract <T extends Company> T addAllRepresentatives(List<BasePerson> representatives);

Минава в raw mode и става (всичко raw или нищо raw):

public abstract Company addAllRepresentatives(List representatives);

Забележи, моля - List, а не List<BasePerson>. Но това не е същото като:

@Override public BulgarianJSC addAllRepresentatives(List<BasePerson> representatives);

Защого List и List не съвпадат, не се наследяват и това не може да бъде override.

Затона и това не се компилира. Но пък това ще се компилира заради raw mode:

public abstract <T extends Company> T addAllRepresentatives(List<BasePerson> representatives);

@Override public BulgarianJSC addAllRepresentatives(List representatives) //<-- no generics

И това (защото базовия метод не се обръща в raw):

public abstract Company addAllRepresentatives(List<BasePerson> representatives);

@Override public BulgarianJSC addAllRepresentatives(List<BasePerson> representatives);

Въпросът ти беше супер, благодаря!