OOP-relaterede spørgsmål er en integreret del af det tekniske interview til en Java-udviklerstilling i en IT-virksomhed. I denne artikel vil vi tale om et princip i OOP - polymorfi. Vi vil fokusere på de aspekter, der ofte bliver spurgt om under interviews, og giver også et par eksempler for klarhedens skyld.

Hvad er polymorfi i Java?

Polymorfi er et programs evne til at behandle objekter med samme grænseflade på samme måde uden information om objektets specifikke type. Hvis du besvarer et spørgsmål om, hvad polymorfi er, vil du højst sandsynligt blive bedt om at forklare, hvad du mente. Uden at udløse en masse yderligere spørgsmål, læg det hele for intervieweren igen. Interviewtid: polymorfi i Java - 1Du kan starte med, at OOP-tilgangen går ud på at bygge et Java-program baseret på interaktionen mellem objekter, som er baseret på klasser. Klasser er tidligere skrevne tegninger (skabeloner), der bruges til at skabe objekter i programmet. Desuden har en klasse altid en bestemt type, som med god programmeringsstil har et navn, der antyder dens formål. Yderligere kan det bemærkes, at da Java er stærkt skrevet, skal programkoden altid angive en objekttype, når variabler erklæres. Læg dertil det faktum, at streng indtastning forbedrer kodesikkerheden og pålideligheden og gør det muligt, selv ved kompilering, at forhindre fejl på grund af inkompatibilitetstyper (for eksempel forsøg på at dividere en streng med et tal). Naturligvis skal compileren "kende" den deklarerede type – det kan være en klasse fra JDK eller en, som vi selv har oprettet. Påpeg over for intervieweren, at vores kode ikke kun kan bruge genstande af den type, der er angivet i erklæringen, men også dens efterkommere.Dette er en vigtig pointe: Vi kan arbejde med mange forskellige typer som en enkelt type (forudsat at disse typer er afledt af en basistype). Dette betyder også, at hvis vi erklærer en variabel, hvis type er en superklasse, så kan vi tildele en forekomst af en af ​​dens efterkommere til den variabel. Intervieweren vil kunne lide det, hvis du giver et eksempel. Vælg en klasse, der kunne deles af (en basisklasse for) flere klasser, og få et par af dem til at arve den. Grundklasse:

public class Dancer {
    private String name;
    private int age;

    public Dancer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void dance() {
        System.out.println(toString() + " I dance like everyone else.");
    }

    @Override
    public String toString() {
        Return "I'm " + name + ". I'm " + age + " years old.";
    }
}
I underklasserne skal du tilsidesætte metoden for basisklassen:

public class ElectricBoogieDancer extends Dancer {
    public ElectricBoogieDancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString () + " I dance the electric boogie!");
    }
}

public class Breakdancer extends Dancer {

    public Breakdancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString() + " I breakdance!");
    }
}
Et eksempel på polymorfi og hvordan disse objekter kan bruges i et program:

public class Main {

    public static void main(String[] args) {
        Dancer dancer = new Dancer("Fred", 18);

        Dancer breakdancer = new Breakdancer("Jay", 19); // Widening conversion to the base type 
        Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20); // Widening conversion to the base type

        List<dancer> disco = Arrays.asList(dancer, breakdancer, electricBoogieDancer);
        for (Dancer d : disco) {
            d.dance(); // Call the polymorphic method
        }
    }
}
I hovedmetoden skal du vise, at linjerne

Dancer breakdancer = new Breakdancer("Jay", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20);
erklære en variabel i en superklasse og tildele den et objekt, der er en forekomst af en af ​​dens efterkommere. Du vil højst sandsynligt blive spurgt, hvorfor compileren ikke vipper ud ved inkonsistensen af ​​de typer, der er erklæret på venstre og højre side af tildelingsoperatoren - Java er trods alt stærkt skrevet. Forklar, at en udvidende typekonvertering er på arbejde her - en reference til et objekt behandles som en reference til dets basisklasse. Hvad mere er, efter at have stødt på en sådan konstruktion i koden, udfører compileren konverteringen automatisk og implicit. Eksempelkoden viser, at typen, der er deklareret på venstre side af opgaveoperatøren ( Dancer ), har flere former (typer), som er deklareret på højre side ( Breakdancer , ElectricBoogieDancer). Hver form kan have sin egen unikke adfærd med hensyn til den generelle funktionalitet defineret i superklassen (dansemetoden ) . Det vil sige, at en metode, der er erklæret i en superklasse, kan implementeres anderledes i dens efterkommere. I dette tilfælde har vi at gøre med metodeoverstyring, hvilket er præcis det, der skaber flere former (adfærd). Dette kan ses ved at køre koden i hovedmetoden: Programoutput: Jeg er Fred. Jeg er 18 år. Jeg danser som alle andre. Jeg er Jay. Jeg er 19 år gammel. Jeg breakdancer! Jeg er Marcia. Jeg er 20 år gammel. Jeg danser den elektriske boogie! Hvis vi ikke tilsidesætter metoden i underklasserne, så får vi ikke anderledes adfærd. For eksempel,ElectricBoogieDancer- klasser, så vil outputtet af programmet være dette: Jeg er Fred. Jeg er 18 år. Jeg danser som alle andre. Jeg er Jay. Jeg er 19 år gammel. Jeg danser som alle andre. Jeg er Marcia. Jeg er 20 år gammel. Jeg danser som alle andre. Og det betyder, at det simpelthen ikke giver mening at lave Breakdancer- og ElectricBoogieDancer- klasserne. Hvor er princippet om polymorfi manifesteret? Hvor bruges et objekt i programmet uden kendskab til dets specifikke type? I vores eksempel sker det, når dance() -metoden kaldes på Dancer d -objektet. I Java betyder polymorfi, at programmet ikke behøver at vide, om objektet er enBreakdancer eller ElectricBoogieDancer . Det vigtige er, at det er en efterkommer af Dancer- klassen. Og hvis du nævner efterkommere, skal du bemærke, at arv i Java ikke bare udvider , men også implementerer. Nu er det tid til at nævne, at Java ikke understøtter multipel nedarvning - hver type kan have en forælder (superklasse) og et ubegrænset antal efterkommere (underklasser). Derfor bruges grænseflader til at tilføje flere sæt funktioner til klasser. Sammenlignet med underklasser (arv) er grænseflader mindre koblet til den overordnede klasse. De bruges meget bredt. I Java er en grænseflade en referencetype, så programmet kan erklære en variabel af grænsefladetypen. Nu er det tid til at give et eksempel. Opret en grænseflade:

public interface CanSwim {
    void swim();
}
For klarhedens skyld tager vi forskellige ikke-relaterede klasser og får dem til at implementere grænsefladen:

public class Human implements CanSwim {
    private String name;
    private int age;

    public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void swim() {
        System.out.println(toString()+" I swim with an inflated tube.");
    }

    @Override
    public String toString() {
        return "I'm " + name + ". I'm " + age + " years old.";
    }

}
 
public class Fish implements CanSwim {
    private String name;

    public Fish(String name) {
        this.name = name;
    }

    @Override
    public void swim() {
        System.out.println("I'm a fish. My name is " + name + ". I swim by moving my fins.");

    }

public class UBoat implements CanSwim {

    private int speed;

    public UBoat(int speed) {
        this.speed = speed;
    }

    @Override
    public void swim() {
        System.out.println("I'm a submarine that swims through the water by rotating screw propellers. My speed is " + speed + " knots.");
    }
}
hovedmetode :

public class Main {

    public static void main(String[] args) {
        CanSwim human = new Human("John", 6);
        CanSwim fish = new Fish("Whale");
        CanSwim boat = new UBoat(25);

        List<swim> swimmers = Arrays.asList(human, fish, boat);
        for (Swim s : swimmers) {
            s.swim();
        }
    }
}
Resultaterne, der kalder en polymorf metode defineret i en grænseflade, viser os forskellene i adfærden for de typer, der implementerer denne grænseflade. I vores tilfælde er det de forskellige strenge, der vises af svømmemetoden . Efter at have studeret vores eksempel kan intervieweren spørge, hvorfor denne kode køres i hovedmetoden

for (Swim s : swimmers) {
            s.swim();        
}
forårsager, at de overordnede metoder, der er defineret i vores underklasser, kaldes? Hvordan vælges metodens ønskede implementering, mens programmet kører? For at besvare disse spørgsmål skal du forklare sen (dynamisk) binding. Binding betyder etablering af en mapping mellem et metodekald og dets specifikke klasseimplementering. I det væsentlige bestemmer koden, hvilken af ​​de tre metoder, der er defineret i klasserne, der skal udføres. Java bruger som standard sen binding, dvs. binding sker ved kørsel og ikke ved kompilering, som det er tilfældet med tidlig binding. Dette betyder, at når compileren kompilerer denne kode

for (Swim s : swimmers) {
            s.swim();        
}
den ved ikke, hvilken klasse ( menneske , fisk eller ubåd ) der har koden, der vil blive udført, når svømmeturenmetode kaldes. Dette bestemmes kun, når programmet køres, takket være den dynamiske bindingsmekanisme (kontrol af et objekts type under kørsel og valg af den korrekte implementering for denne type). Hvis du bliver spurgt, hvordan dette implementeres, kan du svare, at ved indlæsning og initialisering af objekter, bygger JVM tabeller i hukommelsen og forbinder variable med deres værdier og objekter med deres metoder. Hvis en klasse er nedarvet eller implementerer en grænseflade, er den første opgave at kontrollere for tilstedeværelsen af ​​tilsidesatte metoder. Hvis der er nogen, er de bundet til denne type. Hvis ikke, flyttes søgningen efter en matchende metode til den klasse, der er et trin højere (forælderen) og så videre op til roden i et multilevel hierarki. Når det kommer til polymorfi i OOP og dens implementering i kode, vi bemærker, at det er god praksis at bruge abstrakte klasser og grænseflader til at give abstrakte definitioner af basisklasser. Denne praksis følger af abstraktionsprincippet - at identificere almindelig adfærd og egenskaber og placere dem i en abstrakt klasse, eller kun identificere almindelig adfærd og sætte den i en grænseflade. Design og oprettelse af et objekthierarki baseret på grænseflader og klassearv er påkrævet for at implementere polymorfi. Med hensyn til polymorfi og innovationer i Java, bemærker vi, at fra og med Java 8, når du opretter abstrakte klasser og grænseflader, er det muligt at bruge eller kun identificere almindelig adfærd og sætte den i en grænseflade. Design og oprettelse af et objekthierarki baseret på grænseflader og klassearv er påkrævet for at implementere polymorfi. Med hensyn til polymorfi og innovationer i Java, bemærker vi, at fra og med Java 8, når du opretter abstrakte klasser og grænseflader, er det muligt at bruge eller kun identificere almindelig adfærd og sætte den i en grænseflade. Design og oprettelse af et objekthierarki baseret på grænseflader og klassearv er påkrævet for at implementere polymorfi. Med hensyn til polymorfi og innovationer i Java, bemærker vi, at fra og med Java 8, når du opretter abstrakte klasser og grænseflader, er det muligt at brugestandard nøgleord til at skrive en standardimplementering for abstrakte metoder i basisklasser. For eksempel:

public interface CanSwim {
    default void swim() {
        System.out.println("I just swim");
    }
}
Nogle gange spørger interviewere om, hvordan metoder i basisklasser skal deklareres, så princippet om polymorfi ikke bliver overtrådt. Svaret er enkelt: Disse metoder må ikke være statiske , private eller endelige . Private gør en metode kun tilgængelig inden for en klasse, så du vil ikke være i stand til at tilsidesætte den i en underklasse. Static knytter en metode til klassen i stedet for ethvert objekt, så superklassens metode vil altid blive kaldt. Og endelig gør en metode uforanderlig og skjult fra underklasser.

Hvad giver polymorfi os?

Du vil højst sandsynligt også blive spurgt om, hvordan polymorfi gavner os. Du kan svare kort på dette uden at blive hængende i de behårede detaljer:
  1. Det gør det muligt at erstatte klasseimplementeringer. Test er bygget på det.
  2. Det letter udvidelsesmulighederne, hvilket gør det meget nemmere at skabe et fundament, der kan bygges på i fremtiden. Tilføjelse af nye typer baseret på eksisterende er den mest almindelige måde at udvide funktionaliteten af ​​OOP-programmer på.
  3. Det giver dig mulighed for at kombinere objekter, der deler en fælles type eller adfærd i én samling eller array og håndtere dem ensartet (som i vores eksempler, hvor vi tvang alle til at danse() eller svømme() :)
  4. Fleksibilitet ved oprettelse af nye typer: du kan vælge forældrenes implementering af en metode eller tilsidesætte den i en underklasse.

Nogle afskedsord

Polymorfi er et meget vigtigt og omfattende emne. Det er emnet for næsten halvdelen af ​​denne artikel om OOP i Java og udgør en god del af sprogets fundament. Du vil ikke kunne undgå at definere dette princip i et interview. Hvis du ikke ved det eller ikke forstår det, vil interviewet formentlig slutte. Så du skal ikke være sløv – vurder din viden inden interviewet og genopfrisk den, hvis det er nødvendigt.

Mere læsning: