JavaRush /Blog Java /Random-PL /Różnica między klasami abstrakcyjnymi a interfejsami

Różnica między klasami abstrakcyjnymi a interfejsami

Opublikowano w grupie Random-PL
Cześć! W tym wykładzie porozmawiamy o tym, czym klasy abstrakcyjne różnią się od interfejsów i przyjrzymy się przykładom z powszechnymi klasami abstrakcyjnymi. Różnica między klasami abstrakcyjnymi a interfejsami - 1Oddzielny wykład poświęciliśmy różnicom pomiędzy klasą abstrakcyjną a interfejsem, gdyż temat jest bardzo ważny. W 90% przyszłych rozmów będziesz pytany o różnicę między tymi pojęciami. Dlatego pamiętaj, aby rozumieć to, co czytasz, a jeśli czegoś nie rozumiesz do końca, zapoznaj się z dodatkowymi źródłami. Wiemy więc, czym jest klasa abstrakcyjna i czym jest interfejs. Przyjrzyjmy się teraz ich różnicom.
  1. Interfejs opisuje jedynie zachowanie. Nie ma fortuny. Ale klasa abstrakcyjna ma stan: opisuje oba.

    Weźmy jako przykład klasę abstrakcyjną Birdi interfejs Flyable:

    public abstract class Bird {
       private String species;
       private int age;
    
       public abstract void fly();
    
       public String getSpecies() {
           return species;
       }
    
       public void setSpecies(String species) {
           this.species = species;
       }
    
       public int getAge() {
           return age;
       }
    
       public void setAge(int age) {
           this.age = age;
       }
    }

    Stwórzmy klasę ptaka Mockingjay(kosogłos) i dziedziczmy po Bird:

    public class Mockingjay extends Bird {
    
       @Override
       public void fly() {
           System.out.println(„Leć, ptaszku!”);
       }
    
       public static void main(String[] args) {
    
           Mockingjay someBird = new Mockingjay();
           someBird.setAge(19);
           System.out.println(someBird.getAge());
       }
    }

    Jak widać, możemy łatwo uzyskać dostęp do stanu klasy abstrakcyjnej - jej zmiennych species(typ) i age(wiek).

    Ale jeśli spróbujemy zrobić to samo z interfejsem, obraz będzie inny. Możemy spróbować dodać do niego zmienne:

    public interface Flyable {
    
       String species = new String();
       int age = 10;
    
       public void fly();
    }
    
    public interface Flyable {
    
       private String species = new String(); // błąd
       private int age = 10; // również błąd
    
       public void fly();
    }

    Nie będziemy nawet mogli tworzyć prywatnych zmiennych w interfejsie. Dlaczego? Ponieważ modyfikator private został stworzony, aby ukryć implementację przed użytkownikiem. Ale wewnątrz interfejsu nie ma żadnej implementacji: nie ma tam nic do ukrycia.

    Interfejs opisuje jedynie zachowanie. W związku z tym nie będziemy mogli zaimplementować modułów pobierających i ustawiających wewnątrz interfejsu. Taka jest natura interfejsu: ma on dotyczyć zachowania, a nie stanu.

    Java8 wprowadziła domyślne metody interfejsu, które mają implementację. Znacie je już, więc nie będziemy ich powtarzać.

  2. Klasa abstrakcyjna łączy i łączy klasy, które są ze sobą bardzo blisko powiązane. Jednocześnie ten sam interfejs mogą być implementowane przez klasy, które nie mają ze sobą nic wspólnego.

    Wróćmy do naszego przykładu z ptakami.

    BirdAby na jej podstawie stworzyć ptaki, potrzebna jest nasza klasa abstrakcyjna . Tylko ptaki i nikt więcej! Oczywiście, że będą inne.

    Różnica między klasami abstrakcyjnymi a interfejsami - 2

    Z interfejsem Flyablewszystko jest inne. Opisuje jedynie zachowanie odpowiadające jego nazwie - „latanie”. Definicja „latającego”, „zdolnego do latania” obejmuje wiele obiektów, które nie są ze sobą powiązane.

    Różnica między klasami abstrakcyjnymi a interfejsami - 3

    Te 4 podmioty nie są ze sobą w żaden sposób powiązane. Cóż mogę powiedzieć, nie wszystkie są nawet animowane. Jednak wszystkie Flyablepotrafią latać.

    Nie bylibyśmy w stanie opisać ich za pomocą klasy abstrakcyjnej. Nie mają wspólnego stanu ani identycznych pól. Do scharakteryzowania samolotu będziemy prawdopodobnie potrzebować pól „model”, „rok produkcji” i „maksymalna liczba pasażerów”. Dla Carlsona są pola na wszystkie słodycze, które dziś zjadł, oraz lista gier, w które będzie grał z Dzieciakiem. Dla komara… ech… nawet nie wiemy… Może „poziom irytacji”? :)

    Najważniejsze jest to, że nie możemy ich opisać za pomocą klasy abstrakcyjnej. Są zbyt różni. Ale istnieje powszechne zachowanie: potrafią latać. Interfejs jest idealny do opisywania wszystkiego na świecie, co potrafi latać, pływać, skakać lub zachowywać się w inny sposób.

  3. Klasy mogą implementować dowolną liczbę interfejsów, ale mogą dziedziczyć tylko z jednej klasy.

    Rozmawialiśmy już o tym nie raz. W Javie nie ma wielokrotnego dziedziczenia, ale istnieje wiele implementacji. Punkt ten częściowo wynika z poprzedniego: interfejs łączy wiele różnych klas, które często nie mają ze sobą nic wspólnego, a klasa abstrakcyjna jest tworzona dla grupy klas, które są bardzo blisko siebie. Dlatego logiczne jest, że można dziedziczyć tylko z jednej takiej klasy. Klasa abstrakcyjna opisuje relację „jest”.

