Cześć! W tej lekcji porozmawiamy o tym, czym klasy abstrakcyjne różnią się od interfejsów i rozważymy kilka przykładów z typowymi klasami abstrakcyjnymi. Różnica między klasami abstrakcyjnymi a interfejsami - 1Różnicom między klasą abstrakcyjną a interfejsem poświęciliśmy osobną lekcję, ponieważ ten temat jest bardzo ważny. W 90% przyszłych rozmów kwalifikacyjnych zostaniesz zapytany o różnicę między tymi pojęciami. Oznacza to, że powinieneś wiedzieć, co czytasz. A jeśli czegoś nie do końca rozumiesz, przeczytaj dodatkowe źródła. Wiemy więc, czym jest klasa abstrakcyjna i czym jest interfejs. Teraz omówimy ich różnice.
  1. Interfejs opisuje tylko zachowanie. Nie ma stanu. Ale klasa abstrakcyjna zawiera stan: opisuje oba.

    Weźmy na przykład Birdklasę abstrakcyjną i CanFlyinterfejs:

    
    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 MockingJayklasę ptaka i sprawmy, by dziedziczyła Bird:

    
    public class MockingJay extends Bird {
    
       @Override
       public void fly() {
           System.out.println("Fly, bird!");
       }
    
       public static void main(String[] args) {
    
           MockingJay someBird = new MockingJay();
           someBird.setAge(19);
           System.out.println(someBird.getAge());
       }
    }
    

    Jak widać, mamy łatwy dostęp do stanu klasy abstrakcyjnej — jej speciesi agezmiennych.

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

    
    public interface CanFly {
    
       String species = new String();
       int age = 10;
    
       public void fly();
    }
    
    public interface CanFly {
    
       private String species = new String(); // Error
       private int age = 10; // Another error
    
       public void fly();
    }
    

    Nie możemy nawet zadeklarować zmiennych prywatnych w interfejsie. Dlaczego? Ponieważ prywatny modyfikator został stworzony, aby ukryć implementację przed użytkownikiem. A interfejs nie ma w sobie żadnej implementacji: nie ma nic do ukrycia.

    Interfejs opisuje tylko zachowanie. W związku z tym nie możemy implementować getterów i setterów wewnątrz interfejsu. Taka jest natura interfejsów: są one potrzebne do pracy z zachowaniem, a nie ze stanem.

    Java 8 wprowadziła domyślne metody dla interfejsów, które mają implementację. Znacie je już, więc nie będziemy się powtarzać.

  2. Klasa abstrakcyjna łączy i jednoczy klasy, które są ze sobą bardzo blisko spokrewnione. Jednocześnie pojedynczy interfejs może być implementowany przez klasy, które nie mają ze sobą absolutnie nic wspólnego.

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

    Nasza Birdklasa abstrakcyjna jest potrzebna do tworzenia ptaków opartych na tej klasie. Tylko ptaki i nic więcej! Oczywiście będą różne gatunki ptaków.

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

    Dzięki CanFlyinterfejsowi każdy radzi sobie na swój sposób. Opisuje tylko zachowanie (latanie) związane z jego nazwą. Wiele niezwiązanych ze sobą rzeczy „może latać”.

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

    Te 4 podmioty nie są ze sobą powiązane. Oni nawet nie wszyscy żyją. Jednak oni wszyscy CanFly.

    Nie mogliśmy ich opisać za pomocą klasy abstrakcyjnej. Nie mają tego samego stanu ani identycznych pól. Do zdefiniowania samolotu potrzebowalibyśmy zapewne pól na model, rok produkcji i maksymalną liczbę pasażerów. Dla Carlsona potrzebowalibyśmy pola na wszystkie słodycze, które dziś zjadł, oraz listę gier, w które będzie grał ze swoim młodszym bratem. Jak na komara… eee… nawet nie wiem… Może „poziom irytacji”? :)

    Chodzi o to, że nie możemy ich opisać za pomocą klasy abstrakcyjnej. Są zbyt różne. Ale mają wspólne zachowanie: potrafią latać. Interfejs jest idealny do opisywania wszystkiego na świecie, co może latać, pływać, skakać lub wykazywać inne zachowania.

  3. Klasy mogą implementować dowolną liczbę interfejsów, ale mogą dziedziczyć tylko jedną klasę.

    Wspominaliśmy już o tym nie raz. Java nie ma wielokrotnego dziedziczenia klas, ale obsługuje wielokrotne dziedziczenie interfejsów. Ten punkt wynika częściowo z poprzedniego: interfejs łączy wiele różnych klas, które często nie mają ze sobą nic wspólnego, podczas gdy klasa abstrakcyjna jest tworzona dla grupy bardzo blisko spokrewnionych klas. Dlatego sensowne jest, aby dziedziczyć tylko jedną taką klasę. Klasa abstrakcyjna opisuje relację „jest-a”.

Standardowe interfejsy: InputStream i OutputStream

Omówiliśmy już różne klasy odpowiedzialne za strumienie wejściowe i wyjściowe. Rozważmy InputStreami OutputStream. Ogólnie rzecz biorąc, nie są to wcale interfejsy, ale raczej całkowicie autentyczne klasy abstrakcyjne. Teraz już wiesz, co to oznacza, więc praca z nimi będzie znacznie łatwiejsza :) InputStreamto klasa abstrakcyjna odpowiedzialna za wprowadzanie bajtów. Java ma kilka klas, które dziedziczą InputStream. Każdy z nich jest przeznaczony do odbioru danych z różnych źródeł. Ponieważ InputStreamjest elementem nadrzędnym, udostępnia kilka metod ułatwiających pracę ze strumieniami danych. Każdy potomek InputStreamma następujące metody:
  • int available()zwraca liczbę bajtów dostępnych do odczytu;
  • close()zamyka strumień wejściowy;
  • int read()zwraca całkowitą reprezentację następnego dostępnego bajtu w strumieniu. Jeśli osiągnięto koniec strumienia, zwrócone zostanie -1;
  • int read(byte[] buffer)próbuje wczytać bajty do bufora i zwraca liczbę odczytanych bajtów. Gdy dojdzie do końca pliku, zwraca -1;
  • int read(byte[] buffer, int byteOffset, int byteCount)zapisuje część bloku bajtów. Jest używany, gdy tablica bajtów mogła nie zostać całkowicie wypełniona. Gdy dojdzie do końca pliku, zwraca -1;
  • long skip(long byteCount)pomija bajty byteCount w strumieniu wejściowym i zwraca liczbę zignorowanych bajtów.
Zalecam zapoznanie się z pełną listą metod . W rzeczywistości istnieje więcej niż dziesięć klas dla dzieci. Na przykład oto kilka:
  1. FileInputStream: najczęstszy typ InputStream. Służy do odczytywania informacji z pliku;
  2. StringBufferInputStream: Kolejny pomocny rodzaj plików InputStream. Konwertuje ciąg znaków na InputStream;
  3. BufferedInputStream: Buforowany strumień wejściowy. Stosowany jest najczęściej w celu zwiększenia wydajności.
Pamiętasz, jak poszliśmy BufferedReaderi powiedzieliśmy, że nie musisz tego używać? Kiedy piszemy:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
…nie musisz używać BufferedReader: An InputStreamReadermoże wykonać zadanie. Ale BufferedReaderpoprawia wydajność i może również odczytywać całe wiersze danych zamiast pojedynczych znaków. To samo dotyczy BufferedInputStream! Klasa gromadzi dane wejściowe w specjalnym buforze bez ciągłego dostępu do urządzenia wejściowego. Rozważmy 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("Character read: " + c);
           }
       } catch(Exception e) {

           e.printStackTrace();

       } finally {

           inputStream.close();
           buffer.close();
       }
   }
}
W tym przykładzie odczytujemy dane z pliku znajdującego się na komputerze pod adresem „ D:/Users/UserName/someFile.txt ”. Tworzymy 2 obiekty — a FileInputStreami a, BufferedInputStreamktóre go „zawijają”. Następnie odczytujemy bajty z pliku i konwertujemy je na znaki. I robimy to, aż plik się skończy. Jak widać, nie ma tu nic skomplikowanego. Możesz skopiować ten kod i uruchomić go na prawdziwym pliku na swoim komputerze :) Klasa OutputStreamjest klasą abstrakcyjną, która reprezentuje wyjściowy strumień bajtów. Jak już wiesz, jest to przeciwieństwo pliku InputStream. Nie jest odpowiedzialny za skądś odczytywanie danych, ale raczej za wysyłanie danych dokądś . Podobnie jak InputStream, ta abstrakcyjna klasa daje wszystkim swoim potomkom zestaw wygodnych metod:
  • void 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 licznika bajtów z tablicy, zaczynając od pozycji przesunięcia.
Oto niektórzy potomkowie tej OutputStreamklasy:
  1. DataOutputStream. Strumień wyjściowy, który zawiera metody pisania standardowych typów danych Java.

    Bardzo prosta klasa do pisania prymitywnych typów danych i ciągów znaków w Javie. Prawdopodobnie zrozumiesz następujący kod nawet bez wyjaśnienia:

    
    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(), writeLong(), writeShort(), i tak dalej.


  2. FileOutputStream. Ta klasa implementuje mechanizm wysyłania danych do pliku na dysku. Nawiasem mówiąc, użyliśmy go już w ostatnim przykładzie. Czy zauważyłeś? Przekazaliśmy go do DataOutputStream, który działał jako „opakowanie”.

  3. BufferedOutputStream. Buforowany strumień wyjściowy. Tu też nie ma nic skomplikowanego. Jego cel jest analogiczny do BufferedInputStream(lub BufferedReader). Zamiast zwykłego sekwencyjnego odczytu danych, zapisuje dane przy użyciu specjalnego „kumulacyjnego” bufora. Bufor umożliwia zmniejszenie liczby dostępów do ujścia danych, zwiększając w ten sposób 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!"; // We'll convert this string to a byte array and write it to a file
    
               byte[] buffer = text.getBytes();
    
               bufferedStream.write(buffer, 0, buffer.length);
         }
    }
    

    Ponownie możesz sam pobawić się tym kodem i sprawdzić, czy będzie działał na prawdziwych plikach na twoim komputerze.

FileInputStreamO , FileOutputStreami , będziemy mieli osobną lekcję BuffreredInputStream, więc jest to wystarczająca informacja dla pierwszego znajomego. Otóż ​​to! Mamy nadzieję, że rozumiesz różnice między interfejsami a klasami abstrakcyjnymi i jesteś gotowy odpowiedzieć na każde pytanie, nawet podchwytliwe :)