Sprawa jest taka, że przy zmiennych lokalnych (w jakiejś procedurze czy konstruktorze), ale nie parametrach czy zwracanemu typowi, to nie ma to wielkiego znaczenia. Z zewnątrz nie ma żadnej różnicy, o ile twój kod robi to co ma robić. Dobrze jest jednak zawsze używać najbardziej ogólnej klasy bazowej, dzięki temu możemy bezboleśnie podmienić implementację. Oczywiście, jeżeli korzystamy z jakiejś fajnej własności klasy pochodnej to użycie pola typu klasy pochodnej jest uzasadnione.
Zaleta używania klas bazowych uwidacznia się np przy stosowaniu kolekcji. Powiedzmy, że stworzyliśmy sobie listę poleceniem:
List<Obiekt> lista = new ArrayList<Obiekt>();
Wszystko pięknie fajnie, lista jest kompaktowa, ale któregoś dnia otrzymaliśmy dodatkowe warunki na naszą funkcjonalność i np teraz często musimy wstawiać elementy w środek listy. Wiemy, że LinkedList lepiej nadaje się do insertów w środek listy niż ArrayList. Dzięki temu, że utworzyliśmy pole o typie wspólnej klasy bazowej tych różnych implementacji list to możemy bardzo szybko zmienić wykorzystywaną implementację. Wystarczy, że zmienimy tą linijkę na:
List<Obiekt> lista = new LinkedList<Obiekt>();
Oczywiście moglibyśmy nie stosować tutaj typu bazowego jako typu pola, ale np gdybyśmy przez przypadek stosowali metody klasy ArrayList, które nie należą do klasy LinkedList, to zamiana nie byłaby taka trywialna.
Jeżeli tworzymy jakieś API, które ma być używane przez ludzi z zewnątrz to zawsze należy zwracać najbardziej ogólny typ, który oferuje potrzebną funkcjonalność. Można sobie np tworzyć interfejsy i je implementować w prywatnych klasach (ale akurat to trochę rzadko się stosuje przynajmniej z tego co ja widziałem). Dzięki temu użytkownicy naszej klasy dostaną funkcjonalność, którą chcą, ale np nie będą mogli zrobić sobie podklasy naszej prywatnej klasy w prosty sposób (będą mogli jednak użyć wzorca dekorator na interfejsie - dekorator i tak jednak nie złamie potencjalnej hermetyzacji).
Zwracanie ogólnych klas jest nawet zalecane wewnątrz jakiegoś projektu - znacznie ułatwia to refaktoryzację. Biorąc na przykład powyższy przykład (sorry za gramatykę) to jeśli używalibyśmy ArrayList w wielu klasach rozrzuconych po całym projekcie to potem ciężko by było zmienić to na LinkedList. Oczywiście nie należy na siłę robić ogólnych typów czy interfejsów - jeżeli różnice w funkcjonalności są bardzo duże to nie ma sensu tworzyć ogólnego typu. Ustalenie kiedy uogólnianie jest opłacalne, jest raczej kwestią doświadczenia, musisz porobić trochę projektów, porefaktoryzować i sam się przekonasz.