Scala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM).
Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet.
Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode.
La oss ta en titt på hvordan alt dette blir implementert.
Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen.
Klikk her for å lese et kollisjonskurs om grunnleggende om JVM.Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .
Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder.
Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters.
La oss se på følgende eksempel:
class Person(val name:String) { }
La oss ta en titt inn i klassen Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig.
Hvis vi bytter ut val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
Hvis noen val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.
Merk at en slik implementering av klassenivå val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.
La oss anta at vi har en metode, m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
Hvordan er hver av disse referansene til m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
I det konstante bassenget ser vi at referansen til metoden m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.
Med andre ord, tilordne en metode til en variabel definert med val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.
Imidlertid bruker du nøkkelordet def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.
Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.
Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen:
lazy val m4 = m
Løping javap -p -v Person.class
vil nå avsløre følgende:
Constant pool: #20 = Fieldref #2.#19 // Person.bitmapFå hendene skitne med Scala JVM Bytecode
Scala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM).
Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet.
Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode.
La oss ta en titt på hvordan alt dette blir implementert.
Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen.
Klikk her for å lese et kollisjonskurs om grunnleggende om JVM.Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .
Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder.
Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters.
La oss se på følgende eksempel:
class Person(val name:String) { }
La oss ta en titt inn i klassen Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig.
Hvis vi bytter ut val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
Hvis noen val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.
Merk at en slik implementering av klassenivå val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.
La oss anta at vi har en metode, m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
Hvordan er hver av disse referansene til m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
I det konstante bassenget ser vi at referansen til metoden m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.
Med andre ord, tilordne en metode til en variabel definert med val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.
Imidlertid bruker du nøkkelordet def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.
Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.
Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen:
lazy val m4 = m
Løping javap -p -v Person.class
vil nå avsløre følgende:
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
I dette tilfellet verdien av feltet m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap$0
for å spore tilstanden. Metode m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap$0
til 1, slik at neste gang m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.
Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.
Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet.
Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil.
La oss nå se på følgende Scala-kildekode:
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.
Ved å kompilere denne koden genereres fire klassefiler:
Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Det skjulte Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Metoden oppretter et Printer
. Deretter opprettes en Hello$$anonfun$1
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.
La oss ta en titt på den anonyme funksjonsklassen, Hello$$anonfun$1.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Ser tilbake på Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.
Til slutt, for fullstendighet, la oss se på bytekoden for Printer.class
:
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).
Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program.
Gå under panseret med Scala: Utforsk hvordan dette kraftige språket implementeres i JVM bytecode. kvitringScalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Det produseres to enheter: Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
Når en klasse implementerer denne egenskapen og kaller metoden isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.
Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid.
Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet object
. La oss se på følgende singleton-klasse:
object Config { val home_dir = '/home/user' }
Kompilatoren produserer to klassefiler:
Config.class
er ganske enkel:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Dette er bare en dekoratør for det syntetiske Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
Den består av følgende:
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.
Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket.
Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja!
Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til .class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her .
Dekompilering av klassefiler med
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen
javap
Java leveres med javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.
La oss kompilere følgende kildekode:
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
Kompilere dette med scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til -p
alternativet inkluderer private medlemmer:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til -c
alternativ:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke -v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:
Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene.
Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde:
Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer.
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten.
For eksempel en Java-klasse som bruker et konstant heltall 365
kan ha en konstant bassengoppføring med følgende bytekode:
x03 00 00 01 6D
Den første byten, x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:
#14 = Integer 365
Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet:
println( 'Calculating perimeter...' )
Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype:
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen .
En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data.
En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor.
JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
Den faktiske bytekoden kan se ut slik:
xB2 00 17 x12 19 xB6 00 1D x27 ...
Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. javap
oversetter nyttig bytekoden til en lesbar form som viser:
Operander som vises med et pundtegn, for eksempel #23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.
Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon .
Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet.
En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste.
Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket:
sideLength * this.numSides
er samlet til følgende i vår getPerimeter()
metode:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker.
Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.
Instruksjoner som iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.
Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen.
Scala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM).
Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet.
Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode.
La oss ta en titt på hvordan alt dette blir implementert.
Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen.
Klikk her for å lese et kollisjonskurs om grunnleggende om JVM.Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .
Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder.
Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters.
La oss se på følgende eksempel:
class Person(val name:String) { }
La oss ta en titt inn i klassen Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig.
Hvis vi bytter ut val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
Hvis noen val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.
Merk at en slik implementering av klassenivå val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.
La oss anta at vi har en metode, m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
Hvordan er hver av disse referansene til m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
I det konstante bassenget ser vi at referansen til metoden m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.
Med andre ord, tilordne en metode til en variabel definert med val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.
Imidlertid bruker du nøkkelordet def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.
Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.
Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen:
lazy val m4 = m
Løping javap -p -v Person.class
vil nå avsløre følgende:
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
I dette tilfellet verdien av feltet m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap$0
for å spore tilstanden. Metode m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap$0
til 1, slik at neste gang m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.
Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.
Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet.
Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil.
La oss nå se på følgende Scala-kildekode:
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.
Ved å kompilere denne koden genereres fire klassefiler:
Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Det skjulte Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Metoden oppretter et Printer
. Deretter opprettes en Hello$$anonfun$1
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.
La oss ta en titt på den anonyme funksjonsklassen, Hello$$anonfun$1.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Ser tilbake på Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.
Til slutt, for fullstendighet, la oss se på bytekoden for Printer.class
:
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).
Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program.
Gå under panseret med Scala: Utforsk hvordan dette kraftige språket implementeres i JVM bytecode. kvitringScalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Det produseres to enheter: Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
Når en klasse implementerer denne egenskapen og kaller metoden isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.
Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid.
Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet object
. La oss se på følgende singleton-klasse:
object Config { val home_dir = '/home/user' }
Kompilatoren produserer to klassefiler:
Config.class
er ganske enkel:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Dette er bare en dekoratør for det syntetiske Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
Den består av følgende:
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.
Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket.
Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja!
Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til .class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her .
Dekompilering av klassefiler med
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen
javap
Java leveres med javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.
La oss kompilere følgende kildekode:
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
Kompilere dette med scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til -p
alternativet inkluderer private medlemmer:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til -c
alternativ:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke -v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:
Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene.
Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde:
Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer.
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten.
For eksempel en Java-klasse som bruker et konstant heltall 365
kan ha en konstant bassengoppføring med følgende bytekode:
x03 00 00 01 6D
Den første byten, x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:
#14 = Integer 365
Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet:
println( 'Calculating perimeter...' )
Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype:
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen .
En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data.
En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor.
JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
Den faktiske bytekoden kan se ut slik:
xB2 00 17 x12 19 xB6 00 1D x27 ...
Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. javap
oversetter nyttig bytekoden til en lesbar form som viser:
Operander som vises med et pundtegn, for eksempel #23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.
Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon .
Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet.
En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste.
Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket:
sideLength * this.numSides
er samlet til følgende i vår getPerimeter()
metode:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker.
Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.
Instruksjoner som iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.
Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen.
; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmapScala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM).
Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet.
Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode.
La oss ta en titt på hvordan alt dette blir implementert.
Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen.
Klikk her for å lese et kollisjonskurs om grunnleggende om JVM.Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .
Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder.
Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters.
La oss se på følgende eksempel:
class Person(val name:String) { }
La oss ta en titt inn i klassen Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig.
Hvis vi bytter ut val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
Hvis noen val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.
Merk at en slik implementering av klassenivå val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.
La oss anta at vi har en metode, m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
Hvordan er hver av disse referansene til m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
I det konstante bassenget ser vi at referansen til metoden m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.
Med andre ord, tilordne en metode til en variabel definert med val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.
Imidlertid bruker du nøkkelordet def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.
Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.
Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen:
lazy val m4 = m
Løping javap -p -v Person.class
vil nå avsløre følgende:
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
I dette tilfellet verdien av feltet m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap$0
for å spore tilstanden. Metode m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap$0
til 1, slik at neste gang m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.
Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.
Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet.
Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil.
La oss nå se på følgende Scala-kildekode:
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.
Ved å kompilere denne koden genereres fire klassefiler:
Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Det skjulte Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Metoden oppretter et Printer
. Deretter opprettes en Hello$$anonfun$1
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.
La oss ta en titt på den anonyme funksjonsklassen, Hello$$anonfun$1.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Ser tilbake på Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.
Til slutt, for fullstendighet, la oss se på bytekoden for Printer.class
:
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).
Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program.
Gå under panseret med Scala: Utforsk hvordan dette kraftige språket implementeres i JVM bytecode. kvitringScalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Det produseres to enheter: Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
Når en klasse implementerer denne egenskapen og kaller metoden isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.
Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid.
Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet object
. La oss se på følgende singleton-klasse:
object Config { val home_dir = '/home/user' }
Kompilatoren produserer to klassefiler:
Config.class
er ganske enkel:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Dette er bare en dekoratør for det syntetiske Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
Den består av følgende:
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.
Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket.
Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja!
Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til .class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her .
Dekompilering av klassefiler med
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen
javap
Java leveres med javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.
La oss kompilere følgende kildekode:
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
Kompilere dette med scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til -p
alternativet inkluderer private medlemmer:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til -c
alternativ:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke -v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:
Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene.
Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde:
Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer.
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten.
For eksempel en Java-klasse som bruker et konstant heltall 365
kan ha en konstant bassengoppføring med følgende bytekode:
x03 00 00 01 6D
Den første byten, x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:
#14 = Integer 365
Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet:
println( 'Calculating perimeter...' )
Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype:
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen .
En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data.
En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor.
JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
Den faktiske bytekoden kan se ut slik:
xB2 00 17 x12 19 xB6 00 1D x27 ...
Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. javap
oversetter nyttig bytekoden til en lesbar form som viser:
Operander som vises med et pundtegn, for eksempel #23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.
Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon .
Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet.
En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste.
Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket:
sideLength * this.numSides
er samlet til følgende i vår getPerimeter()
metode:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker.
Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.
Instruksjoner som iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.
Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen.
:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmapScala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM).
Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet.
Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode.
La oss ta en titt på hvordan alt dette blir implementert.
Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen.
Klikk her for å lese et kollisjonskurs om grunnleggende om JVM.Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .
Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder.
Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters.
La oss se på følgende eksempel:
class Person(val name:String) { }
La oss ta en titt inn i klassen Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig.
Hvis vi bytter ut val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
Hvis noen val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.
Merk at en slik implementering av klassenivå val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.
La oss anta at vi har en metode, m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
Hvordan er hver av disse referansene til m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
I det konstante bassenget ser vi at referansen til metoden m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.
Med andre ord, tilordne en metode til en variabel definert med val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.
Imidlertid bruker du nøkkelordet def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.
Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.
Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen:
lazy val m4 = m
Løping javap -p -v Person.class
vil nå avsløre følgende:
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
I dette tilfellet verdien av feltet m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap$0
for å spore tilstanden. Metode m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap$0
til 1, slik at neste gang m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.
Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.
Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet.
Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil.
La oss nå se på følgende Scala-kildekode:
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.
Ved å kompilere denne koden genereres fire klassefiler:
Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Det skjulte Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Metoden oppretter et Printer
. Deretter opprettes en Hello$$anonfun$1
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.
La oss ta en titt på den anonyme funksjonsklassen, Hello$$anonfun$1.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Ser tilbake på Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.
Til slutt, for fullstendighet, la oss se på bytekoden for Printer.class
:
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).
Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program.
Gå under panseret med Scala: Utforsk hvordan dette kraftige språket implementeres i JVM bytecode. kvitringScalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Det produseres to enheter: Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
Når en klasse implementerer denne egenskapen og kaller metoden isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.
Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid.
Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet object
. La oss se på følgende singleton-klasse:
object Config { val home_dir = '/home/user' }
Kompilatoren produserer to klassefiler:
Config.class
er ganske enkel:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Dette er bare en dekoratør for det syntetiske Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
Den består av følgende:
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.
Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket.
Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja!
Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til .class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her .
Dekompilering av klassefiler med
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen
javap
Java leveres med javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.
La oss kompilere følgende kildekode:
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
Kompilere dette med scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til -p
alternativet inkluderer private medlemmer:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til -c
alternativ:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke -v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:
Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene.
Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde:
Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer.
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten.
For eksempel en Java-klasse som bruker et konstant heltall 365
kan ha en konstant bassengoppføring med følgende bytekode:
x03 00 00 01 6D
Den første byten, x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:
#14 = Integer 365
Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet:
println( 'Calculating perimeter...' )
Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype:
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen .
En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data.
En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor.
JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
Den faktiske bytekoden kan se ut slik:
xB2 00 17 x12 19 xB6 00 1D x27 ...
Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. javap
oversetter nyttig bytekoden til en lesbar form som viser:
Operander som vises med et pundtegn, for eksempel #23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.
Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon .
Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet.
En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste.
Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket:
sideLength * this.numSides
er samlet til følgende i vår getPerimeter()
metode:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker.
Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.
Instruksjoner som iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.
Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen.
:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmapScala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM).
Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet.
Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode.
La oss ta en titt på hvordan alt dette blir implementert.
Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen.
Klikk her for å lese et kollisjonskurs om grunnleggende om JVM.Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .
Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder.
Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters.
La oss se på følgende eksempel:
class Person(val name:String) { }
La oss ta en titt inn i klassen Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig.
Hvis vi bytter ut val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
Hvis noen val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.
Merk at en slik implementering av klassenivå val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.
La oss anta at vi har en metode, m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
Hvordan er hver av disse referansene til m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
I det konstante bassenget ser vi at referansen til metoden m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.
Med andre ord, tilordne en metode til en variabel definert med val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.
Imidlertid bruker du nøkkelordet def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.
Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.
Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen:
lazy val m4 = m
Løping javap -p -v Person.class
vil nå avsløre følgende:
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
I dette tilfellet verdien av feltet m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap$0
for å spore tilstanden. Metode m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap$0
til 1, slik at neste gang m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.
Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.
Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet.
Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil.
La oss nå se på følgende Scala-kildekode:
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.
Ved å kompilere denne koden genereres fire klassefiler:
Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Det skjulte Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Metoden oppretter et Printer
. Deretter opprettes en Hello$$anonfun$1
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.
La oss ta en titt på den anonyme funksjonsklassen, Hello$$anonfun$1.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Ser tilbake på Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.
Til slutt, for fullstendighet, la oss se på bytekoden for Printer.class
:
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).
Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program.
Gå under panseret med Scala: Utforsk hvordan dette kraftige språket implementeres i JVM bytecode. kvitringScalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Det produseres to enheter: Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
Når en klasse implementerer denne egenskapen og kaller metoden isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.
Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid.
Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet object
. La oss se på følgende singleton-klasse:
object Config { val home_dir = '/home/user' }
Kompilatoren produserer to klassefiler:
Config.class
er ganske enkel:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Dette er bare en dekoratør for det syntetiske Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
Den består av følgende:
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.
Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket.
Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja!
Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til .class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her .
Dekompilering av klassefiler med
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen
javap
Java leveres med javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.
La oss kompilere følgende kildekode:
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
Kompilere dette med scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til -p
alternativet inkluderer private medlemmer:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til -c
alternativ:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke -v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:
Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene.
Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde:
Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer.
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten.
For eksempel en Java-klasse som bruker et konstant heltall 365
kan ha en konstant bassengoppføring med følgende bytekode:
x03 00 00 01 6D
Den første byten, x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:
#14 = Integer 365
Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet:
println( 'Calculating perimeter...' )
Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype:
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen .
En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data.
En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor.
JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
Den faktiske bytekoden kan se ut slik:
xB2 00 17 x12 19 xB6 00 1D x27 ...
Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. javap
oversetter nyttig bytekoden til en lesbar form som viser:
Operander som vises med et pundtegn, for eksempel #23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.
Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon .
Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet.
En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste.
Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket:
sideLength * this.numSides
er samlet til følgende i vår getPerimeter()
metode:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker.
Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.
Instruksjoner som iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.
Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen.
:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturnI dette tilfellet verdien av feltet Scala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM). Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet. Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode. La oss ta en titt på hvordan alt dette blir implementert. Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen. Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder. Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters. La oss se på følgende eksempel: La oss ta en titt inn i klassen Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig. Hvis vi bytter ut Hvis noen Merk at en slik implementering av klassenivå La oss anta at vi har en metode, Hvordan er hver av disse referansene til I det konstante bassenget ser vi at referansen til metoden Med andre ord, tilordne en metode til en variabel definert med Imidlertid bruker du nøkkelordet Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen: Løping I dette tilfellet verdien av feltet Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet. Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil. La oss nå se på følgende Scala-kildekode: Ved å kompilere denne koden genereres fire klassefiler: Det skjulte Metoden oppretter et La oss ta en titt på den anonyme funksjonsklassen, Ser tilbake på Til slutt, for fullstendighet, la oss se på bytekoden for Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program. Scalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres: Det produseres to enheter: Når en klasse implementerer denne egenskapen og kaller metoden Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid. Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet Kompilatoren produserer to klassefiler: Dette er bare en dekoratør for det syntetiske Den består av følgende: Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket. Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja! Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her . Dekompilering av klassefiler med Java leveres med La oss kompilere følgende kildekode: Kompilere dette med Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene. Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde: Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer. Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten. For eksempel en Java-klasse som bruker et konstant heltall Den første byten, Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet: Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen . En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data. En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor. JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping Den faktiske bytekoden kan se ut slik: Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. Operander som vises med et pundtegn, for eksempel Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon . Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet. En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste. Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket: er samlet til følgende i vår Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker. Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter Instruksjoner som Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen. Scala-språket har fortsatt å bli populært de siste årene, takket være den utmerkede kombinasjonen av funksjonelle og objektorienterte programvareutviklingsprinsipper , og implementeringen av den velprøvde Java Virtual Machine (JVM). Selv om Stige kompilerer til Java bytecode, er den designet for å forbedre mange av de oppfattede manglene ved Java-språket. Tilbyr full funksjonell programmeringsstøtte, inneholder Scalas kjernesyntaks mange implisitte strukturer som må bygges eksplisitt av Java-programmerere, noen med betydelig kompleksitet. Å lage et språk som kompileres til Java bytecode, krever en dyp forståelse av det indre arbeidet til Java Virtual Machine. For å sette pris på hva Scalas utviklere har oppnådd, er det nødvendig å gå under panseret, og utforske hvordan Scalas kildekode tolkes av kompilatoren for å produsere effektiv og effektiv JVM-bytecode. La oss ta en titt på hvordan alt dette blir implementert. Å lese denne artikkelen krever grunnleggende forståelse av Java Virtual Machine bytecode. Komplett virtuell maskinspesifikasjon kan fås fra Oracles offisielle dokumentasjon . Å lese hele spesifikasjonen er ikke avgjørende for å forstå denne artikkelen, så for en rask introduksjon til det grunnleggende har jeg utarbeidet en kort guide nederst i artikkelen. Et verktøy er nødvendig for å demontere Java-bytekoden for å reprodusere eksemplene nedenfor, og for å fortsette med videre undersøkelse. Java Development Kit gir sitt eget kommandolinjeverktøy, Og selvfølgelig er en installasjon av Scala-kompilatoren nødvendig for lesere som ønsker å følge med eksemplene. Denne artikkelen ble skrevet ved hjelp av Skala 2.11.7 . Ulike versjoner av Scala kan produsere litt forskjellige bytekoder. Selv om Java-konvensjonen alltid gir getter- og settermetoder for offentlige attributter, må Java-programmerere skrive disse selv, til tross for at mønsteret for hver ikke har endret seg i flere tiår. Scala, derimot, gir standard getters og setters. La oss se på følgende eksempel: La oss ta en titt inn i klassen Vi kan se at for hvert felt i Scala-klassen genereres et felt og dets getter-metode. Feltet er privat og endelig, mens metoden er offentlig. Hvis vi bytter ut Hvis noen Merk at en slik implementering av klassenivå La oss anta at vi har en metode, Hvordan er hver av disse referansene til I det konstante bassenget ser vi at referansen til metoden Med andre ord, tilordne en metode til en variabel definert med Imidlertid bruker du nøkkelordet Så som vi kan se, tilbyr Scala tre måter å jobbe med klassefelt på, og disse spesifiseres enkelt via nøkkelordene Mer komplisert kode produseres når man erklærer en lat verdi. Anta at vi har lagt til følgende felt i den tidligere definerte klassen: Løping I dette tilfellet verdien av feltet Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet. Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil. La oss nå se på følgende Scala-kildekode: Ved å kompilere denne koden genereres fire klassefiler: Det skjulte Metoden oppretter et La oss ta en titt på den anonyme funksjonsklassen, Ser tilbake på Til slutt, for fullstendighet, la oss se på bytekoden for Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program. Scalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres: Det produseres to enheter: Når en klasse implementerer denne egenskapen og kaller metoden Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid. Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet Kompilatoren produserer to klassefiler: Dette er bare en dekoratør for det syntetiske Den består av følgende: Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket. Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja! Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her . Dekompilering av klassefiler med Java leveres med La oss kompilere følgende kildekode: Kompilere dette med Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene. Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde: Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer. Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten. For eksempel en Java-klasse som bruker et konstant heltall Den første byten, Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet: Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen . En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data. En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor. JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping Den faktiske bytekoden kan se ut slik: Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. Operander som vises med et pundtegn, for eksempel Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon . Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet. En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste. Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket: er samlet til følgende i vår Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker. Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter Instruksjoner som Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen. m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap
for å spore tilstanden. Metode Få hendene skitne med Scala JVM Bytecode
Forutsetninger
javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .Standard Getters og Setters
class Person(val name:String) { }
Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.Variabel- og funksjonsdefinisjoner
m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.Latverdier
lazy val m4 = m
javap -p -v Person.class
vil nå avsløre følgende:Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap$0
for å spore tilstanden. Metode m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap$0
til 1, slik at neste gang m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.Fungere som verdi
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Printer
. Deretter opprettes en Hello$$anonfun$1
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.Hello$$anonfun$1.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.Printer.class
:public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).Scala trekk
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Singletoner
object
. La oss se på følgende singleton-klasse:object Config { val home_dir = '/home/user' }
Config.class
er ganske enkel:public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.Konklusjon
Java Virtual Machine: A Crash Course
.class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen Dekompilering av klassefiler med
javap
javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-p
alternativet inkluderer private medlemmer:$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-c
alternativ:$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
-v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:Konstant basseng
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
365
kan ha en konstant bassengoppføring med følgende bytekode:x03 00 00 01 6D
x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:#14 = Integer 365
println( 'Calculating perimeter...' )
CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
Felt- og metodetabeller
JVM Bytecode
javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
xB2 00 17 x12 19 xB6 00 1D x27 ...
javap
oversetter nyttig bytekoden til en lesbar form som viser:
#23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.Metodeanrop og samtalestakken
Utførelse på Operand Stack
sideLength * this.numSides
getPerimeter()
metode: 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Lokale variabler
this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap
til 1, slik at neste gang Få hendene skitne med Scala JVM Bytecode
Forutsetninger
javap
, som vi skal bruke her. En rask demonstrasjon av hvordan javap
verk er inkludert i guiden nederst .Standard Getters og Setters
class Person(val name:String) { }
Person
. Hvis vi kompilerer denne filen med scalac
, kjører du $ javap -p Person.class
gir oss:Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
val
med var
i Person
kilde og kompilere på nytt, deretter feltets final
modifikator blir droppet, og settermetoden blir også lagt til:Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
val
eller var
er definert inne i klassens kropp, deretter blir det tilsvarende private felt og tilgangsmetoder opprettet, og initialisert på riktig måte ved oppretting av forekomst.val
og var
felt betyr at hvis noen variabler brukes på klassenivå for å lagre mellomverdier, og aldri er tilgjengelig direkte av programmereren, vil initialisering av hvert slikt felt legge til en til to metoder for klassens fotavtrykk. Legger til et private
modifikator for slike felt betyr ikke at de tilsvarende aksessorer vil bli droppet. De vil bare bli private.Variabel- og funksjonsdefinisjoner
m()
, og lager tre forskjellige Scala-referanser til denne funksjonen:class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
konstruert? Når gjør m
bli henrettet i hvert tilfelle? La oss ta en titt på den resulterende bytekoden. Følgende utdata viser resultatene av javap -v Person.class
(utelater mye overflødig produksjon):Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
m()
lagres ved indeks #30
. I konstruktorkoden ser vi at denne metoden påkalles to ganger under initialisering, med instruksjonen invokevirtual #30
vises først ved byteforskyvning 11, deretter ved forskyvning 19. Den første påkallingen blir fulgt av instruksjonen putfield #22
som tilordner resultatet av denne metoden til feltet m1
, referert til av indeks #22
i det konstante bassenget. Den andre påkallingen blir fulgt av det samme mønsteret, denne gangen tildeles verdien til feltet m2
, indeksert som #24
i det konstante bassenget.val
eller var
tildeler bare resultat av metoden til den variabelen. Vi kan se at metodene m1()
og m2()
som er opprettet er rett og slett getters for disse variablene. Når det gjelder var m2
, ser vi også at setter m2_$eq(int)
blir opprettet, som oppfører seg som alle andre settere, og overskriver verdien i feltet.def
gir et annet resultat. I stedet for å hente en feltverdi for å returnere, er metoden m3()
inkluderer også instruksjonen invokevirtual #30
. Det vil si at hver gang denne metoden kalles, kaller den da m()
, og returnerer resultatet av denne metoden.val
, var
og def
. I Java måtte vi implementere de nødvendige settere og getters eksplisitt, og slik manuelt skrevet kjeleplatekode ville være mye mindre uttrykksfull og mer feilutsatt.Latverdier
lazy val m4 = m
javap -p -v Person.class
vil nå avsløre følgende:Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
m4
beregnes ikke før den er nødvendig. Den spesielle, private metoden m4$lzycompute()
blir produsert for å beregne lat verdi, og feltet bitmap$0
for å spore tilstanden. Metode m4()
sjekker om dette feltets verdi er 0, noe som indikerer at m4
er ikke initialisert ennå, i så fall m4$lzycompute()
blir påkalt, fyller m4
og returnerer verdien. Denne private metoden setter også verdien til bitmap$0
til 1, slik at neste gang m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.Fungere som verdi
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Printer
. Deretter opprettes en Hello$$anonfun$1
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.Hello$$anonfun$1.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.Printer.class
:public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).Scala trekk
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Singletoner
object
. La oss se på følgende singleton-klasse:object Config { val home_dir = '/home/user' }
Config.class
er ganske enkel:public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.Konklusjon
Java Virtual Machine: A Crash Course
.class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen Dekompilering av klassefiler med
javap
javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-p
alternativet inkluderer private medlemmer:$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-c
alternativ:$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
-v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:Konstant basseng
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
365
kan ha en konstant bassengoppføring med følgende bytekode:x03 00 00 01 6D
x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:#14 = Integer 365
println( 'Calculating perimeter...' )
CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
Felt- og metodetabeller
JVM Bytecode
javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
xB2 00 17 x12 19 xB6 00 1D x27 ...
javap
oversetter nyttig bytekoden til en lesbar form som viser:
#23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.Metodeanrop og samtalestakken
Utførelse på Operand Stack
sideLength * this.numSides
getPerimeter()
metode: 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Lokale variabler
this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.m4()
kalles det vil hoppe over å starte initialiseringsmetoden, og i stedet bare returnere verdien til m4
.
Bytekoden Scala produserer her er designet for å være både trådsikker og effektiv. For å være trådsikker bruker metoden for lat beregning monitorenter
/ monitorexit
par instruksjoner. Metoden forblir effektiv siden ytelsen til denne synkroniseringen bare skjer ved første avlesning av lat verdi.
Bare en bit er nødvendig for å indikere tilstanden til lat verdi. Så hvis det ikke er mer enn 32 late verdier, kan et enkelt int-felt spore dem alle. Hvis mer enn en lat verdi er definert i kildekoden, vil bytekoden ovenfor bli modifisert av kompilatoren for å implementere en bitmaske for dette formålet.
Igjen, Scala lar oss enkelt dra nytte av en bestemt type oppførsel som må implementeres eksplisitt i Java, noe som sparer krefter og reduserer risikoen for skrivefeil.
La oss nå se på følgende Scala-kildekode:
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
klasse har ett felt, output
, med typen String => Unit
: en funksjon som tar en String
og returnerer et objekt av typen Unit
(ligner på void
i Java). I hovedmetoden oppretter vi ett av disse objektene, og tildeler dette feltet til å være en anonym funksjon som skriver ut en gitt streng.
Ved å kompilere denne koden genereres fire klassefiler:
Hello.class
er en innpakningsklasse hvis hovedmetode ganske enkelt kaller Hello$.main()
:
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Det skjulte Hello$.class
inneholder den virkelige implementeringen av hovedmetoden. For å se på bytekoden, må du sørge for at du slipper riktig unna $
i henhold til reglene for kommandoskallet ditt, for å unngå dets tolkning som spesiell karakter:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun 7: dup 8: invokespecial #19 // Method Hello$$anonfun.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Metoden oppretter et Printer
. Deretter opprettes en Hello$$anonfun
, som inneholder vår anonyme funksjon s => println(s)
. Printer
er initialisert med dette objektet som output
felt. Dette feltet blir deretter lastet på stabelen og utført med operanden 'Hello'
.
La oss ta en titt på den anonyme funksjonsklassen, Hello$$anonfun.class
, nedenfor. Vi kan se at det utvider Scalas Function1
(som AbstractFunction1
) ved å implementere apply()
metode. Egentlig skaper den to apply()
metoder, den ene innpakker den andre, som sammen utfører typekontroll (i dette tilfellet at inngangen er a String
), og utfører den anonyme funksjonen (skriver ut inngangen med println()
).
public final class Hello$$anonfun extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Ser tilbake på Hello$.main()
metoden ovenfor, kan vi se at ved offset 21 utføres utførelsen av den anonyme funksjonen av et anrop til dets apply( Object )
metode.
Til slutt, for fullstendighet, la oss se på bytekoden for Printer.class
:
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
Vi kan se at den anonyme funksjonen her blir behandlet akkurat som hvilken som helst val
variabel. Den lagres i klassefeltet output
, og getter output()
er skapt. Den eneste forskjellen er at denne variabelen nå må implementere Scala-grensesnittet scala.Function1
(som AbstractFunction1
gjør).
Så kostnaden for denne elegante Scala-funksjonen er de underliggende verktøyklassene, laget for å representere og utføre en enkelt anonym funksjon som kan brukes som en verdi. Du bør ta hensyn til antall slike funksjoner, samt detaljer om VM-implementeringen din, for å finne ut hva det betyr for ditt spesifikke program.
Gå under panseret med Scala: Utforsk hvordan dette kraftige språket implementeres i JVM bytecode. kvitringScalas egenskaper er lik grensesnitt i Java. Følgende trekk definerer to metodesignaturer, og gir en standardimplementering av den andre. La oss se hvordan den implementeres:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Det produseres to enheter: Similarity.class
, grensesnittet som deklarerer begge metodene, og den syntetiske klassen, Similarity$class.class
, som gir standardimplementeringen:
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
Når en klasse implementerer denne egenskapen og kaller metoden isNotSimilar
, genererer Scala-kompilatoren bytekodeinstruksjonen invokestatic
å kalle den statiske metoden som tilbys av den medfølgende klassen.
Komplekse polymorfisme og arvestrukturer kan opprettes fra egenskaper. For eksempel kan flere trekk, så vel som implementeringsklassen, overstyre en metode med samme signatur, og ringe super.methodName()
å overføre kontrollen til neste trekk. Når Scala-kompilatoren møter slike samtaler, gjør det:
invokestatic
instruksjon.Dermed kan vi se at det kraftige konseptet med egenskaper er implementert på JVM-nivå på en måte som ikke fører til betydelig overhead, og Scala-programmerere kan ha glede av denne funksjonen uten å bekymre seg for at det vil være for dyrt ved kjøretid.
Scala gir en eksplisitt definisjon av singleton-klasser ved å bruke nøkkelordet object
. La oss se på følgende singleton-klasse:
object Config { val home_dir = '/home/user' }
Kompilatoren produserer to klassefiler:
Config.class
er ganske enkel:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Dette er bare en dekoratør for det syntetiske Config$
klasse som innebærer singletons funksjonalitet. Undersøk den klassen med javap -p -c
produserer følgende bykode:
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
Den består av følgende:
MODULE$
, gjennom hvilken andre objekter får tilgang til dette singleton-objektet.{}
(også kjent som klasseinitialisereren) og den private metoden Config$
, brukt til å initialisere MODULE$
og sett feltene til standardverdierhome_dir
. I dette tilfellet er det bare en metode. Hvis singleton har flere felt, vil det ha flere getters, så vel som settere for mutable felt.Singleton er et populært og nyttig designmønster. Java-språket gir ikke en direkte måte å spesifisere det på språknivå; Det er heller utviklerens ansvar å implementere det i Java-kilde. Scala, derimot, gir en klar og praktisk måte å erklære en singleton eksplisitt ved hjelp av object
nøkkelord. Som vi kan se under panseret, er det implementert på en rimelig og naturlig måte.
Vi har nå sett hvordan Scala samler flere implisitte og funksjonelle programmeringsfunksjoner til sofistikerte Java-bykodestrukturer. Med dette glimt av Scalas indre arbeid, kan vi få en dypere forståelse av Scalas kraft, og hjelpe oss å få mest mulig ut av dette kraftige språket.
Vi har også nå verktøyene til å utforske språket selv. Det er mange nyttige funksjoner i Scala-syntaksen som ikke dekkes av denne artikkelen, for eksempel case-klasser, karri og listeforståelse. Jeg oppfordrer deg til å undersøke Scalas implementering av disse strukturene selv, slik at du kan lære å bli en neste nivå Scala-ninja!
Akkurat som Java-kompilatoren konverterer Scala-kompilatoren kildekoden til .class
filer som inneholder Java bytecode som skal kjøres av Java Virtual Machine. For å forstå hvordan de to språkene skiller seg under panseret, er det nødvendig å forstå systemet de begge retter seg mot. Her presenterer vi en kort oversikt over noen hovedelementer i Java Virtual Machine-arkitekturen, klassefilstruktur og grunnleggende om assembler.
Vær oppmerksom på at denne veiledningen bare dekker minimumet for å gjøre det mulig å følge med på artikkelen ovenfor. Selv om mange hovedkomponenter i JVM ikke blir diskutert her, kan du finne fullstendige detaljer i de offisielle dokumentene, her .
Dekompilering av klassefiler med
javap
Konstant basseng
Felt- og metodetabeller
JVM Bytecode
Metodeanrop og samtalestakken
Utførelse på Operand Stack
Lokale variabler
Gå tilbake til toppen
javap
Java leveres med javap
kommandolinjeverktøy, som dekompilerer .class
filer til en lesbar form. Siden Scala- og Java-klassefiler begge målretter mot samme JVM, javap
kan brukes til å undersøke klassefiler samlet av Scala.
La oss kompilere følgende kildekode:
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
Kompilere dette med scalac RegularPolygon.scala
vil produsere RegularPolygon.class
. Hvis vi kjører javap RegularPolygon.class
vi får se følgende:
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er en veldig enkel oversikt over klassefilen som bare viser navnene og typene på de offentlige medlemmene i klassen. Legger til -p
alternativet inkluderer private medlemmer:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Dette er fortsatt ikke mye informasjon. For å se hvordan metodene implementeres i Java bytecode, la oss legge til -c
alternativ:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
Det er litt mer interessant. For å virkelig få hele historien, bør vi imidlertid bruke -v
eller -verbose
alternativ, som i javap -p -v RegularPolygon.class
:
Her ser vi endelig hva som egentlig er i klassefilen. Hva betyr alt dette? La oss ta en titt på noen av de viktigste delene.
Utviklingssyklusen for C ++ applikasjoner inkluderer kompilerings- og koblingsstadier. Utviklingssyklusen for Java hopper over et eksplisitt koblingsstadium fordi kobling skjer på kjøretid. Klassefilen må støtte denne kjøretidskoblingen. Dette betyr at når kildekoden refererer til hvilket som helst felt eller metode, må den resulterende bytekoden beholde relevante referanser i symbolsk form, klar til å bli referert når applikasjonen er lastet inn i minnet og faktiske adresser kan løses av kjøretidskoblingen. Denne symbolske formen må inneholde:
Klassefilformatsspesifikasjonen inkluderer en del av filen som heter konstant basseng , en tabell over alle referanser som linkeren trenger. Den inneholder oppføringer av forskjellige typer.
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
Den første byten i hver oppføring er en numerisk tag som indikerer typen oppføring. De resterende byte gir informasjon om verdien av oppføringen. Antall byte og regler for tolkning avhenger av typen som er angitt av den første byten.
For eksempel en Java-klasse som bruker et konstant heltall 365
kan ha en konstant bassengoppføring med følgende bytekode:
x03 00 00 01 6D
Den første byten, x03
, identifiserer oppføringstypen, CONSTANT_Integer
. Dette informerer koblingen om at de neste fire byte inneholder verdien av heltallet. (Merk at 365 i heksadesimal er x16D
). Hvis dette er den 14. oppføringen i det konstante bassenget, javap -v
vil gjengi det slik:
#14 = Integer 365
Mange konstante typer er sammensatt av referanser til mer “primitive” konstante typer andre steder i det konstante bassenget. Eksempelkoden vår inneholder for eksempel utsagnet:
println( 'Calculating perimeter...' )
Bruk av en strengkonstant vil gi to oppføringer i den konstante puljen: en oppføring med typen CONSTANT_String
, og en annen oppføring av typen CONSTANT_Utf8
. Oppføringen av typen Constant_UTF8
inneholder den faktiske UTF8-representasjonen av strengverdien. Oppføringen av typen CONSTANT_String
inneholder en referanse til CONSTANT_Utf8
inngang:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
En slik komplikasjon er nødvendig fordi det er andre typer konstante bassengoppføringer som refererer til oppføringer av typen Utf8
og som ikke er oppføringer av typen String
. For eksempel vil enhver referanse til et klasseattributt produsere et CONSTANT_Fieldref
type, som inneholder en serie referanser til klassenavn, attributtnavn og attributtype:
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
For mer informasjon om det konstante bassenget, se JVM-dokumentasjonen .
En klassefil inneholder en feltbord som inneholder informasjon om hvert felt (dvs. attributt) definert i klassen. Dette er referanser til konstante bassengoppføringer som beskriver feltets navn og type, samt tilgangskontrollflagg og andre relevante data.
En lik metodetabell er tilstede i klassefilen. I tillegg til navn og typeinformasjon inneholder den for hver ikke-abstrakte metode de faktiske bytekodeinstruksjonene som skal utføres av JVM, samt datastrukturer som brukes av metodens stabelramme, beskrevet nedenfor.
JVM bruker sitt eget interne instruksjonssett for å utføre kompilert kode. Løping javap
med -c
alternativet inkluderer kompilerte metodeimplementeringer i utdataene. Hvis vi undersøker vår RegularPolygon.class
fil på denne måten, vil vi se følgende utdata for getPerimeter()
metode:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
Den faktiske bytekoden kan se ut slik:
xB2 00 17 x12 19 xB6 00 1D x27 ...
Hver instruksjon starter med en byte opcode identifisering av JVM-instruksjonen, etterfulgt av null eller flere instruksjonsoperander som skal opereres, avhengig av formatet til den spesifikke instruksjonen. Disse er vanligvis enten konstante verdier, eller referanser til den konstante puljen. javap
oversetter nyttig bytekoden til en lesbar form som viser:
Operander som vises med et pundtegn, for eksempel #23
, er referanser til oppføringer i den konstante puljen. Som vi kan se, javap
gir også nyttige kommentarer i utdataene, som identifiserer hva det refereres til fra bassenget.
Vi diskuterer noen av de vanlige instruksjonene nedenfor. For detaljert informasjon om hele JVM-instruksjonen, se dokumentasjon .
Hver metodeanrop må kunne kjøre med sin egen kontekst, som inkluderer ting som lokalt deklarerte variabler, eller argumenter som ble sendt til metoden. Sammen utgjør disse en stabelramme . Ved påkallelse av en metode opprettes en ny ramme og plasseres på toppen av ringestabel . Når metoden returnerer, fjernes den nåværende rammen fra samtalestakken og kastes, og rammen som var i kraft før metoden ble kalt, blir gjenopprettet.
En stabelramme inkluderer noen få forskjellige strukturer. To viktige er operandstakken og lokal variabeltabell , diskutert neste.
Mange JVM-instruksjoner fungerer på rammene operandstakken . I stedet for å spesifisere en konstant operand eksplisitt i bytekoden, tar disse instruksjonene i stedet verdiene på toppen av operandstakken som inngang. Vanligvis blir disse verdiene fjernet fra stakken i prosessen. Noen instruksjoner plasserer også nye verdier på toppen av bunken. På denne måten kan JVM-instruksjoner kombineres for å utføre komplekse operasjoner. For eksempel uttrykket:
sideLength * this.numSides
er samlet til følgende i vår getPerimeter()
metode:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
, skyver objektreferansen fra spor 1 i den lokale variabeltabellen (diskutert neste) på operandstakken. I dette tilfellet er dette metodeargumentet sideLength
.- Den neste instruksjonen, aload_0
, skyver objektreferansen i spalte 0 i den lokale variabeltabellen til operandstakken. I praksis er dette nesten alltid referansen til this
, den nåværende klassen.invokevirtual #31
, som utfører forekomstmetoden numSides()
. invokevirtual
spretter den øverste operanden (referansen til this
) av bunken for å identifisere fra hvilken klasse den må kalle metoden. Når metoden kommer tilbake, skyves resultatet på bunken.numSides
) i heltallformat. Det må konverteres til et dobbelt flytpunktsformat for å multiplisere det med en annen dobbel verdi. Instruksjonen i2d
popper heltallets verdi fra stabelen, konverterer den til flytende punktformat og skyver den tilbake på stabelen.this.numSides
på toppen, etterfulgt av verdien av sideLength
argument som ble overført til metoden. dmul
popper disse to øverste verdiene fra stabelen, utfører multiplikasjon med flytende punkt på dem og skyver resultatet på stabelen.Når en metode kalles, opprettes en ny operandstak som en del av dens stableramme, der operasjoner vil bli utført. Vi må være forsiktige med terminologi her: ordet 'stack' kan referere til ringestabel , bunken med rammer som gir kontekst for metodeutførelse, eller til en bestemt ramme operandstakken , som JVM-instruksjoner bruker.
Hver stableramme holder et bord med lokale variabler . Dette inkluderer vanligvis en referanse til this
objekt, eventuelle argumenter som ble sendt da metoden ble kalt, og eventuelle lokale variabler deklarert i metodens kropp. Løping javap
med -v
alternativet inkluderer informasjon om hvordan hver metodes stabelramme skal settes opp, inkludert den lokale variabeltabellen:
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
I dette eksemplet er det to lokale variabler. Variabelen i spor 0 heter this
, med typen RegularPolygon
. Dette er referansen til metodens egen klasse. Variabelen i spor 1 heter sideLength
, med typen D
(indikerer en dobbel). Dette er argumentet som sendes til vår getPerimeter()
metode.
Instruksjoner som iload_1
, fstore_2
eller aload [n]
, overfører forskjellige typer lokale variabler mellom operandstakken og den lokale variabeltabellen. Siden det første elementet i tabellen vanligvis er henvisningen til this
, vil instruksjonen aload_0
er ofte sett i en hvilken som helst metode som fungerer i sin egen klasse.
Dette avslutter vår gjennomgang av grunnleggende om JVM. Klikk her for å gå tilbake til hovedartikkelen.