-
Interfejs opisuje tylko zachowanie. Nie ma stanu. Ale klasa abstrakcyjna zawiera stan: opisuje oba.
Weźmy na przykład
Bird
klasę abstrakcyjną iCanFly
interfejs: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
MockingJay
klasę ptaka i sprawmy, by dziedziczyłaBird
: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
species
iage
zmiennych.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ć.
-
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
Bird
klasa 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.Dzięki
CanFly
interfejsowi 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ć”.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.
-
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żmyInputStream
i 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 :) InputStream
to 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ż InputStream
jest elementem nadrzędnym, udostępnia kilka metod ułatwiających pracę ze strumieniami danych. Każdy potomek InputStream
ma 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.
FileInputStream
: najczęstszy typInputStream
. Służy do odczytywania informacji z pliku;StringBufferInputStream
: Kolejny pomocny rodzaj plikówInputStream
. Konwertuje ciąg znaków naInputStream
;BufferedInputStream
: Buforowany strumień wejściowy. Stosowany jest najczęściej w celu zwiększenia wydajności.
BufferedReader
i powiedzieliśmy, że nie musisz tego używać? Kiedy piszemy:
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
…nie musisz używać BufferedReader
: An InputStreamReader
może wykonać zadanie. Ale BufferedReader
poprawia 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 FileInputStream
i a, BufferedInputStream
któ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 OutputStream
jest 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.
OutputStream
klasy:
-
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. 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”.BufferedOutputStream
. Buforowany strumień wyjściowy. Tu też nie ma nic skomplikowanego. Jego cel jest analogiczny doBufferedInputStream
(lubBufferedReader
). 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.
FileInputStream
O , FileOutputStream
i , 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 :)
GO TO FULL VERSION