CodeGym /Java blogg /Slumpmässig /Skillnaden mellan abstrakta klasser och gränssnitt
John Squirrels
Nivå
San Francisco

Skillnaden mellan abstrakta klasser och gränssnitt

Publicerad i gruppen
Hej! I den här lektionen kommer vi att prata om hur abstrakta klasser skiljer sig från gränssnitt och överväga några exempel med vanliga abstrakta klasser. Skillnaden mellan abstrakta klasser och gränssnitt - 1Vi har ägnat en separat lektion åt skillnaderna mellan en abstrakt klass och ett gränssnitt, eftersom det här ämnet är mycket viktigt. Du kommer att få frågan om skillnaden mellan dessa begrepp i 90 % av framtida intervjuer. Det betyder att du bör vara säker på att ta reda på vad du läser. Och om du inte helt förstår något, läs ytterligare källor. Så vi vet vad en abstrakt klass är och vad ett gränssnitt är. Nu ska vi gå igenom deras skillnader.
  1. Ett gränssnitt beskriver bara beteende. Den har ingen stat. Men en abstrakt klass inkluderar tillstånd: den beskriver båda.

    Ta till exempel den Birdabstrakta klassen och CanFlygränssnittet:

    
    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;
       }
    }
    

    Låt oss skapa en MockingJayfågelklass och få den att ärva 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());
       }
    }
    

    Som du kan se kan vi enkelt komma åt den abstrakta klassens tillstånd - dess speciesoch agevariabler.

    Men om vi försöker göra samma sak med ett gränssnitt är bilden en annan. Vi kan försöka lägga till variabler till den:

    
    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();
    }
    

    Vi kan inte ens deklarera privata variabler i ett gränssnitt. Varför? Eftersom den privata modifieraren skapades för att dölja implementeringen för användaren. Och ett gränssnitt har ingen implementering inuti det: det finns inget att dölja.

    Ett gränssnitt beskriver bara beteende. Följaktligen kan vi inte implementera getters och seters i ett gränssnitt. Detta är gränssnittens natur: de behövs för att arbeta med beteende, inte stat.

    Java 8 introducerade standardmetoder för gränssnitt som har en implementering. Du känner redan till dem, så vi kommer inte att upprepa oss.

  2. En abstrakt klass kopplar ihop och förenar klasser som är mycket nära besläktade. Samtidigt kan ett enda gränssnitt implementeras av klasser som absolut inte har något gemensamt.

    Låt oss återgå till vårt exempel med fåglar.

    Vår Birdabstrakta klass behövs för att skapa fåglar som är baserade på den klassen. Bara fåglar och inget annat! Självklart kommer det att finnas olika sorters fåglar.

    Skillnaden mellan abstrakta klasser och gränssnitt - 2

    Med CanFlygränssnittet kommer alla vidare på sitt eget sätt. Den beskriver bara beteendet (flygning) som är förknippat med dess namn. Många orelaterade saker "kan flyga".

    Skillnaden mellan abstrakta klasser och gränssnitt - 3

    Dessa 4 enheter är inte relaterade till varandra. De lever inte ens alla. Men de alla CanFly.

    Vi kunde inte beskriva dem med en abstrakt klass. De delar inte samma tillstånd eller identiska fält. För att definiera ett flygplan skulle vi förmodligen behöva fält för modell, tillverkningsår och maximalt antal passagerare. För Carlson skulle vi behöva åkrar för allt godis han åt idag, och en lista över de spel han kommer att spela med sin lillebror. För en mygga, ...eh... Jag vet inte ens... Kanske, en "irritationsnivå"? :)

    Poängen är att vi inte kan använda en abstrakt klass för att beskriva dem. De är för olika. Men de har delat beteende: de kan flyga. Ett gränssnitt är perfekt för att beskriva allt i världen som kan flyga, simma, hoppa eller uppvisa något annat beteende.

  3. Klasser kan implementera så många gränssnitt du vill, men de kan bara ärva en klass.

    Vi har redan nämnt detta mer än en gång. Java har inte flera arv av klasser, men det stöder flera arv av gränssnitt. Denna punkt följer delvis från den föregående: ett gränssnitt kopplar ihop många olika klasser som ofta inte har något annat gemensamt, medan en abstrakt klass skapas för en grupp mycket närbesläktade klasser. Därför är det vettigt att du bara kan ärva en sådan klass. En abstrakt klass beskriver ett "är-ett"-förhållande.

Standardgränssnitt: InputStream och OutputStream

Vi har redan gått igenom olika klasser som ansvarar för in- och utströmmar. Låt oss överväga InputStreamoch OutputStream. I allmänhet är dessa inte gränssnitt alls, utan snarare helt äkta abstrakta klasser. Nu vet du vad det betyder, så det blir mycket lättare att arbeta med dem :) InputStreamär en abstrakt klass som ansvarar för byte-inmatning. Java har flera klasser som ärver InputStream. Var och en av dem är utformad för att ta emot data från olika källor. Eftersom InputStreamär förälder, tillhandahåller det flera metoder som gör det enkelt att arbeta med dataströmmar. Varje ättling till InputStreamhar dessa metoder:
  • int available()returnerar antalet byte tillgängliga för läsning;
  • close()stänger ingångsströmmen;
  • int read()returnerar en heltalsrepresentation av nästa tillgängliga byte i strömmen. Om slutet av strömmen har nåtts kommer -1 att returneras;
  • int read(byte[] buffer)försöker läsa byte till buffert, och returnerar antalet lästa byte. När den når slutet av filen returnerar den -1;
  • int read(byte[] buffer, int byteOffset, int byteCount)skriver en del av ett block av byte. Den används när byte-arrayen kanske inte har fyllts helt. När den når slutet av filen returnerar den -1;
  • long skip(long byteCount)hoppar över byteCount byte i inmatningsströmmen och returnerar antalet ignorerade byte.
Jag rekommenderar att du studerar hela listan över metoder . Det finns faktiskt mer än tio barnklasser. Till exempel, här är några:
  1. FileInputStream: den vanligaste typen av InputStream. Den används för att läsa information från en fil;
  2. StringBufferInputStream: En annan användbar typ av InputStream. Den konverterar en sträng till en InputStream;
  3. BufferedInputStream: En buffrad ingångsström. Det används oftast för att öka prestandan.
Kommer du ihåg när vi gick fram BufferedReaderoch sa att du inte behöver använda den? När vi skriver:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
…du behöver inte använda BufferedReader: En InputStreamReaderkan göra jobbet. Men BufferedReaderförbättrar prestandan och kan också läsa hela rader med data snarare än enskilda tecken. Samma sak gäller BufferedInputStream! Klassen ackumulerar indata i en speciell buffert utan att ständigt komma åt inmatningsenheten. Låt oss överväga ett exempel:

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();
       }
   }
}
I det här exemplet läser vi data från en fil som finns på en dator på ' D:/Users/UserName/someFile.txt ' . Vi skapar 2 objekt — ett FileInputStreamoch ett BufferedInputStreamsom "lindar" det. Sedan läser vi bytes från filen och konverterar dem till tecken. Och det gör vi tills filen tar slut. Som du kan se är det inget komplicerat här. Du kan kopiera den här koden och köra den på en riktig fil på din dator :) OutputStreamKlassen är en abstrakt klass som representerar en utdataström av bytes. Som du redan vet är detta motsatsen till en InputStream. Det är inte ansvarigt för att läsa data någonstans, utan snarare för att skicka data någonstans . Som InputStream, den här abstrakta klassen ger alla sina ättlingar en uppsättning bekväma metoder:
  • void close()stänger utströmmen;
  • void flush()rensar alla utgångsbuffertar;
  • abstract void write(int oneByte)skriver 1 byte till utgångsströmmen;
  • void write(byte[] buffer)skriver en byte-array till utgångsströmmen;
  • void write(byte[] buffer, int offset, int count)skriver ett intervall av räknebyte från en array, med start vid offsetpositionen.
Här är några av klassens ättlingar OutputStream:
  1. DataOutputStream. En utdataström som inkluderar metoder för att skriva standard Java-datatyper.

    En mycket enkel klass för att skriva primitiva Java-datatyper och strängar. Du kommer förmodligen att förstå följande kod även utan en förklaring:

    
    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);
    
       }
    }
    

    Den har separata metoder för varje typ — , , , writeDouble()och writeLong()writeShort()vidare.


  2. FileOutputStream. Denna klass implementerar en mekanism för att skicka data till en fil på disken. Förresten, vi använde det redan i det förra exemplet. Märkte du? Vi skickade det till DataOutputStream, som fungerade som en "inpackning".

  3. BufferedOutputStream. En buffrad utström. Det är inte heller något komplicerat här. Dess syfte är analogt med BufferedInputStream(eller BufferedReader). Istället för den vanliga sekventiella läsningen av data, skriver den data med hjälp av en speciell "kumulativ" buffert. Bufferten gör det möjligt att minska antalet gånger datasänkan nås och därigenom öka prestandan.

    
    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);
         }
    }
    

    Återigen, du kan själv leka med den här koden och verifiera att den fungerar på riktiga filer på din dator.

Vi kommer att ha en separat lektion om , FileInputStreamoch FileOutputStream, BuffreredInputStreamså detta är tillräckligt med information för en första bekantskap. Det är allt! Vi hoppas att du förstår skillnaderna mellan gränssnitt och abstrakta klasser och är redo att svara på alla frågor, även trickfrågor :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION