: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

Прљаве руке помоћу Сцала ЈВМ битецоде-а

Сцала језик наставља да стиче популарност током последњих неколико година, захваљујући својој одличној комбинацији функционални и објектно оријентисани принципи развоја софтвера , и његова примена поврх доказане Јава виртуелне машине (ЈВМ).

Иако Мердевине компајлира се у Јава бајт код, дизајниран је да побољша многе уочене недостатке Јава језика. Нудећи потпуну функционалну подршку за програмирање, Сцала-ова основна синтакса садржи многе имплицитне структуре које Јава програмери морају изричито изградити, од којих неке укључују знатну сложеност.

Стварање језика који се компајлира у бајт код Јава захтева дубоко разумевање унутрашњег рада Јава виртуелне машине. Да бисмо увидели шта су Сцала-ини програмери постигли, потребно је проћи испод хаубе и истражити како компајлер тумачи изворни код Сцала-е како би произвео ефикасан и ефективан ЈВМ битецоде.



Погледајмо како су све ове ствари примењене.

Предуслови

Читање овог чланка захтева основно разумевање бајткода Јава виртуелне машине. Комплетну спецификацију виртуелне машине можете добити од Орацлеова званична документација . Читање целе спецификације није пресудно за разумевање овог чланка, па сам за дно кратког увода у основе припремио кратак водич на дну чланка.

Кликните овде да бисте прочитали курс о паду о основама ЈВМ-а.

Услужни програм је потребан за растављање Јава бајт-кода за репродукцију примера наведених у наставку и за наставак даљег испитивања. Јава Девелопмент Кит обезбеђује сопствени услужни програм за командну линију, javap, који ћемо овде користити. Брза демонстрација како javap радови су укључени у водичу на дну .

И наравно, радна инсталација компајлера Сцала неопходна је читаоцима који желе да следе примере. Овај чланак је написан помоћу Скала 2.11.7 . Различите верзије Сцале могу произвести мало другачији бајт код.

Подразумевани гетери и сетери

Иако Јава конвенција увек обезбеђује методе добивања и постављања за јавне атрибуте, Јава програмери су дужни да их сами напишу, упркос чињеници да се образац за сваки од њих није мењао деценијама. За разлику од тога, Сцала нуди подразумеване гетере и постављаче.

Погледајмо следећи пример:

class Person(val name:String) { }

Погледајмо класу Person. Ако компајлирамо ову датотеку са scalac, тада се изводи $ javap -p Person.class даје нам:

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 са var у Person извор и прекомпајлирајте, затим поље је final модификатор се испушта, а додаје се и метода постављача:

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 или var је дефинисан унутар тела класе, затим се креирају одговарајућа приватна поља и методе приступа и одговарајуће иницијализују приликом креирања инстанце.

Имајте на уму да таква примена нивоа класе val и var поља значи да ће се, ако се неке променљиве користе на нивоу класе за складиштење средњих вредности, а програмер им никада не приступи директно, иницијализација сваког таквог поља додати један до два метода у отисак класе. Додавање private модификатор за таква поља не значи да ће одговарајући приступници бити одбачени. Они ће тек постати приватни.

Дефиниције променљивих и функција

Претпоставимо да имамо метод, m(), и креирамо три различите референце у стилу Сцала на ову функцију:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Каква је свака од ових референци на m изграђена? Када се m извршити у сваком случају? Погледајмо резултујући бајт код. Следећи излаз приказује резултате javap -v Person.class (изостављајући пуно сувишних резултата):

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() се чува у индексу #30. У коду конструктора видимо да се ова метода позива два пута током иницијализације, са упутством invokevirtual #30 појављује се прво у одмаку бајта 11, а затим одмаку 19. Првом позиву следи упутство putfield #22 која додељује резултат ове методе пољу m1, на које се позива индекс #22 у сталном базену. Друго позивање прати исти образац, овај пут додељујући вредност пољу m2, индексирано на #24 у сталном базену.

Другим речима, додељивање методе променљивој дефинисаној са val или var само додељује резултат методе на ту променљиву. Видимо да су методе m1() и m2() који су створени су једноставно гетери за ове променљиве. У случају var m2, такође видимо да је постављач m2_$eq(int) креира се, који се понаша као и сваки други постављач, преписујући вредност у пољу.

Међутим, коришћењем кључне речи def даје другачији резултат. Уместо да преузме вредност поља за враћање, метода m3() такође укључује упутство invokevirtual #30. Односно, сваки пут када се ова метода позове, она тада позива m() и враћа резултат ове методе.

Дакле, као што видимо, Сцала пружа три начина рада са пољима класе, а она се лако наводе помоћу кључних речи val, var и def. У Јави бисмо морали изричито да имплементирамо потребне постављаче и гетере, а такав ручно написан шаблонски код био би много мање изражајан и склонији грешкама.

Лене вредности

Компликованији код се производи када се декларише лења вредност. Претпоставимо да смо у претходно дефинисану класу додали следеће поље:

lazy val m4 = m

Трчање javap -p -v Person.class сада ће открити следеће:

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 се не рачуна док није потребно. Посебна, приватна метода m4$lzycompute() се производи за израчунавање лење вредности, а поље bitmap$0 да прати његово стање. Метод m4() проверава да ли је вредност овог поља 0, што значи да је m4 још увек није иницијализован, у ком случају m4$lzycompute() се позива, попуњавајући m4 и враћање његове вредности. Овај приватни метод такође поставља вредност bitmap$0 на 1, тако да следећи пут m4() назива се прескочит ће позивање методе иницијализације и умјесто тога једноставно вратити вриједност m4.

Резултати првог позива на Сцала лењу вредност.

Бајткод који Сцала овде производи је дизајниран да буде сигуран и ефикасан. Да би био сигуран за нит, метода лењег израчуна користи monitorenter / monitorexit пар упутстава. Метода остаје ефикасна, јер се режијске перформансе ове синхронизације јављају само при првом читању лење вредности.

За означавање стања лење вредности потребан је само један бит. Дакле, ако нема више од 32 лење вредности, једно инт поље може их све пратити. Ако је у изворном коду дефинисано више од једне лење вредности, преводилац ће модификовати горњи бајт код да би у ту сврху применио битну маску.

Опет, Сцала нам омогућава да лако искористимо одређене врсте понашања које би морале бити експлицитно примењене у Јави, штедећи напор и смањујући ризик од грешака у куцању.

Функција као вредност

Сада погледајмо следећи Сцала изворни код:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Тхе Printer класа има једно поље, output, са типом String => Unit: функција која узима String и враћа објект типа Unit (слично void у Јави). У главној методи креирамо један од ових објеката и додељујемо овом пољу анонимну функцију која исписује дати низ.

Компајлирањем овог кода генеришу се четири датотеке класе:

Изворни код је компајлиран у четири датотеке класе.

Hello.class је класа омотача чија главна метода једноставно позива 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 садржи стварну примену главне методе. Да бисте погледали његов бајт код, уверите се да сте исправно избегли $ према правилима ваше командне љуске, да бисте избегли њено тумачење као посебан карактер:

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. Затим креира Hello$$anonfun$1, која садржи нашу анонимну функцију s => println(s). Тхе Printer се иницијализује овим објектом као output поље. Ово поље се затим учита у стек и извршава са операндом 'Hello'.

Погледајмо класу анонимних функција, Hello$$anonfun$1.class, доле. Можемо видети да проширује Сцала'с Function1 (као AbstractFunction1) применом apply() метода. Заправо, он ствара два apply() методе, једна умотавајући другу, које заједно извршавају проверу типа (у овом случају, да је улаз String) и извршавају анонимну функцију (испис улаза са 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() горе описане методе, можемо видети да се у одмаку 21 извршавање анонимне функције покреће позивом на apply( Object ) метода.

На крају, за потпуност, погледајмо бајт код за 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 променљива. Чува се у пољу класе output, а гетер output() је створен. Једина разлика је у томе што ова променљива сада мора да имплементира Сцала интерфејс scala.Function1 (што AbstractFunction1 чини).

Дакле, цена ове елегантне Сцала функције су основне класе услужних програма, створене да представљају и извршавају једну анонимну функцију која се може користити као вредност. Требали бисте узети у обзир број таквих функција, као и детаље ваше имплементације ВМ-а, да бисте схватили шта то значи за вашу одређену апликацију.

Проћи испод хаубе са Сцалом: Истражите како је овај моћни језик имплементиран у ЈВМ бајткод. Твеет

Сцала Траитс

Скалине особине сличне су интерфејсима у Јави. Следећа особина дефинише два потписа методе и пружа подразумевану имплементацију другог. Погледајмо како се примењује:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изворни код је компајлиран у две датотеке класе.

Израђују се два ентитета: Similarity.class, интерфејс који декларише обе методе и синтетичка класа, Similarity$class.class, пружајући подразумевану имплементацију:

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, преводилац Сцала генерише наредбу бајт кода invokestatic да позове статички метод који пружа пратећа класа.

Комплексни полиморфизам и наследне структуре могу се створити од особина. На пример, више особина, као и класа примене, сви могу надјачати методу са истим потписом, позивајући super.methodName() да се контрола пренесе на следећу особину. Када компајлер Сцала наиђе на такве позиве, он:

Тако можемо видети да се моћан концепт особина примењује на нивоу ЈВМ-а на начин који не доводи до значајних трошкова, а програмери Сцала могу уживати у овој функцији не бринући се да ће она бити прескупа током извођења.

Једнокреветне

Сцала даје експлицитну дефиницију синглетон класа користећи кључну реч object. Размотримо следећу класу синглетон:

object Config { val home_dir = '/home/user' }

Компајлер производи две датотеке класе:

Изворни код је компајлиран у две датотеке класе.

Config.class је прилично једноставан:

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$ класа која уграђује синглетон-ову функционалност. Испитивање те класе са javap -p -c производи следећи бајт код:

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

Састоји се од следећег:

Синглетон је популаран и користан образац дизајна. Језик Јава не пружа директан начин да га одредите на нивоу језика; већ је одговорност програмера да је примени у Јава извору. Сцала, с друге стране, пружа јасан и прикладан начин да се синглетон експлицитно декларише помоћу object кључна реч. Као што видимо гледајући испод хаубе, имплементиран је на приступачан и природан начин.

Закључак

Сада смо видели како Сцала компајлира неколико имплицитних и функционалних програмских карактеристика у софистициране Јава структуре бајт кода. Са овим погледом на унутрашње деловање Сцале, можемо дубље увидети Сцала-ину моћ, помажући нам да максимално искористимо овај моћни језик.

Такође сада имамо алате да сами истражујемо језик. Много је корисних функција синтаксе Сцала које нису обрађене у овом чланку, као што су класе случајева, каририрање и разумевање листе. Предлажем вам да сами истражите Сцала-ову примену ових структура како бисте научили како да будете Сцала ниња следећег нивоа!


Јава виртуелна машина: курс пада

Баш као и Јава компајлер, Сцала компајлер претвара изворни код у .class датотеке које садрже Јава бајт код који извршава Јава виртуелна машина. Да би се разумело како се два језика разликују испод хаубе, неопходно је разумети систем који циљају оба. Овде представљамо кратки преглед неких главних елемената архитектуре Јава виртуелне машине, структуре датотека класе и основа асемблера.

Имајте на уму да ће овај водич покривати само минимум који ће омогућити праћење заједно са горњим чланком. Иако се овде не расправља о многим главним компонентама ЈВМ-а, потпуни детаљи могу се наћи у званичним документима, овде .

Декомпајлирање датотека класе са javap
Константни базен
Табеле поља и метода
ЈВМ Битецоде
Позиви метода и скуп позива
Извршење на стеку операнда
Локалне променљиве
Повратак на врх

Декомпајлирање датотека класе са javap

Јава се испоручује са javap услужни програм командне линије, који декомпајлира .class датотеке у читљивом облику. Пошто датотеке класе Сцала и Јава циљају исти ЈВМ, javap може се користити за испитивање датотека класа које је саставила Сцала.

Саставимо следећи изворни код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компајлирајући ово са scalac RegularPolygon.scala произвешће RegularPolygon.class. Ако онда покренемо javap RegularPolygon.class видећемо следеће:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Ово је врло једноставна рашчламба датотеке класе која једноставно приказује имена и типове јавних чланова класе. Додавање -p опција укључује приватне чланове:

$ 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 опција:

$ 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 или -verbose опција, као у javap -p -v RegularPolygon.class:

Комплетан садржај датотеке класе Јава.

Овде коначно видимо шта се заиста налази у датотеци предавања. Шта све ово значи? Погледајмо неке од најважнијих делова.

Константни базен

Развојни циклус за Ц ++ апликације укључује фазе компајлирања и повезивања. Развојни циклус за Јаву прескаче експлицитну фазу повезивања, јер се повезивање дешава током извршавања. Датотека класе мора подржавати ово рунтиме повезивање. То значи да када се изворни код односи на било које поље или методу, резултујући бајт код мора да садржи релевантне референце у симболичном облику, спремне за дереференцирање након што се апликација учита у меморију и извршилац може да разреши стварне адресе. Овај симболички образац мора да садржи:

Спецификација формата датотеке класе укључује одељак датотеке који се назива стални базен , табела свих референци потребних повезивачу. Садржи уносе различитих врста.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Први бајт сваког уноса је нумеричка ознака која означава врсту уноса. Преостали бајтови пружају информације о вредности уноса. Број бајтова и правила за њихово тумачење зависе од врсте назначене првим бајтом.

На пример, Јава класа која користи константни цели број 365 може имати константан унос базена са следећим бајт кодом:

x03 00 00 01 6D

Први бајт, x03, идентификује тип уноса, CONSTANT_Integer. Ово обавештава повезивач да следећа четири бајта садрже вредност целог броја. (Имајте на уму да је 365 у хексадецималном облику x16D). Ако је ово 14. унос у константном спремишту, javap -v приказаће га овако:

#14 = Integer 365

Многи типови константи састоје се од референци на „примитивније“ типове константи негде другде у спремишту константи. На пример, наш пример кода садржи изјаву:

println( 'Calculating perimeter...' )

Коришћење стринг константе произвешће два уноса у спремишту константи: један унос са типом CONSTANT_String , и још један унос типа CONSTANT_Utf8. Унос типа Constant_UTF8 садржи стварни УТФ8 приказ вредности низа. Унос типа CONSTANT_String садржи референцу на CONSTANT_Utf8 улаз:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Таква компликација је неопходна јер постоје друге врсте константних уноса у спремишту које се односе на уносе типа Utf8 а то нису уноси типа String. На пример, свака референца на атрибут класе произвешће CONSTANT_Fieldref типе, који садржи низ референци на име класе, име атрибута и тип атрибута:

#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

За више детаља о константном базену, погледајте ЈВМ документација .

Табеле поља и метода

Датотека класе садржи пољски сто који садржи информације о сваком пољу (тј. атрибуту) дефинисаном у класи. То су референце на константне уносе у спремишту који описују име и тип поља, као и заставице за контролу приступа и друге релевантне податке.

Сличан табела метода је присутан у датотеци класе. Међутим, поред података о имену и типу, за сваку неастрактну методу садржи стварне инструкције бајт-кода које ЈВМ треба да изврши, као и структуре података које користи оквир стека методе, описане у наставку.

ЈВМ Битецоде

ЈВМ користи сопствени интерни скуп инструкција за извршавање компајлираног кода. Трчање javap са -c опција укључује компајлиране имплементације метода у излаз. Ако испитамо наш RegularPolygon.class датотеку на овај начин, видећемо следећи излаз за наше getPerimeter() метода:

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 корисно преводи бајт код у читљив облик који приказује:

