-
Interfejs opisuje jedynie zachowanie. Nie ma fortuny. Ale klasa abstrakcyjna ma stan: opisuje oba.
Weźmy jako przykład klasę abstrakcyjną
Bird
i interfejsFlyable
: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 poBird
: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) iage
(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ć.
-
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.
Bird
Aby na jej podstawie stworzyć ptaki, potrzebna jest nasza klasa abstrakcyjna . Tylko ptaki i nikt więcej! Oczywiście, że będą inne.Z interfejsem
Flyable
wszystko 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.Te 4 podmioty nie są ze sobą w żaden sposób powiązane. Cóż mogę powiedzieć, nie wszystkie są nawet animowane. Jednak wszystkie
Flyable
potrafią 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.
-
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 naInputStream
i 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ż InputStream
jest 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)
pomijabyteCount
, bajt wejściowy, zwracający liczbę ignorowanych bajtów.
FileInputStream
: najpopularniejszy typInputStream
. Służy do odczytywania informacji z pliku;StringBufferInputStream
: kolejny przydatny typInputStream
. Zamienia ciąg znaków w strumień danych wejściowychInputStream
;BufferedInputStream
: buforowany strumień wejściowy. Najczęściej stosuje się go w celu poprawy wydajności.
BufferedReader
i powiedzieliśmy, że nie musimy z tego korzystać? Kiedy piszemy:
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
... BufferedReader
nie ma potrzeby go używać: InputStreamReader
spełni swoje zadanie. Robi to jednak BufferedReader
wydajniej i co więcej, potrafi czytać dane całymi liniami, a nie pojedynczymi znakami. Wszystko BufferedInputStream
jest 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 - FileInputStream
i BufferedInputStream
jako 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 OutputStream
jest 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 InputStream
ta 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.
OutputStream
:
-
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()
, iwriteLong()
takwriteShort()
dalej. -
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”. -
BufferedOutputStream
. Buforowany strumień wyjściowy. Nic też skomplikowanego, istota jest taka sama jak wBufferedInputStream
(lubBufferedReader
„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.
InputStream
wejś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 :) OutputStream
FileInputStream
FileOutputStream
BufferedInputStream
GO TO FULL VERSION