Standardowe interfejsy wejściowe i wyjściowe

Przeszliśmy już przez różne klasy odpowiedzialne za strumieniowe przesyłanie danych wejściowych i wyjściowych. Spójrzmy na InputStreami OutputStream. Generalnie nie są to interfejsy, ale prawdziwe klasy abstrakcyjne. Teraz już wiesz, czym one są, więc praca z nimi będzie znacznie łatwiejsza :) InputStream- jest to klasa abstrakcyjna odpowiedzialna za wprowadzanie bajtów. Java ma szereg klas, które dziedziczą z InputStream. Każdy z nich jest skonfigurowany do odbierania danych z różnych źródeł. Ponieważ InputStreamjest rodzicem, udostępnia kilka metod wygodnej pracy ze strumieniami danych. Każde dziecko ma następujące metody InputStream:
  • int available()zwraca liczbę bajtów dostępnych do odczytu;
  • close()zamyka źródło wejściowe;
  • int read()zwraca całkowitą reprezentację następnego dostępnego bajtu w strumieniu. Jeśli osiągnięty zostanie koniec strumienia, zwrócona zostanie liczba -1;
  • int read(byte[] buffer)próbuje wczytać bajty do bufora, zwracając liczbę odczytanych bajtów. Kiedy dojdzie do końca pliku, zwraca -1;
  • int read(byte[] buffer, int byteOffset, int byteCount)odczytuje część bloku bajtów. Używane, gdy istnieje możliwość, że blok danych nie został całkowicie wypełniony. Gdy dotrze do końca pliku, zwraca -1;
  • long skip(long byteCount)pomija byteCount, bajt wejściowy, zwracający liczbę ignorowanych bajtów.
Radzę przestudiować pełną listę metod . W rzeczywistości istnieje kilkanaście klas następczych. Oto kilka przykładów:
  1. FileInputStream: najpopularniejszy typ InputStream. Służy do odczytywania informacji z pliku;
  2. StringBufferInputStream: kolejny przydatny typ InputStream. Zamienia ciąg znaków w strumień danych wejściowych InputStream;
  3. BufferedInputStream: buforowany strumień wejściowy. Najczęściej stosuje się go w celu poprawy wydajności.
Pamiętasz, jak przechodziliśmy obok BufferedReaderi powiedzieliśmy, że nie musimy z tego korzystać? Kiedy piszemy:
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
... BufferedReadernie ma potrzeby go używać: InputStreamReaderspełni swoje zadanie. Robi to jednak BufferedReaderwydajniej i co więcej, potrafi czytać dane całymi liniami, a nie pojedynczymi znakami. Wszystko BufferedInputStreamjest takie samo! Klasa gromadzi dane wejściowe w specjalnym buforze bez ciągłego dostępu do urządzenia wejściowego. Spójrzmy na przykład:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

public class BufferedInputExample {