Операнди који се приказују знаком фунте, као што је #23, референца су на уносе у спремишту константи. Као што видимо, javap такође даје корисне коментаре у излазу, идентификујући шта се тачно референцира из базена.

У наставку ћемо размотрити неколико уобичајених упутстава. За детаљне информације о комплетном скупу ЈВМ упутстава погледајте документација .

Позиви метода и скуп позива

Сваки позив методе мора бити у могућности да се изводи са сопственим контекстом, који укључује ствари као што су локално декларисане променљиве или аргументи који су прослеђени методи. Заједно, ови чине а стацк фраме . По позивању методе креира се нови оквир који се поставља на врх скуп позива . Када се метода врати, тренутни оквир се уклања из низа позива и одбацује, а оквир који је био на снази пре позивања методе се враћа.

Оквир слога укључује неколико различитих структура. Две важне су стек операнда и табела локалних променљивих , о којој се даље говори.

ЈВМ низ позива.

Извршење на стеку операнда

Многа ЈВМ упутства делују на њиховим оквирима стек операнда . Уместо да експлицитно наведу константни операнд у бајт-коду, ова упутства уместо тога узимају вредности на врху стека операнда као улаз. Обично се ове вредности уклањају из стека у процесу. Нека упутства такође постављају нове вредности на врх стека. На овај начин се ЈВМ упутства могу комбиновати за обављање сложених операција. На пример, израз:

sideLength * this.numSides

је компајлирано на следећи начин у нашем getPerimeter() метода:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

ЈВМ упутства могу да раде на стеку операнда за обављање сложених функција.

Када се метода позове, нови стек операнда се креира као део њеног оквира стека, где ће се извршавати операције. Овде морамо бити опрезни са терминологијом: реч „стог“ може се односити на скуп позива , сноп оквира који пружа контекст за извршавање методе или одређеном оквиру стек операнда , на основу којих делују ЈВМ упутства.

Локалне променљиве

Сваки оквир стека садржи таблицу локалне променљиве . То обично укључује референцу на this објецт, било који аргументи који су прослеђени када је метода позвана, и све локалне променљиве декларисане у телу методе. Трчање javap са -v опција ће садржати информације о томе како треба поставити оквир стека сваке методе, укључујући његову табелу локалних променљивих:

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

У овом примеру постоје две локалне променљиве. Променљива у слоту 0 назива се this, са типом RegularPolygon. Ово је референца на властиту класу методе. Променљива у слоту 1 се назива sideLength, са типом D (што указује на двоструко). Ово је аргумент који се преноси на наш getPerimeter() метода.

Упутства као што су iload_1, fstore_2 или aload [n], преносе различите врсте локалних променљивих између стека операнда и табеле локалних променљивих. Будући да је прва ставка у табели обично референца на this, упутство aload_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

Прљаве руке помоћу Сцала ЈВМ битецоде-а

Сцала језик наставља да стиче популарност током последњих неколико година, захваљујући својој одличној комбинацији функционални и објектно оријентисани принципи развоја софтвера , и његова примена поврх доказане Јава виртуелне машине (ЈВМ).

Иако Мердевине компајлира се у Јава бајт код, дизајниран је да побољша многе уочене недостатке Јава језика. Нудећи потпуну функционалну подршку за програмирање, Сцала-ова основна синтакса садржи многе имплицитне структуре које Јава програмери морају изричито изградити, од којих неке укључују знатну сложеност.

Стварање језика који се компајлира у бајт код Јава захтева дубоко разумевање унутрашњег рада Јава виртуелне машине. Да бисмо увидели шта су Сцала-ини програмери постигли, потребно је проћи испод хаубе и истражити како компајлер тумачи изворни код Сцала-е како би произвео ефикасан и ефективан ЈВМ битецоде.



Погледајмо како су све ове ствари примењене.

Предуслови

Читање овог чланка захтева основно разумевање бајткода Јава виртуелне машине. Комплетну спецификацију виртуелне машине можете добити од Орацлеова званична документација . Читање целе спецификације није пресудно за разумевање овог чланка, па сам за дно кратког увода у основе припремио кратак водич на дну чланка.

Кликните овде да бисте прочитали курс о паду о основама ЈВМ-а.

Услужни програм је потребан за растављање Јава бајт-кода за репродукцију примера наведених у наставку и за наставак даљег испитивања. Јава Девелопмент Кит обезбеђује сопствени услужни програм за командну линију, javap, који ћемо овде користити. Брза демонстрација како javap радови су укључени у водичу на дну .

И наравно, радна инсталација компајлера Сцала неопходна је читаоцима који желе да следе примере. Овај чланак је написан помоћу Скала 2.11.7 . Различите верзије Сцале могу произвести мало другачији бајт код.

Подразумевани гетери и сетери

Иако Јава конвенција увек обезбеђује методе добивања и постављања за јавне атрибуте, Јава програмери су дужни да их сами напишу, упркос чињеници да се образац за сваки од њих није мењао деценијама. За разлику од тога, Сцала нуди подразумеване гетере и постављаче.

Погледајмо следећи пример:

class Person(val name:String) { }

Погледајмо класу Person. Ако компајлирамо ову датотеку са scalac, тада се изводи $ javap -p Person.class даје нам:

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 са var у Person извор и прекомпајлирајте, затим поље је final модификатор се испушта, а додаје се и метода постављача:

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 или var је дефинисан унутар тела класе, затим се креирају одговарајућа приватна поља и методе приступа и одговарајуће иницијализују приликом креирања инстанце.

Имајте на уму да таква примена нивоа класе val и var поља значи да ће се, ако се неке променљиве користе на нивоу класе за складиштење средњих вредности, а програмер им никада не приступи директно, иницијализација сваког таквог поља додати један до два метода у отисак класе. Додавање private модификатор за таква поља не значи да ће одговарајући приступници бити одбачени. Они ће тек постати приватни.

Дефиниције променљивих и функција

Претпоставимо да имамо метод, m(), и креирамо три различите референце у стилу Сцала на ову функцију:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Каква је свака од ових референци на m изграђена? Када се m извршити у сваком случају? Погледајмо резултујући бајт код. Следећи излаз приказује резултате javap -v Person.class (изостављајући пуно сувишних резултата):

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() се чува у индексу #30. У коду конструктора видимо да се ова метода позива два пута током иницијализације, са упутством invokevirtual #30 појављује се прво у одмаку бајта 11, а затим одмаку 19. Првом позиву следи упутство putfield #22 која додељује резултат ове методе пољу m1, на које се позива индекс #22 у сталном базену. Друго позивање прати исти образац, овај пут додељујући вредност пољу m2, индексирано на #24 у сталном базену.

Другим речима, додељивање методе променљивој дефинисаној са val или var само додељује резултат методе на ту променљиву. Видимо да су методе m1() и m2() који су створени су једноставно гетери за ове променљиве. У случају var m2, такође видимо да је постављач m2_$eq(int) креира се, који се понаша као и сваки други постављач, преписујући вредност у пољу.

Међутим, коришћењем кључне речи def даје другачији резултат. Уместо да преузме вредност поља за враћање, метода m3() такође укључује упутство invokevirtual #30. Односно, сваки пут када се ова метода позове, она тада позива m() и враћа резултат ове методе.

Дакле, као што видимо, Сцала пружа три начина рада са пољима класе, а она се лако наводе помоћу кључних речи val, var и def. У Јави бисмо морали изричито да имплементирамо потребне постављаче и гетере, а такав ручно написан шаблонски код био би много мање изражајан и склонији грешкама.

Лене вредности

Компликованији код се производи када се декларише лења вредност. Претпоставимо да смо у претходно дефинисану класу додали следеће поље:

lazy val m4 = m

Трчање javap -p -v Person.class сада ће открити следеће:

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 се не рачуна док није потребно. Посебна, приватна метода m4$lzycompute() се производи за израчунавање лење вредности, а поље bitmap$0 да прати његово стање. Метод m4() проверава да ли је вредност овог поља 0, што значи да је m4 још увек није иницијализован, у ком случају m4$lzycompute() се позива, попуњавајући m4 и враћање његове вредности. Овај приватни метод такође поставља вредност bitmap$0 на 1, тако да следећи пут m4() назива се прескочит ће позивање методе иницијализације и умјесто тога једноставно вратити вриједност m4.

Резултати првог позива на Сцала лењу вредност.

Бајткод који Сцала овде производи је дизајниран да буде сигуран и ефикасан. Да би био сигуран за нит, метода лењег израчуна користи monitorenter / monitorexit пар упутстава. Метода остаје ефикасна, јер се режијске перформансе ове синхронизације јављају само при првом читању лење вредности.

За означавање стања лење вредности потребан је само један бит. Дакле, ако нема више од 32 лење вредности, једно инт поље може их све пратити. Ако је у изворном коду дефинисано више од једне лење вредности, преводилац ће модификовати горњи бајт код да би у ту сврху применио битну маску.

Опет, Сцала нам омогућава да лако искористимо одређене врсте понашања које би морале бити експлицитно примењене у Јави, штедећи напор и смањујући ризик од грешака у куцању.

Функција као вредност

Сада погледајмо следећи Сцала изворни код:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Тхе Printer класа има једно поље, output, са типом String => Unit: функција која узима String и враћа објект типа Unit (слично void у Јави). У главној методи креирамо један од ових објеката и додељујемо овом пољу анонимну функцију која исписује дати низ.

Компајлирањем овог кода генеришу се четири датотеке класе:

Изворни код је компајлиран у четири датотеке класе.

Hello.class је класа омотача чија главна метода једноставно позива 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 садржи стварну примену главне методе. Да бисте погледали његов бајт код, уверите се да сте исправно избегли $ према правилима ваше командне љуске, да бисте избегли њено тумачење као посебан карактер:

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. Затим креира Hello$$anonfun$1, која садржи нашу анонимну функцију s => println(s). Тхе Printer се иницијализује овим објектом као output поље. Ово поље се затим учита у стек и извршава са операндом 'Hello'.

Погледајмо класу анонимних функција, Hello$$anonfun$1.class, доле. Можемо видети да проширује Сцала'с Function1 (као AbstractFunction1) применом apply() метода. Заправо, он ствара два apply() методе, једна умотавајући другу, које заједно извршавају проверу типа (у овом случају, да је улаз String) и извршавају анонимну функцију (испис улаза са 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() горе описане методе, можемо видети да се у одмаку 21 извршавање анонимне функције покреће позивом на apply( Object ) метода.

На крају, за потпуност, погледајмо бајт код за 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 променљива. Чува се у пољу класе output, а гетер output() је створен. Једина разлика је у томе што ова променљива сада мора да имплементира Сцала интерфејс scala.Function1 (што AbstractFunction1 чини).

Дакле, цена ове елегантне Сцала функције су основне класе услужних програма, створене да представљају и извршавају једну анонимну функцију која се може користити као вредност. Требали бисте узети у обзир број таквих функција, као и детаље ваше имплементације ВМ-а, да бисте схватили шта то значи за вашу одређену апликацију.

Проћи испод хаубе са Сцалом: Истражите како је овај моћни језик имплементиран у ЈВМ бајткод. Твеет

Сцала Траитс

Скалине особине сличне су интерфејсима у Јави. Следећа особина дефинише два потписа методе и пружа подразумевану имплементацију другог. Погледајмо како се примењује:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изворни код је компајлиран у две датотеке класе.

Израђују се два ентитета: Similarity.class, интерфејс који декларише обе методе и синтетичка класа, Similarity$class.class, пружајући подразумевану имплементацију:

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, преводилац Сцала генерише наредбу бајт кода invokestatic да позове статички метод који пружа пратећа класа.

Комплексни полиморфизам и наследне структуре могу се створити од особина. На пример, више особина, као и класа примене, сви могу надјачати методу са истим потписом, позивајући super.methodName() да се контрола пренесе на следећу особину. Када компајлер Сцала наиђе на такве позиве, он:

Тако можемо видети да се моћан концепт особина примењује на нивоу ЈВМ-а на начин који не доводи до значајних трошкова, а програмери Сцала могу уживати у овој функцији не бринући се да ће она бити прескупа током извођења.

Једнокреветне

Сцала даје експлицитну дефиницију синглетон класа користећи кључну реч object. Размотримо следећу класу синглетон:

object Config { val home_dir = '/home/user' }

Компајлер производи две датотеке класе:

Изворни код је компајлиран у две датотеке класе.

Config.class је прилично једноставан:

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$ класа која уграђује синглетон-ову функционалност. Испитивање те класе са javap -p -c производи следећи бајт код:

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

Састоји се од следећег:

Синглетон је популаран и користан образац дизајна. Језик Јава не пружа директан начин да га одредите на нивоу језика; већ је одговорност програмера да је примени у Јава извору. Сцала, с друге стране, пружа јасан и прикладан начин да се синглетон експлицитно декларише помоћу object кључна реч. Као што видимо гледајући испод хаубе, имплементиран је на приступачан и природан начин.

Закључак

Сада смо видели како Сцала компајлира неколико имплицитних и функционалних програмских карактеристика у софистициране Јава структуре бајт кода. Са овим погледом на унутрашње деловање Сцале, можемо дубље увидети Сцала-ину моћ, помажући нам да максимално искористимо овај моћни језик.

Такође сада имамо алате да сами истражујемо језик. Много је корисних функција синтаксе Сцала које нису обрађене у овом чланку, као што су класе случајева, каририрање и разумевање листе. Предлажем вам да сами истражите Сцала-ову примену ових структура како бисте научили како да будете Сцала ниња следећег нивоа!


Јава виртуелна машина: курс пада

Баш као и Јава компајлер, Сцала компајлер претвара изворни код у .class датотеке које садрже Јава бајт код који извршава Јава виртуелна машина. Да би се разумело како се два језика разликују испод хаубе, неопходно је разумети систем који циљају оба. Овде представљамо кратки преглед неких главних елемената архитектуре Јава виртуелне машине, структуре датотека класе и основа асемблера.

Имајте на уму да ће овај водич покривати само минимум који ће омогућити праћење заједно са горњим чланком. Иако се овде не расправља о многим главним компонентама ЈВМ-а, потпуни детаљи могу се наћи у званичним документима, овде .

Декомпајлирање датотека класе са javap
Константни базен
Табеле поља и метода
ЈВМ Битецоде
Позиви метода и скуп позива
Извршење на стеку операнда
Локалне променљиве
Повратак на врх

Декомпајлирање датотека класе са javap

Јава се испоручује са javap услужни програм командне линије, који декомпајлира .class датотеке у читљивом облику. Пошто датотеке класе Сцала и Јава циљају исти ЈВМ, javap може се користити за испитивање датотека класа које је саставила Сцала.

Саставимо следећи изворни код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компајлирајући ово са scalac RegularPolygon.scala произвешће RegularPolygon.class. Ако онда покренемо javap RegularPolygon.class видећемо следеће:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Ово је врло једноставна рашчламба датотеке класе која једноставно приказује имена и типове јавних чланова класе. Додавање -p опција укључује приватне чланове:

$ 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 опција:

$ 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 или -verbose опција, као у javap -p -v RegularPolygon.class:

Комплетан садржај датотеке класе Јава.

Овде коначно видимо шта се заиста налази у датотеци предавања. Шта све ово значи? Погледајмо неке од најважнијих делова.

Константни базен

Развојни циклус за Ц ++ апликације укључује фазе компајлирања и повезивања. Развојни циклус за Јаву прескаче експлицитну фазу повезивања, јер се повезивање дешава током извршавања. Датотека класе мора подржавати ово рунтиме повезивање. То значи да када се изворни код односи на било које поље или методу, резултујући бајт код мора да садржи релевантне референце у симболичном облику, спремне за дереференцирање након што се апликација учита у меморију и извршилац може да разреши стварне адресе. Овај симболички образац мора да садржи:

Спецификација формата датотеке класе укључује одељак датотеке који се назива стални базен , табела свих референци потребних повезивачу. Садржи уносе различитих врста.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Први бајт сваког уноса је нумеричка ознака која означава врсту уноса. Преостали бајтови пружају информације о вредности уноса. Број бајтова и правила за њихово тумачење зависе од врсте назначене првим бајтом.

На пример, Јава класа која користи константни цели број 365 може имати константан унос базена са следећим бајт кодом:

x03 00 00 01 6D

Први бајт, x03, идентификује тип уноса, CONSTANT_Integer. Ово обавештава повезивач да следећа четири бајта садрже вредност целог броја. (Имајте на уму да је 365 у хексадецималном облику x16D). Ако је ово 14. унос у константном спремишту, javap -v приказаће га овако:

#14 = Integer 365

Многи типови константи састоје се од референци на „примитивније“ типове константи негде другде у спремишту константи. На пример, наш пример кода садржи изјаву:

println( 'Calculating perimeter...' )

Коришћење стринг константе произвешће два уноса у спремишту константи: један унос са типом CONSTANT_String , и још један унос типа CONSTANT_Utf8. Унос типа Constant_UTF8 садржи стварни УТФ8 приказ вредности низа. Унос типа CONSTANT_String садржи референцу на CONSTANT_Utf8 улаз:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Таква компликација је неопходна јер постоје друге врсте константних уноса у спремишту које се односе на уносе типа Utf8 а то нису уноси типа String. На пример, свака референца на атрибут класе произвешће CONSTANT_Fieldref типе, који садржи низ референци на име класе, име атрибута и тип атрибута:

#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

За више детаља о константном базену, погледајте ЈВМ документација .

Табеле поља и метода

Датотека класе садржи пољски сто који садржи информације о сваком пољу (тј. атрибуту) дефинисаном у класи. То су референце на константне уносе у спремишту који описују име и тип поља, као и заставице за контролу приступа и друге релевантне податке.

Сличан табела метода је присутан у датотеци класе. Међутим, поред података о имену и типу, за сваку неастрактну методу садржи стварне инструкције бајт-кода које ЈВМ треба да изврши, као и структуре података које користи оквир стека методе, описане у наставку.

ЈВМ Битецоде

ЈВМ користи сопствени интерни скуп инструкција за извршавање компајлираног кода. Трчање javap са -c опција укључује компајлиране имплементације метода у излаз. Ако испитамо наш RegularPolygon.class датотеку на овај начин, видећемо следећи излаз за наше getPerimeter() метода:

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 корисно преводи бајт код у читљив облик који приказује:

Операнди који се приказују знаком фунте, као што је #23, референца су на уносе у спремишту константи. Као што видимо, javap такође даје корисне коментаре у излазу, идентификујући шта се тачно референцира из базена.

У наставку ћемо размотрити неколико уобичајених упутстава. За детаљне информације о комплетном скупу ЈВМ упутстава погледајте документација .

Позиви метода и скуп позива

Сваки позив методе мора бити у могућности да се изводи са сопственим контекстом, који укључује ствари као што су локално декларисане променљиве или аргументи који су прослеђени методи. Заједно, ови чине а стацк фраме . По позивању методе креира се нови оквир који се поставља на врх скуп позива . Када се метода врати, тренутни оквир се уклања из низа позива и одбацује, а оквир који је био на снази пре позивања методе се враћа.

Оквир слога укључује неколико различитих структура. Две важне су стек операнда и табела локалних променљивих , о којој се даље говори.

ЈВМ низ позива.

Извршење на стеку операнда

Многа ЈВМ упутства делују на њиховим оквирима стек операнда . Уместо да експлицитно наведу константни операнд у бајт-коду, ова упутства уместо тога узимају вредности на врху стека операнда као улаз. Обично се ове вредности уклањају из стека у процесу. Нека упутства такође постављају нове вредности на врх стека. На овај начин се ЈВМ упутства могу комбиновати за обављање сложених операција. На пример, израз:

sideLength * this.numSides

је компајлирано на следећи начин у нашем getPerimeter() метода:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

ЈВМ упутства могу да раде на стеку операнда за обављање сложених функција.

Када се метода позове, нови стек операнда се креира као део њеног оквира стека, где ће се извршавати операције. Овде морамо бити опрезни са терминологијом: реч „стог“ може се односити на скуп позива , сноп оквира који пружа контекст за извршавање методе или одређеном оквиру стек операнда , на основу којих делују ЈВМ упутства.

Локалне променљиве

Сваки оквир стека садржи таблицу локалне променљиве . То обично укључује референцу на this објецт, било који аргументи који су прослеђени када је метода позвана, и све локалне променљиве декларисане у телу методе. Трчање javap са -v опција ће садржати информације о томе како треба поставити оквир стека сваке методе, укључујући његову табелу локалних променљивих:

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

У овом примеру постоје две локалне променљиве. Променљива у слоту 0 назива се this, са типом RegularPolygon. Ово је референца на властиту класу методе. Променљива у слоту 1 се назива sideLength, са типом D (што указује на двоструко). Ово је аргумент који се преноси на наш getPerimeter() метода.

Упутства као што су iload_1, fstore_2 или aload [n], преносе различите врсте локалних променљивих између стека операнда и табеле локалних променљивих. Будући да је прва ставка у табели обично референца на this, упутство aload_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

Прљаве руке помоћу Сцала ЈВМ битецоде-а

Сцала језик наставља да стиче популарност током последњих неколико година, захваљујући својој одличној комбинацији функционални и објектно оријентисани принципи развоја софтвера , и његова примена поврх доказане Јава виртуелне машине (ЈВМ).

Иако Мердевине компајлира се у Јава бајт код, дизајниран је да побољша многе уочене недостатке Јава језика. Нудећи потпуну функционалну подршку за програмирање, Сцала-ова основна синтакса садржи многе имплицитне структуре које Јава програмери морају изричито изградити, од којих неке укључују знатну сложеност.

Стварање језика који се компајлира у бајт код Јава захтева дубоко разумевање унутрашњег рада Јава виртуелне машине. Да бисмо увидели шта су Сцала-ини програмери постигли, потребно је проћи испод хаубе и истражити како компајлер тумачи изворни код Сцала-е како би произвео ефикасан и ефективан ЈВМ битецоде.



Погледајмо како су све ове ствари примењене.

Предуслови

Читање овог чланка захтева основно разумевање бајткода Јава виртуелне машине. Комплетну спецификацију виртуелне машине можете добити од Орацлеова званична документација . Читање целе спецификације није пресудно за разумевање овог чланка, па сам за дно кратког увода у основе припремио кратак водич на дну чланка.

Кликните овде да бисте прочитали курс о паду о основама ЈВМ-а.

Услужни програм је потребан за растављање Јава бајт-кода за репродукцију примера наведених у наставку и за наставак даљег испитивања. Јава Девелопмент Кит обезбеђује сопствени услужни програм за командну линију, javap, који ћемо овде користити. Брза демонстрација како javap радови су укључени у водичу на дну .

И наравно, радна инсталација компајлера Сцала неопходна је читаоцима који желе да следе примере. Овај чланак је написан помоћу Скала 2.11.7 . Различите верзије Сцале могу произвести мало другачији бајт код.

Подразумевани гетери и сетери

Иако Јава конвенција увек обезбеђује методе добивања и постављања за јавне атрибуте, Јава програмери су дужни да их сами напишу, упркос чињеници да се образац за сваки од њих није мењао деценијама. За разлику од тога, Сцала нуди подразумеване гетере и постављаче.

Погледајмо следећи пример:

class Person(val name:String) { }

Погледајмо класу Person. Ако компајлирамо ову датотеку са scalac, тада се изводи $ javap -p Person.class даје нам:

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 са var у Person извор и прекомпајлирајте, затим поље је final модификатор се испушта, а додаје се и метода постављача:

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 или var је дефинисан унутар тела класе, затим се креирају одговарајућа приватна поља и методе приступа и одговарајуће иницијализују приликом креирања инстанце.

Имајте на уму да таква примена нивоа класе val и var поља значи да ће се, ако се неке променљиве користе на нивоу класе за складиштење средњих вредности, а програмер им никада не приступи директно, иницијализација сваког таквог поља додати један до два метода у отисак класе. Додавање private модификатор за таква поља не значи да ће одговарајући приступници бити одбачени. Они ће тек постати приватни.

Дефиниције променљивих и функција

Претпоставимо да имамо метод, m(), и креирамо три различите референце у стилу Сцала на ову функцију:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Каква је свака од ових референци на m изграђена? Када се m извршити у сваком случају? Погледајмо резултујући бајт код. Следећи излаз приказује резултате javap -v Person.class (изостављајући пуно сувишних резултата):

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() се чува у индексу #30. У коду конструктора видимо да се ова метода позива два пута током иницијализације, са упутством invokevirtual #30 појављује се прво у одмаку бајта 11, а затим одмаку 19. Првом позиву следи упутство putfield #22 која додељује резултат ове методе пољу m1, на које се позива индекс #22 у сталном базену. Друго позивање прати исти образац, овај пут додељујући вредност пољу m2, индексирано на #24 у сталном базену.

Другим речима, додељивање методе променљивој дефинисаној са val или var само додељује резултат методе на ту променљиву. Видимо да су методе m1() и m2() који су створени су једноставно гетери за ове променљиве. У случају var m2, такође видимо да је постављач m2_$eq(int) креира се, који се понаша као и сваки други постављач, преписујући вредност у пољу.

Међутим, коришћењем кључне речи def даје другачији резултат. Уместо да преузме вредност поља за враћање, метода m3() такође укључује упутство invokevirtual #30. Односно, сваки пут када се ова метода позове, она тада позива m() и враћа резултат ове методе.

Дакле, као што видимо, Сцала пружа три начина рада са пољима класе, а она се лако наводе помоћу кључних речи val, var и def. У Јави бисмо морали изричито да имплементирамо потребне постављаче и гетере, а такав ручно написан шаблонски код био би много мање изражајан и склонији грешкама.

Лене вредности

Компликованији код се производи када се декларише лења вредност. Претпоставимо да смо у претходно дефинисану класу додали следеће поље:

lazy val m4 = m

Трчање javap -p -v Person.class сада ће открити следеће:

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 се не рачуна док није потребно. Посебна, приватна метода m4$lzycompute() се производи за израчунавање лење вредности, а поље bitmap$0 да прати његово стање. Метод m4() проверава да ли је вредност овог поља 0, што значи да је m4 још увек није иницијализован, у ком случају m4$lzycompute() се позива, попуњавајући m4 и враћање његове вредности. Овај приватни метод такође поставља вредност bitmap$0 на 1, тако да следећи пут m4() назива се прескочит ће позивање методе иницијализације и умјесто тога једноставно вратити вриједност m4.

Резултати првог позива на Сцала лењу вредност.

Бајткод који Сцала овде производи је дизајниран да буде сигуран и ефикасан. Да би био сигуран за нит, метода лењег израчуна користи monitorenter / monitorexit пар упутстава. Метода остаје ефикасна, јер се режијске перформансе ове синхронизације јављају само при првом читању лење вредности.

За означавање стања лење вредности потребан је само један бит. Дакле, ако нема више од 32 лење вредности, једно инт поље може их све пратити. Ако је у изворном коду дефинисано више од једне лење вредности, преводилац ће модификовати горњи бајт код да би у ту сврху применио битну маску.

Опет, Сцала нам омогућава да лако искористимо одређене врсте понашања које би морале бити експлицитно примењене у Јави, штедећи напор и смањујући ризик од грешака у куцању.

Функција као вредност

Сада погледајмо следећи Сцала изворни код:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Тхе Printer класа има једно поље, output, са типом String => Unit: функција која узима String и враћа објект типа Unit (слично void у Јави). У главној методи креирамо један од ових објеката и додељујемо овом пољу анонимну функцију која исписује дати низ.

Компајлирањем овог кода генеришу се четири датотеке класе:

Изворни код је компајлиран у четири датотеке класе.

Hello.class је класа омотача чија главна метода једноставно позива 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 садржи стварну примену главне методе. Да бисте погледали његов бајт код, уверите се да сте исправно избегли $ према правилима ваше командне љуске, да бисте избегли њено тумачење као посебан карактер:

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. Затим креира Hello$$anonfun$1, која садржи нашу анонимну функцију s => println(s). Тхе Printer се иницијализује овим објектом као output поље. Ово поље се затим учита у стек и извршава са операндом 'Hello'.

Погледајмо класу анонимних функција, Hello$$anonfun$1.class, доле. Можемо видети да проширује Сцала'с Function1 (као AbstractFunction1) применом apply() метода. Заправо, он ствара два apply() методе, једна умотавајући другу, које заједно извршавају проверу типа (у овом случају, да је улаз String) и извршавају анонимну функцију (испис улаза са 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() горе описане методе, можемо видети да се у одмаку 21 извршавање анонимне функције покреће позивом на apply( Object ) метода.

На крају, за потпуност, погледајмо бајт код за 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 променљива. Чува се у пољу класе output, а гетер output() је створен. Једина разлика је у томе што ова променљива сада мора да имплементира Сцала интерфејс scala.Function1 (што AbstractFunction1 чини).

Дакле, цена ове елегантне Сцала функције су основне класе услужних програма, створене да представљају и извршавају једну анонимну функцију која се може користити као вредност. Требали бисте узети у обзир број таквих функција, као и детаље ваше имплементације ВМ-а, да бисте схватили шта то значи за вашу одређену апликацију.

Проћи испод хаубе са Сцалом: Истражите како је овај моћни језик имплементиран у ЈВМ бајткод. Твеет

Сцала Траитс

Скалине особине сличне су интерфејсима у Јави. Следећа особина дефинише два потписа методе и пружа подразумевану имплементацију другог. Погледајмо како се примењује:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изворни код је компајлиран у две датотеке класе.

Израђују се два ентитета: Similarity.class, интерфејс који декларише обе методе и синтетичка класа, Similarity$class.class, пружајући подразумевану имплементацију:

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, преводилац Сцала генерише наредбу бајт кода invokestatic да позове статички метод који пружа пратећа класа.

Комплексни полиморфизам и наследне структуре могу се створити од особина. На пример, више особина, као и класа примене, сви могу надјачати методу са истим потписом, позивајући super.methodName() да се контрола пренесе на следећу особину. Када компајлер Сцала наиђе на такве позиве, он:

Тако можемо видети да се моћан концепт особина примењује на нивоу ЈВМ-а на начин који не доводи до значајних трошкова, а програмери Сцала могу уживати у овој функцији не бринући се да ће она бити прескупа током извођења.

Једнокреветне

Сцала даје експлицитну дефиницију синглетон класа користећи кључну реч object. Размотримо следећу класу синглетон:

object Config { val home_dir = '/home/user' }

Компајлер производи две датотеке класе:

Изворни код је компајлиран у две датотеке класе.

Config.class је прилично једноставан:

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$ класа која уграђује синглетон-ову функционалност. Испитивање те класе са javap -p -c производи следећи бајт код:

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

Састоји се од следећег:

Синглетон је популаран и користан образац дизајна. Језик Јава не пружа директан начин да га одредите на нивоу језика; већ је одговорност програмера да је примени у Јава извору. Сцала, с друге стране, пружа јасан и прикладан начин да се синглетон експлицитно декларише помоћу object кључна реч. Као што видимо гледајући испод хаубе, имплементиран је на приступачан и природан начин.

Закључак

Сада смо видели како Сцала компајлира неколико имплицитних и функционалних програмских карактеристика у софистициране Јава структуре бајт кода. Са овим погледом на унутрашње деловање Сцале, можемо дубље увидети Сцала-ину моћ, помажући нам да максимално искористимо овај моћни језик.

Такође сада имамо алате да сами истражујемо језик. Много је корисних функција синтаксе Сцала које нису обрађене у овом чланку, као што су класе случајева, каририрање и разумевање листе. Предлажем вам да сами истражите Сцала-ову примену ових структура како бисте научили како да будете Сцала ниња следећег нивоа!


Јава виртуелна машина: курс пада

Баш као и Јава компајлер, Сцала компајлер претвара изворни код у .class датотеке које садрже Јава бајт код који извршава Јава виртуелна машина. Да би се разумело како се два језика разликују испод хаубе, неопходно је разумети систем који циљају оба. Овде представљамо кратки преглед неких главних елемената архитектуре Јава виртуелне машине, структуре датотека класе и основа асемблера.

Имајте на уму да ће овај водич покривати само минимум који ће омогућити праћење заједно са горњим чланком. Иако се овде не расправља о многим главним компонентама ЈВМ-а, потпуни детаљи могу се наћи у званичним документима, овде .

Декомпајлирање датотека класе са javap
Константни базен
Табеле поља и метода
ЈВМ Битецоде
Позиви метода и скуп позива
Извршење на стеку операнда
Локалне променљиве
Повратак на врх

Декомпајлирање датотека класе са javap

Јава се испоручује са javap услужни програм командне линије, који декомпајлира .class датотеке у читљивом облику. Пошто датотеке класе Сцала и Јава циљају исти ЈВМ, javap може се користити за испитивање датотека класа које је саставила Сцала.

Саставимо следећи изворни код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компајлирајући ово са scalac RegularPolygon.scala произвешће RegularPolygon.class. Ако онда покренемо javap RegularPolygon.class видећемо следеће:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Ово је врло једноставна рашчламба датотеке класе која једноставно приказује имена и типове јавних чланова класе. Додавање -p опција укључује приватне чланове:

$ 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 опција:

$ 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 или -verbose опција, као у javap -p -v RegularPolygon.class:

Комплетан садржај датотеке класе Јава.

Овде коначно видимо шта се заиста налази у датотеци предавања. Шта све ово значи? Погледајмо неке од најважнијих делова.

Константни базен

Развојни циклус за Ц ++ апликације укључује фазе компајлирања и повезивања. Развојни циклус за Јаву прескаче експлицитну фазу повезивања, јер се повезивање дешава током извршавања. Датотека класе мора подржавати ово рунтиме повезивање. То значи да када се изворни код односи на било које поље или методу, резултујући бајт код мора да садржи релевантне референце у симболичном облику, спремне за дереференцирање након што се апликација учита у меморију и извршилац може да разреши стварне адресе. Овај симболички образац мора да садржи:

Спецификација формата датотеке класе укључује одељак датотеке који се назива стални базен , табела свих референци потребних повезивачу. Садржи уносе различитих врста.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Први бајт сваког уноса је нумеричка ознака која означава врсту уноса. Преостали бајтови пружају информације о вредности уноса. Број бајтова и правила за њихово тумачење зависе од врсте назначене првим бајтом.

На пример, Јава класа која користи константни цели број 365 може имати константан унос базена са следећим бајт кодом:

x03 00 00 01 6D

Први бајт, x03, идентификује тип уноса, CONSTANT_Integer. Ово обавештава повезивач да следећа четири бајта садрже вредност целог броја. (Имајте на уму да је 365 у хексадецималном облику x16D). Ако је ово 14. унос у константном спремишту, javap -v приказаће га овако:

#14 = Integer 365

Многи типови константи састоје се од референци на „примитивније“ типове константи негде другде у спремишту константи. На пример, наш пример кода садржи изјаву:

println( 'Calculating perimeter...' )

Коришћење стринг константе произвешће два уноса у спремишту константи: један унос са типом CONSTANT_String , и још један унос типа CONSTANT_Utf8. Унос типа Constant_UTF8 садржи стварни УТФ8 приказ вредности низа. Унос типа CONSTANT_String садржи референцу на CONSTANT_Utf8 улаз:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Таква компликација је неопходна јер постоје друге врсте константних уноса у спремишту које се односе на уносе типа Utf8 а то нису уноси типа String. На пример, свака референца на атрибут класе произвешће CONSTANT_Fieldref типе, који садржи низ референци на име класе, име атрибута и тип атрибута:

#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

За више детаља о константном базену, погледајте ЈВМ документација .

Табеле поља и метода

Датотека класе садржи пољски сто који садржи информације о сваком пољу (тј. атрибуту) дефинисаном у класи. То су референце на константне уносе у спремишту који описују име и тип поља, као и заставице за контролу приступа и друге релевантне податке.

Сличан табела метода је присутан у датотеци класе. Међутим, поред података о имену и типу, за сваку неастрактну методу садржи стварне инструкције бајт-кода које ЈВМ треба да изврши, као и структуре података које користи оквир стека методе, описане у наставку.

ЈВМ Битецоде

ЈВМ користи сопствени интерни скуп инструкција за извршавање компајлираног кода. Трчање javap са -c опција укључује компајлиране имплементације метода у излаз. Ако испитамо наш RegularPolygon.class датотеку на овај начин, видећемо следећи излаз за наше getPerimeter() метода:

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 корисно преводи бајт код у читљив облик који приказује:

Операнди који се приказују знаком фунте, као што је #23, референца су на уносе у спремишту константи. Као што видимо, javap такође даје корисне коментаре у излазу, идентификујући шта се тачно референцира из базена.

У наставку ћемо размотрити неколико уобичајених упутстава. За детаљне информације о комплетном скупу ЈВМ упутстава погледајте документација .

Позиви метода и скуп позива

Сваки позив методе мора бити у могућности да се изводи са сопственим контекстом, који укључује ствари као што су локално декларисане променљиве или аргументи који су прослеђени методи. Заједно, ови чине а стацк фраме . По позивању методе креира се нови оквир који се поставља на врх скуп позива . Када се метода врати, тренутни оквир се уклања из низа позива и одбацује, а оквир који је био на снази пре позивања методе се враћа.

Оквир слога укључује неколико различитих структура. Две важне су стек операнда и табела локалних променљивих , о којој се даље говори.

ЈВМ низ позива.

Извршење на стеку операнда

Многа ЈВМ упутства делују на њиховим оквирима стек операнда . Уместо да експлицитно наведу константни операнд у бајт-коду, ова упутства уместо тога узимају вредности на врху стека операнда као улаз. Обично се ове вредности уклањају из стека у процесу. Нека упутства такође постављају нове вредности на врх стека. На овај начин се ЈВМ упутства могу комбиновати за обављање сложених операција. На пример, израз:

sideLength * this.numSides

је компајлирано на следећи начин у нашем getPerimeter() метода:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

ЈВМ упутства могу да раде на стеку операнда за обављање сложених функција.

Када се метода позове, нови стек операнда се креира као део њеног оквира стека, где ће се извршавати операције. Овде морамо бити опрезни са терминологијом: реч „стог“ може се односити на скуп позива , сноп оквира који пружа контекст за извршавање методе или одређеном оквиру стек операнда , на основу којих делују ЈВМ упутства.

Локалне променљиве

Сваки оквир стека садржи таблицу локалне променљиве . То обично укључује референцу на this објецт, било који аргументи који су прослеђени када је метода позвана, и све локалне променљиве декларисане у телу методе. Трчање javap са -v опција ће садржати информације о томе како треба поставити оквир стека сваке методе, укључујући његову табелу локалних променљивих:

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

У овом примеру постоје две локалне променљиве. Променљива у слоту 0 назива се this, са типом RegularPolygon. Ово је референца на властиту класу методе. Променљива у слоту 1 се назива sideLength, са типом D (што указује на двоструко). Ово је аргумент који се преноси на наш getPerimeter() метода.

Упутства као што су iload_1, fstore_2 или aload [n], преносе различите врсте локалних променљивих између стека операнда и табеле локалних променљивих. Будући да је прва ставка у табели обично референца на this, упутство aload_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

Прљаве руке помоћу Сцала ЈВМ битецоде-а

Сцала језик наставља да стиче популарност током последњих неколико година, захваљујући својој одличној комбинацији функционални и објектно оријентисани принципи развоја софтвера , и његова примена поврх доказане Јава виртуелне машине (ЈВМ).

Иако Мердевине компајлира се у Јава бајт код, дизајниран је да побољша многе уочене недостатке Јава језика. Нудећи потпуну функционалну подршку за програмирање, Сцала-ова основна синтакса садржи многе имплицитне структуре које Јава програмери морају изричито изградити, од којих неке укључују знатну сложеност.

Стварање језика који се компајлира у бајт код Јава захтева дубоко разумевање унутрашњег рада Јава виртуелне машине. Да бисмо увидели шта су Сцала-ини програмери постигли, потребно је проћи испод хаубе и истражити како компајлер тумачи изворни код Сцала-е како би произвео ефикасан и ефективан ЈВМ битецоде.



Погледајмо како су све ове ствари примењене.

Предуслови

Читање овог чланка захтева основно разумевање бајткода Јава виртуелне машине. Комплетну спецификацију виртуелне машине можете добити од Орацлеова званична документација . Читање целе спецификације није пресудно за разумевање овог чланка, па сам за дно кратког увода у основе припремио кратак водич на дну чланка.

Кликните овде да бисте прочитали курс о паду о основама ЈВМ-а.

Услужни програм је потребан за растављање Јава бајт-кода за репродукцију примера наведених у наставку и за наставак даљег испитивања. Јава Девелопмент Кит обезбеђује сопствени услужни програм за командну линију, javap, који ћемо овде користити. Брза демонстрација како javap радови су укључени у водичу на дну .

И наравно, радна инсталација компајлера Сцала неопходна је читаоцима који желе да следе примере. Овај чланак је написан помоћу Скала 2.11.7 . Различите верзије Сцале могу произвести мало другачији бајт код.

Подразумевани гетери и сетери

Иако Јава конвенција увек обезбеђује методе добивања и постављања за јавне атрибуте, Јава програмери су дужни да их сами напишу, упркос чињеници да се образац за сваки од њих није мењао деценијама. За разлику од тога, Сцала нуди подразумеване гетере и постављаче.

Погледајмо следећи пример:

class Person(val name:String) { }

Погледајмо класу Person. Ако компајлирамо ову датотеку са scalac, тада се изводи $ javap -p Person.class даје нам:

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 са var у Person извор и прекомпајлирајте, затим поље је final модификатор се испушта, а додаје се и метода постављача:

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 или var је дефинисан унутар тела класе, затим се креирају одговарајућа приватна поља и методе приступа и одговарајуће иницијализују приликом креирања инстанце.

Имајте на уму да таква примена нивоа класе val и var поља значи да ће се, ако се неке променљиве користе на нивоу класе за складиштење средњих вредности, а програмер им никада не приступи директно, иницијализација сваког таквог поља додати један до два метода у отисак класе. Додавање private модификатор за таква поља не значи да ће одговарајући приступници бити одбачени. Они ће тек постати приватни.

Дефиниције променљивих и функција

Претпоставимо да имамо метод, m(), и креирамо три различите референце у стилу Сцала на ову функцију:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Каква је свака од ових референци на m изграђена? Када се m извршити у сваком случају? Погледајмо резултујући бајт код. Следећи излаз приказује резултате javap -v Person.class (изостављајући пуно сувишних резултата):

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() се чува у индексу #30. У коду конструктора видимо да се ова метода позива два пута током иницијализације, са упутством invokevirtual #30 појављује се прво у одмаку бајта 11, а затим одмаку 19. Првом позиву следи упутство putfield #22 која додељује резултат ове методе пољу m1, на које се позива индекс #22 у сталном базену. Друго позивање прати исти образац, овај пут додељујући вредност пољу m2, индексирано на #24 у сталном базену.

Другим речима, додељивање методе променљивој дефинисаној са val или var само додељује резултат методе на ту променљиву. Видимо да су методе m1() и m2() који су створени су једноставно гетери за ове променљиве. У случају var m2, такође видимо да је постављач m2_$eq(int) креира се, који се понаша као и сваки други постављач, преписујући вредност у пољу.

Међутим, коришћењем кључне речи def даје другачији резултат. Уместо да преузме вредност поља за враћање, метода m3() такође укључује упутство invokevirtual #30. Односно, сваки пут када се ова метода позове, она тада позива m() и враћа резултат ове методе.

Дакле, као што видимо, Сцала пружа три начина рада са пољима класе, а она се лако наводе помоћу кључних речи val, var и def. У Јави бисмо морали изричито да имплементирамо потребне постављаче и гетере, а такав ручно написан шаблонски код био би много мање изражајан и склонији грешкама.

Лене вредности

Компликованији код се производи када се декларише лења вредност. Претпоставимо да смо у претходно дефинисану класу додали следеће поље:

lazy val m4 = m

Трчање javap -p -v Person.class сада ће открити следеће:

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 се не рачуна док није потребно. Посебна, приватна метода m4$lzycompute() се производи за израчунавање лење вредности, а поље bitmap$0 да прати његово стање. Метод m4() проверава да ли је вредност овог поља 0, што значи да је m4 још увек није иницијализован, у ком случају m4$lzycompute() се позива, попуњавајући m4 и враћање његове вредности. Овај приватни метод такође поставља вредност bitmap$0 на 1, тако да следећи пут m4() назива се прескочит ће позивање методе иницијализације и умјесто тога једноставно вратити вриједност m4.

Резултати првог позива на Сцала лењу вредност.

Бајткод који Сцала овде производи је дизајниран да буде сигуран и ефикасан. Да би био сигуран за нит, метода лењег израчуна користи monitorenter / monitorexit пар упутстава. Метода остаје ефикасна, јер се режијске перформансе ове синхронизације јављају само при првом читању лење вредности.

За означавање стања лење вредности потребан је само један бит. Дакле, ако нема више од 32 лење вредности, једно инт поље може их све пратити. Ако је у изворном коду дефинисано више од једне лење вредности, преводилац ће модификовати горњи бајт код да би у ту сврху применио битну маску.

Опет, Сцала нам омогућава да лако искористимо одређене врсте понашања које би морале бити експлицитно примењене у Јави, штедећи напор и смањујући ризик од грешака у куцању.

Функција као вредност

Сада погледајмо следећи Сцала изворни код:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Тхе Printer класа има једно поље, output, са типом String => Unit: функција која узима String и враћа објект типа Unit (слично void у Јави). У главној методи креирамо један од ових објеката и додељујемо овом пољу анонимну функцију која исписује дати низ.

Компајлирањем овог кода генеришу се четири датотеке класе:

Изворни код је компајлиран у четири датотеке класе.

Hello.class је класа омотача чија главна метода једноставно позива 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 садржи стварну примену главне методе. Да бисте погледали његов бајт код, уверите се да сте исправно избегли $ према правилима ваше командне љуске, да бисте избегли њено тумачење као посебан карактер:

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. Затим креира Hello$$anonfun$1, која садржи нашу анонимну функцију s => println(s). Тхе Printer се иницијализује овим објектом као output поље. Ово поље се затим учита у стек и извршава са операндом 'Hello'.

Погледајмо класу анонимних функција, Hello$$anonfun$1.class, доле. Можемо видети да проширује Сцала'с Function1 (као AbstractFunction1) применом apply() метода. Заправо, он ствара два apply() методе, једна умотавајући другу, које заједно извршавају проверу типа (у овом случају, да је улаз String) и извршавају анонимну функцију (испис улаза са 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() горе описане методе, можемо видети да се у одмаку 21 извршавање анонимне функције покреће позивом на apply( Object ) метода.

На крају, за потпуност, погледајмо бајт код за 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 променљива. Чува се у пољу класе output, а гетер output() је створен. Једина разлика је у томе што ова променљива сада мора да имплементира Сцала интерфејс scala.Function1 (што AbstractFunction1 чини).

Дакле, цена ове елегантне Сцала функције су основне класе услужних програма, створене да представљају и извршавају једну анонимну функцију која се може користити као вредност. Требали бисте узети у обзир број таквих функција, као и детаље ваше имплементације ВМ-а, да бисте схватили шта то значи за вашу одређену апликацију.

Проћи испод хаубе са Сцалом: Истражите како је овај моћни језик имплементиран у ЈВМ бајткод. Твеет

Сцала Траитс

Скалине особине сличне су интерфејсима у Јави. Следећа особина дефинише два потписа методе и пружа подразумевану имплементацију другог. Погледајмо како се примењује:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изворни код је компајлиран у две датотеке класе.

Израђују се два ентитета: Similarity.class, интерфејс који декларише обе методе и синтетичка класа, Similarity$class.class, пружајући подразумевану имплементацију:

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, преводилац Сцала генерише наредбу бајт кода invokestatic да позове статички метод који пружа пратећа класа.

Комплексни полиморфизам и наследне структуре могу се створити од особина. На пример, више особина, као и класа примене, сви могу надјачати методу са истим потписом, позивајући super.methodName() да се контрола пренесе на следећу особину. Када компајлер Сцала наиђе на такве позиве, он:

Тако можемо видети да се моћан концепт особина примењује на нивоу ЈВМ-а на начин који не доводи до значајних трошкова, а програмери Сцала могу уживати у овој функцији не бринући се да ће она бити прескупа током извођења.

Једнокреветне

Сцала даје експлицитну дефиницију синглетон класа користећи кључну реч object. Размотримо следећу класу синглетон:

object Config { val home_dir = '/home/user' }

Компајлер производи две датотеке класе:

Изворни код је компајлиран у две датотеке класе.

Config.class је прилично једноставан:

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$ класа која уграђује синглетон-ову функционалност. Испитивање те класе са javap -p -c производи следећи бајт код:

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

Састоји се од следећег:

Синглетон је популаран и користан образац дизајна. Језик Јава не пружа директан начин да га одредите на нивоу језика; већ је одговорност програмера да је примени у Јава извору. Сцала, с друге стране, пружа јасан и прикладан начин да се синглетон експлицитно декларише помоћу object кључна реч. Као што видимо гледајући испод хаубе, имплементиран је на приступачан и природан начин.

Закључак

Сада смо видели како Сцала компајлира неколико имплицитних и функционалних програмских карактеристика у софистициране Јава структуре бајт кода. Са овим погледом на унутрашње деловање Сцале, можемо дубље увидети Сцала-ину моћ, помажући нам да максимално искористимо овај моћни језик.

Такође сада имамо алате да сами истражујемо језик. Много је корисних функција синтаксе Сцала које нису обрађене у овом чланку, као што су класе случајева, каририрање и разумевање листе. Предлажем вам да сами истражите Сцала-ову примену ових структура како бисте научили како да будете Сцала ниња следећег нивоа!


Јава виртуелна машина: курс пада

Баш као и Јава компајлер, Сцала компајлер претвара изворни код у .class датотеке које садрже Јава бајт код који извршава Јава виртуелна машина. Да би се разумело како се два језика разликују испод хаубе, неопходно је разумети систем који циљају оба. Овде представљамо кратки преглед неких главних елемената архитектуре Јава виртуелне машине, структуре датотека класе и основа асемблера.

Имајте на уму да ће овај водич покривати само минимум који ће омогућити праћење заједно са горњим чланком. Иако се овде не расправља о многим главним компонентама ЈВМ-а, потпуни детаљи могу се наћи у званичним документима, овде .

Декомпајлирање датотека класе са javap
Константни базен
Табеле поља и метода
ЈВМ Битецоде
Позиви метода и скуп позива
Извршење на стеку операнда
Локалне променљиве
Повратак на врх

Декомпајлирање датотека класе са javap

Јава се испоручује са javap услужни програм командне линије, који декомпајлира .class датотеке у читљивом облику. Пошто датотеке класе Сцала и Јава циљају исти ЈВМ, javap може се користити за испитивање датотека класа које је саставила Сцала.

Саставимо следећи изворни код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компајлирајући ово са scalac RegularPolygon.scala произвешће RegularPolygon.class. Ако онда покренемо javap RegularPolygon.class видећемо следеће:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Ово је врло једноставна рашчламба датотеке класе која једноставно приказује имена и типове јавних чланова класе. Додавање -p опција укључује приватне чланове:

$ 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 опција:

$ 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 или -verbose опција, као у javap -p -v RegularPolygon.class:

Комплетан садржај датотеке класе Јава.

Овде коначно видимо шта се заиста налази у датотеци предавања. Шта све ово значи? Погледајмо неке од најважнијих делова.

Константни базен

Развојни циклус за Ц ++ апликације укључује фазе компајлирања и повезивања. Развојни циклус за Јаву прескаче експлицитну фазу повезивања, јер се повезивање дешава током извршавања. Датотека класе мора подржавати ово рунтиме повезивање. То значи да када се изворни код односи на било које поље или методу, резултујући бајт код мора да садржи релевантне референце у симболичном облику, спремне за дереференцирање након што се апликација учита у меморију и извршилац може да разреши стварне адресе. Овај симболички образац мора да садржи:

Спецификација формата датотеке класе укључује одељак датотеке који се назива стални базен , табела свих референци потребних повезивачу. Садржи уносе различитих врста.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Први бајт сваког уноса је нумеричка ознака која означава врсту уноса. Преостали бајтови пружају информације о вредности уноса. Број бајтова и правила за њихово тумачење зависе од врсте назначене првим бајтом.

На пример, Јава класа која користи константни цели број 365 може имати константан унос базена са следећим бајт кодом:

x03 00 00 01 6D

Први бајт, x03, идентификује тип уноса, CONSTANT_Integer. Ово обавештава повезивач да следећа четири бајта садрже вредност целог броја. (Имајте на уму да је 365 у хексадецималном облику x16D). Ако је ово 14. унос у константном спремишту, javap -v приказаће га овако:

#14 = Integer 365

Многи типови константи састоје се од референци на „примитивније“ типове константи негде другде у спремишту константи. На пример, наш пример кода садржи изјаву:

println( 'Calculating perimeter...' )

Коришћење стринг константе произвешће два уноса у спремишту константи: један унос са типом CONSTANT_String , и још један унос типа CONSTANT_Utf8. Унос типа Constant_UTF8 садржи стварни УТФ8 приказ вредности низа. Унос типа CONSTANT_String садржи референцу на CONSTANT_Utf8 улаз:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Таква компликација је неопходна јер постоје друге врсте константних уноса у спремишту које се односе на уносе типа Utf8 а то нису уноси типа String. На пример, свака референца на атрибут класе произвешће CONSTANT_Fieldref типе, који садржи низ референци на име класе, име атрибута и тип атрибута:

#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

За више детаља о константном базену, погледајте ЈВМ документација .

Табеле поља и метода

Датотека класе садржи пољски сто који садржи информације о сваком пољу (тј. атрибуту) дефинисаном у класи. То су референце на константне уносе у спремишту који описују име и тип поља, као и заставице за контролу приступа и друге релевантне податке.

Сличан табела метода је присутан у датотеци класе. Међутим, поред података о имену и типу, за сваку неастрактну методу садржи стварне инструкције бајт-кода које ЈВМ треба да изврши, као и структуре података које користи оквир стека методе, описане у наставку.

ЈВМ Битецоде

ЈВМ користи сопствени интерни скуп инструкција за извршавање компајлираног кода. Трчање javap са -c опција укључује компајлиране имплементације метода у излаз. Ако испитамо наш RegularPolygon.class датотеку на овај начин, видећемо следећи излаз за наше getPerimeter() метода:

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 корисно преводи бајт код у читљив облик који приказује:

Операнди који се приказују знаком фунте, као што је #23, референца су на уносе у спремишту константи. Као што видимо, javap такође даје корисне коментаре у излазу, идентификујући шта се тачно референцира из базена.

У наставку ћемо размотрити неколико уобичајених упутстава. За детаљне информације о комплетном скупу ЈВМ упутстава погледајте документација .

Позиви метода и скуп позива

Сваки позив методе мора бити у могућности да се изводи са сопственим контекстом, који укључује ствари као што су локално декларисане променљиве или аргументи који су прослеђени методи. Заједно, ови чине а стацк фраме . По позивању методе креира се нови оквир који се поставља на врх скуп позива . Када се метода врати, тренутни оквир се уклања из низа позива и одбацује, а оквир који је био на снази пре позивања методе се враћа.

Оквир слога укључује неколико различитих структура. Две важне су стек операнда и табела локалних променљивих , о којој се даље говори.

ЈВМ низ позива.

Извршење на стеку операнда

Многа ЈВМ упутства делују на њиховим оквирима стек операнда . Уместо да експлицитно наведу константни операнд у бајт-коду, ова упутства уместо тога узимају вредности на врху стека операнда као улаз. Обично се ове вредности уклањају из стека у процесу. Нека упутства такође постављају нове вредности на врх стека. На овај начин се ЈВМ упутства могу комбиновати за обављање сложених операција. На пример, израз:

sideLength * this.numSides

је компајлирано на следећи начин у нашем getPerimeter() метода:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

ЈВМ упутства могу да раде на стеку операнда за обављање сложених функција.

Када се метода позове, нови стек операнда се креира као део њеног оквира стека, где ће се извршавати операције. Овде морамо бити опрезни са терминологијом: реч „стог“ може се односити на скуп позива , сноп оквира који пружа контекст за извршавање методе или одређеном оквиру стек операнда , на основу којих делују ЈВМ упутства.

Локалне променљиве

Сваки оквир стека садржи таблицу локалне променљиве . То обично укључује референцу на this објецт, било који аргументи који су прослеђени када је метода позвана, и све локалне променљиве декларисане у телу методе. Трчање javap са -v опција ће садржати информације о томе како треба поставити оквир стека сваке методе, укључујући његову табелу локалних променљивих:

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

У овом примеру постоје две локалне променљиве. Променљива у слоту 0 назива се this, са типом RegularPolygon. Ово је референца на властиту класу методе. Променљива у слоту 1 се назива sideLength, са типом D (што указује на двоструко). Ово је аргумент који се преноси на наш getPerimeter() метода.

Упутства као што су iload_1, fstore_2 или aload [n], преносе различите врсте локалних променљивих између стека операнда и табеле локалних променљивих. Будући да је прва ставка у табели обично референца на this, упутство aload_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 се не рачуна док није потребно. Посебна, приватна метода m4$lzycompute() се производи за израчунавање лење вредности, а поље bitmap

Прљаве руке помоћу Сцала ЈВМ битецоде-а

Сцала језик наставља да стиче популарност током последњих неколико година, захваљујући својој одличној комбинацији функционални и објектно оријентисани принципи развоја софтвера , и његова примена поврх доказане Јава виртуелне машине (ЈВМ).

Иако Мердевине компајлира се у Јава бајт код, дизајниран је да побољша многе уочене недостатке Јава језика. Нудећи потпуну функционалну подршку за програмирање, Сцала-ова основна синтакса садржи многе имплицитне структуре које Јава програмери морају изричито изградити, од којих неке укључују знатну сложеност.

Стварање језика који се компајлира у бајт код Јава захтева дубоко разумевање унутрашњег рада Јава виртуелне машине. Да бисмо увидели шта су Сцала-ини програмери постигли, потребно је проћи испод хаубе и истражити како компајлер тумачи изворни код Сцала-е како би произвео ефикасан и ефективан ЈВМ битецоде.



Погледајмо како су све ове ствари примењене.

Предуслови

Читање овог чланка захтева основно разумевање бајткода Јава виртуелне машине. Комплетну спецификацију виртуелне машине можете добити од Орацлеова званична документација . Читање целе спецификације није пресудно за разумевање овог чланка, па сам за дно кратког увода у основе припремио кратак водич на дну чланка.

Кликните овде да бисте прочитали курс о паду о основама ЈВМ-а.

Услужни програм је потребан за растављање Јава бајт-кода за репродукцију примера наведених у наставку и за наставак даљег испитивања. Јава Девелопмент Кит обезбеђује сопствени услужни програм за командну линију, javap, који ћемо овде користити. Брза демонстрација како javap радови су укључени у водичу на дну .

И наравно, радна инсталација компајлера Сцала неопходна је читаоцима који желе да следе примере. Овај чланак је написан помоћу Скала 2.11.7 . Различите верзије Сцале могу произвести мало другачији бајт код.

Подразумевани гетери и сетери

Иако Јава конвенција увек обезбеђује методе добивања и постављања за јавне атрибуте, Јава програмери су дужни да их сами напишу, упркос чињеници да се образац за сваки од њих није мењао деценијама. За разлику од тога, Сцала нуди подразумеване гетере и постављаче.

Погледајмо следећи пример:

class Person(val name:String) { }

Погледајмо класу Person. Ако компајлирамо ову датотеку са scalac, тада се изводи $ javap -p Person.class даје нам:

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 са var у Person извор и прекомпајлирајте, затим поље је final модификатор се испушта, а додаје се и метода постављача:

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 или var је дефинисан унутар тела класе, затим се креирају одговарајућа приватна поља и методе приступа и одговарајуће иницијализују приликом креирања инстанце.

Имајте на уму да таква примена нивоа класе val и var поља значи да ће се, ако се неке променљиве користе на нивоу класе за складиштење средњих вредности, а програмер им никада не приступи директно, иницијализација сваког таквог поља додати један до два метода у отисак класе. Додавање private модификатор за таква поља не значи да ће одговарајући приступници бити одбачени. Они ће тек постати приватни.

Дефиниције променљивих и функција

Претпоставимо да имамо метод, m(), и креирамо три различите референце у стилу Сцала на ову функцију:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Каква је свака од ових референци на m изграђена? Када се m извршити у сваком случају? Погледајмо резултујући бајт код. Следећи излаз приказује резултате javap -v Person.class (изостављајући пуно сувишних резултата):

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() се чува у индексу #30. У коду конструктора видимо да се ова метода позива два пута током иницијализације, са упутством invokevirtual #30 појављује се прво у одмаку бајта 11, а затим одмаку 19. Првом позиву следи упутство putfield #22 која додељује резултат ове методе пољу m1, на које се позива индекс #22 у сталном базену. Друго позивање прати исти образац, овај пут додељујући вредност пољу m2, индексирано на #24 у сталном базену.

Другим речима, додељивање методе променљивој дефинисаној са val или var само додељује резултат методе на ту променљиву. Видимо да су методе m1() и m2() који су створени су једноставно гетери за ове променљиве. У случају var m2, такође видимо да је постављач m2_$eq(int) креира се, који се понаша као и сваки други постављач, преписујући вредност у пољу.

Међутим, коришћењем кључне речи def даје другачији резултат. Уместо да преузме вредност поља за враћање, метода m3() такође укључује упутство invokevirtual #30. Односно, сваки пут када се ова метода позове, она тада позива m() и враћа резултат ове методе.

Дакле, као што видимо, Сцала пружа три начина рада са пољима класе, а она се лако наводе помоћу кључних речи val, var и def. У Јави бисмо морали изричито да имплементирамо потребне постављаче и гетере, а такав ручно написан шаблонски код био би много мање изражајан и склонији грешкама.

Лене вредности

Компликованији код се производи када се декларише лења вредност. Претпоставимо да смо у претходно дефинисану класу додали следеће поље:

lazy val m4 = m

Трчање javap -p -v Person.class сада ће открити следеће:

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 се не рачуна док није потребно. Посебна, приватна метода m4$lzycompute() се производи за израчунавање лење вредности, а поље bitmap$0 да прати његово стање. Метод m4() проверава да ли је вредност овог поља 0, што значи да је m4 још увек није иницијализован, у ком случају m4$lzycompute() се позива, попуњавајући m4 и враћање његове вредности. Овај приватни метод такође поставља вредност bitmap$0 на 1, тако да следећи пут m4() назива се прескочит ће позивање методе иницијализације и умјесто тога једноставно вратити вриједност m4.

Резултати првог позива на Сцала лењу вредност.

Бајткод који Сцала овде производи је дизајниран да буде сигуран и ефикасан. Да би био сигуран за нит, метода лењег израчуна користи monitorenter / monitorexit пар упутстава. Метода остаје ефикасна, јер се режијске перформансе ове синхронизације јављају само при првом читању лење вредности.

За означавање стања лење вредности потребан је само један бит. Дакле, ако нема више од 32 лење вредности, једно инт поље може их све пратити. Ако је у изворном коду дефинисано више од једне лење вредности, преводилац ће модификовати горњи бајт код да би у ту сврху применио битну маску.

Опет, Сцала нам омогућава да лако искористимо одређене врсте понашања које би морале бити експлицитно примењене у Јави, штедећи напор и смањујући ризик од грешака у куцању.

Функција као вредност

Сада погледајмо следећи Сцала изворни код:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Тхе Printer класа има једно поље, output, са типом String => Unit: функција која узима String и враћа објект типа Unit (слично void у Јави). У главној методи креирамо један од ових објеката и додељујемо овом пољу анонимну функцију која исписује дати низ.

Компајлирањем овог кода генеришу се четири датотеке класе:

Изворни код је компајлиран у четири датотеке класе.

Hello.class је класа омотача чија главна метода једноставно позива 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 садржи стварну примену главне методе. Да бисте погледали његов бајт код, уверите се да сте исправно избегли $ према правилима ваше командне љуске, да бисте избегли њено тумачење као посебан карактер:

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. Затим креира Hello$$anonfun$1, која садржи нашу анонимну функцију s => println(s). Тхе Printer се иницијализује овим објектом као output поље. Ово поље се затим учита у стек и извршава са операндом 'Hello'.

Погледајмо класу анонимних функција, Hello$$anonfun$1.class, доле. Можемо видети да проширује Сцала'с Function1 (као AbstractFunction1) применом apply() метода. Заправо, он ствара два apply() методе, једна умотавајући другу, које заједно извршавају проверу типа (у овом случају, да је улаз String) и извршавају анонимну функцију (испис улаза са 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() горе описане методе, можемо видети да се у одмаку 21 извршавање анонимне функције покреће позивом на apply( Object ) метода.

На крају, за потпуност, погледајмо бајт код за 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 променљива. Чува се у пољу класе output, а гетер output() је створен. Једина разлика је у томе што ова променљива сада мора да имплементира Сцала интерфејс scala.Function1 (што AbstractFunction1 чини).

Дакле, цена ове елегантне Сцала функције су основне класе услужних програма, створене да представљају и извршавају једну анонимну функцију која се може користити као вредност. Требали бисте узети у обзир број таквих функција, као и детаље ваше имплементације ВМ-а, да бисте схватили шта то значи за вашу одређену апликацију.

Проћи испод хаубе са Сцалом: Истражите како је овај моћни језик имплементиран у ЈВМ бајткод. Твеет

Сцала Траитс

Скалине особине сличне су интерфејсима у Јави. Следећа особина дефинише два потписа методе и пружа подразумевану имплементацију другог. Погледајмо како се примењује:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изворни код је компајлиран у две датотеке класе.

Израђују се два ентитета: Similarity.class, интерфејс који декларише обе методе и синтетичка класа, Similarity$class.class, пружајући подразумевану имплементацију:

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, преводилац Сцала генерише наредбу бајт кода invokestatic да позове статички метод који пружа пратећа класа.

Комплексни полиморфизам и наследне структуре могу се створити од особина. На пример, више особина, као и класа примене, сви могу надјачати методу са истим потписом, позивајући super.methodName() да се контрола пренесе на следећу особину. Када компајлер Сцала наиђе на такве позиве, он:

Тако можемо видети да се моћан концепт особина примењује на нивоу ЈВМ-а на начин који не доводи до значајних трошкова, а програмери Сцала могу уживати у овој функцији не бринући се да ће она бити прескупа током извођења.

Једнокреветне

Сцала даје експлицитну дефиницију синглетон класа користећи кључну реч object. Размотримо следећу класу синглетон:

object Config { val home_dir = '/home/user' }

Компајлер производи две датотеке класе:

Изворни код је компајлиран у две датотеке класе.

Config.class је прилично једноставан:

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$ класа која уграђује синглетон-ову функционалност. Испитивање те класе са javap -p -c производи следећи бајт код:

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

Састоји се од следећег:

Синглетон је популаран и користан образац дизајна. Језик Јава не пружа директан начин да га одредите на нивоу језика; већ је одговорност програмера да је примени у Јава извору. Сцала, с друге стране, пружа јасан и прикладан начин да се синглетон експлицитно декларише помоћу object кључна реч. Као што видимо гледајући испод хаубе, имплементиран је на приступачан и природан начин.

Закључак

Сада смо видели како Сцала компајлира неколико имплицитних и функционалних програмских карактеристика у софистициране Јава структуре бајт кода. Са овим погледом на унутрашње деловање Сцале, можемо дубље увидети Сцала-ину моћ, помажући нам да максимално искористимо овај моћни језик.

Такође сада имамо алате да сами истражујемо језик. Много је корисних функција синтаксе Сцала које нису обрађене у овом чланку, као што су класе случајева, каририрање и разумевање листе. Предлажем вам да сами истражите Сцала-ову примену ових структура како бисте научили како да будете Сцала ниња следећег нивоа!


Јава виртуелна машина: курс пада

Баш као и Јава компајлер, Сцала компајлер претвара изворни код у .class датотеке које садрже Јава бајт код који извршава Јава виртуелна машина. Да би се разумело како се два језика разликују испод хаубе, неопходно је разумети систем који циљају оба. Овде представљамо кратки преглед неких главних елемената архитектуре Јава виртуелне машине, структуре датотека класе и основа асемблера.

Имајте на уму да ће овај водич покривати само минимум који ће омогућити праћење заједно са горњим чланком. Иако се овде не расправља о многим главним компонентама ЈВМ-а, потпуни детаљи могу се наћи у званичним документима, овде .

Декомпајлирање датотека класе са javap
Константни базен
Табеле поља и метода
ЈВМ Битецоде
Позиви метода и скуп позива
Извршење на стеку операнда
Локалне променљиве
Повратак на врх

Декомпајлирање датотека класе са javap

Јава се испоручује са javap услужни програм командне линије, који декомпајлира .class датотеке у читљивом облику. Пошто датотеке класе Сцала и Јава циљају исти ЈВМ, javap може се користити за испитивање датотека класа које је саставила Сцала.

Саставимо следећи изворни код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компајлирајући ово са scalac RegularPolygon.scala произвешће RegularPolygon.class. Ако онда покренемо javap RegularPolygon.class видећемо следеће:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Ово је врло једноставна рашчламба датотеке класе која једноставно приказује имена и типове јавних чланова класе. Додавање -p опција укључује приватне чланове:

$ 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 опција:

$ 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 или -verbose опција, као у javap -p -v RegularPolygon.class:

Комплетан садржај датотеке класе Јава.

Овде коначно видимо шта се заиста налази у датотеци предавања. Шта све ово значи? Погледајмо неке од најважнијих делова.

Константни базен

Развојни циклус за Ц ++ апликације укључује фазе компајлирања и повезивања. Развојни циклус за Јаву прескаче експлицитну фазу повезивања, јер се повезивање дешава током извршавања. Датотека класе мора подржавати ово рунтиме повезивање. То значи да када се изворни код односи на било које поље или методу, резултујући бајт код мора да садржи релевантне референце у симболичном облику, спремне за дереференцирање након што се апликација учита у меморију и извршилац може да разреши стварне адресе. Овај симболички образац мора да садржи:

Спецификација формата датотеке класе укључује одељак датотеке који се назива стални базен , табела свих референци потребних повезивачу. Садржи уносе различитих врста.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Први бајт сваког уноса је нумеричка ознака која означава врсту уноса. Преостали бајтови пружају информације о вредности уноса. Број бајтова и правила за њихово тумачење зависе од врсте назначене првим бајтом.

На пример, Јава класа која користи константни цели број 365 може имати константан унос базена са следећим бајт кодом:

x03 00 00 01 6D

Први бајт, x03, идентификује тип уноса, CONSTANT_Integer. Ово обавештава повезивач да следећа четири бајта садрже вредност целог броја. (Имајте на уму да је 365 у хексадецималном облику x16D). Ако је ово 14. унос у константном спремишту, javap -v приказаће га овако:

#14 = Integer 365

Многи типови константи састоје се од референци на „примитивније“ типове константи негде другде у спремишту константи. На пример, наш пример кода садржи изјаву:

println( 'Calculating perimeter...' )

Коришћење стринг константе произвешће два уноса у спремишту константи: један унос са типом CONSTANT_String , и још један унос типа CONSTANT_Utf8. Унос типа Constant_UTF8 садржи стварни УТФ8 приказ вредности низа. Унос типа CONSTANT_String садржи референцу на CONSTANT_Utf8 улаз:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Таква компликација је неопходна јер постоје друге врсте константних уноса у спремишту које се односе на уносе типа Utf8 а то нису уноси типа String. На пример, свака референца на атрибут класе произвешће CONSTANT_Fieldref типе, који садржи низ референци на име класе, име атрибута и тип атрибута:

#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

За више детаља о константном базену, погледајте ЈВМ документација .

Табеле поља и метода

Датотека класе садржи пољски сто који садржи информације о сваком пољу (тј. атрибуту) дефинисаном у класи. То су референце на константне уносе у спремишту који описују име и тип поља, као и заставице за контролу приступа и друге релевантне податке.

Сличан табела метода је присутан у датотеци класе. Међутим, поред података о имену и типу, за сваку неастрактну методу садржи стварне инструкције бајт-кода које ЈВМ треба да изврши, као и структуре података које користи оквир стека методе, описане у наставку.

ЈВМ Битецоде

ЈВМ користи сопствени интерни скуп инструкција за извршавање компајлираног кода. Трчање javap са -c опција укључује компајлиране имплементације метода у излаз. Ако испитамо наш RegularPolygon.class датотеку на овај начин, видећемо следећи излаз за наше getPerimeter() метода:

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 корисно преводи бајт код у читљив облик који приказује:

Операнди који се приказују знаком фунте, као што је #23, референца су на уносе у спремишту константи. Као што видимо, javap такође даје корисне коментаре у излазу, идентификујући шта се тачно референцира из базена.

У наставку ћемо размотрити неколико уобичајених упутстава. За детаљне информације о комплетном скупу ЈВМ упутстава погледајте документација .

Позиви метода и скуп позива

Сваки позив методе мора бити у могућности да се изводи са сопственим контекстом, који укључује ствари као што су локално декларисане променљиве или аргументи који су прослеђени методи. Заједно, ови чине а стацк фраме . По позивању методе креира се нови оквир који се поставља на врх скуп позива . Када се метода врати, тренутни оквир се уклања из низа позива и одбацује, а оквир који је био на снази пре позивања методе се враћа.

Оквир слога укључује неколико различитих структура. Две важне су стек операнда и табела локалних променљивих , о којој се даље говори.

ЈВМ низ позива.

Извршење на стеку операнда

Многа ЈВМ упутства делују на њиховим оквирима стек операнда . Уместо да експлицитно наведу константни операнд у бајт-коду, ова упутства уместо тога узимају вредности на врху стека операнда као улаз. Обично се ове вредности уклањају из стека у процесу. Нека упутства такође постављају нове вредности на врх стека. На овај начин се ЈВМ упутства могу комбиновати за обављање сложених операција. На пример, израз:

sideLength * this.numSides

је компајлирано на следећи начин у нашем getPerimeter() метода:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

ЈВМ упутства могу да раде на стеку операнда за обављање сложених функција.

Када се метода позове, нови стек операнда се креира као део њеног оквира стека, где ће се извршавати операције. Овде морамо бити опрезни са терминологијом: реч „стог“ може се односити на скуп позива , сноп оквира који пружа контекст за извршавање методе или одређеном оквиру стек операнда , на основу којих делују ЈВМ упутства.

Локалне променљиве

Сваки оквир стека садржи таблицу локалне променљиве . То обично укључује референцу на this објецт, било који аргументи који су прослеђени када је метода позвана, и све локалне променљиве декларисане у телу методе. Трчање javap са -v опција ће садржати информације о томе како треба поставити оквир стека сваке методе, укључујући његову табелу локалних променљивих:

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

У овом примеру постоје две локалне променљиве. Променљива у слоту 0 назива се this, са типом RegularPolygon. Ово је референца на властиту класу методе. Променљива у слоту 1 се назива sideLength, са типом D (што указује на двоструко). Ово је аргумент који се преноси на наш getPerimeter() метода.

Упутства као што су iload_1, fstore_2 или aload [n], преносе различите врсте локалних променљивих између стека операнда и табеле локалних променљивих. Будући да је прва ставка у табели обично референца на this, упутство aload_0 је уобичајено за сваку методу која делује на сопственој класи.

Овим је завршено наше пролазак кроз основе ЈВМ-а. Кликните овде за повратак на главни чланак.

да прати његово стање. Метод m4() проверава да ли је вредност овог поља 0, што значи да је m4 још увек није иницијализован, у ком случају m4$lzycompute() се позива, попуњавајући m4 и враћање његове вредности. Овај приватни метод такође поставља вредност bitmap

Прљаве руке помоћу Сцала ЈВМ битецоде-а

Сцала језик наставља да стиче популарност током последњих неколико година, захваљујући својој одличној комбинацији функционални и објектно оријентисани принципи развоја софтвера , и његова примена поврх доказане Јава виртуелне машине (ЈВМ).

Иако Мердевине компајлира се у Јава бајт код, дизајниран је да побољша многе уочене недостатке Јава језика. Нудећи потпуну функционалну подршку за програмирање, Сцала-ова основна синтакса садржи многе имплицитне структуре које Јава програмери морају изричито изградити, од којих неке укључују знатну сложеност.

Стварање језика који се компајлира у бајт код Јава захтева дубоко разумевање унутрашњег рада Јава виртуелне машине. Да бисмо увидели шта су Сцала-ини програмери постигли, потребно је проћи испод хаубе и истражити како компајлер тумачи изворни код Сцала-е како би произвео ефикасан и ефективан ЈВМ битецоде.



Погледајмо како су све ове ствари примењене.

Предуслови

Читање овог чланка захтева основно разумевање бајткода Јава виртуелне машине. Комплетну спецификацију виртуелне машине можете добити од Орацлеова званична документација . Читање целе спецификације није пресудно за разумевање овог чланка, па сам за дно кратког увода у основе припремио кратак водич на дну чланка.

Кликните овде да бисте прочитали курс о паду о основама ЈВМ-а.

Услужни програм је потребан за растављање Јава бајт-кода за репродукцију примера наведених у наставку и за наставак даљег испитивања. Јава Девелопмент Кит обезбеђује сопствени услужни програм за командну линију, javap, који ћемо овде користити. Брза демонстрација како javap радови су укључени у водичу на дну .

И наравно, радна инсталација компајлера Сцала неопходна је читаоцима који желе да следе примере. Овај чланак је написан помоћу Скала 2.11.7 . Различите верзије Сцале могу произвести мало другачији бајт код.

Подразумевани гетери и сетери

Иако Јава конвенција увек обезбеђује методе добивања и постављања за јавне атрибуте, Јава програмери су дужни да их сами напишу, упркос чињеници да се образац за сваки од њих није мењао деценијама. За разлику од тога, Сцала нуди подразумеване гетере и постављаче.

Погледајмо следећи пример:

class Person(val name:String) { }

Погледајмо класу Person. Ако компајлирамо ову датотеку са scalac, тада се изводи $ javap -p Person.class даје нам:

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 са var у Person извор и прекомпајлирајте, затим поље је final модификатор се испушта, а додаје се и метода постављача:

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 или var је дефинисан унутар тела класе, затим се креирају одговарајућа приватна поља и методе приступа и одговарајуће иницијализују приликом креирања инстанце.

Имајте на уму да таква примена нивоа класе val и var поља значи да ће се, ако се неке променљиве користе на нивоу класе за складиштење средњих вредности, а програмер им никада не приступи директно, иницијализација сваког таквог поља додати један до два метода у отисак класе. Додавање private модификатор за таква поља не значи да ће одговарајући приступници бити одбачени. Они ће тек постати приватни.

Дефиниције променљивих и функција

Претпоставимо да имамо метод, m(), и креирамо три различите референце у стилу Сцала на ову функцију:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Каква је свака од ових референци на m изграђена? Када се m извршити у сваком случају? Погледајмо резултујући бајт код. Следећи излаз приказује резултате javap -v Person.class (изостављајући пуно сувишних резултата):

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() се чува у индексу #30. У коду конструктора видимо да се ова метода позива два пута током иницијализације, са упутством invokevirtual #30 појављује се прво у одмаку бајта 11, а затим одмаку 19. Првом позиву следи упутство putfield #22 која додељује резултат ове методе пољу m1, на које се позива индекс #22 у сталном базену. Друго позивање прати исти образац, овај пут додељујући вредност пољу m2, индексирано на #24 у сталном базену.

Другим речима, додељивање методе променљивој дефинисаној са val или var само додељује резултат методе на ту променљиву. Видимо да су методе m1() и m2() који су створени су једноставно гетери за ове променљиве. У случају var m2, такође видимо да је постављач m2_$eq(int) креира се, који се понаша као и сваки други постављач, преписујући вредност у пољу.

Међутим, коришћењем кључне речи def даје другачији резултат. Уместо да преузме вредност поља за враћање, метода m3() такође укључује упутство invokevirtual #30. Односно, сваки пут када се ова метода позове, она тада позива m() и враћа резултат ове методе.

Дакле, као што видимо, Сцала пружа три начина рада са пољима класе, а она се лако наводе помоћу кључних речи val, var и def. У Јави бисмо морали изричито да имплементирамо потребне постављаче и гетере, а такав ручно написан шаблонски код био би много мање изражајан и склонији грешкама.

Лене вредности

Компликованији код се производи када се декларише лења вредност. Претпоставимо да смо у претходно дефинисану класу додали следеће поље:

lazy val m4 = m

Трчање javap -p -v Person.class сада ће открити следеће:

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 се не рачуна док није потребно. Посебна, приватна метода m4$lzycompute() се производи за израчунавање лење вредности, а поље bitmap$0 да прати његово стање. Метод m4() проверава да ли је вредност овог поља 0, што значи да је m4 још увек није иницијализован, у ком случају m4$lzycompute() се позива, попуњавајући m4 и враћање његове вредности. Овај приватни метод такође поставља вредност bitmap$0 на 1, тако да следећи пут m4() назива се прескочит ће позивање методе иницијализације и умјесто тога једноставно вратити вриједност m4.

Резултати првог позива на Сцала лењу вредност.

Бајткод који Сцала овде производи је дизајниран да буде сигуран и ефикасан. Да би био сигуран за нит, метода лењег израчуна користи monitorenter / monitorexit пар упутстава. Метода остаје ефикасна, јер се режијске перформансе ове синхронизације јављају само при првом читању лење вредности.

За означавање стања лење вредности потребан је само један бит. Дакле, ако нема више од 32 лење вредности, једно инт поље може их све пратити. Ако је у изворном коду дефинисано више од једне лење вредности, преводилац ће модификовати горњи бајт код да би у ту сврху применио битну маску.

Опет, Сцала нам омогућава да лако искористимо одређене врсте понашања које би морале бити експлицитно примењене у Јави, штедећи напор и смањујући ризик од грешака у куцању.

Функција као вредност

Сада погледајмо следећи Сцала изворни код:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Тхе Printer класа има једно поље, output, са типом String => Unit: функција која узима String и враћа објект типа Unit (слично void у Јави). У главној методи креирамо један од ових објеката и додељујемо овом пољу анонимну функцију која исписује дати низ.

Компајлирањем овог кода генеришу се четири датотеке класе:

Изворни код је компајлиран у четири датотеке класе.

Hello.class је класа омотача чија главна метода једноставно позива 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 садржи стварну примену главне методе. Да бисте погледали његов бајт код, уверите се да сте исправно избегли $ према правилима ваше командне љуске, да бисте избегли њено тумачење као посебан карактер:

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. Затим креира Hello$$anonfun$1, која садржи нашу анонимну функцију s => println(s). Тхе Printer се иницијализује овим објектом као output поље. Ово поље се затим учита у стек и извршава са операндом 'Hello'.

Погледајмо класу анонимних функција, Hello$$anonfun$1.class, доле. Можемо видети да проширује Сцала'с Function1 (као AbstractFunction1) применом apply() метода. Заправо, он ствара два apply() методе, једна умотавајући другу, које заједно извршавају проверу типа (у овом случају, да је улаз String) и извршавају анонимну функцију (испис улаза са 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() горе описане методе, можемо видети да се у одмаку 21 извршавање анонимне функције покреће позивом на apply( Object ) метода.

На крају, за потпуност, погледајмо бајт код за 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 променљива. Чува се у пољу класе output, а гетер output() је створен. Једина разлика је у томе што ова променљива сада мора да имплементира Сцала интерфејс scala.Function1 (што AbstractFunction1 чини).

Дакле, цена ове елегантне Сцала функције су основне класе услужних програма, створене да представљају и извршавају једну анонимну функцију која се може користити као вредност. Требали бисте узети у обзир број таквих функција, као и детаље ваше имплементације ВМ-а, да бисте схватили шта то значи за вашу одређену апликацију.

Проћи испод хаубе са Сцалом: Истражите како је овај моћни језик имплементиран у ЈВМ бајткод. Твеет

Сцала Траитс

Скалине особине сличне су интерфејсима у Јави. Следећа особина дефинише два потписа методе и пружа подразумевану имплементацију другог. Погледајмо како се примењује:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изворни код је компајлиран у две датотеке класе.

Израђују се два ентитета: Similarity.class, интерфејс који декларише обе методе и синтетичка класа, Similarity$class.class, пружајући подразумевану имплементацију:

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, преводилац Сцала генерише наредбу бајт кода invokestatic да позове статички метод који пружа пратећа класа.

Комплексни полиморфизам и наследне структуре могу се створити од особина. На пример, више особина, као и класа примене, сви могу надјачати методу са истим потписом, позивајући super.methodName() да се контрола пренесе на следећу особину. Када компајлер Сцала наиђе на такве позиве, он:

Тако можемо видети да се моћан концепт особина примењује на нивоу ЈВМ-а на начин који не доводи до значајних трошкова, а програмери Сцала могу уживати у овој функцији не бринући се да ће она бити прескупа током извођења.

Једнокреветне

Сцала даје експлицитну дефиницију синглетон класа користећи кључну реч object. Размотримо следећу класу синглетон:

object Config { val home_dir = '/home/user' }

Компајлер производи две датотеке класе:

Изворни код је компајлиран у две датотеке класе.

Config.class је прилично једноставан:

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$ класа која уграђује синглетон-ову функционалност. Испитивање те класе са javap -p -c производи следећи бајт код:

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

Састоји се од следећег:

Синглетон је популаран и користан образац дизајна. Језик Јава не пружа директан начин да га одредите на нивоу језика; већ је одговорност програмера да је примени у Јава извору. Сцала, с друге стране, пружа јасан и прикладан начин да се синглетон експлицитно декларише помоћу object кључна реч. Као што видимо гледајући испод хаубе, имплементиран је на приступачан и природан начин.

Закључак

Сада смо видели како Сцала компајлира неколико имплицитних и функционалних програмских карактеристика у софистициране Јава структуре бајт кода. Са овим погледом на унутрашње деловање Сцале, можемо дубље увидети Сцала-ину моћ, помажући нам да максимално искористимо овај моћни језик.

Такође сада имамо алате да сами истражујемо језик. Много је корисних функција синтаксе Сцала које нису обрађене у овом чланку, као што су класе случајева, каририрање и разумевање листе. Предлажем вам да сами истражите Сцала-ову примену ових структура како бисте научили како да будете Сцала ниња следећег нивоа!


Јава виртуелна машина: курс пада

Баш као и Јава компајлер, Сцала компајлер претвара изворни код у .class датотеке које садрже Јава бајт код који извршава Јава виртуелна машина. Да би се разумело како се два језика разликују испод хаубе, неопходно је разумети систем који циљају оба. Овде представљамо кратки преглед неких главних елемената архитектуре Јава виртуелне машине, структуре датотека класе и основа асемблера.

Имајте на уму да ће овај водич покривати само минимум који ће омогућити праћење заједно са горњим чланком. Иако се овде не расправља о многим главним компонентама ЈВМ-а, потпуни детаљи могу се наћи у званичним документима, овде .

Декомпајлирање датотека класе са javap
Константни базен
Табеле поља и метода
ЈВМ Битецоде
Позиви метода и скуп позива
Извршење на стеку операнда
Локалне променљиве
Повратак на врх

Декомпајлирање датотека класе са javap

Јава се испоручује са javap услужни програм командне линије, који декомпајлира .class датотеке у читљивом облику. Пошто датотеке класе Сцала и Јава циљају исти ЈВМ, javap може се користити за испитивање датотека класа које је саставила Сцала.

Саставимо следећи изворни код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компајлирајући ово са scalac RegularPolygon.scala произвешће RegularPolygon.class. Ако онда покренемо javap RegularPolygon.class видећемо следеће:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Ово је врло једноставна рашчламба датотеке класе која једноставно приказује имена и типове јавних чланова класе. Додавање -p опција укључује приватне чланове:

$ 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 опција:

$ 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 или -verbose опција, као у javap -p -v RegularPolygon.class:

Комплетан садржај датотеке класе Јава.

Овде коначно видимо шта се заиста налази у датотеци предавања. Шта све ово значи? Погледајмо неке од најважнијих делова.

Константни базен

Развојни циклус за Ц ++ апликације укључује фазе компајлирања и повезивања. Развојни циклус за Јаву прескаче експлицитну фазу повезивања, јер се повезивање дешава током извршавања. Датотека класе мора подржавати ово рунтиме повезивање. То значи да када се изворни код односи на било које поље или методу, резултујући бајт код мора да садржи релевантне референце у симболичном облику, спремне за дереференцирање након што се апликација учита у меморију и извршилац може да разреши стварне адресе. Овај симболички образац мора да садржи:

Спецификација формата датотеке класе укључује одељак датотеке који се назива стални базен , табела свих референци потребних повезивачу. Садржи уносе различитих врста.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Први бајт сваког уноса је нумеричка ознака која означава врсту уноса. Преостали бајтови пружају информације о вредности уноса. Број бајтова и правила за њихово тумачење зависе од врсте назначене првим бајтом.

На пример, Јава класа која користи константни цели број 365 може имати константан унос базена са следећим бајт кодом:

x03 00 00 01 6D

Први бајт, x03, идентификује тип уноса, CONSTANT_Integer. Ово обавештава повезивач да следећа четири бајта садрже вредност целог броја. (Имајте на уму да је 365 у хексадецималном облику x16D). Ако је ово 14. унос у константном спремишту, javap -v приказаће га овако:

#14 = Integer 365

Многи типови константи састоје се од референци на „примитивније“ типове константи негде другде у спремишту константи. На пример, наш пример кода садржи изјаву:

println( 'Calculating perimeter...' )

Коришћење стринг константе произвешће два уноса у спремишту константи: један унос са типом CONSTANT_String , и још један унос типа CONSTANT_Utf8. Унос типа Constant_UTF8 садржи стварни УТФ8 приказ вредности низа. Унос типа CONSTANT_String садржи референцу на CONSTANT_Utf8 улаз:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Таква компликација је неопходна јер постоје друге врсте константних уноса у спремишту које се односе на уносе типа Utf8 а то нису уноси типа String. На пример, свака референца на атрибут класе произвешће CONSTANT_Fieldref типе, који садржи низ референци на име класе, име атрибута и тип атрибута:

#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

За више детаља о константном базену, погледајте ЈВМ документација .

Табеле поља и метода

Датотека класе садржи пољски сто који садржи информације о сваком пољу (тј. атрибуту) дефинисаном у класи. То су референце на константне уносе у спремишту који описују име и тип поља, као и заставице за контролу приступа и друге релевантне податке.

Сличан табела метода је присутан у датотеци класе. Међутим, поред података о имену и типу, за сваку неастрактну методу садржи стварне инструкције бајт-кода које ЈВМ треба да изврши, као и структуре података које користи оквир стека методе, описане у наставку.

ЈВМ Битецоде

ЈВМ користи сопствени интерни скуп инструкција за извршавање компајлираног кода. Трчање javap са -c опција укључује компајлиране имплементације метода у излаз. Ако испитамо наш RegularPolygon.class датотеку на овај начин, видећемо следећи излаз за наше getPerimeter() метода:

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 корисно преводи бајт код у читљив облик који приказује:

Операнди који се приказују знаком фунте, као што је #23, референца су на уносе у спремишту константи. Као што видимо, javap такође даје корисне коментаре у излазу, идентификујући шта се тачно референцира из базена.

У наставку ћемо размотрити неколико уобичајених упутстава. За детаљне информације о комплетном скупу ЈВМ упутстава погледајте документација .

Позиви метода и скуп позива

Сваки позив методе мора бити у могућности да се изводи са сопственим контекстом, који укључује ствари као што су локално декларисане променљиве или аргументи који су прослеђени методи. Заједно, ови чине а стацк фраме . По позивању методе креира се нови оквир који се поставља на врх скуп позива . Када се метода врати, тренутни оквир се уклања из низа позива и одбацује, а оквир који је био на снази пре позивања методе се враћа.

Оквир слога укључује неколико различитих структура. Две важне су стек операнда и табела локалних променљивих , о којој се даље говори.

ЈВМ низ позива.

Извршење на стеку операнда

Многа ЈВМ упутства делују на њиховим оквирима стек операнда . Уместо да експлицитно наведу константни операнд у бајт-коду, ова упутства уместо тога узимају вредности на врху стека операнда као улаз. Обично се ове вредности уклањају из стека у процесу. Нека упутства такође постављају нове вредности на врх стека. На овај начин се ЈВМ упутства могу комбиновати за обављање сложених операција. На пример, израз:

sideLength * this.numSides

је компајлирано на следећи начин у нашем getPerimeter() метода:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

ЈВМ упутства могу да раде на стеку операнда за обављање сложених функција.

Када се метода позове, нови стек операнда се креира као део њеног оквира стека, где ће се извршавати операције. Овде морамо бити опрезни са терминологијом: реч „стог“ може се односити на скуп позива , сноп оквира који пружа контекст за извршавање методе или одређеном оквиру стек операнда , на основу којих делују ЈВМ упутства.

Локалне променљиве

Сваки оквир стека садржи таблицу локалне променљиве . То обично укључује референцу на this објецт, било који аргументи који су прослеђени када је метода позвана, и све локалне променљиве декларисане у телу методе. Трчање javap са -v опција ће садржати информације о томе како треба поставити оквир стека сваке методе, укључујући његову табелу локалних променљивих:

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

У овом примеру постоје две локалне променљиве. Променљива у слоту 0 назива се this, са типом RegularPolygon. Ово је референца на властиту класу методе. Променљива у слоту 1 се назива sideLength, са типом D (што указује на двоструко). Ово је аргумент који се преноси на наш getPerimeter() метода.

Упутства као што су iload_1, fstore_2 или aload [n], преносе различите врсте локалних променљивих између стека операнда и табеле локалних променљивих. Будући да је прва ставка у табели обично референца на this, упутство aload_0 је уобичајено за сваку методу која делује на сопственој класи.

Овим је завршено наше пролазак кроз основе ЈВМ-а. Кликните овде за повратак на главни чланак.

на 1, тако да следећи пут m4() назива се прескочит ће позивање методе иницијализације и умјесто тога једноставно вратити вриједност m4.

Резултати првог позива на Сцала лењу вредност.

Бајткод који Сцала овде производи је дизајниран да буде сигуран и ефикасан. Да би био сигуран за нит, метода лењег израчуна користи monitorenter / monitorexit пар упутстава. Метода остаје ефикасна, јер се режијске перформансе ове синхронизације јављају само при првом читању лење вредности.

За означавање стања лење вредности потребан је само један бит. Дакле, ако нема више од 32 лење вредности, једно инт поље може их све пратити. Ако је у изворном коду дефинисано више од једне лење вредности, преводилац ће модификовати горњи бајт код да би у ту сврху применио битну маску.

Опет, Сцала нам омогућава да лако искористимо одређене врсте понашања које би морале бити експлицитно примењене у Јави, штедећи напор и смањујући ризик од грешака у куцању.

Функција као вредност

Сада погледајмо следећи Сцала изворни код:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Тхе Printer класа има једно поље, output, са типом String => Unit: функција која узима String и враћа објект типа Unit (слично void у Јави). У главној методи креирамо један од ових објеката и додељујемо овом пољу анонимну функцију која исписује дати низ.

Компајлирањем овог кода генеришу се четири датотеке класе:

Изворни код је компајлиран у четири датотеке класе.

Hello.class је класа омотача чија главна метода једноставно позива 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 садржи стварну примену главне методе. Да бисте погледали његов бајт код, уверите се да сте исправно избегли $ према правилима ваше командне љуске, да бисте избегли њено тумачење као посебан карактер:

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

Метода ствара Printer. Затим креира Hello$$anonfun, која садржи нашу анонимну функцију s => println(s). Тхе Printer се иницијализује овим објектом као output поље. Ово поље се затим учита у стек и извршава са операндом 'Hello'.

Погледајмо класу анонимних функција, Hello$$anonfun.class, доле. Можемо видети да проширује Сцала'с Function1 (као AbstractFunction1) применом apply() метода. Заправо, он ствара два apply() методе, једна умотавајући другу, које заједно извршавају проверу типа (у овом случају, да је улаз String) и извршавају анонимну функцију (испис улаза са 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

Осврћући се на Hello$.main() горе описане методе, можемо видети да се у одмаку 21 извршавање анонимне функције покреће позивом на apply( Object ) метода.

На крају, за потпуност, погледајмо бајт код за 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 променљива. Чува се у пољу класе output, а гетер output() је створен. Једина разлика је у томе што ова променљива сада мора да имплементира Сцала интерфејс scala.Function1 (што AbstractFunction1 чини).

Дакле, цена ове елегантне Сцала функције су основне класе услужних програма, створене да представљају и извршавају једну анонимну функцију која се може користити као вредност. Требали бисте узети у обзир број таквих функција, као и детаље ваше имплементације ВМ-а, да бисте схватили шта то значи за вашу одређену апликацију.

Проћи испод хаубе са Сцалом: Истражите како је овај моћни језик имплементиран у ЈВМ бајткод. Твеет

Сцала Траитс

Скалине особине сличне су интерфејсима у Јави. Следећа особина дефинише два потписа методе и пружа подразумевану имплементацију другог. Погледајмо како се примењује:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изворни код је компајлиран у две датотеке класе.

Израђују се два ентитета: Similarity.class, интерфејс који декларише обе методе и синтетичка класа, Similarity$class.class, пружајући подразумевану имплементацију:

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, преводилац Сцала генерише наредбу бајт кода invokestatic да позове статички метод који пружа пратећа класа.

Комплексни полиморфизам и наследне структуре могу се створити од особина. На пример, више особина, као и класа примене, сви могу надјачати методу са истим потписом, позивајући super.methodName() да се контрола пренесе на следећу особину. Када компајлер Сцала наиђе на такве позиве, он:

Тако можемо видети да се моћан концепт особина примењује на нивоу ЈВМ-а на начин који не доводи до значајних трошкова, а програмери Сцала могу уживати у овој функцији не бринући се да ће она бити прескупа током извођења.

Једнокреветне

Сцала даје експлицитну дефиницију синглетон класа користећи кључну реч object. Размотримо следећу класу синглетон:

object Config { val home_dir = '/home/user' }

Компајлер производи две датотеке класе:

Изворни код је компајлиран у две датотеке класе.

Config.class је прилично једноставан:

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$ класа која уграђује синглетон-ову функционалност. Испитивање те класе са javap -p -c производи следећи бајт код:

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

Састоји се од следећег:

Синглетон је популаран и користан образац дизајна. Језик Јава не пружа директан начин да га одредите на нивоу језика; већ је одговорност програмера да је примени у Јава извору. Сцала, с друге стране, пружа јасан и прикладан начин да се синглетон експлицитно декларише помоћу object кључна реч. Као што видимо гледајући испод хаубе, имплементиран је на приступачан и природан начин.

Закључак

Сада смо видели како Сцала компајлира неколико имплицитних и функционалних програмских карактеристика у софистициране Јава структуре бајт кода. Са овим погледом на унутрашње деловање Сцале, можемо дубље увидети Сцала-ину моћ, помажући нам да максимално искористимо овај моћни језик.

Такође сада имамо алате да сами истражујемо језик. Много је корисних функција синтаксе Сцала које нису обрађене у овом чланку, као што су класе случајева, каририрање и разумевање листе. Предлажем вам да сами истражите Сцала-ову примену ових структура како бисте научили како да будете Сцала ниња следећег нивоа!


Јава виртуелна машина: курс пада

Баш као и Јава компајлер, Сцала компајлер претвара изворни код у .class датотеке које садрже Јава бајт код који извршава Јава виртуелна машина. Да би се разумело како се два језика разликују испод хаубе, неопходно је разумети систем који циљају оба. Овде представљамо кратки преглед неких главних елемената архитектуре Јава виртуелне машине, структуре датотека класе и основа асемблера.

Имајте на уму да ће овај водич покривати само минимум који ће омогућити праћење заједно са горњим чланком. Иако се овде не расправља о многим главним компонентама ЈВМ-а, потпуни детаљи могу се наћи у званичним документима, овде .

Декомпајлирање датотека класе са javap
Константни базен
Табеле поља и метода
ЈВМ Битецоде
Позиви метода и скуп позива
Извршење на стеку операнда
Локалне променљиве
Повратак на врх

Декомпајлирање датотека класе са javap

Јава се испоручује са javap услужни програм командне линије, који декомпајлира .class датотеке у читљивом облику. Пошто датотеке класе Сцала и Јава циљају исти ЈВМ, javap може се користити за испитивање датотека класа које је саставила Сцала.

Саставимо следећи изворни код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компајлирајући ово са scalac RegularPolygon.scala произвешће RegularPolygon.class. Ако онда покренемо javap RegularPolygon.class видећемо следеће:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Ово је врло једноставна рашчламба датотеке класе која једноставно приказује имена и типове јавних чланова класе. Додавање -p опција укључује приватне чланове:

$ 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 опција:

$ 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 или -verbose опција, као у javap -p -v RegularPolygon.class:

Комплетан садржај датотеке класе Јава.

Овде коначно видимо шта се заиста налази у датотеци предавања. Шта све ово значи? Погледајмо неке од најважнијих делова.

Константни базен

Развојни циклус за Ц ++ апликације укључује фазе компајлирања и повезивања. Развојни циклус за Јаву прескаче експлицитну фазу повезивања, јер се повезивање дешава током извршавања. Датотека класе мора подржавати ово рунтиме повезивање. То значи да када се изворни код односи на било које поље или методу, резултујући бајт код мора да садржи релевантне референце у симболичном облику, спремне за дереференцирање након што се апликација учита у меморију и извршилац може да разреши стварне адресе. Овај симболички образац мора да садржи:

Спецификација формата датотеке класе укључује одељак датотеке који се назива стални базен , табела свих референци потребних повезивачу. Садржи уносе различитих врста.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Први бајт сваког уноса је нумеричка ознака која означава врсту уноса. Преостали бајтови пружају информације о вредности уноса. Број бајтова и правила за њихово тумачење зависе од врсте назначене првим бајтом.

На пример, Јава класа која користи константни цели број 365 може имати константан унос базена са следећим бајт кодом:

x03 00 00 01 6D

Први бајт, x03, идентификује тип уноса, CONSTANT_Integer. Ово обавештава повезивач да следећа четири бајта садрже вредност целог броја. (Имајте на уму да је 365 у хексадецималном облику x16D). Ако је ово 14. унос у константном спремишту, javap -v приказаће га овако:

#14 = Integer 365

Многи типови константи састоје се од референци на „примитивније“ типове константи негде другде у спремишту константи. На пример, наш пример кода садржи изјаву:

println( 'Calculating perimeter...' )

Коришћење стринг константе произвешће два уноса у спремишту константи: један унос са типом CONSTANT_String , и још један унос типа CONSTANT_Utf8. Унос типа Constant_UTF8 садржи стварни УТФ8 приказ вредности низа. Унос типа CONSTANT_String садржи референцу на CONSTANT_Utf8 улаз:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Таква компликација је неопходна јер постоје друге врсте константних уноса у спремишту које се односе на уносе типа Utf8 а то нису уноси типа String. На пример, свака референца на атрибут класе произвешће CONSTANT_Fieldref типе, који садржи низ референци на име класе, име атрибута и тип атрибута:

#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

За више детаља о константном базену, погледајте ЈВМ документација .

Табеле поља и метода

Датотека класе садржи пољски сто који садржи информације о сваком пољу (тј. атрибуту) дефинисаном у класи. То су референце на константне уносе у спремишту који описују име и тип поља, као и заставице за контролу приступа и друге релевантне податке.

Сличан табела метода је присутан у датотеци класе. Међутим, поред података о имену и типу, за сваку неастрактну методу садржи стварне инструкције бајт-кода које ЈВМ треба да изврши, као и структуре података које користи оквир стека методе, описане у наставку.

ЈВМ Битецоде

ЈВМ користи сопствени интерни скуп инструкција за извршавање компајлираног кода. Трчање javap са -c опција укључује компајлиране имплементације метода у излаз. Ако испитамо наш RegularPolygon.class датотеку на овај начин, видећемо следећи излаз за наше getPerimeter() метода:

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 корисно преводи бајт код у читљив облик који приказује:

Операнди који се приказују знаком фунте, као што је #23, референца су на уносе у спремишту константи. Као што видимо, javap такође даје корисне коментаре у излазу, идентификујући шта се тачно референцира из базена.

У наставку ћемо размотрити неколико уобичајених упутстава. За детаљне информације о комплетном скупу ЈВМ упутстава погледајте документација .

Позиви метода и скуп позива

Сваки позив методе мора бити у могућности да се изводи са сопственим контекстом, који укључује ствари као што су локално декларисане променљиве или аргументи који су прослеђени методи. Заједно, ови чине а стацк фраме . По позивању методе креира се нови оквир који се поставља на врх скуп позива . Када се метода врати, тренутни оквир се уклања из низа позива и одбацује, а оквир који је био на снази пре позивања методе се враћа.

Оквир слога укључује неколико различитих структура. Две важне су стек операнда и табела локалних променљивих , о којој се даље говори.

ЈВМ низ позива.

Извршење на стеку операнда

Многа ЈВМ упутства делују на њиховим оквирима стек операнда . Уместо да експлицитно наведу константни операнд у бајт-коду, ова упутства уместо тога узимају вредности на врху стека операнда као улаз. Обично се ове вредности уклањају из стека у процесу. Нека упутства такође постављају нове вредности на врх стека. На овај начин се ЈВМ упутства могу комбиновати за обављање сложених операција. На пример, израз:

sideLength * this.numSides

је компајлирано на следећи начин у нашем getPerimeter() метода:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

ЈВМ упутства могу да раде на стеку операнда за обављање сложених функција.

Када се метода позове, нови стек операнда се креира као део њеног оквира стека, где ће се извршавати операције. Овде морамо бити опрезни са терминологијом: реч „стог“ може се односити на скуп позива , сноп оквира који пружа контекст за извршавање методе или одређеном оквиру стек операнда , на основу којих делују ЈВМ упутства.

Локалне променљиве

Сваки оквир стека садржи таблицу локалне променљиве . То обично укључује референцу на this објецт, било који аргументи који су прослеђени када је метода позвана, и све локалне променљиве декларисане у телу методе. Трчање javap са -v опција ће садржати информације о томе како треба поставити оквир стека сваке методе, укључујући његову табелу локалних променљивих:

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

У овом примеру постоје две локалне променљиве. Променљива у слоту 0 назива се this, са типом RegularPolygon. Ово је референца на властиту класу методе. Променљива у слоту 1 се назива sideLength, са типом D (што указује на двоструко). Ово је аргумент који се преноси на наш getPerimeter() метода.

Упутства као што су iload_1, fstore_2 или aload [n], преносе различите врсте локалних променљивих између стека операнда и табеле локалних променљивих. Будући да је прва ставка у табели обично референца на this, упутство aload_0 је уобичајено за сваку методу која делује на сопственој класи.

Овим је завршено наше пролазак кроз основе ЈВМ-а. Кликните овде за повратак на главни чланак.