   public static void main(String[] args) throws Exception {
       InputStream inputStream = null;
       BufferedInputStream buffer = null;

       try {

           inputStream = new FileInputStream("D:/Users/UserName/someFile.txt");

           buffer = new BufferedInputStream(inputStream);

           while(buffer.available()>0) {

               char c = (char)buffer.read();

               System.out.println(„Znak został odczytany” + c);
           }
       } catch(Exception e) {

           e.printStackTrace();

       } finally {

           inputStream.close();
           buffer.close();
       }
   }
}
W tym przykładzie odczytujemy dane z pliku, który znajduje się na komputerze pod adresem „D:/Users/UserName/someFile.txt” . Tworzymy 2 obiekty - FileInputStreami BufferedInputStreamjako jego „opakowanie”. Następnie odczytujemy bajty z pliku i konwertujemy je na znaki. I tak dalej, aż plik się zakończy. Jak widać, nie ma tu nic skomplikowanego. Możesz skopiować ten kod i uruchomić go w jakimś prawdziwym pliku przechowywanym na twoim komputerze :) Klasa OutputStreamjest klasą abstrakcyjną, która definiuje wyjściowy strumień bajtów. Jak już rozumiesz, jest to antypoda InputStream„a”. Odpowiada nie za to, skąd odczytać dane, ale za to, dokąd je wysłać . Podobnie jak InputStreamta klasa abstrakcyjna udostępnia wszystkim potomkom grupę metod umożliwiających wygodną pracę:
  • int close()zamyka strumień wyjściowy;
  • void flush()czyści wszystkie bufory wyjściowe;
  • abstract void write (int oneByte)zapisuje 1 bajt do strumienia wyjściowego;
  • void write (byte[] buffer)zapisuje tablicę bajtów do strumienia wyjściowego;
  • void write (byte[] buffer, int offset, int count)zapisuje zakres zliczonych bajtów z tablicy, zaczynając od przesunięcia pozycji.
Oto niektórzy potomkowie tej klasy OutputStream:
  1. DataOutputStream. Strumień wyjściowy zawierający metody pisania standardowych typów danych Java.

    Bardzo prosta klasa do pisania prymitywnych typów i ciągów Java. Z pewnością zrozumiesz napisany kod nawet bez wyjaśnień:

    import java.io.*;
    
    public class DataOutputStreamExample {
    
       public static void main(String[] args) throws IOException {
    
           DataOutputStream dos = new DataOutputStream(new FileOutputStream("testFile.txt"));
    
           dos.writeUTF("SomeString");
           dos.writeInt(22);
           dos.writeDouble(1.21323);
           dos.writeBoolean(true);
    
       }
    }

    Ma osobne metody dla każdego typu - writeDouble(), i writeLong()tak writeShort()dalej.

  2. Klasa FileOutputStream . Implementuje mechanizm przesyłania danych do pliku na dysku. Swoją drogą, użyliśmy go już w poprzednim przykładzie, zauważyłeś? Przekazaliśmy go do strumienia DataOutputStream, który pełnił rolę „opakowania”.

  3. BufferedOutputStream. Buforowany strumień wyjściowy. Nic też skomplikowanego, istota jest taka sama jak w BufferedInputStream(lub BufferedReader„a”). Zamiast zwykłego sekwencyjnego zapisu danych, stosuje się zapis poprzez specjalny bufor „przechowujący”. Używając bufora, można zmniejszyć liczbę podróży w obie strony do miejsca docelowego danych, a tym samym poprawić wydajność.

    import java.io.*;
    
    public class DataOutputStreamExample {
    
       public static void main(String[] args) throws IOException {
    
           FileOutputStream outputStream = new FileOutputStream("D:/Users/Username/someFile.txt");
           BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
    
           String text = "I love Java!"; // przekonwertujemy ten łańcuch na tablicę bajtów i zapiszemy go w pliku
    
           byte[] buffer = text.getBytes();
    
           bufferedStream.write(buffer, 0, buffer.length);
           bufferedStream.close();
       }
    }

    Ponownie możesz sam „pobawić się” tym kodem i sprawdzić, jak będzie on działał na rzeczywistych plikach na Twoim komputerze.

O spadkobiercach można przeczytać także w materiale „System InputStreamwejścia /wyjścia ”. Aha , i będziemy mieli też osobny wykład, więc informacji na ich temat wystarczy do pierwszej znajomości. To wszystko! Mamy nadzieję, że dobrze rozumiesz różnice między interfejsami a klasami abstrakcyjnymi i jesteś gotowy odpowiedzieć na każde, nawet trudne pytanie :) OutputStreamFileInputStreamFileOutputStreamBufferedInputStream
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION