Atomic Kotlin

485

Transcript of Atomic Kotlin

AtomicKotlin

BruceEckelandSvetlanaIsakova

Thisbookisforsaleathttp://leanpub.com/AtomicKotlin

Thisversionwaspublishedon2020-12-21

*****

ThisisaLeanpubbook.LeanpubempowersauthorsandpublisherswiththeLeanPublishingprocess.LeanPublishingistheactofpublishinganin-progressebookusinglightweighttoolsandmanyiterationstogetreaderfeedback,pivotuntilyouhavetherightbookandbuildtractiononceyoudo.

*****

©2020MindviewLLC

ISBNforEPUBversion:978-0-9818725-4-4

ISBNforMOBIversion:978-0-9818725-4-4

TableofContents

Copyright

SectionI:ProgrammingBasicsIntroductionWhyKotlin?Hello,World!var&valDataTypesFunctionsifExpressionsStringTemplatesNumberTypesBooleansRepetitionwithwhileLooping&RangesTheinKeywordExpressions&StatementsSummary1

SectionII:IntroductiontoObjectsObjectsEverywhereCreatingClassesPropertiesConstructorsConstrainingVisibilityPackagesTestingExceptionsListsVariableArgumentListsSetsMaps

PropertyAccessorsSummary2

SectionIII:UsabilityExtensionFunctionsNamed&DefaultArgumentsOverloadingwhenExpressionsEnumerationsDataClassesDestructuringDeclarationsNullableTypesSafeCalls&theElvisOperatorNon-NullAssertionsExtensionsforNullableTypesIntroductiontoGenericsExtensionPropertiesbreak&continue

SectionIV:FunctionalProgrammingLambdasTheImportanceofLambdasOperationsonCollectionsMemberReferencesHigher-OrderFunctionsManipulatingListsBuildingMapsSequencesLocalFunctionsFoldingListsRecursion

SectionV:Object-OrientedProgrammingInterfacesComplexConstructorsSecondaryConstructorsInheritance

BaseClassInitializationAbstractClassesUpcastingPolymorphismCompositionInheritance&ExtensionsClassDelegationDowncastingSealedClassesTypeCheckingNestedClassesObjectsInnerClassesCompanionObjects

SectionVI:PreventingFailureExceptionHandlingCheckInstructionsTheNothingTypeResourceCleanupLoggingUnitTesting

SectionVII:PowerToolsExtensionLambdasScopeFunctionsCreatingGenericsOperatorOverloadingUsingOperatorsPropertyDelegationPropertyDelegationToolsLazyInitializationLateInitialization

AppendicesAppendixA:AtomicTestAppendixB:JavaInteroperability

Copyright

AtomicKotlinByBruceEckel,President,MindView,LLC,andSvetlanaIsakova,JetBrainssro.

Copyright©2021,MindViewLLC.

eBookISBN978-0-9818725-4-4

PrintBookISBN978-0-9818725-5-1

TheeBookISBNcoverstheStepikandLeanpubeBookdistributions,bothavailablethroughAtomicKotlin.com.

Pleasepurchasethisbookthroughwww.AtomicKotlin.com,tosupportitscontinuedmaintenanceandupdates.

Allrightsreserved.PrintedintheUnitedStatesofAmerica.Thispublicationisprotectedbycopyright,andpermissionmustbeobtainedfromthepublisherpriortoanyprohibitedreproduction,storageinaretrievalsystem,ortransmissioninanyformorbyanymeans,electronic,mechanical,photocopying,recording,orlikewise.Forinformationregardingpermissions,seeAtomicKotlin.com.

CreatedinCrestedButte,Colorado,USA,andMunich,Germany.

TextprintedintheUnitedStates

Ebook:Version1.0,December2020

FirstprintingJanuary2021

CoverdesignbyDanielWill-Harris,www.Will-Harris.com

Manyofthedesignationsusedbymanufacturersandsellerstodistinguishtheirproductsareclaimedastrademarks.Wherethosedesignationsappearinthisbook,andthepublisherwasawareofatrademarkclaim,thedesignationsareprintedwithinitialcapitallettersorinallcapitals.

TheKotlintrademarkbelongstotheKotlinFoundation.JavaisatrademarkorregisteredtrademarkofOracle,Inc.intheUnitedStatesandothercountries.WindowsisaregisteredtrademarkofMicrosoftCorporationintheUnitedStatesandothercountries.Allotherproductnamesandcompanynamesmentionedhereinarethepropertyoftheirrespectiveowners.

Theauthorsandpublisherhavetakencareinthepreparationofthisbook,butmakenoexpressedorimpliedwarrantyofanykindandassumenoresponsibilityforerrorsoromissions.Noliabilityisassumedforincidentalorconsequentialdamagesinconnectionwithorarisingoutoftheuseoftheinformationorprogramscontainedherein.

Visitusatwww.AtomicKotlin.com.

SourceCodeAllthesourcecodeforthisbookisavailableascopyrightedfreeware,distributedviaGithub.Toensureyouhavethemostcurrentversion,thisistheofficialcodedistributionsite.Youmayusethiscodeinclassroomandothereducationalsituationsaslongasyoucitethisbookasthesource.

Theprimarygoalofthiscopyrightistoensurethatthesourceofthecodeisproperlycited,andtopreventyoufromrepublishingthecodewithoutpermission.(Aslongasthisbookiscited,usingexamplesfromthebookinmostmediaisgenerallynotaproblem.)

Ineachsource-codefileyoufindareferencetothefollowingcopyrightnotice:

//Copyright.txt

ThiscomputersourcecodeisCopyright©2021MindViewLLC.

AllRightsReserved.

Permissiontouse,copy,modify,anddistributethis

computersourcecode(SourceCode)anditsdocumentation

withoutfeeandwithoutawrittenagreementforthe

purposessetforthbelowisherebygranted,providedthat

theabovecopyrightnotice,thisparagraphandthe

followingfivenumberedparagraphsappearinallcopies.

1.PermissionisgrantedtocompiletheSourceCodeandto

includethecompiledcode,inexecutableformatonly,in

personalandcommercialsoftwareprograms.

2.PermissionisgrantedtousetheSourceCodewithout

modificationinclassroomsituations,includingin

presentationmaterials,providedthatthebook"Atomic

Kotlin"iscitedastheorigin.

3.PermissiontoincorporatetheSourceCodeintoprinted

mediamaybeobtainedbycontacting:

MindViewLLC,POBox969,CrestedButte,CO81224

[email protected]

4.TheSourceCodeanddocumentationarecopyrightedby

MindViewLLC.TheSourcecodeisprovidedwithoutexpress

orimpliedwarrantyofanykind,includinganyimplied

warrantyofmerchantability,fitnessforaparticular

purposeornon-infringement.MindViewLLCdoesnot

warrantthattheoperationofanyprogramthatincludesthe

SourceCodewillbeuninterruptedorerror-free.MindView

LLCmakesnorepresentationaboutthesuitabilityofthe

SourceCodeorofanysoftwarethatincludestheSource

Codeforanypurpose.Theentireriskastothequality

andperformanceofanyprogramthatincludestheSource

CodeiswiththeuseroftheSourceCode.Theuser

understandsthattheSourceCodewasdevelopedforresearch

andinstructionalpurposesandisadvisednottorely

exclusivelyforanyreasonontheSourceCodeorany

programthatincludestheSourceCode.ShouldtheSource

Codeoranyresultingsoftwareprovedefective,theuser

assumesthecostofallnecessaryservicing,repair,or

correction.

5.INNOEVENTSHALLMINDVIEWLLC,ORITSPUBLISHERBE

LIABLETOANYPARTYUNDERANYLEGALTHEORYFORDIRECT,

INDIRECT,SPECIAL,INCIDENTAL,ORCONSEQUENTIALDAMAGES,

INCLUDINGLOSTPROFITS,BUSINESSINTERRUPTION,LOSSOF

BUSINESSINFORMATION,ORANYOTHERPECUNIARYLOSS,ORFOR

PERSONALINJURIES,ARISINGOUTOFTHEUSEOFTHISSOURCE

CODEANDITSDOCUMENTATION,ORARISINGOUTOFTHEINABILITY

TOUSEANYRESULTINGPROGRAM,EVENIFMINDVIEWLLC,OR

ITSPUBLISHERHASBEENADVISEDOFTHEPOSSIBILITYOFSUCH

DAMAGE.MINDVIEWLLCSPECIFICALLYDISCLAIMSANY

WARRANTIES,INCLUDING,BUTNOTLIMITEDTO,THEIMPLIED

WARRANTIESOFMERCHANTABILITYANDFITNESSFORAPARTICULAR

PURPOSE.THESOURCECODEANDDOCUMENTATIONPROVIDED

HEREUNDERISONAN"ASIS"BASIS,WITHOUTANYACCOMPANYING

SERVICESFROMMINDVIEWLLC,ANDMINDVIEWLLCHASNO

OBLIGATIONSTOPROVIDEMAINTENANCE,SUPPORT,UPDATES,

ENHANCEMENTS,ORMODIFICATIONS.

PleasenotethatMindViewLLCmaintainsaWebsitewhichis

thesoledistributionpointforelectroniccopiesofthe

SourceCode,whereitisfreelyavailableundertheterms

statedabove:

https://github.com/BruceEckel/AtomicKotlinExamples

Ifyouthinkyou'vefoundanerrorintheSourceCode,

pleasesubmitacorrectionat:

https://github.com/BruceEckel/AtomicKotlinExamples/issues

Youmayusethecodeinyourprojectsandintheclassroom(includingyourpresentationmaterials)aslongasthecopyrightnoticethatappearsineachsourcefileisretained.

SECTIONI:PROGRAMMINGBASICS

Therewassomethingamazinglyenticingaboutprogramming—VintCerf

Thissectionisforreaderswhoarelearningtoprogram.Ifyou’reanexperiencedprogrammer,skipforwardtoSummary1andSummary2.

Introduction

Thisbookisfordedicatednovicesandexperiencedprogrammers.

You’reanoviceifyoudon’thavepriorprogrammingknowledge,but“dedicated”becausewegiveyoujustenoughtofigureitoutonyourown.Whenyou’refinished,you’llhaveasolidfoundationinprogrammingandinKotlin.

Ifyou’reanexperiencedprogrammer,skipforwardtoSummary1andSummary2,thenproceedfromthere.

The“Atomic”partofthebooktitlereferstoatomsasthesmallestindivisibleunits.Inthisbook,wetrytointroduceonlyoneconceptperchapter,sothechapterscannotbefurthersubdivided—thuswecallthematoms.

ConceptsAllprogramminglanguagesconsistoffeatures.Youapplythesefeaturestoproduceresults.Kotlinispowerful—notonlydoesithavearichsetoffeatures,butyoucanusuallyexpressthosefeaturesinnumerousways.

Ifeverythingisdumpedonyoutooquickly,youmightcomeawaythinkingKotlinis“toocomplicated.”

Thisbookattemptstopreventoverwhelm.Weteachyouthelanguagecarefullyanddeliberately,usingthefollowingprinciples:

1. Babystepsandsmallwins.Wecastoffthetyrannyofthechapter.Instead,wepresenteachsmallstepasanatomicconceptorsimplyatom,whichlookslikeatinychapter.Wetrytopresentonlyonenewconceptperatom.Atypicalatomcontainsoneormoresmall,runnablepiecesofcodeandtheoutputitproduces.

2. Noforwardreferences.Asmuchaspossible,weavoidsaying,“Thesefeaturesareexplainedinalateratom.”

3. Noreferencestootherprogramminglanguages.Wedosoonlywhennecessary.Ananalogytoafeatureinalanguageyoudon’tunderstandisn’t

helpful.4. Showdon’ttell.Insteadofverballydescribingafeature,weprefer

examplesandoutput.It’sbettertoseeafeatureincode.5. Practicebeforetheory.Wetrytoshowthemechanicsofthelanguagefirst,

thentellwhythosefeaturesexist.Thisisbackwardsfrom“traditional”teaching,butitoftenseemstoworkbetter.

Ifyouknowthefeatures,youcanworkoutthemeaning.It’susuallyeasiertounderstandasinglepageofKotlinthanitistounderstandtheequivalentcodeinanotherlanguage.

WhereIstheIndex?ThisbookiswritteninMarkdownandproducedwithLeanpub.Unfortunately,neitherMarkdownnorLeanpubsupportsindexes.However,bycreatingthesmallest-possiblechapters(atoms)consistingofasingletopicineachatom,thetableofcontentsactsasakindofindex.Inaddition,theeBookversionsallowforelectronicsearchingacrossthebook.

Cross-ReferencesAreferencetoanatominthebooklookslikethis:Introduction,whichinthiscasereferstothecurrentatom.InthevariouseBookformats,thisproducesahyperlinktothatatom.

FormattingInthisbook:

Italicsintroduceanewtermorconcept,andsometimesemphasizeanidea.Fixed-widthfontindicatesprogramkeywords,identifiersandfilenames.Thecodeexamplesarealsointhisfont,andarecolorizedintheeBookversionsofthebook.Inprose,wefollowafunctionnamewithemptyparentheses,asinfunc().Thisremindsthereadertheyarelookingatafunction.TomaketheeBookeasytoreadonalldevicesandallowtheusertoincreasethefontsize,welimitourcodelistingwidthto47characters.Attimesthisrequirescompromise,butwefeeltheresultsareworthit.Toachievethesewidthswemayremovespacesthatmightotherwisebe

includedinmanyformattingstyles—inparticular,weusetwo-spaceindentsratherthanthestandardfourspaces.

SampletheBookWeprovideafreesampleoftheelectronicbookatAtomicKotlin.com.Thesampleincludesthefirsttwosectionsintheirentirety,alongwithseveralsubsequentatoms.Thiswayyoucantryoutthebookanddecideifit’sagoodfitforyou.

Thecompletebookisforsale,bothasaprintbookandaneBook.Ifyoulikewhatwe’vedoneinthefreesample,pleasesupportusandhelpuscontinueourworkbypayingforwhatyouuse.Wehopethebookhelps,andweappreciateyoursponsorship.

IntheageoftheInternet,itdoesn’tseempossibletocontrolanypieceofinformation.You’llprobablyfindtheelectronicversionofthisbookinnumerousplaces.Ifyouareunabletopayforthebookrightnowandyoudodownloaditfromoneofthesesites,please“payitforward.”Forexample,helpsomeoneelselearnthelanguageonceyou’velearnedit.Orhelpsomeoneinanywaytheyneed.Perhapsinthefutureyou’llbebetteroff,andthenyoucanpayforthebook.

ExercisesandSolutionsMostatomsinAtomicKotlinareaccompaniedbyahandfulofsmallexercises.Toimproveyourunderstanding,werecommendsolvingtheexercisesimmediatelyafterreadingtheatom.MostoftheexercisesarecheckedautomaticallybytheJetBrainsIntelliJIDEAintegrateddevelopmentenvironment(IDE),soyoucanseeyourprogressandgethintsifyougetstuck.

Youcanfindthefollowinglinksathttp://AtomicKotlin.com/exercises/.

Tosolvetheexercises,installIntelliJIDEAwiththeEduToolspluginbyfollowingthesetutorials:

1. InstallIntelliJIDEAandtheEduToolsPlugin.2. OpentheAtomicKotlincourseandsolvetheexercises.

Inthecourse,you’llfindsolutionsforallexercises.Ifyou’restuckonanexercise,checkforhintsortrypeekingatthesolution.Westillrecommendimplementingityourself.

Ifyouhaveanyproblemssettingupandrunningthecourse,pleasereadTheTroubleshootingGuide.Ifthatdoesn’tsolveyourproblem,pleasecontactthesupportteamasmentionedintheguide.

Ifyoufindamistakeinthecoursecontent(forexample,atestforataskproducesthewrongresult),pleaseuseourissuetrackertoreporttheproblemwiththisprefilledform.Notethatyou’llneedtologinintoYouTrack.Weappreciateyourtimeinhelpingtoimprovethecourse!

SeminarsYoucanfindinformationaboutliveseminarsandotherlearningtoolsatAtomicKotlin.com.

ConferencesBrucecreatesOpen-SpacesconferencessuchastheWinterTechForum.JointhemailinglistatAtomicKotlin.comtostayinformedaboutouractivitiesandwherewearespeaking.

SupportUsThiswasabigproject.Ittooktimeandefforttoproducethisbookandaccompanyingsupportmaterials.Ifyouenjoythisbookandwanttoseemorethingslikeit,pleasesupportus:

Blog,tweet,etc.andtellyourfriends.Thisisagrassrootsmarketingeffortsoeverythingyoudowillhelp.PurchaseaneBookorprintversionofthisbookatAtomicKotlin.com.CheckAtomicKotlin.comforothersupportproductsorevents.

AboutUsBruceEckelistheauthorofthemulti-award-winningThinkinginJavaandThinkinginC++,andanumberofotherbooksoncomputerprogrammingincludingAtomicScala.He’sgivenhundredsofpresentationsthroughouttheworldandputsonalternativeconferencesandeventsliketheWinterTechForum

anddeveloperretreats.BrucehasaBSinappliedphysicsandanMSincomputerengineering.Hisblogisatwww.BruceEckel.comandhisconsulting,trainingandconferencebusinessisMindviewLLC.

SvetlanaIsakovabeganasamemberoftheKotlincompilerteam,andisnowadeveloperadvocateforJetBrains.SheteachesKotlinandspeaksatconferencesworldwide,andiscoauthorofthebookKotlininAction.

Acknowledgements

TheKotlinLanguageDesignTeamandcontributors.ThedevelopersofLeanpub,whichmadepublishingthisbooksomucheasier.

DedicationsFormybelovedfather,E.WayneEckel.April1,1924—November23,2016.Youfirsttaughtmeaboutmachines,tools,anddesign.

Formyfather,SergeyLvovichIsakov,whopassedawaysoearlyandwhowewillalwaysmiss.

AbouttheCoverDanielWill-HarrisdesignedthecoverbasedontheKotlinlogo.

WhyKotlin?

WegiveanoverviewofthehistoricaldevelopmentofprogramminglanguagessoyoucanunderstandwhereKotlinfitsandwhyyoumightwanttolearnit.Thisatomintroducessometopicswhich,ifyouareanovice,mightseemtoocomplicatedrightnow.Feelfreetoskipthisatomandcomebacktoitafteryou’vereadmoreofthebook.

Programsmustbewrittenforpeopletoread,andonlyincidentallyformachinestoexecute.—HaroldAbelson,StructureandInterpretationofComputerPrograms

Programminglanguagedesignisanevolutionarypathfromservingtheneedsofthemachinetoservingtheneedsoftheprogrammer.

Aprogramminglanguageisinventedbyalanguagedesignerandimplementedasoneormoreprogramsthatactastoolsforusingthelanguage.Theimplementerisusuallythelanguagedesigner,atleastinitially.

Earlylanguagesfocusedonhardwarelimitations.Ascomputersbecomemorepowerful,newerlanguagesshifttowardmoresophisticatedprogrammingwithanemphasisonreliability.Theselanguagescanchoosefeaturesbasedonthepsychologyofprogramming.

Everyprogramminglanguageisacollectionofexperiments.Historically,programminglanguagedesignhasbeenasuccessionofguessesandassumptionsaboutwhatwillmakeprogrammersmoreproductive.Someofthoseexperimentsfail,somearemildlysuccessfulandsomeareverysuccessful.

Welearnfromtheexperimentsineachnewlanguage.Somelanguagesaddressissuesthatturnouttobeincidentalratherthanessential,ortheenvironmentchanges(fasterprocessors,cheapermemory,newunderstandingofprogrammingandlanguages)andthatissuebecomeslessimportantoreveninconsequential.Ifthoseideasbecomeobsoleteandthelanguagedoesn’tevolve,itfadesfromuse.

Theoriginalprogrammersworkeddirectlywithnumbersrepresentingprocessormachineinstructions.Thisapproachproducednumerouserrors,andassemblylanguagewascreatedtoreplacethenumberswithmnemonicopcodes—wordsthatprogrammerscouldmoreeasilyrememberandread,alongwithotherhelpfultools.However,therewasstillaone-to-onecorrespondencebetweenassembly-languageinstructionsandmachineinstructions,andprogrammershadtowriteeachlineofassemblycode.Inaddition,eachcomputerprocessoruseditsowndistinctassemblylanguage.

Developingprogramsinassemblylanguageisexceedinglyexpensive.Higher-levellanguageshelpsolvethatproblembycreatingalevelofabstractionawayfromlow-levelassemblylanguages.

CompilersandInterpretersKotliniscompiledratherthaninterpreted.Theinstructionsofaninterpretedlanguageareexecuteddirectlybyaseparateprogramcalledaninterpreter.Incontrast,thesourcecodeofacompiledlanguageisconvertedintoadifferentrepresentationthatrunsasitsownprogram,eitherdirectlyonahardwareprocessororonavirtualmachinethatemulatesaprocessor:

LanguagessuchasC,C++,GoandRustcompileintomachinecodethatrunsdirectlyontheunderlyinghardwarecentralprocessingunit(CPU).LanguageslikeJavaandKotlincompileintobytecodewhichisanintermediate-levelformat

thatdoesn’trundirectlyonthehardwareCPU,butinsteadonavirtualmachine,whichisaprogramthatexecutesbytecodeinstructions.TheJVMversionofKotlinrunsontheJavaVirtualMachine(JVM).

Portabilityisanimportantbenefitofavirtualmachine.Thesamebytecodecanrunoneverycomputerthathasavirtualmachine.Virtualmachinescanbeoptimizedforparticularhardwareandtosolvespeedproblems.TheJVMcontainsmanyyearsofsuchoptimizations,andhasbeenimplementedonmanyplatforms.

Atcompiletime,thecodeischeckedbythecompilertodiscovercompile-timeerrors.(IntelliJIDEAandotherdevelopmentenvironmentshighlighttheseerrorswhenyouinputthecode,soyoucanquicklydiscoverandfixanyproblems).Iftherearenocompile-timeerrors,thesourcecodewillbecompiledintobytecode.

Aruntimeerrorcannotbedetectedatcompiletime,soitonlyemergeswhenyouruntheprogram.Typically,runtimeerrorsaremoredifficulttodiscoverandmoreexpensivetofix.Statically-typedlanguageslikeKotlindiscoverasmanyerrorsaspossibleatcompiletime,whiledynamiclanguagesperformtheirsafetychecksatruntime(somedynamiclanguagesdon’tperformasmanysafetychecksastheymight).

LanguagesthatInfluencedKotlinKotlindrawsitsideasandfeaturesfrommanylanguages,andthoselanguageswereinfluencedbyearlierlanguages.It’shelpfultoknowsomeprogramming-languagehistorytogainperspectiveonhowwegottoKotlin.Thelanguagesdescribedherearechosenfortheirinfluenceonthelanguagesthatfollowedthem.AlltheselanguagesultimatelyinspiredthedesignofKotlin,sometimesbybeinganexampleofwhatnottodo.

FORTRAN:FORmulaTRANslation(1957)Designedforusebyscientistsandengineers,Fortran’sgoalwastomakeiteasiertoencodeequations.Finely-tunedandtestedFortranlibrariesarestillinusetoday,buttheyaretypically“wrapped”tomakethemcallablefromotherlanguages.

LISP:LIStProcessor(1958)

Ratherthanbeingapplication-specific,LISPembodiedessentialprogrammingconcepts;itwasthecomputerscientist’slanguageandthefirstfunctionalprogramminglanguage(You’lllearnaboutfunctionalprogramminginthisbook).Thetradeoffforitspowerandflexibilitywasefficiency:LISPwastypicallytooexpensivetorunonearlymachines,andonlyinrecentdecadeshavemachinesbecomefastenoughtoproducearesurgenceintheuseofLISP.Forexample,theGNUEmacseditoriswrittenentirelyinLISP,andcanbeextendedusingLISP.

ALGOL:ALGOrithmicLanguage(1958)Arguablythemostinfluentialofthe1950’slanguagesbecauseitintroducedsyntaxthatpersistedinmanysubsequentlanguages.Forexample,Canditsderivativesare“ALGOL-like”languages.

COBOL:COmmonBusiness-OrientedLanguage(1959)Designedforbusiness,finance,andadministrativedataprocessing.IthasanEnglish-likesyntax,andwasintendedtobeself-documentingandhighlyreadable.Althoughthisintentgenerallyfailed—COBOLisfamousforbugsintroducedbyamisplacedperiod—theUSDepartmentofDefenseforcedwidespreadadoptiononmainframecomputers,andsystemsarestillrunning(andrequiringmaintenance)today.

BASIC:Beginners’All-purposeSymbolicInstructionCode(1964)BASICwasoneoftheearlyattemptstomakeprogrammingaccessible.Whileverysuccessful,itsfeaturesandsyntaxwerelimited,soitwasonlypartlyhelpfulforpeoplewhoneededtolearnmoresophisticatedlanguages.Itispredominantlyaninterpretedlanguage,whichmeansthattorunityouneedtheoriginalcodefortheprogram.Despitethat,manyusefulprogramswerewritteninBASIC,inparticularasascriptinglanguageforMicrosoft’s“Office”products.BASICmightevenbethoughtofasthefirst“open”programminglanguage,aspeoplemadenumerousvariationsofit.

Simula67,theOriginalObject-OrientedLanguage(1967)Asimulationtypicallyinvolvesmany“objects”interactingwitheachother.Differentobjectshavedifferentcharacteristicsandbehaviors.Languagesthat

existedatthetimewereawkwardtouseforsimulations,soSimula(another“ALGOL-like”language)wasdevelopedtoprovidedirectsupportforcreatingsimulationobjects.Itturnsoutthattheseideasarealsousefulforgeneral-purposeprogramming,andthiswasthegenesisofObject-Oriented(OO)languages.

Pascal(1970)Pascalincreasedcompilationspeedbyrestrictingthelanguagesoitcouldbeimplementedasasingle-passcompiler.Thelanguageforcedtheprogrammertostructuretheircodeinaparticularwayandimposedsomewhatawkwardandless-readableconstraintsonprogramorganization.Asprocessorsbecamefaster,memorycheaper,andcompilertechnologybetter,theimpactoftheseconstraintsbecametoocostly.

AnimplementationofPascal,TurboPascalfromBorland,initiallyworkedonCP/MmachinesandthenmadethemovetoearlyMS-DOS(precursortoWindows),laterevolvingintotheDelphilanguageforWindows.Byputtingeverythinginmemory,TurboPascalcompiledatlightningspeedsonveryunderpoweredmachines,dramaticallyimprovingtheprogrammingexperience.Itscreator,AndersHejlsberg,laterwentontodesignbothC#andTypeScript.

NiklausWirth,theinventorofPascal,createdsubsequentlanguages:Modula,Modula-2andOberon.Asthenameimplies,Modulafocusedondividingprogramsintomodules,forbetterorganizationandfastercompilation.Mostmodernlanguagessupportseparatecompilationandsomeformofmodulesystem.

C(1972)Despitetheincreasingnumberofhigher-levellanguages,programmerswerestillwritingassemblylanguage.Thisisoftencalledsystemsprogramming,becauseitisdoneattheleveloftheoperatingsystem,butitalsoincludesembeddedprogrammingfordedicatedphysicaldevices.Thisisnotonlyarduousandexpensive(Brucebeganhiscareerwritingassemblylanguageforembeddedsystems),butitisn’tportable—assemblylanguagecanonlyrunontheprocessoritiswrittenfor.Cwasdesignedtobea“high-levelassemblylanguage”thatisstillcloseenoughtothehardwarethatyourarelyneedtowriteassembly.Moreimportantly,aCprogramrunsonanyprocessorwithaCcompiler.Cdecoupledtheprogramfromtheprocessor,whichsolvedahugeandexpensiveproblem.As

aresult,formerassembly-languageprogrammerscouldbevastlymoreproductiveinC.Chasbeensoeffectivethatrecentlanguages(notablyGoandRust)arestillattemptingtousurpitforsystemsprogramming.

Smalltalk(1972)Designedfromthebeginningtobepurelyobject-oriented,SmalltalksignificantlymovedOOandlanguagetheoryforwardbybeingaplatformforexperimentationanddemonstratingrapidapplicationdevelopment.However,itwascreatedwhenlanguageswerestillproprietary,andtheentrypriceforaSmalltalksystemcouldbeinthethousands.Itwasinterpreted,soyouneededaSmalltalkenvironmenttorunprograms.Open-sourceSmalltalkimplementationsdidnotappearuntilaftertheprogrammingworldhadmovedon.SmalltalkprogrammershavecontributedgreatinsightsbenefittinglaterOOlanguageslikeC++andJava.

C++:ABetterCwithObjects(1983)BjarneStroustrupcreatedC++becausehewantedabetterCandhewantedsupportfortheobject-orientedconstructshehadexperiencedwhileusingSimula-67.BrucewasamemberoftheC++StandardsCommitteeforitsfirsteightyears,andwrotethreebooksonC++includingThinkinginC++.

Backwards-compatibilitywithCwasafoundationalprincipleofC++design,soCcodecanbecompiledinC++withvirtuallynochanges.Thisprovidedaneasymigrationpath—programmerscouldcontinuetoprograminC,receivethebenefitsofC++,andslowlyexperimentwithC++featureswhilestillbeingproductive.MostcriticismsofC++canbetracedtotheconstraintofbackwardscompatibilitywithC.

OneoftheproblemswithCwastheissueofmemorymanagement.Theprogrammermustfirstacquirememory,thenrunanoperationusingthatmemory,thenreleasethememory.Forgettingtoreleasememoryiscalledamemoryleakandcanresultinusinguptheavailablememoryandcrashingtheprocess.TheinitialversionofC++madesomeinnovationsinthisarea,alongwithconstructorstoensureproperinitialization.Laterversionsofthelanguagehavemadesignificantimprovementsinmemorymanagement.

Python:FriendlyandFlexible(1990)

Python’sdesigner,GuidoVanRossum,createdthelanguagebasedonhisinspirationof“programmingforeveryone.”HisnurturingofthePythoncommunityhasgivenitthereputationofbeingthefriendliestandmostsupportivecommunityintheprogrammingworld.Pythonwasoneofthefirstopen-sourcelanguages,resultinginimplementationsonvirtuallyeveryplatformincludingembeddedsystemsandmachinelearning.Itsdynamismandease-of-usemakesitidealforautomatingsmall,repetitivetasksbutitsfeaturesalsosupportthecreationoflarge,complexprograms.

Pythonisatrue“grass-roots”language;itneverhadacompanypromotingitandtheattitudeofitsfanswastoneverpushthelanguage,butsimplytohelpanyonelearnitwhowantsto.Thelanguagecontinuestosteadilyimprove,andinrecentyearsitspopularityhasskyrocketed.

PythonmayhavebeenthefirstmainstreamlanguagetocombinefunctionalandOOprogramming.ItpredatedJavawithautomaticmemorymanagementusinggarbagecollection(youtypicallyneverhavetoallocateorreleasememoryyourself)andtheabilitytorunprogramsonmultipleplatforms.

Haskell:PureFunctionalProgramming(1990)InspiredbyMiranda(1985),aproprietarylanguage,Haskellwascreatedasanopenstandardforpurefunctionalprogrammingresearch,althoughithasalsobeenusedforproducts.SyntaxandideasfromHaskellhaveinfluencedanumberofsubsequentlanguagesincludingKotlin.

Java:VirtualMachinesandGarbageCollection(1995)JamesGoslingandhisteamweregiventhetaskofwritingcodeforaTVset-topbox.Theydecidedtheydidn’tlikeC++andinsteadofcreatingthebox,createdtheJavalanguage.Thecompany,SunMicrosystems,putanenormousmarketingpushbehindthefreelanguage(stillanewideaatthetime)toattemptdominationoftheemergingInternetlandscape.

ThisperceivedtimewindowforInternetdominationputalotofpressureonJavalanguagedesign,resultinginasignificantnumberofflaws(ThebookThinkinginJavailluminatestheseflawssoreadersarepreparedtocopewiththem).BrianGoetzatOracle,thecurrentleaddeveloperofJava,hasmaderemarkableandsurprisingimprovementsinJavadespitetheconstraintsheinherited.Although

Javawasremarkablysuccessful,animportantKotlindesigngoalistofixJava’sflawssoprogrammerscanbemoreproductive.

Java’ssuccesscamefromtwoinnovativefeatures:avirtualmachineandgarbagecollection.Thesewereavailableinotherlanguages—forexample,LISP,SmalltalkandPythonhavegarbagecollectionandUCSDPascalranonavirtualmachine—buttheywereneverconsideredpracticalformainstreamlanguages.Javachangedthat,andindoingsomadeprogrammerssignificantlymoreproductive.

Avirtualmachineisanintermediatelayerbetweenthelanguageandthehardware.Thelanguagedoesn’thavetogeneratemachinecodeforaparticularprocessor;itonlyneedstogenerateanintermediatelanguage(bytecode)thatrunsonthevirtualmachine.Virtualmachinesrequireprocessingpowerand,beforeJava,werebelievedtobeimpractical.TheJavaVirtualMachine(JVM)gaverisetoJava’sslogan“writeonce,runeverywhere.”Inaddition,otherlanguagescanbemoreeasilydevelopedbytargetingtheJVM;examplesincludeGroovy,aJava-likescriptinglanguage,andClojure,aversionofLISP.

Garbagecollectionsolvestheproblemofforgettingtoreleasememory,orwhenit’sdifficulttoknowwhenapieceofstorageisnolongerused.Projectshavebeensignificantlydelayedorevencancelledbecauseofmemoryleaks.Althoughgarbagecollectionappearsinsomepriorlanguages,itwasbelievedtoproduceanunacceptableamountofoverheaduntilJavademonstrateditspracticality.

JavaScript:JavainNameOnly(1995)TheoriginalWebbrowsersimplycopiedanddisplayedpagesfromaWebserver.Webbrowsersproliferated,becominganewprogrammingplatformthatneededlanguagesupport.Javawantedtobethislanguagebutwastooawkwardforthejob.JavaScriptbeganasLiveScriptandwasbuiltintoNetScapeNavigator,oneofthefirstWebbrowsers.RenamingittoJavaScriptwasamarketingploybyNetScape,asthelanguagehasonlyavaguesimilaritytoJava.

AstheWebtookoff,JavaScriptbecametremendouslyimportant.However,thebehaviorofJavaScriptwassounpredictablethatDouglasCrockfordwroteabookwiththetongue-in-cheektitleJavaScript,theGoodParts,wherehedemonstratedalltheproblemswiththelanguagesoprogrammerscouldavoidthem.SubsequentimprovementsbytheECMAScriptcommitteehavemade

JavaScriptunrecognizeabletoanoriginalJavaScriptprogrammer.Itisnowconsideredastableandmaturelanguage.

Webassembly(WASM)wasderivedfromJavaScripttobeakindofbytecodeforwebbrowsers.ItoftenrunsmuchfasterthanJavaScriptandcanbegeneratedbyotherlanguages.Atthiswriting,theKotlinteamisworkingtoaddWASMasatarget.

C#:Javafor.NET(2000)C#wasdesignedtoprovidesomeoftheimportantabilitiesofJavaonthe.NET(Windows)platform,whilefreeingdesignersfromtheconstraintoffollowingtheJavalanguage.TheresultincludednumerousimprovementsoverJava.Forexample,C#developedtheconceptofextensionfunctions,whichareheavilyusedinKotlin.C#alsobecamesignificantlymorefunctionalthanJava.ManyC#featuresclearlyinfluencedKotlindesign.

Scala:SCALAble(2003)MartinOderskycreatedScalatorunontheJavavirtualmachine:TopiggybackontheworkdoneontheJVM,tointeractwithJavaprograms,andpossiblywiththeideathatitmightdisplaceJava.Asaresearcher,OderskyandhisteamusedScalaasaplatformtoexperimentwithlanguagefeatures,notablythosenotincludedinJava.

TheseexperimentswereilluminatingandanumberofthemfoundtheirwayintoKotlin,usuallyinamodifiedform.Forexample,theabilitytoredefineoperatorslike+foruseinspecialcasesiscalledoperatoroverloading.ThiswasincludedinC++butnotJava.Scalaaddedoperatoroverloadingbutalsoallowsyoutoinventnewoperatorsbycombininganysequenceofcharacters.Thisoftenproducesconfusingcode.AlimitedformofoperatoroverloadingisincludedinKotlin,butyoucanonlyoverloadoperatorsthatalreadyexist.

Scalaisalsoanobject-functionalhybrid,likePythonbutwithafocusonpurefunctionsandstrictobjects.ThishelpedinspireKotlin’schoicetoalsobeanobject-functionalhybrid.

LikeScala,KotlinrunsontheJVMbutitinteractswithJavafarmoreeasilythanScaladoes.Inaddition,KotlintargetsJavaScript,theAndroidOS,anditgeneratesnativecodeforotherplatforms.

AtomicKotlinevolvedfromtheideasandmaterialinAtomicScala.

Groovy:ADynamicJVMLanguage(2007)Dynamiclanguagesareappealingbecausetheyaremoreinteractiveandconcisethanstaticlanguages.TherehavebeennumerousattemptstoproduceamoredynamicprogrammingexperienceontheJVM,includingJython(Python)andClojure(adialectofLisp).Groovywasthefirsttoachievewideacceptance.

Atfirstglance,Groovyappearstobeacleaned-upversionofJava,producingamorepleasantprogrammingexperience.MostJavacodewillrununchangedinGroovy,soJavaprogrammerscanbequicklyproductive,laterlearningthemoresophisticatedfeaturesthatprovidenotableprogrammingimprovementsoverJava.

TheKotlinoperators?.and?:thatdealwiththeproblemofemptinessfirstappearedinGroovy.

TherearenumerousGroovyfeaturesthatarerecognizeableinKotlin.Someofthosefeaturesalsoappearinotherlanguages,whichprobablypushedharderforthemtobeincludedinKotlin.

WhyKotlin?(Introduced2011,Version1.0:2016)JustasC++wasinitiallyintendedtobe“abetterC,”Kotlinwasinitiallyorientedtowardsbeing“abetterJava.”Ithassinceevolvedsignificantlybeyondthatgoal.

Kotlinpragmaticallychoosesonlythemostsuccessfulandhelpfulfeaturesfromotherprogramminglanguages—afterthosefeatureshavebeenfield-testedandprovenespeciallyvaluable.

Thus,ifyouarecomingfromanotherlanguage,youmightrecognizesomefeaturesofthatlanguageinKotlin.Thisisintentional:Kotlinmaximizesproductivitybyleveragingtestedconcepts.

ReadabilityReadabilityisaprimarygoalinthedesignofthelanguage.Kotlinsyntaxisconcise—itrequiresnoceremonyformostscenarios,butcanstillexpresscomplexideas.

ToolingKotlincomesfromJetBrains,acompanythatspecializesindevelopertooling.Ithasfirst-classtoolingsupport,andmanylanguagefeaturesweredesignedwithtoolinginmind.

Multi-ParadigmKotlinsupportsmultipleprogrammingparadigms,whicharegentlyintroducedinthisbook:

ImperativeprogrammingFunctionalprogrammingObject-orientedprogramming

Multi-PlatformKotlinsourcecodecanbecompiledtodifferenttargetplatforms:

JVM.ThesourcecodecompilesintoJVMbytecode(.classfiles),whichcanthenberunonanyJavaVirtualMachine(JVM).Android.AndroiditsownruntimecalledART(thepredecessorwascalledDalvik).TheKotlinsourcecodeiscompiledintoDalvikExecutableFormat(.dexfiles).JavaScript,toruninsideawebbrowser.NativeBinariesbygeneratingmachinecodeforspecificplatformsandCPUs.

Thisbookfocusesonthelanguageitself,usingtheJVMastheonlytargetplatform.Onceyouknowthelanguage,youcanapplyKotlintodifferentapplicationandtargetplatforms.

TwoKotlinFeaturesThisatomdoesnotassumeyouareaprogrammer,whichmakesithardtoexplainmostofthebenefitsofKotlinoverthealternatives.Thereare,however,twotopicswhichareveryimpactfulandcanbeexplainedatthisearlyjuncture:Javainteroperabilityandtheissueofindicating“novalue.”

EffortlessJavaInteroperability

Tobe“abetterC,”C++mustbebackwardscompatiblewiththesyntaxofC,butKotlindoesnothavetobebackwardscompatiblewiththesyntaxofJava—itonlyneedstoworkwiththeJVM.ThisfreestheKotlindesignerstocreateamuchcleanerandmorepowerfulsyntax,withoutthevisualnoiseandcomplicationthatcluttersJava.

ForKotlintobe“abetterJava,”theexperienceoftryingitmustbepleasantandfrictionless,soKotlinenableseffortlessintegrationwithexistingJavaprojects.YoucanwriteasmallpieceofKotlinfunctionalityandslipitinamidstyourexistingJavacode.TheJavacodedoesn’tevenknowtheKotlincodeisthere—itjustlookslikemoreJavacode.

Companiesofteninvestigateanewlanguagebybuildingastandaloneprogramwiththatlanguage.Ideally,thisprogramisbeneficialbutnonessential,soiftheprojectfailsitcanbeterminatedwithminimaldamage.Noteverycompanywantstospendthekindofresourcesnecessaryforthistypeofexperimentation.BecauseKotlinseamlesslyintegratesintoanexistingJavasystem(andbenefitsfromthatsystem’stests),itbecomesverycheaporevenfreetotryKotlintoseewhetherit’sagoodfit.

Inaddition,JetBrains,thecompanythatcreatesKotlin,providesIntelliJIDEAina“Community”(free)version,whichincludessupportforbothJavaandKotlinalongwiththeabilitytoeasilyintegratethetwo.ItevenhasatoolthattakesJavacodeand(mostly)rewritesittoKotlin.

AppendixBcoversJavainteroperability.

RepresentingEmptinessAnespeciallybeneficialKotlinfeatureisitssolutiontoachallengingprogrammingproblem.

Whatdoyoudowhensomeonehandsyouadictionaryandasksyoutolookupawordthatdoesn’texist?Youcouldguaranteeresultsbymakingupdefinitionsforunknownwords.Amoreusefulapproachisjusttosay,“There’snodefinitionforthatword.”Thisdemonstratesasignificantprobleminprogramming:Howdoyouindicate“novalue”forapieceofstoragethatisuninitialized,orfortheresultofanoperation?

Thenullreferencewasinventedin1965forALGOLbyTonyHoare,wholatercalledit“mybillion-dollarmistake.”Oneproblemwasthatitwastoosimple—sometimesbeingtoldaroomisemptyisn’tenough;youmightneedtoknow,forexample,whyitisempty.Thisleadstothesecondproblem:theimplementation.Forefficiency’ssake,itwastypicallyjustaspecialvaluethatcouldfitinasmallamountofmemory,andwhatbetterthanthememoryalreadyallocatedforthatinformation?

TheoriginalClanguagedidnotautomaticallyinitializestorage,whichcausednumerousproblems.C++improvedthesituationbysettingnewly-allocatedstoragetoallzeroes.Thus,ifanumericalvalueisn’tinitialized,itissimplyanumericalzero.Thisdidn’tseemsobadbutitalloweduninitializedvaluestoquietlyslipthroughthecracks(newerCandC++compilersoftenwarnyouaboutthese).Worse,ifapieceofstoragewasapointer—usedtoindicate(“pointto”)anotherpieceofstorage—anullpointerwouldpointatlocationzeroinmemory,whichisalmostcertainlynotwhatyouwant.

Javapreventsaccessestouninitializedvaluesbyreportingsucherrorsatruntime.Althoughthisdiscoversuninitializedvalues,itdoesn’tsolvetheproblembecausetheonlywayyoucanverifythatyourprogramwon’tcrashisbyrunningit.ThereareswarmsofthesekindsofbugsinJavacode,andprogrammerswastehugeamountsoftimefindingthem.

Kotlinsolvesthisproblembypreventingoperationsthatmightcausenullerrorsatcompiletime,beforetheprogramcanrun.Thisisthesingle-mostcelebratedfeaturebyJavaprogrammersadoptingKotlin.ThisonefeaturecanminimizeoreliminateJava’snullerrors.

AnAbundanceofBenefitsThetwofeatureswewereabletoexplainhere(withoutrequiringmoreprogrammingknowledge)makeahugedifferencewhetherornotyou’reaJavaprogrammer.IfKotlinisyourfirstlanguageandyouenduponaprojectthatneedsmoreprogrammers,itismucheasiertorecruitoneofthemanyexistingJavaprogrammersintoKotlin.

Kotlinhasmanyotherbenefits,whichwecannotexplainuntilyouknowmoreaboutprogramming.That’swhattherestofthebookisfor.

-

Languagesareoftenselectedbypassion,notreason…I’mtryingtomakeKotlinalanguagethatislovedforareason.—AndreyBreslav,KotlinLeadLanguageDesigner.

Hello,World!

“Hello,world!”isaprogramcommonlyusedtodemonstratethebasicsyntaxofprogramminglanguages.

Wedevelopthisprograminseveralstepssoyouunderstanditsparts.

First,let’sexamineanemptyprogramthatdoesnothingatall:

//HelloWorld/EmptyProgram.kt

funmain(){

//Programcodehere...

}

Theexamplestartswithacomment,whichisilluminatingtextthatisignoredbyKotlin.//(twoforwardslashes)beginsacommentthatgoestotheendofthecurrentline:

//Single-linecomment

Kotlinignoresthe//andeverythingafterituntiltheendoftheline.Onthefollowingline,itpaysattentionagain.

Thefirstlineofeachexampleinthisbookisacommentstartingwiththenameofthethesubdirectorycontainingthesource-codefile(Here,HelloWorld)followedbythenameofthefile:EmptyProgram.kt.Theexamplesubdirectoryforeachatomcorrespondstothenameofthatatom.

keywordsarereservedbythelanguageandgivenspecialmeaning.Thekeywordfunisshortforfunction.Afunctionisacollectionofcodethatcanbeexecutedusingthatfunction’sname(wespendalotoftimeonfunctionsthroughoutthebook).Thefunction’snamefollowsthefunkeyword,sointhiscaseit’smain()(inprose,wefollowthefunctionnamewithparentheses).

main()isactuallyaspecialnameforafunction;itindicatesthe“entrypoint”foraKotlinprogram.AKotlinprogramcanhavemanyfunctionswithmany

differentnames,butmain()istheonethat’sautomaticallycalledwhenyouexecutetheprogram.

Theparameterlistfollowsthefunctionnameandisenclosedbyparentheses.Here,wedon’tpassanythingintomain()sotheparameterlistisempty.

Thefunctionbodyappearsaftertheparameterlist.Itbeginswithanopeningbrace({)andendswithaclosingbrace(}).Thefunctionbodycontainsstatementsandexpressions.Astatementproducesaneffect,andanexpressionyieldsaresult.

EmptyProgram.ktcontainsnostatementsorexpressionsinthebody,justacomment.

Let’smaketheprogramdisplay“Hello,world!”byaddingalineinthemain()body:

//HelloWorld/HelloWorld.kt

funmain(){

println("Hello,world!")

}

/*Output:

Hello,world!

*/

Thelinethatdisplaysthegreetingbeginswithprintln().Likemain(),println()isafunction.Thislinecallsthefunction,whichexecutesitsbody.Yougivethefunctionname,followedbyparenthesescontainingoneormoreparameters.Inthisbook,whenreferringtoafunctionintheprose,weaddparenthesesafterthenameasareminderthatitisafunction.Here,wesayprintln().

println()takesasingleparameter,whichisaString.YoudefineaStringbyputtingcharactersinsidequotes.

println()movesthecursortoanewlineafterdisplayingitsparameter,sosubsequentoutputappearsonthenextline.Youcanuseprint()instead,whichleavesthecursoronthesameline.

Unlikesomelanguages,youdon’tneedasemicolonattheendofanexpressioninKotlin.It’sonlynecessaryifyouputmorethanoneexpressiononasingleline(thisisdiscouraged).

Forsomeexamplesinthebook,weshowtheoutputattheendofthelisting,insideamultilinecomment.Amultilinecommentstartswitha/*(aforwardslashfollowedbyanasterisk)andcontinues—includinglinebreaks(whichwecallnewlines)—untila*/(anasteriskfollowedbyaforwardslash)endsthecomment:

/*Amultilinecomment

Doesn'tcare

aboutnewlines*/

It’spossibletoaddcodeonthesamelineaftertheclosing*/ofacomment,butit’sconfusing,sopeopledon’tusuallydoit.

Commentsaddinformationthatisn’tobviousfromreadingthecode.Ifcommentsonlyrepeatwhatthecodesays,theybecomeannoyingandpeoplestartignoringthem.Whencodechanges,programmersoftenforgettoupdatecomments,soit’sgoodpracticetousecommentsjudiciously,mainlyforhighlightingtrickyaspectsofyourcode.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

var&val

Whenanidentifierholdsdata,youmustdecidewhetheritcanbereassigned.

Youcreateidentifierstorefertoelementsinyourprogram.Themostbasicdecisionforadataidentifieriswhetheritcanchangeitscontentsduringprogramexecution,orifitcanonlybeassignedonce.Thisiscontrolledbytwokeywords:

var,shortforvariable,whichmeansyoucanreassignitscontents.val,shortforvalue,whichmeansyoucanonlyinitializeit;youcannotreassignit.

Youdefineavarlikethis:

varidentifier=initialization

Thevarkeywordisfollowedbytheidentifier,anequalssignandthentheinitializationvalue.Theidentifierbeginswithaletteroranunderscore,followedbyletters,numbersandunderscores.Upperandlowercasearedistinguished(sothisvalueandthisValuearedifferent).

Herearesomevardefinitions:

//VarAndVal/Vars.kt

funmain(){

varwhole=11//[1]

varfractional=1.4//[2]

varwords="TwasBrillig"//[3]

println(whole)

println(fractional)

println(words)

}

/*Output:

11

1.4

TwasBrillig

*/

Inthisbookwemarklineswithcommentednumbersinsquarebracketssowecanrefertotheminthetextlikethis:

[1]Createavarnamedwholeandstore11init.[2]Storethe“fractionalnumber”1.4inthevarfractional.[3]Storesometext(aString)inthevarwords.

Notethatprintln()cantakeanysinglevalueasanargument.

Asthenamevariableimplies,avarcanvary.Thatis,youcanchangethedatastoredinavar.Wesaythatavarismutable:

//VarAndVal/AVarIsMutable.kt

funmain(){

varsum=1

sum=sum+2

sum+=3

println(sum)

}

/*Output:

6

*/

Theassignmentsum=sum+2takesthecurrentvalueofsum,addstwo,andassignstheresultbackintosum.

Theassignmentsum+=3meansthesameassum=sum+3.The+=operatortakesthepreviousvaluestoredinsumandincreasesitby3,thenassignsthatnewresultbacktosum.

Changingthevaluestoredinavarisausefulwaytoexpresschanges.However,whenthecomplexityofaprogramincreases,yourcodeisclearer,saferandeasiertounderstandifthevaluesrepresentedbyyouridentifierscannotchange—thatis,theycannotbereassigned.Wespecifyanunchangingidentifierusingthevalkeywordinsteadofvar.Avalcanonlybeassignedonce,whenitiscreated:

validentifier=initialization

Thevalkeywordcomesfromvalue,indicatingsomethingthatcannotchange—itisimmutable.Choosevalinsteadofvarwheneverpossible.TheVars.ktexampleatthebeginningofthisatomcanberewrittenusingvals:

//VarAndVal/Vals.kt

funmain(){

valwhole=11

//whole=15//Error//[1]

valfractional=1.4

valwords="TwasBrillig"

println(whole)

println(fractional)

println(words)

}

/*Output:

11

1.4

TwasBrillig

*/

[1]Onceyouinitializeaval,youcan’treassignit.Ifwetrytoreassignwholetoadifferentnumber,Kotlincomplains,saying“Valcannotbereassigned.”

Choosingdescriptivenamesforyouridentifiersmakesyourcodeeasiertounderstandandoftenreducestheneedforcomments.InVals.kt,youhavenoideawhatwholerepresents.Ifyourprogramisstoringthenumber11torepresentthetimeofdaywhenyougetcoffee,it’smoreobvioustoothersifyounameitcoffeetimeandeasiertoreadifit’scoffeeTime(followingKotlinstyle,wemakethefirstletterlowercase).

-

varsareusefulwhendatamustchangeastheprogramisrunning.Thissoundslikeacommonrequirement,butturnsouttobeavoidableinpractice.Ingeneral,yourprogramsareeasiertoextendandmaintainifyouusevals.However,onrareoccasionsit’stoocomplextosolveaproblemusingonlyvals.Forthatreason,Kotlingivesyoutheflexibilityofvars.However,asyouspendmoretimewithvalsyou’lldiscoverthatyoualmostneverneedvarsandthatyourprogramsaresaferandmorereliablewithoutthem.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

DataTypes

Datacanhavedifferenttypes.

Tosolveamathproblem,youwriteanexpression:

5.9+6

Youknowthataddingthosenumbersproducesanothernumber.Kotlinknowsthattoo.Youknowthatoneisafractionalnumber(5.9),whichKotlincallsaDouble,andtheotherisawholenumber(6),whichKotlincallsanInt.Youknowtheresultisafractionalnumber.

Atype(alsocalleddatatype)tellsKotlinhowyouintendtousethatdata.Atypeprovidesasetofvaluesfromwhichanexpressionmaytakeitsvalues.Atypedefinestheoperationsthatcanbeperformedonthedata,themeaningofthedata,andhowvaluesofthattypecanbestored.

Kotlinusestypestoverifythatyourexpressionsarecorrect.Intheaboveexpression,KotlincreatesanewvalueoftypeDoubletoholdtheresult.

Kotlintriestoadapttowhatyouneed.Ifyouaskittodosomethingthatviolatestyperulesitproducesanerrormessage.Forexample,tryaddingaStringandanumber:

//DataTypes/StringPlusNumber.kt

funmain(){

println("Sally"+5.9)

}

/*Output:

Sally5.9

*/

TypestellKotlinhowtousethemcorrectly.Inthiscase,thetyperulestellKotlinhowtoaddanumbertoaString:byappendingthetwovaluesandcreatingaStringtoholdtheresult.

NowtrymultiplyingaStringandaDoublebychangingthe+inStringPlusNumber.kttoa*:

"Sally"*5.9

Combiningtypesthiswaydoesn’tmakesensetoKotlin,soitgivesyouanerror.

Invar&val,westoredseveraltypes.Kotlinfiguredoutthetypesforus,basedonhowweusedthem.Thisiscalledtypeinference.

Wecanbemoreverboseandspecifythetype:

validentifier:Type=initialization

Youstartwiththevalorvarkeyword,followedbytheidentifier,acolon,thetype,an=,andtheinitializationvalue.Soinsteadofsaying:

valn=1

varp=1.2

Youcansay:

valn:Int=1

varp:Double=1.2

We’vetoldKotlinthatnisanIntandpisaDouble,ratherthanlettingitinferthetype.

HerearesomeofKotlin’sbasictypes:

//DataTypes/Types.kt

funmain(){

valwhole:Int=11//[1]

valfractional:Double=1.4//[2]

valtrueOrFalse:Boolean=true//[3]

valwords:String="Avalue"//[4]

valcharacter:Char='z'//[5]

vallines:String="""Triplequoteslet

youhavemanylines

inyourstring"""//[6]

println(whole)

println(fractional)

println(trueOrFalse)

println(words)

println(character)

println(lines)

}

/*Output:

11

1.4

true

Avalue

z

Triplequoteslet

youhavemanylines

inyourstring

*/

[1]TheIntdatatypeisaninteger,whichmeansitonlyholdswholenumbers.[2]Toholdfractionalnumbers,useaDouble.[3]ABooleandatatypeonlyholdsthetwospecialvaluestrueandfalse.[4]AStringholdsasequenceofcharacters.Youassignavalueusingadouble-quotedString.[5]ACharholdsonecharacter.[6]Ifyouhavemanylinesand/orspecialcharacters,surroundthemwithtriple-double-quotes(thisisatriple-quotedString).

Kotlinusestypeinferencetodeterminethemeaningofmixedtypes.WhenmixingIntsandDoublesduringaddition,forexample,Kotlindecidesthetypefortheresultingvalue:

//DataTypes/Inference.kt

funmain(){

valn=1+1.2

println(n)

}

/*Output:

2.2

*/

WhenyouaddanInttoaDoubleusingtypeinference,KotlindeterminesthattheresultnisaDoubleandensuresthatitfollowsalltherulesforDoubles.

Kotlin’stypeinferenceispartofitsstrategyofdoingworkfortheprogrammer.Ifyouleaveoutthetypedeclaration,Kotlincanusuallyinferit.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Functions

Afunctionislikeasmallprogramthathasitsownname,andcanbeexecuted(invoked)bycallingthatnamefromanotherfunction.

Afunctioncombinesagroupofactivities,andisthemostbasicwaytoorganizeyourprogramsandtore-usecode.

Youpassinformationintoafunction,andthefunctionusesthatinformationtocalculateandproducearesult.Thebasicformofafunctionis:

funfunctionName(p1:Type1,p2:Type2,...):ReturnType{

linesofcode

returnresult

}

p1andp2aretheparameters:theinformationyoupassintothefunction.Eachparameterhasanidentifiername(p1,p2)followedbyacolonandthetypeofthatparameter.Theclosingparenthesisoftheparameterlistisfollowedbyacolonandthetypeofresultproducedbythefunction.Thelinesofcodeinthefunctionbodyareenclosedincurlybraces.Theexpressionfollowingthereturnkeywordistheresultthefunctionproduceswhenit’sfinished.

Aparameterishowyoudefinewhatispassedintoafunction—it’stheplaceholder.Anargumentistheactualvaluethatyoupassintothefunction.

Thecombinationofname,parametersandreturntypeiscalledthefunctionsignature.

Here’sasimplefunctioncalledmultiplyByTwo():

//Functions/MultiplyByTwo.kt

funmultiplyByTwo(x:Int):Int{//[1]

println("InsidemultiplyByTwo")//[2]

returnx*2

}

funmain(){

valr=multiplyByTwo(5)//[3]

println(r)

}

/*Output:

InsidemultiplyByTwo

10

*/

[1]Noticethefunkeyword,thefunctionname,andtheparameterlistconsistingofasingleparameter.ThisfunctiontakesanIntparameterandreturnsanInt.[2]Thesetwolinesarethebodyofthefunction.Thefinallinereturnsthevalueofitscalculationx*2astheresultofthefunction.[3]Thislinecallsthefunctionwithanappropriateargument,andcapturestheresultintovalr.Afunctioncallmimicstheformofitsdeclaration:thefunctionname,followedbyargumentsinsideparentheses.

Thefunctioncodeisexecutedbycallingthefunction,usingthefunctionnamemultiplyByTwo()asanabbreviationforthatcode.Thisiswhyfunctionsarethemostbasicformofsimplificationandcodereuseinprogramming.Youcanalsothinkofafunctionasanexpressionwithsubstitutablevalues(theparameters).

println()isalsoafunctioncall—itjusthappenstobeprovidedbyKotlin.WerefertofunctionsdefinedbyKotlinaslibraryfunctions.

Ifthefunctiondoesn’tprovideameaningfulresult,itsreturntypeisUnit.YoucanspecifyUnitexplicitlyifyouwant,butKotlinletsyouomitit:

//Functions/SayHello.kt

funsayHello(){

println("Hallo!")

}

funsayGoodbye():Unit{

println("AufWiedersehen!")

}

funmain(){

sayHello()

sayGoodbye()

}

/*Output:

Hallo!

AufWiedersehen!

*/

BothsayHello()andsayGoodbye()returnUnit,butsayHello()leavesouttheexplicitdeclaration.Themain()functionalsoreturnsUnit.

Ifafunctionisonlyasingleexpression,youcanusetheabbreviatedsyntaxofanequalssignfollowedbytheexpression:

funfunctionName(arg1:Type1,arg2:Type2,...):ReturnType=expression

Afunctionbodysurroundedbycurlybracesiscalledablockbody.Afunctionbodyusingtheequalssyntaxiscalledanexpressionbody.

Here,multiplyByThree()usesanexpressionbody:

//Functions/MultiplyByThree.kt

funmultiplyByThree(x:Int):Int=x*3

funmain(){

println(multiplyByThree(5))

}

/*Output:

15

*/

Thisisashortversionofsayingreturnx*3insideablockbody.

Kotlininfersthereturntypeofafunctionthathasanexpressionbody:

//Functions/MultiplyByFour.kt

funmultiplyByFour(x:Int)=x*4

funmain(){

valresult:Int=multiplyByFour(5)

println(result)

}

/*Output:

20

*/

KotlininfersthatmultiplyByFour()returnsanInt.

Kotlincanonlyinferreturntypesforexpressionbodies.Ifafunctionhasablockbodyandyouomititstype,thatfunctionreturnsUnit.

-

Whenwritingfunctions,choosedescriptivenames.Thismakesthecodeeasiertoread,andcanoftenreducetheneedforcodecomments.Wecan’talwaysbeasdescriptiveaswewouldpreferwiththefunctionnamesinthisbookbecausewe’reconstrainedbylinewidths.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ifExpressions

Anifexpressionmakesachoice.

Theifkeywordtestsanexpressiontoseewhetherit’strueorfalseandperformsanactionbasedontheresult.Atrue-or-falseexpressioniscalledaBoolean,afterthemathematicianGeorgeBoolewhoinventedthelogicbehindtheseexpressions.Here’sanexampleusingthe>(greaterthan)and<(lessthan)symbols:

//IfExpressions/If1.kt

funmain(){

if(1>0)

println("It'strue!")

if(10<11){

println("10<11")

println("tenislessthaneleven")

}

}

/*Output:

It'strue!

10<11

tenislessthaneleven

*/

Theexpressioninsidetheparenthesesaftertheifmustevaluatetotrueorfalse.Iftrue,thefollowingexpressionisexecuted.Toexecutemultiplelines,placethemwithincurlybraces.

WecancreateaBooleanexpressioninoneplace,anduseitinanother:

//IfExpressions/If2.kt

funmain(){

valx:Boolean=1>=1

if(x)

println("It'strue!")

}

/*Output:

It'strue!

*/

BecausexisBoolean,theifcantestitdirectlybysayingif(x).

TheBoolean>=operatorreturnstrueiftheexpressionontheleftsideoftheoperatorisgreaterthanorequaltothatontheright.Likewise,<=returnstrueiftheexpressionontheleftsideislessthanorequaltothatontheright.

Theelsekeywordallowsyoutohandlebothtrueandfalsepaths:

//IfExpressions/If3.kt

funmain(){

valn:Int=-11

if(n>0)

println("It'spositive")

else

println("It'snegativeorzero")

}

/*Output:

It'snegativeorzero

*/

Theelsekeywordisonlyusedinconjunctionwithif.Youarenotlimitedtoasinglecheck—youcantestmultiplecombinationsbycombiningelseandif:

//IfExpressions/If4.kt

funmain(){

valn:Int=-11

if(n>0)

println("It'spositive")

elseif(n==0)

println("It'szero")

else

println("It'snegative")

}

/*Output:

It'snegative

*/

Hereweuse==tochecktwonumbersforequality.!=testsforinequality.

Thetypicalpatternistostartwithif,followedbyasmanyelseifclausesasyouneed,endingwithafinalelseforanythingthatdoesn’tmatchalltheprevioustests.Whenanifexpressionreachesacertainsizeandcomplexityyou’llprobablyuseawhenexpressioninstead.whenisdescribedlaterinthebook,inwhenExpressions.

The“not”operator!testsfortheoppositeofaBooleanexpression:

//IfExpressions/If5.kt

funmain(){

valy:Boolean=false

if(!y)

println("!yistrue")

}

/*Output:

!yistrue

*/

Toverbalizeif(!y),say“ifnoty.”

Theentireifisanexpression,soitcanproducearesult:

//IfExpressions/If6.kt

funmain(){

valnum=10

valresult=if(num>100)4else42

println(result)

}

/*Output:

42

*/

Here,westorethevalueproducedbytheentireifexpressioninanintermediateidentifiercalledresult.Iftheconditionissatisfied,thefirstbranchproducesresult.Ifnot,theelsevaluebecomesresult.

Let’spracticecreatingfunctions.Here’sonethattakesaBooleanparameter:

//IfExpressions/TrueOrFalse.kt

funtrueOrFalse(exp:Boolean):String{

if(exp)

return"It'strue!"//[1]

return"It'sfalse"//[2]

}

funmain(){

valb=1

println(trueOrFalse(b<3))

println(trueOrFalse(b>=3))

}

/*Output:

It'strue!

It'sfalse

*/

TheBooleanparameterexpispassedtothefunctiontrueOrFalse().Iftheargumentispassedasanexpression,suchasb<3,thatexpressionisfirstevaluatedandtheresultispassedtothefunction.trueOrFalse()testsexpandiftheresultistrue,line[1]isexecuted,otherwiseline[2]isexecuted.

[1]returnsays,“Leavethefunctionandproducethisvalueasthefunction’sresult.”Noticethatreturncanappearanywhereinafunction

anddoesnothavetobeattheend.

Ratherthanusingreturnasinthepreviousexample,youcanusetheelsekeywordtoproducetheresultasanexpression:

//IfExpressions/OneOrTheOther.kt

funoneOrTheOther(exp:Boolean):String=

if(exp)

"True!"//No'return'necessary

else

"False"

funmain(){

valx=1

println(oneOrTheOther(x==1))

println(oneOrTheOther(x==2))

}

/*Output:

True!

False

*/

InsteadoftwoexpressionsintrueOrFalse(),oneOrTheOther()isasingleexpression.Theresultofthatexpressionistheresultofthefunction,sotheifexpressionbecomesthefunctionbody.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

StringTemplates

AStringtemplateisaprogrammaticwaytogenerateaString.

Ifyouputa$beforeanidentifiername,theStringtemplatewillinsertthatidentifier’scontentsintotheString:

//StringTemplates/StringTemplates.kt

funmain(){

valanswer=42

println("Found$answer!")//[1]

println("printinga$1")//[2]

}

/*Output:

Found42!

printinga$1

*/

[1]$answersubstitutesthevalueofanswer.[2]Ifwhatfollowsthe$isn’trecognizableasaprogramidentifier,nothingspecialhappens.

YoucanalsoinsertvaluesintoaStringusingconcatenation(+):

//StringTemplates/StringConcatenation.kt

funmain(){

vals="hi\n"//\nisanewlinecharacter

valn=11

vald=3.14

println("first:"+s+"second:"+

n+",third:"+d)

}

/*Output:

first:hi

second:11,third:3.14

*/

Placinganexpressioninside${}evaluatesit.ThereturnvalueisconvertedtoaStringandinsertedintotheresultingString:

//StringTemplates/ExpressionInTemplate.kt

funmain(){

valcondition=true

println(

"${if(condition)'a'else'b'}")//[1]

valx=11

println("$x+4=${x+4}")

}

/*Output:

a

11+4=15

*/

[1]if(condition)'a'else'b'isevaluatedandtheresultissubstitutedfortheentire${}expression.

WhenaStringmustincludeaspecialcharacter,suchasaquote,youcaneitherescapethatcharacterwitha\(backslash),oruseaStringliteralintriplequotes:

//StringTemplates/TripleQuotes.kt

funmain(){

vals="value"

println("s=\"$s\".")

println("""s="$s".""")

}

/*Output:

s="value".

s="value".

*/

Withtriplequotes,youinsertavalueofanexpressionthesamewayyoudoitforasingle-quotedString.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

NumberTypes

Differenttypesofnumbersarestoredindifferentways.

Ifyoucreateanidentifierandassignanintegervaluetoit,KotlininferstheInttype:

//NumberTypes/InferInt.kt

funmain(){

valmillion=1_000_000//InfersInt

println(million)

}

/*Output:

1000000

*/

Forreadability,Kotlinallowsunderscoreswithinnumericalvalues.

Thebasicmathematicaloperatorsfornumbersaretheonesavailableinmostprogramminglanguages:addition(+),subtraction(-),division(/),multiplication(*)andmodulus(%),whichproducestheremainderfromintegerdivision:

//NumberTypes/Modulus.kt

funmain(){

valnumerator:Int=19

valdenominator:Int=10

println(numerator%denominator)

}

/*Output:

9

*/

Integerdivisiontruncatesitsresult:

//NumberTypes/IntDivisionTruncates.kt

funmain(){

valnumerator:Int=19

valdenominator:Int=10

println(numerator/denominator)

}

/*Output:

1

*/

Iftheoperationhadroundedtheresult,theoutputwouldbe2.

Theorderofoperationsfollowsbasicarithmetic:

//NumberTypes/OpOrder.kt

funmain(){

println(45+5*6)

}

/*Output:

75

*/

Themultiplicationoperation5*6isperformedfirst,followedbytheaddition45+30.

Ifyouwant45+5tohappenfirst,useparentheses:

//NumberTypes/OpOrderParens.kt

funmain(){

println((45+5)*6)

}

/*Output:

300

*/

Nowlet’scalculatebodymassindex(BMI),whichisweightinkilogramsdividedbythesquareoftheheightinmeters.IfyouhaveaBMIoflessthan18.5,youareunderweight.Between18.5and24.9isnormalweight.BMIof25andhigherisoverweight.Thisexamplealsoshowsthepreferredformattingstylewhenyoucan’tfitthefunction’sparametersonasingleline:

//NumberTypes/BMIMetric.kt

funbmiMetric(

weight:Double,

height:Double

):String{

valbmi=weight/(height*height)//[1]

returnif(bmi<18.5)"Underweight"

elseif(bmi<25)"Normalweight"

else"Overweight"

}

funmain(){

valweight=72.57//160lbs

valheight=1.727//68inches

valstatus=bmiMetric(weight,height)

println(status)

}

/*Output:

Normalweight

*/

[1]Ifyouremovetheparentheses,youdivideweightbyheightthenmultiplythatresultbyheight.That’samuchlargernumber,andthewronganswer.

bmiMetric()usesDoublesfortheweightandheight.ADoubleholdsverylargeandverysmallfloating-pointnumbers.

Here’saversionusingEnglishunits,representedbyIntparameters:

//NumberTypes/BMIEnglish.kt

funbmiEnglish(

weight:Int,

height:Int

):String{

valbmi=

weight/(height*height)*703.07//[1]

returnif(bmi<18.5)"Underweight"

elseif(bmi<25)"Normalweight"

else"Overweight"

}

funmain(){

valweight=160

valheight=68

valstatus=bmiEnglish(weight,height)

println(status)

}

/*Output:

Underweight

*/

WhydoestheresultdifferfrombmiMetric(),whichusesDoubles?Whenyoudivideanintegerbyanotherinteger,Kotlinproducesanintegerresult.Thestandardwaytodealwiththeremainderduringintegerdivisionistruncation,meaning“chopitoffandthrowitaway”(there’snorounding).Soifyoudivide5by2youget2,and7/10iszero.WhenKotlincalculatesbmiinexpression[1],itdivides160by68*68andgetszero.Itthenmultiplieszeroby703.07togetzero.

Toavoidthisproblem,move703.07tothefrontofthecalculation.ThecalculationsarethenforcedtobeDouble:

valbmi=703.07*weight/(height*height)

TheDoubleparametersinbmiMetric()preventthisproblem.Convertcomputationstothedesiredtypeasearlyaspossibletopreserveaccuracy.

Allprogramminglanguageshavelimitstowhattheycanstorewithinaninteger.Kotlin’sInttypecantakevaluesbetween-231and+231-1,aconstraintoftheInt32-bitrepresentation.IfyousumormultiplytwoIntsthatarebigenough,you’lloverflowtheresult:

//NumberTypes/IntegerOverflow.kt

funmain(){

vali:Int=Int.MAX_VALUE

println(i+i)

}

/*Output:

-2

*/

Int.MAX_VALUEisapredefinedvaluewhichisthelargestnumberanIntcanhold.

Theoverflowproducesaresultthatisclearlyincorrect,asitisbothnegativeandmuchsmallerthanweexpect.Kotlinissuesawarningwheneveritdetectsapotentialoverflow.

Preventingoverflowisyourresponsibilityasadeveloper.Kotlincan’talwaysdetectoverflowduringcompilation,anditdoesn’tpreventoverflowbecausethatwouldproduceanunacceptableperformanceimpact.

Ifyourprogramcontainslargenumbers,youcanuseLongs,whichaccommodatevaluesfrom-263to+263-1.TodefineavaloftypeLong,youcanspecifythetypeexplicitlyorputLattheendofanumericliteral,whichtellsKotlintotreatthatvalueasaLong:

//NumberTypes/LongConstants.kt

funmain(){

vali=0//InfersInt

vall1=0L//LcreatesLong

vall2:Long=0//Explicittype

println("$l1$l2")

}

/*Output:

00

*/

BychangingtoLongswepreventtheoverflowinIntegerOverflow.kt:

//NumberTypes/UsingLongs.kt

funmain(){

vali=Int.MAX_VALUE

println(0L+i+i)//[1]

println(1_000_000*1_000_000L)//[2]

}

/*Output:

4294967294

1000000000000

*/

Usinganumericliteralinboth[1]and[2]forcesLongcalculations,andalsoproducesaresultoftypeLong.ThelocationwheretheLappearsisunimportant.IfoneofthevaluesisLong,theresultingexpressionisLong.

AlthoughtheycanholdmuchlargervaluesthanInts,Longsstillhavesizelimitations:

//NumberTypes/BiggestLong.kt

funmain(){

println(Long.MAX_VALUE)

}

/*Output:

9223372036854775807

*/

Long.MAX_VALUEisthelargestvalueaLongcanhold.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Booleans

ifExpressionsdemonstratedthe“not”operator!,whichnegatesaBooleanvalue.ThisatomintroducesmoreBooleanAlgebra.

Westartwiththeoperators“and”and“or”:

&&(and):ProducestrueonlyiftheBooleanexpressionontheleftoftheoperatorandtheoneontherightarebothtrue.||(or):Producestrueifeithertheexpressionontheleftorrightoftheoperatoristrue,orifbotharetrue.

Inthisexample,wedeterminewhetherabusinessisopenorclosed,basedonthehour:

//Booleans/Open1.kt

funisOpen1(hour:Int){

valopen=9

valclosed=20

println("Operatinghours:$open-$closed")

valstatus=

if(hour>=open&&hour<=closed)//[1]

true

else

false

println("Open:$status")

}

funmain()=isOpen1(6)

/*Output:

Operatinghours:9-20

Open:false

*/

main()isasinglefunctioncall,sowecanuseanexpressionbodyasdescribedinFunctions.

Theifexpressionin[1]Checkswhetherhourisbetweentheopeningtimeandclosingtime,sowecombinetheexpressionswiththeBoolean&&(and).

Theifexpressioncanbesimplified.Theresultoftheexpressionif(cond)trueelsefalseisjustcond:

//Booleans/Open2.kt

funisOpen2(hour:Int){

valopen=9

valclosed=20

println("Operatinghours:$open-$closed")

valstatus=hour>=open&&hour<=closed

println("Open:$status")

}

funmain()=isOpen2(6)

/*Output:

Operatinghours:9-20

Open:false

*/

Let’sreversethelogicandcheckwhetherthebusinessiscurrentlyclosed.The“or”operator||producestrueifatleastoneoftheconditionsissatisfied:

//Booleans/Closed.kt

funisClosed(hour:Int){

valopen=9

valclosed=20

println("Operatinghours:$open-$closed")

valstatus=hour<open||hour>closed

println("Closed:$status")

}

funmain()=isClosed(6)

/*Output:

Operatinghours:9-20

Closed:true

*/

Booleanoperatorsenablecomplicatedlogicincompactexpressions.However,thingscaneasilybecomeconfusing.Striveforreadabilityandspecifyyourintentionsexplicitly.

Here’sanexampleofacomplicatedBooleanexpressionwheredifferentevaluationorderproducesdifferentresults:

//Booleans/EvaluationOrder.kt

funmain(){

valsunny=true

valhoursSleep=6

valexercise=false

valtemp=55

//[1]:

valhappy1=sunny&&temp>50||

exercise&&hoursSleep>7

println(happy1)

//[2]:

valsameHappy1=(sunny&&temp>50)||

(exercise&&hoursSleep>7)

println(sameHappy1)

//[3]:

valnotSame=

(sunny&&temp>50||exercise)&&

hoursSleep>7

println(notSame)

}

/*Output:

true

true

false

*/

TheBooleanexpressionsaresunny,temp>50,exercise,andhoursSleep>7.Wereadhappy1as“It’ssunnyandthetemperatureisgreaterthan50orI’veexercisedandhadmorethan7hoursofsleep.”Butdoes&&haveprecedenceover||,ortheopposite?

Theexpressionin[1]usesKotlin’sdefaultevaluationorder.Thisproducesthesameresultastheexpressionin[2]because,withoutparentheses,the“ands”areevaluatedfirst,thenthe“or”.Theexpressionin[3]usesparenthesestoproduceadifferentresult.In[3],we’reonlyhappyifwegetatleast7hoursofsleep.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Repetitionwithwhile

Computersareidealforrepetitivetasks.

Themostbasicformofrepetitionusesthewhilekeyword.ThisrepeatsablockaslongasthecontrollingBooleanexpressionistrue:

while(Boolean-expression){

//Codetoberepeated

}

TheBooleanexpressionisevaluatedonceatthebeginningoftheloopandagainbeforeeachfurtheriterationthroughtheblock.

//RepetitionWithWhile/WhileLoop.kt

funcondition(i:Int)=i<100//[1]

funmain(){

vari=0

while(condition(i)){//[2]

print(".")

i+=10//[3]

}

}

/*Output:

..........

*/

[1]Thecomparisonoperator<producesaBooleanresult,soKotlininfersBooleanastheresulttypeforcondition().[2]Theconditionalexpressionforthewhilesays:“repeatthestatementsinthebodyaslongascondition()returnstrue.”[3]The+=operatoradds10toiandassignstheresulttoiinasingleoperation(imustbeavarforthistowork).Thisisequivalentto:

i=i+10

There’sasecondwaytousewhile,inconjunctionwiththedokeyword:

do{

//Codetoberepeated

}while(Boolean-expression)

RewritingWhileLoop.kttouseado-whileproduces:

//RepetitionWithWhile/DoWhileLoop.kt

funmain(){

vari=0

do{

print(".")

i+=10

}while(condition(i))

}

/*Output:

..........

*/

Thesoledifferencebetweenwhileanddo-whileisthatthebodyofthedo-whilealwaysexecutesatleastonce,eveniftheBooleanexpressioninitiallyproducesfalse.Inawhile,iftheconditionalisfalsethefirsttime,thenthebodyneverexecutes.Inpractice,do-whileislesscommonthanwhile.

Theshortversionsofassignmentoperatorsareavailableforallthearithmeticoperations:+=,-=,*=,/=,and%=.Thisuses-=and%=:

//RepetitionWithWhile/AssignmentOperators.kt

funmain(){

varn=10

vald=3

print(n)

while(n>d){

n-=d

print("-$d")

}

println("=$n")

varm=10

print(m)

m%=d

println("%$d=$m")

}

/*Output:

10-3-3-3=1

10%3=1

*/

Tocalculatetheremainderoftheintegerdivisionoftwonaturalnumbers,westartwithawhileloop,thenusetheremainderoperator.

Adding1andsubtracting1fromanumberaresocommonthattheyhavetheirownincrementanddecrementoperators:++and--.Youcanreplacei+=1withi++:

//RepetitionWithWhile/IncrementOperator.kt

funmain(){

vari=0

while(i<4){

print(".")

i++

}

}

/*Output:

....

*/

Inpractice,whileloopsarenotusedforiteratingoverarangeofnumbers.Theforloopisusedinstead.Thisiscoveredinthenextatom.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Looping&Ranges

Theforkeywordexecutesablockofcodeforeachvalueinasequence.

Thesetofvaluescanbearangeofintegers,aString,or,asyou’llseelaterinthebook,acollectionofitems.Theinkeywordindicatesthatyouaresteppingthroughvalues:

for(vinvalues){

//Dosomethingwithv

}

Eachtimethroughtheloop,visgiventhenextelementinvalues.

Here’saforlooprepeatinganactionafixednumberoftimes:

//LoopingAndRanges/RepeatThreeTimes.kt

funmain(){

for(iin1..3){

println("Hey$i!")

}

}

/*Output:

Hey1!

Hey2!

Hey3!

*/

Theoutputshowstheindexireceivingeachvalueintherangefrom1to3.

Arangeisanintervalofvaluesdefinedbyapairofendpoints.Therearetwobasicwaystodefineranges:

//LoopingAndRanges/DefiningRanges.kt

funmain(){

valrange1=1..10//[1]

valrange2=0until10//[2]

println(range1)

println(range2)

}

/*Output:

1..10

0..9

*/

[1]Using..syntaxincludesbothboundsintheresultingrange.[2]untilexcludestheend.Theoutputshowsthat10isnotpartoftherange.

Displayingarangeproducesareadableformat.

Thissumsthenumbersfrom10to100:

//LoopingAndRanges/SumUsingRange.kt

funmain(){

varsum=0

for(nin10..100){

sum+=n

}

println("sum=$sum")

}

/*Output:

sum=5005

*/

Youcaniterateoverarangeinreverseorder.Youcanalsouseastepvaluetochangetheintervalfromthedefaultof1:

//LoopingAndRanges/ForWithRanges.kt

funshowRange(r:IntProgression){

for(iinr){

print("$i")

}

print("//$r")

println()

}

funmain(){

showRange(1..5)

showRange(0until5)

showRange(5downTo1)//[1]

showRange(0..9step2)//[2]

showRange(0until10step3)//[3]

showRange(9downTo2step3)

}

/*Output:

12345//1..5

01234//0..4

54321//5downTo1step1

02468//0..8step2

0369//0..9step3

963//9downTo3step3

*/

[1]downToproducesadecreasingrange.[2]stepchangestheinterval.Here,therangestepsbyavalueoftwoinsteadofone.

[3]untilcanalsobeusedwithstep.Noticehowthisaffectstheoutput.

Ineachcasethesequenceofnumbersformanarithmeticprogression.showRange()acceptsanIntProgressionparameter,whichisabuilt-intypethatincludesIntranges.NoticethattheStringrepresentationofeachIntProgressionasitappearsinoutputcommentforeachlineisoftendifferentfromtherangepassedintoshowRange()—theIntProgressionistranslatingtheinputintoanequivalentcommonform.

Youcanalsoproducearangeofcharacters.Thisforiteratesfromatoz:

//LoopingAndRanges/ForWithCharRange.kt

funmain(){

for(cin'a'..'z'){

print(c)

}

}

/*Output:

abcdefghijklmnopqrstuvwxyz

*/

Youcaniterateoverarangeofelementsthatarewholequantities,likeintegersandcharacters,butnotfloating-pointvalues.

Squarebracketsaccesscharactersbyindex.BecausewestartcountingcharactersinaStringatzero,s[0]selectsthefirstcharacteroftheStrings.Selectings.lastIndexproducesthefinalindexnumber:

//LoopingAndRanges/IndexIntoString.kt

funmain(){

vals="abc"

for(iin0..s.lastIndex){

print(s[i]+1)

}

}

/*Output:

bcd

*/

Sometimespeopledescribes[0]as“thezerothcharacter.”

CharactersarestoredasnumberscorrespondingtotheirASCIIcodes,soaddinganintegertoacharacterproducesanewcharactercorrespondingtothenewcodevalue:

//LoopingAndRanges/AddingIntToChar.kt

funmain(){

valch:Char='a'

println(ch+25)

println(ch<'z')

}

/*Output:

z

true

*/

Thesecondprintln()showsthatyoucancomparecharactercodes.

AforloopcaniterateoverStringsdirectly:

//LoopingAndRanges/IterateOverString.kt

funmain(){

for(chin"Jnskhm"){

print(ch+1)

}

}

/*Output:

Kotlin!

*/

chreceiveseachcharacterinturn.

Inthefollowingexample,thefunctionhasChar()iteratesovertheStringsandtestswhetheritcontainsagivencharacterch.Thereturninthemiddleofthefunctionstopsthefunctionwhentheanswerisfound:

//LoopingAndRanges/HasChar.kt

funhasChar(s:String,ch:Char):Boolean{

for(cins){

if(c==ch)returntrue

}

returnfalse

}

funmain(){

println(hasChar("kotlin",'t'))

println(hasChar("kotlin",'a'))

}

/*Output:

true

false

*/

ThenextatomshowsthathasChar()isunnecessary—youcanusebuilt-insyntaxinstead.

Ifyousimplywanttorepeatanactionafixednumberoftimes,youmayuserepeat()insteadofaforloop:

//LoopingAndRanges/RepeatHi.kt

funmain(){

repeat(2){

println("hi!")

}

}

/*Output:

hi!

hi!

*/

repeat()isastandardlibraryfunction,notakeyword.You’llseehowitwascreatedmuchlaterinthebook.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

TheinKeyword

Theinkeywordtestswhetheravalueiswithinarange.

//InKeyword/MembershipInRange.kt

funmain(){

valpercent=35

println(percentin1..100)

}

/*Output:

true

*/

InBooleans,youlearnedtocheckboundsexplicitly:

//InKeyword/MembershipUsingBounds.kt

funmain(){

valpercent=35

println(0<=percent&&percent<=100)

}

/*Output:

true

*/

0<=x&&x<=100islogicallyequivalenttoxin0..100.IntelliJIDEAsuggestsautomaticallyreplacingthefirstformwiththesecond,whichiseasiertoreadandunderstand.

Theinkeywordisusedforbothiterationandmembership.Anininsidethecontrolexpressionofaforloopmeansiteration,otherwiseinchecksmembership:

//InKeyword/IterationVsMembership.kt

funmain(){

valvalues=1..3

for(vinvalues){

println("iteration$v")

}

valv=2

if(vinvalues)

println("$visamemberof$values")

}

/*Output:

iteration1

iteration2

iteration3

2isamemberof1..3

*/

Theinkeywordisnotlimitedtoranges.YoucanalsocheckwhetheracharacterisapartofaString.ThefollowingexampleusesininsteadofhasChar()fromthepreviousatom:

//InKeyword/InString.kt

funmain(){

println('t'in"kotlin")

println('a'in"kotlin")

}

/*Output:

true

false

*/

Laterinthebookyou’llseethatinworkswithothertypes,aswell.

Here,intestswhetheracharacterbelongstoarangeofcharacters:

//InKeyword/CharRange.kt

funisDigit(ch:Char)=chin'0'..'9'

funnotDigit(ch:Char)=

ch!in'0'..'9'//[1]

funmain(){

println(isDigit('a'))

println(isDigit('5'))

println(notDigit('z'))

}

/*Output:

false

true

true

*/

[1]!inchecksthatavaluedoesn’tbelongtoarange.

YoucancreateaDoublerange,butyoucanonlyuseittocheckformembership:

//InKeyword/FloatingPointRange.kt

funinFloatRange(n:Double){

valr=1.0..10.0

println("$nin$r?${ninr}")

}

funmain(){

inFloatRange(0.999999)

inFloatRange(5.0)

inFloatRange(10.0)

inFloatRange(10.0000001)

}

/*Output:

0.999999in1.0..10.0?false

5.0in1.0..10.0?true

10.0in1.0..10.0?true

10.0000001in1.0..10.0?false

*/

Floating-pointrangescanonlybecreatedusing..becauseuntilwouldmeanexcludingafloating-pointnumberasanendpoint,whichdoesn’tmakesense.

YoucancheckwhetheraStringisamemberofarangeofStrings:

//InKeyword/StringRange.kt

funmain(){

println("ab"in"aa".."az")

println("ba"in"aa".."az")

}

/*Output:

true

false

*/

HereKotlinusesalphabeticcomparison.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Expressions&Statements

Statementsandexpressionsarethesmallestusefulfragmentsofcodeinmostprogramminglanguages.

There’sabasicdifference:astatementhasaneffect,butproducesnoresult.Anexpressionalwaysproducesaresult.

Becauseitdoesn’tproducearesult,astatementmustchangethestateofitssurroundingstobeuseful.Anotherwaytosaythisis“astatementiscalledforitssideeffects”(thatis,whatitdoesotherthanproducingaresult).Asamemoryaid:

Astatementchangesstate.

Onedefinitionof“express”is“toforceorsqueezeout,”asin“toexpressthejuicefromanorange.”So

Anexpressionexpresses.

Thatis,itproducesaresult.

TheforloopisastatementinKotlin.Youcannotassignitbecausethere’snoresult:

//ExpressionsStatements/ForIsAStatement.kt

funmain(){

//Can'tdothis:

//valf=for(iin1..10){}

//Compilererrormessage:

//forisnotanexpression,and

//onlyexpressionsareallowedhere

}

Aforloopisusedforitssideeffects.

Anexpressionproducesavalue,whichcanbeassignedorusedaspartofanotherexpression,whereasastatementisalwaysatop-levelelement.

Everyfunctioncallisanexpression.EvenifthefunctionreturnsUnitandiscalledonlyforitssideeffects,theresultcanstillbeassigned:

//ExpressionsStatements/UnitReturnType.kt

fununitFun()=Unit

funmain(){

println(unitFun())

valu1:Unit=println(42)

println(u1)

valu2=println(0)//Typeinference

println(u2)

}

/*Output:

kotlin.Unit

42

kotlin.Unit

0

kotlin.Unit

*/

TheUnittypecontainsasinglevaluecalledUnit,whichyoucanreturndirectly,asseeninunitFun().Callingprintln()alsoreturnsUnit.Thevalu1capturesthereturnvalueofprintln()andisexplicitlydeclaredasUnitwhileu2usestypeinference.

ifcreatesanexpression,soyoucanassignitsresult:

//ExpressionsStatements/AssigningAnIf.kt

funmain(){

valresult1=if(11>42)9else5

valresult2=if(1<2){

vala=11

a+42

}else42

valresult3=

if('x'<'y')

println("x<y")

else

println("x>y")

println(result1)

println(result2)

println(result3)

}

/*Output:

x<y

5

53

kotlin.Unit

*/

Thefirstoutputlineisx<y,eventhoughresult3isn’tdisplayeduntiltheendofmain().Thishappensbecauseevaluatingresult3callsprintln(),andtheevaluationoccurswhenresult3isdefined.

Noticethataisdefinedinsidetheblockofcodeforresult2.Theresultofthelastexpressionbecomestheresultoftheifexpression;here,it’sthesumof11and42.Butwhatabouta?Onceyouleavethecodeblock(moveoutsidethecurlybraces),youcan’taccessa.Itistemporaryandisdiscardedonceyouexitthescopeofthatblock.

Theincrementoperatori++isalsoanexpression,evenifitlookslikeastatement.KotlinfollowstheapproachusedbyC-likelanguagesandprovidestwoversionsofincrementanddecrementoperatorswithslightlydifferentsemantics.Theprefixoperatorappearsbeforetheoperand,asin++i,andreturnsthevalueaftertheincrementhappens.Youcanreaditas“firstdotheincrement,thenreturntheresultingvalue.”Thepostfixoperatorisplacedaftertheoperand,asini++,andreturnsthevalueofibeforetheincrementoccurs.Youcanreaditas“firstproducetheresult,thendotheincrement.”

//ExpressionsStatements/PostfixVsPrefix.kt

funmain(){

vari=10

println(i++)

println(i)

varj=20

println(++j)

println(j)

}

/*Output:

10

11

21

21

*/

Thedecrementoperatoralsohastwoversions:--iandi--.Usingincrementanddecrementoperatorswithinotherexpressionsisdiscouragedbecauseitcanproduceconfusingcode:

//ExpressionsStatements/Confusing.kt

funmain(){

vari=1

println(i+++++i)

}

Trytoguesswhattheoutputwillbe,thencheckit.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Summary1

ThisatomsummarizesandreviewstheatomsinSectionI,startingatHello,World!andendingwithExpressions&Statements.

Ifyou’reanexperiencedprogrammer,thisshouldbeyourfirstatom.NewprogrammersshouldreadthisatomandperformtheexercisesasareviewofSectionI.

Ifanythingisn’tcleartoyou,studytheassociatedatomforthattopic(thesub-headingscorrespondtoatomtitles).

Hello,World!Kotlinsupportsboth//single-linecomments,and/*-to-*/multilinecomments.Aprogram’sentrypointisthefunctionmain():

//Summary1/Hello.kt

funmain(){

println("Hello,world!")

}

/*Output:

Hello,world!

*/

Thefirstlineofeachexampleinthisbookisacommentcontainingthenameoftheatom’ssubdirectory,followedbya/andthenameofthefile.YoucanfindalltheextractedcodeexamplesviaAtomicKotlin.com.

println()isastandardlibraryfunctionwhichtakesasingleStringparameter(oraparameterthatcanbeconvertedtoaString).println()movesthecursortoanewlineafterdisplayingitsparameter,whileprint()leavesthecursoronthesameline.

Kotlindoesnotrequireasemicolonattheendofanexpressionorstatement.Semicolonsareonlynecessarytoseparatemultipleexpressionsorstatementsonasingleline.

var&val,DataTypesTocreateanunchangingidentifier,usethevalkeywordfollowedbytheidentifiername,acolon,andthetypeforthatvalue.Thenaddanequalssignandthevaluetoassigntothatval:

validentifier:Type=initialization

Onceavalisassigned,itcannotbereassigned.

Kotlin’stypeinferencecanusuallydeterminethetypeautomatically,basedontheinitializationvalue.Thisproducesasimplerdefinition:

validentifier=initialization

Bothofthefollowingarevalid:

valdaysInFebruary=28

valdaysInMarch:Int=31

Avar(variable)definitionlooksthesame,usingvarinsteadofval:

varidentifier1=initialization

varidentifier2:Type=initialization

Unlikeaval,youcanmodifyavar,sothefollowingislegal:

varhoursSpent=20

hoursSpent=25

However,thetypecan’tbechanged,soyougetanerrorifyousay:

hoursSpent=30.5

KotlininferstheInttypewhenhoursSpentisdefined,soitwon’tacceptthechangetoafloating-pointvalue.

FunctionsFunctionsarenamedsubroutines:

funfunctionName(arg1:Type1,arg2:Type2,...):ReturnType{

//Linesofcode...

returnresult

}

Thefunkeywordisfollowedbythefunctionnameandtheparameterlistinparentheses.EachparametermusthaveanexplicittypebecauseKotlincannotinferparametertypes.Thefunctionitselfhasatype,definedinthesamewayasforavarorval(acolonfollowedbythetype).Afunction’stypeisthetypeofthereturnedresult.

Thefunctionsignatureisfollowedbythefunctionbodycontainedwithincurlybraces.Thereturnstatementprovidesthefunction’sreturnvalue.

Youcanuseanabbreviatedsyntaxwhenthefunctionconsistsofasingleexpression:

funfunctionName(arg1:Type1,arg2:Type2,...):ReturnType=result

Thisformiscalledanexpressionbody.Insteadofanopeningcurlybrace,useanequalssignfollowedbytheexpression.YoucanomitthereturntypebecauseKotlininfersit.

Here’safunctionthatproducesthecubeofitsparameter,andanotherthataddsanexclamationpointtoaString:

//Summary1/BasicFunctions.kt

funcube(x:Int):Int{

returnx*x*x

}

funbang(s:String)=s+"!"

funmain(){

println(cube(3))

println(bang("pop"))

}

/*Output:

27

pop!

*/

cube()hasablockbodywithanexplicitreturnstatement.bang()isanexpressionbodyproducingthefunction’sreturnvalue.Kotlininfersbang()’sreturntypetobeString.

BooleansForBooleanalgebra,Kotlinprovidesoperatorssuchas:

!(not)logicallynegatesthevalue(turnstruetofalseandvice-versa).

&&(and)returnstrueonlyifbothconditionsaretrue.||(or)returnstrueifatleastoneoftheconditionsistrue.

//Summary1/Booleans.kt

funmain(){

valopens=9

valcloses=20

println("Operatinghours:$opens-$closes")

valhour=6

println("Currenttime:"+hour)

valisOpen=hour>=opens&&hour<=closes

println("Open:"+isOpen)

println("Notopen:"+!isOpen)

valisClosed=hour<opens||hour>closes

println("Closed:"+isClosed)

}

/*Output:

Operatinghours:9-20

Currenttime:6

Open:false

Notopen:true

Closed:true

*/

isOpen’sinitializeruses&&totestwhetherbothconditionsaretrue.Thefirstconditionhour>=opensisfalse,sotheresultoftheentireexpressionbecomesfalse.TheinitializerforisCloseduses||,producingtrueifatleastoneoftheconditionsistrue.Theexpressionhour<opensistrue,sothewholeexpressionistrue.

ifExpressionsBecauseifisanexpression,itproducesaresult.Thisresultcanbeassignedtoavarorval.Here,youalsoseetheuseoftheelsekeyword:

//Summary1/IfResult.kt

funmain(){

valresult=if(99<100)4else42

println(result)

}

/*Output:

4

*/

Eitherbranchofanifexpressioncanbeamultilineblockofcodesurroundedbycurlybraces:

//Summary1/IfExpression.kt

funmain(){

valactivity="swimming"

valhour=10

valisOpen=if(

activity=="swimming"||

activity=="iceskating"){

valopens=9

valcloses=20

println("Operatinghours:"+

opens+"-"+closes)

hour>=opens&&hour<=closes

}else{

false

}

println(isOpen)

}

/*Output:

Operatinghours:9-20

true

*/

Avaluedefinedinsideablockofcode,suchasopens,isnotaccessibleoutsidethescopeofthatblock.Becausetheyaredefinedgloballytotheifexpression,activityandhourareaccessibleinsidetheifexpression.

Theresultofanifexpressionistheresultofthelastexpressionofthechosenbranch.Here,it’shour>=opens&&hour<=closeswhichistrue.

StringTemplatesYoucaninsertavaluewithinaStringusingStringtemplates.Usea$beforetheidentifiername:

//Summary1/StrTemplates.kt

funmain(){

valanswer=42

println("Found$answer!")//[1]

valcondition=true

println(

"${if(condition)'a'else'b'}")//[2]

println("printinga$1")//[3]

}

/*Output:

Found42!

a

printinga$1

*/

[1]$answersubstitutesthevaluecontainedinanswer.[2]${if(condition)'a'else'b'}evaluatesandsubstitutestheresultoftheexpressioninside${}.

[3]Ifthe$isfollowedbyanythingunrecognizableasaprogramidentifier,nothingspecialhappens.

Usetriple-quotedStringstostoremultilinetextortextwithspecialcharacters:

//Summary1/ThreeQuotes.kt

funjson(q:String,a:Int)="""{

"question":"$q",

"answer":$a

}"""

funmain(){

println(json("TheUltimate",42))

}

/*Output:

{

"question":"TheUltimate",

"answer":42

}

*/

Youdon’tneedtoescapespecialcharacterslike"withinatriple-quotedString.(InaregularStringyouwrite\"toinsertadoublequote).AswithnormalStrings,youcaninsertanidentifieroranexpressionusing$insideatriple-quotedString.

NumberTypesKotlinprovidesintegertypes(Int,Long)andfloatingpointtypes(Double).AwholenumberconstantisIntbydefaultandLongifyouappendanL.AconstantisDoubleifitcontainsadecimalpoint:

//Summary1/NumberTypes.kt

funmain(){

valn=1000//Int

vall=1000L//Long

vald=1000.0//Double

println("$n$l$d")

}

/*Output:

100010001000.0

*/

AnIntholdsvaluesbetween-231and+231-1.Integralvaluescanoverflow;forexample,addinganythingtoInt.MAX_VALUEproducesanoverflow:

//Summary1/Overflow.kt

funmain(){

println(Int.MAX_VALUE+1)

println(Int.MAX_VALUE+1L)

}

/*Output:

-2147483648

2147483648

*/

Inthesecondprintln()statementweappendLto1,forcingthewholeexpressiontobeoftypeLong,whichavoidstheoverflow.(ALongcanholdvaluesbetween-263and+263-1).

WhenyoudivideanIntwithanotherInt,KotlinproducesanIntresult,andanyremainderistruncated.So1/2produces0.IfaDoubleisinvolved,theIntispromotedtoDoublebeforetheoperation,so1.0/2produces0.5.

Youmightexpectd1inthefollowingtoproduce3.4:

//Summary1/Truncation.kt

funmain(){

vald1:Double=3.0+2/5

println(d1)

vald2:Double=3+2.0/5

println(d2)

}

/*Output:

3.0

3.4

*/

Becauseofevaluationorder,itdoesn’t.Kotlinfirstdivides2by5,andintegermathproduces0,yieldingananswerof3.0.Thesameevaluationorderdoesproducetheexpectedresultford2.Dividing2.0by5produces0.4.The3ispromotedtoaDoublebecauseweaddittoaDouble(0.4),whichproduces3.4.

Understandingevaluationorderhelpsyoutodecipherwhataprogramdoes,bothwithlogicaloperations(Booleanexpressions)andwithmathematicaloperations.Ifyou’reunsureaboutevaluationorder,useparenthesestoforceyourintention.Thisalsomakesitcleartothosereadingyourcode.

RepetitionwithwhileAwhileloopcontinuesaslongasthecontrollingBoolean-expressionproducestrue:

while(Boolean-expression){

//Codetoberepeated

}

TheBooleanexpressionisevaluatedonceatthebeginningoftheloopandagainbeforeeachfurtheriteration.

//Summary1/While.kt

funtestCondition(i:Int)=i<100

funmain(){

vari=0

while(testCondition(i)){

print(".")

i+=10

}

}

/*Output:

..........

*/

KotlininfersBooleanastheresulttypefortestCondition().

Theshortversionsofassignmentoperatorsareavailableforallmathematicaloperations(+=,-=,*=,/=,%=).Kotlinalsosupportstheincrementanddecrementoperators++and--,inbothprefixandpostfixform.

whilecanbeusedwiththedokeyword:

do{

//Codetoberepeated

}while(Boolean-expression)

RewritingWhile.kt:

//Summary1/DoWhile.kt

funmain(){

vari=0

do{

print(".")

i+=10

}while(testCondition(i))

}

/*Output:

..........

*/

Thesoledifferencebetweenwhileanddo-whileisthatthebodyofthedo-whilealwaysexecutesatleastonce,eveniftheBooleanexpressionproducesfalsethefirsttime.

Looping&Ranges

Manyprogramminglanguagesindexintoaniterableobjectbysteppingthroughintegers.Kotlin’sforallowsyoutotakeelementsdirectlyfromiterableobjectslikerangesandStrings.Forexample,thisforselectseachcharacterintheString"Kotlin":

//Summary1/StringIteration.kt

funmain(){

for(cin"Kotlin"){

print("$c")

//c+=1//error:

//valcannotbereassigned

}

}

/*Output:

Kotlin

*/

ccan’tbeexplicitlydefinedaseitheravarorval—KotlinautomaticallymakesitavalandinfersitstypeasChar(youcanprovidethetypeexplicitly,butinpracticethisisrarelydone).

Youcanstepthroughintegralvaluesusingranges:

//Summary1/RangeOfInt.kt

funmain(){

for(iin1..10){

print("$i")

}

}

/*Output:

12345678910

*/

Creatingarangewith..includesbothbounds,butuntilexcludesthetopendpoint:1until10isthesameas1..9.Youcanspecifyanincrementvalueusingstep:1..21step3.

TheinKeywordThesameinthatprovidesforloopiterationalsoallowsyoutocheckmembershipinarange.!inreturnstrueifthetestedvalueisn’tintherange:

//Summary1/Membership.kt

funinNumRange(n:Int)=nin50..100

funnotLowerCase(ch:Char)=ch!in'a'..'z'

funmain(){

vali1=11

vali2=100

valc1='K'

valc2='k'

println("$i1${inNumRange(i1)}")

println("$i2${inNumRange(i2)}")

println("$c1${notLowerCase(c1)}")

println("$c2${notLowerCase(c2)}")

}

/*Output:

11false

100true

Ktrue

kfalse

*/

incanalsobeusedtotestmembershipinfloating-pointranges,althoughsuchrangescanonlybedefinedusing..andnotuntil.

Expressions&StatementsThesmallestusefulfragmentofcodeinmostprogramminglanguagesiseitherastatementoranexpression.Thesehaveonebasicdifference:

Astatementchangesstate.Anexpressionexpresses.

Thatis,anexpressionproducesaresult,whileastatementdoesnot.Becauseitdoesn’treturnanything,astatementmustchangethestateofitssurroundings(thatis,createasideeffect)todoanythinguseful.

AlmosteverythinginKotlinisanexpression:

valhours=10

valminutesPerHour=60

valminutes=hours*minutesPerHour

Ineachcase,everythingtotherightofthe=isanexpression,whichproducesaresultthatisassignedtotheidentifierontheleft.

Functionslikeprintln()don’tseemtoproducearesult,butbecausetheyarestillexpressions,theymustreturnsomething.KotlinhasaspecialUnittypeforthese:

//Summary1/UnitReturn.kt

funmain(){

valresult=println("returnsUnit")

println(result)

}

/*Output:

returnsUnit

kotlin.Unit

*/

ExperiencedprogrammersshouldgotoSummary2afterworkingtheexercisesforthisatom.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SECTIONII:INTRODUCTIONTOOBJECTS

Objectsarethefoundationfornumerousmodernlanguages,includingKotlin.

Inanobject-oriented(OO)programminglanguage,youdiscover“nouns”intheproblemyou’resolving,andtranslatethosenounstoobjects.Objectsholddataandperformactions.Anobject-orientedlanguagecreatesandusesobjects.

Kotlinisn’tjustobject-oriented;it’salsofunctional.Functionallanguagesfocusontheactionsyouperform(“verbs”).Kotlinisahybridobject-functionallanguage.

Thissectionexplainsthebasicsofobject-orientedprogramming.SectionIV:FunctionalProgrammingintroducesfunctionalprogramming.SectionV:Object-OrientedProgrammingcoversobject-orientedprogrammingindetail.

ObjectsEverywhere

Objectsstoredatausingproperties(valsandvars)andperformoperationswiththisdatausingfunctions.

Somedefinitions:

Class:Definespropertiesandfunctionsforwhatisessentiallyanewdatatype.Classesarealsocalleduser-definedtypes.Member:Eitherapropertyorafunctionofaclass.Memberfunction:Afunctionthatworksonlywithaspecificclassofobject.Creatinganobject:Makingavalorvarofaclass.Alsocalledcreatinganinstanceofthatclass.

Becauseclassesdefinestateandbehavior,wecanevenrefertoinstancesofbuilt-intypeslikeDoubleorBooleanasobjects.

ConsiderKotlin’sIntRangeclass:

//ObjectsEverywhere/IntRanges.kt

funmain(){

valr1=IntRange(0,10)

valr2=IntRange(5,7)

println(r1)

println(r2)

}

/*Output:

0..10

5..7

*/

Wecreatetwoobjects(instances)oftheIntRangeclass.Eachobjecthasitsownpieceofstorageinmemory.IntRangeisaclass,butaparticularranger1from0to10isanobjectthatisdistinctfromranger2.

NumerousoperationsareavailableforanIntRangeobject.Somearestraightforward,likesum(),andothersrequiremoreunderstandingbeforeyoucanusethem.Ifyoutrycallingonethatneedsarguments,theIDEwillaskforthosearguments.

Tolearnaboutaparticularmemberfunction,lookitupintheKotlindocumentation.Noticethemagnifyingglassiconinthetoprightareaofthepage.ClickonthatandtypeIntRangeintothesearchbox.Clickonkotlin.ranges>IntRangefromtheresultingsearch.You’llseethedocumentationfortheIntRangeclass.Youcanstudyallthememberfunctions—theApplicationProgrammingInterface(API)—oftheclass.Althoughyouwon’tunderstandmostofitatthistime,it’shelpfultobecomecomfortablelookingthingsupintheKotlindocumentation.

AnIntRangeisakindofobject,andadefiningcharacteristicofanobjectisthatyouperformoperationsonit.Insteadof“performinganoperation,”wesaycallingamemberfunction.Tocallamemberfunctionforanobject,startwiththeobjectidentifier,thenadot,thenthenameoftheoperation:

//ObjectsEverywhere/RangeSum.kt

funmain(){

valr=IntRange(0,10)

println(r.sum())

}

/*Output:

55

*/

Becausesum()isamemberfunctiondefinedforIntRange,youcallitbysayingr.sum().ThisaddsupallthenumbersinthatIntRange.

Earlierobject-orientedlanguagesusedthephrase“sendingamessage”todescribecallingamemberfunctionforanobject.Sometimesyou’llstillseethatterminology.

Classescanhavemanyoperations(memberfunctions).It’seasytoexploreclassesusinganIDE(integrateddevelopmentenvironment)thatincludesafeaturecalledcodecompletion.Forexample,ifyoutype.safteranobjectidentifierwithinIntelliJIDEA,itshowsallthemembersofthatobjectthatbeginwiths:

CodeCompletion

Tryusingcodecompletiononotherobjects.Forexample,youcanreverseaStringorconvertallthecharacterstolowercase:

//ObjectsEverywhere/Strings.kt

funmain(){

vals="AbcD"

println(s.reversed())

println(s.toLowerCase())

}

/*Output:

DcbA

abcd

*/

YoucaneasilyconvertaStringtoanintegerandback:

//ObjectsEverywhere/Conversion.kt

funmain(){

vals="123"

println(s.toInt())

vali=123

println(i.toString())

}

/*Output:

123

123

*/

LaterinthebookwediscussstrategiestohandlesituationswhentheStringyouwanttoconvertdoesn’trepresentacorrectintegervalue.

Youcanalsoconvertfromonenumericaltypetoanother.Toavoidconfusion,conversionsbetweennumbertypesareexplicit.Forexample,youconvertanIntitoaLongbycallingi.toLong(),ortoaDoublewithi.toDouble():

//ObjectsEverywhere/NumberConversions.kt

funfraction(numerator:Long,denom:Long)=

numerator.toDouble()/denom

funmain(){

valnum=1

valden=2

valf=fraction(num.toLong(),den.toLong())

println(f)

}

/*Output:

0.5

*/

Well-definedclassesareeasyforaprogrammertounderstand,andproducecodethat’seasytoread.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

CreatingClasses

NotonlycanyouusepredefinedtypeslikeIntRangeandString,youcanalsocreateyourowntypesofobjects.

Indeed,creatingnewtypescomprisesmuchoftheactivityinobject-orientedprogramming.Youcreatenewtypesbydefiningclasses.

Anobjectisapieceofthesolutionforaproblemyou’retryingtosolve.Startbythinkingofobjectsasexpressingconcepts.Asafirstapproximation,ifyoudiscovera“thing”inyourproblem,representthatthingasanobjectinyoursolution.

Supposeyouwanttocreateaprogramtomanageanimalsinazoo.Itmakessensetocategorizethedifferenttypesofanimalsbasedonhowtheybehave,theirneeds,animalstheygetalongwithandthosetheyfightwith.Everythingdifferentaboutaspeciesofanimaliscapturedintheclassificationofthatanimal’sobject.Kotlinusestheclasskeywordtocreateanewtypeofobject:

//CreatingClasses/Animals.kt

//Createsomeclasses:

classGiraffe

classBear

classHippo

funmain(){

//Createsomeobjects:

valg1=Giraffe()

valg2=Giraffe()

valb=Bear()

valh=Hippo()

//Eachobject()isunique:

println(g1)

println(g2)

println(h)

println(b)

}

/*Sampleoutput:

Giraffe@28d93b30

Giraffe@1b6d3586

Hippo@4554617c

Bear@74a14482

*/

Todefineaclass,startwiththeclasskeyword,followedbyanidentifierforyournewclass.Theclassnamemustbeginwithaletter(A-Z,upperorlowercase),butcanincludethingslikenumbersandunderscores.Followingconvention,wecapitalizethefirstletterofaclassname,andlowercasethefirstletterofallvalsandvars.

Animals.ktstartsbydefiningthreenewclasses,thencreatesfourobjects(alsocalledinstances)ofthoseclasses.

Giraffeisaclass,butaparticularfive-year-oldmalegiraffethatlivesinBotswanaisanobject.Eachobjectisdifferentfromallothers,sowegivethemnameslikeg1andg2.

Noticetherathercrypticoutputofthelastfourlines.Thepartbeforethe@istheclassname,andthenumberafterthe@istheaddresswheretheobjectislocatedinyourcomputer’smemory.Yes,that’sanumbereventhoughitincludessomeletters—it’scalled“hexadecimalnotation”.Everyobjectinyourprogramhasitsownuniqueaddress.

Theclassesdefinedhere(Giraffe,Bear,andHippo)areassimpleaspossible:theentireclassdefinitionisasingleline.Morecomplexclassesusecurlybraces({and})tocreateaclassbodycontainingthecharacteristicsandbehaviorsforthatclass.

Afunctiondefinedwithinaclassbelongstothatclass.InKotlin,wecallthesememberfunctionsoftheclass.Someobject-orientedlanguageslikeJavachoosetocallthemmethods,atermthatcamefromearlyobject-orientedlanguageslikeSmalltalk.ToemphasizethefunctionalnatureofKotlin,thedesignerschosetodropthetermmethod,assomebeginnersfoundthedistinctionconfusing.Instead,thetermfunctionisusedthroughoutthelanguage.

Ifitisunambiguous,wewilljustsay“function.”Ifwemustmakethedistinction:

Memberfunctionsbelongtoaclass.Top-levelfunctionsexistbythemselvesandarenotpartofaclass.

Here,bark()belongstotheDogclass:

//CreatingClasses/Dog.kt

classDog{

funbark()="yip!"

}

funmain(){

valdog=Dog()

}

Inmain(),wecreateaDogobjectandassignittovaldog.Kotlinemitsawarningbecauseweneverusedog.

Memberfunctionsarecalled(invoked)withtheobjectname,followedbya.(dot/period),followedbythefunctionnameandparameterlist.Herewecallthemeow()functionanddisplaytheresult:

//CreatingClasses/Cat.kt

classCat{

funmeow()="mrrrow!"

}

funmain(){

valcat=Cat()

//Call'meow()'for'cat':

valm1=cat.meow()

println(m1)

}

/*Output:

mrrrow!

*/

Amemberfunctionactsonaparticularinstanceofaclass.Whenyoucallmeow(),youmustcallitwithanobject.Duringthecall,meow()canaccessothermembersofthatobject.

Whencallingamemberfunction,Kotlinkeepstrackoftheobjectofinterestbysilentlypassingareferencetothatobject.Thatreferenceisavailableinsidethememberfunctionbyusingthekeywordthis.

Memberfunctionshavespecialaccesstootherelementswithinaclass,simplybynamingthoseelements.Youcanalsoexplicitlyqualifyaccesstothoseelementsusingthis.Here,exercise()callsspeak()withandwithoutqualification:

//CreatingClasses/Hamster.kt

classHamster{

funspeak()="Squeak!"

funexercise()=

this.speak()+//Qualifiedwith'this'

speak()+//Without'this'

"Runningonwheel"

}

funmain(){

valhamster=Hamster()

println(hamster.exercise())

}

/*Output:

Squeak!Squeak!Runningonwheel

*/

Inexercise(),wecallspeak()firstwithanexplicitthisandthenomitthequalification.

Sometimesyou’llseecodecontaininganunnecessaryexplicitthis.Thatkindofcodeoftencomesfromprogrammerswhoknowadifferentlanguagewherethisiseitherrequired,orpartofitsstyle.Usingafeatureunnecessarilyisconfusingforthereader,whospendstimetryingtofigureoutwhyyou’redoingit.Werecommendavoidingtheunnecessaryuseofthis.

Outsidetheclass,youmustsayhamster.exercise()andhamster.speak().

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Properties

Apropertyisavarorvalthat’spartofaclass.

Definingapropertymaintainsstatewithinaclass.Maintainingstateistheprimarymotivatingreasonforcreatingaclassratherthanjustwritingoneormorestandalonefunctions.

Avarpropertycanbereassigned,whileavalpropertycan’t.Eachobjectgetsitsownstorageforproperties:

//Properties/Cup.kt

classCup{

varpercentFull=0

}

funmain(){

valc1=Cup()

c1.percentFull=50

valc2=Cup()

c2.percentFull=100

println(c1.percentFull)

println(c2.percentFull)

}

/*Output:

50

100

*/

Definingavarorvalinsideaclasslooksjustlikedefiningitwithinafunction.However,thevarorvalbecomespartofthatclass,andyoumustrefertoitbyspecifyingitsobjectusingdotnotation,placingadotbetweentheobjectandthenameoftheproperty.YoucanseedotnotationusedforeachreferencetopercentFull.

ThepercentFullpropertyrepresentsthestateofthecorrespondingCupobject.c1.percentFullandc2.percentFullcontaindifferentvalues,showingthateachobjecthasitsownstorage.

Amemberfunctioncanrefertoapropertywithinitsobjectwithoutusingdotnotation(thatis,withoutqualifyingit):

//Properties/Cup2.kt

classCup2{

varpercentFull=0

valmax=100

funadd(increase:Int):Int{

percentFull+=increase

if(percentFull>max)

percentFull=max

returnpercentFull

}

}

funmain(){

valcup=Cup2()

cup.add(50)

println(cup.percentFull)

cup.add(70)

println(cup.percentFull)

}

/*Output:

50

100

*/

Theadd()memberfunctiontriestoaddincreasetopercentFullbutensuresthatitdoesn’tgopast100%.

Youmustqualifybothpropertiesandmemberfunctionsfromoutsideaclass.

Youcandefinetop-levelproperties:

//Properties/TopLevelProperty.kt

valconstant=42

varcounter=0

funinc(){

counter++

}

Definingatop-levelvalissafebecauseitcannotbemodified.However,definingamutable(var)top-levelpropertyisconsideredananti-pattern.Asyourprogrambecomesmorecomplicated,itbecomeshardertoreasoncorrectlyaboutsharedmutablestate.Ifeveryoneinyourcodebasecanaccessthevarcounter,youcan’tguaranteeitwillchangecorrectly:whileinc()increasescounterbyone,someotherpartoftheprogrammightdecreasecounterbyten,producingobscurebugs.It’sbesttoguardmutablestatewithinaclass.InConstrainingVisibilityyou’llseehowtomakeittrulyhidden.

Tosaythatvarscanbechangedwhilevalscannotisanoversimplification.Asananalogy,considerahouseasaval,andasofainsidethehouseasavar.Youcanmodifysofabecauseit’savar.Youcan’treassignhouse,though,becauseit’saval:

//Properties/ChangingAVal.kt

classHouse{

varsofa:String=""

}

funmain(){

valhouse=House()

house.sofa="Simplesleepersofa:$89.00"

println(house.sofa)

house.sofa="Newleathersofa:$3,099.00"

println(house.sofa)

//CannotreassignthevaltoanewHouse:

//house=House()

}

/*Output:

Simplesleepersofa:$89.00

Newleathersofa:$3,099.00

*/

Althoughhouseisaval,itsobjectcanbemodifiedbecausesofainclassHouseisavar.Defininghouseasavalonlypreventsitfrombeingreassignedtoanewobject.

Ifwemakeapropertyaval,itcannotbereassigned:

//Properties/AnUnchangingVar.kt

classSofa{

valcover:String="Loveseatcover"

}

funmain(){

varsofa=Sofa()

//Notallowed:

//sofa.cover="Newcover"

//Reassigningavar:

sofa=Sofa()

}

Eventhoughsofaisavar,itsobjectcannotbemodifiedbecausecoverinclassSofaisaval.However,sofacanbereassignedtoanewobject.

We’vetalkedaboutidentifierslikehouseandsofaasiftheywereobjects.Theyareactuallyreferencestoobjects.Onewaytoseethisistoobservethattwoidentifierscanrefertothesameobject:

//Properties/References.kt

classKitchen{

vartable:String="Roundtable"

}

funmain(){

valkitchen1=Kitchen()

valkitchen2=kitchen1

println("kitchen1:${kitchen1.table}")

println("kitchen2:${kitchen2.table}")

kitchen1.table="Squaretable"

println("kitchen1:${kitchen1.table}")

println("kitchen2:${kitchen2.table}")

}

/*Output:

kitchen1:Roundtable

kitchen2:Roundtable

kitchen1:Squaretable

kitchen2:Squaretable

*/

Whenkitchen1modifiestable,kitchen2seesthemodification.kitchen1.tableandkitchen2.tabledisplaythesameoutput.

Rememberthatvarandvalcontrolreferencesratherthanobjects.Avarallowsyoutorebindareferencetoadifferentobject,andavalpreventsyoufromdoingso.

Mutabilitymeansanobjectcanchangeitsstate.Intheexamplesabove,classHouseandclassKitchendefinemutableobjectswhileclassSofadefinesimmutableobjects.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Constructors

Youinitializeanewobjectbypassinginformationtoaconstructor.

Eachobjectisanisolatedworld.Aprogramisacollectionofobjects,socorrectinitializationofeachindividualobjectsolvesalargepartoftheinitializationproblem.Kotlinincludesmechanismstoguaranteeproperobjectinitialization.

Aconstructorislikeaspecialmemberfunctionthatinitializesanewobject.Thesimplestformofaconstructorisasingle-lineclassdefinition:

//Constructors/Wombat.kt

classWombat

funmain(){

valwombat=Wombat()

}

Inmain(),callingWombat()createsaWombatobject.Ifyouarecomingfromanotherobject-orientedlanguageyoumightexpecttoseeanewkeywordusedhere,butnewwouldberedundantinKotlinsoitwasomitted.

Youpassinformationtoaconstructorusingaparameterlist,justlikeafunction.Here,theAlienconstructortakesasingleargument:

//Constructors/Arg.kt

classAlien(name:String){

valgreeting="Poor$name!"

}

funmain(){

valalien=Alien("Mr.Meeseeks")

println(alien.greeting)

//alien.name//Error//[1]

}

/*Output:

PoorMr.Meeseeks!

*/

CreatinganAlienobjectrequirestheargument(tryitwithoutone).nameinitializesthegreetingpropertywithintheconstructor,butitisnotaccessibleoutsidetheconstructor—tryuncommentingline[1].

Ifyouwanttheconstructorparametertobeaccessibleoutsidetheclassbody,defineitasavarorvalintheparameterlist:

//Constructors/VisibleArgs.kt

classMutableNameAlien(varname:String)

classFixedNameAlien(valname:String)

funmain(){

valalien1=

MutableNameAlien("ReverseGiraffe")

valalien2=

FixedNameAlien("KrombopolisMichael")

alien1.name="Parasite"

//Can'tdothis:

//alien2.name="Parasite"

}

Theseclassdefinitionshavenoexplicitclassbodies—thebodiesareimplied.

Whennameisdefinedasavarorval,itbecomesapropertyandisthusaccessibleoutsidetheconstructor.valconstructorparameterscannotbechanged,whilevarconstructorparametersaremutable.

Yourclasscanhavenumerousconstructorparameters:

//Constructors/MultipleArgs.kt

classAlienSpecies(

valname:String,

valeyes:Int,

valhands:Int,

vallegs:Int

){

fundescribe()=

"$namewith$eyeseyes,"+

"$handshandsand$legslegs"

}

funmain(){

valkevin=

AlienSpecies("Zigerion",2,2,2)

valmortyJr=

AlienSpecies("Gazorpian",2,6,2)

println(kevin.describe())

println(mortyJr.describe())

}

/*Output:

Zigerionwith2eyes,2handsand2legs

Gazorpianwith2eyes,6handsand2legs

*/

InComplexConstructors,you’llseethatconstructorscanalsocontaincomplexinitializationlogic.

IfanobjectisusedwhenaStringisexpected,Kotlincallstheobject’stoString()memberfunction.Ifyoudon’twriteone,youstillgetadefaulttoString():

//Constructors/DisplayAlienSpecies.kt

funmain(){

valkrombopulosMichael=

AlienSpecies("Gromflomite",2,2,2)

println(krombopulosMichael)

}

/*Sampleoutput:

AlienSpecies@4d7e1886

*/

ThedefaulttoString()isn’tveryuseful—itproducestheclassnameandthephysicaladdressoftheobject(thisvariesfromoneprogramexecutiontothenext).YoucandefineyourowntoString():

//Constructors/Scientist.kt

classScientist(valname:String){

overridefuntoString():String{

return"Scientist('$name')"

}

}

funmain(){

valzeep=Scientist("ZeepXanflorp")

println(zeep)

}

/*Output:

Scientist('ZeepXanflorp')

*/

overrideisanewkeywordforus.ItisrequiredherebecausetoString()alreadyhasadefinition,theoneproducingtheprimitiveresult.overridetellsKotlinthatyes,wedoactuallywanttoreplacethedefaulttoString()withourowndefinition.Theexplicitnessofoverrideclarifiesthecodeandpreventsmistakes.

AtoString()thatdisplaysthecontentsofanobjectinaconvenientformisusefulforfindingandfixingprogrammingerrors.Tosimplifytheprocessofdebugging,IDEsprovidedebuggersthatallowyoutoobserveeachstepintheexecutionofaprogramandtoseeinsideyourobjects.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ConstrainingVisibility

Ifyouleaveapieceofcodeforafewdaysorweeks,thencomebacktoit,youmightseeamuchbetterwaytowriteit.

Thisisoneoftheprimemotivationsforrefactoring,whichrewritesworkingcodetomakeitmorereadable,understandable,andthusmaintainable.

Thereisatensioninthisdesiretochangeandimproveyourcode.Consumers(clientprogrammers)requireaspectsofyourcodetobestable.Youwanttochangeit,andtheywantittostaythesame.

Thisisparticularlyimportantforlibraries.Consumersofalibrarydon’twanttorewritecodeforanewversionofthatlibrary.However,thelibrarycreatormustbefreetomakemodificationsandimprovements,withthecertaintythattheclientcodewon’tbeaffectedbythosechanges.

Therefore,aprimaryconsiderationinsoftwaredesignis:

Separatethingsthatchangefromthingsthatstaythesame.

Tocontrolvisibility,Kotlinandsomeotherlanguagesprovideaccessmodifiers.Librarycreatorsdecidewhatisandisnotaccessiblebytheclientprogrammerusingthemodifierspublic,private,protected,andinternal.Thisatomcoverspublicandprivate,withabriefintroductiontointernal.Weexplainprotectedlaterinthebook.

Anaccessmodifiersuchasprivateappearsbeforethedefinitionforaclass,function,orproperty.Anaccessmodifieronlycontrolsaccessforthatparticulardefinition.

Apublicdefinitionisaccessiblebyclientprogrammers,sochangestothatdefinitionimpactclientcodedirectly.Ifyoudon’tprovideamodifier,yourdefinitionisautomaticallypublic,sopublicistechnicallyredundant.Youwillsometimesstillspecifypublicforthesakeofclarity.

Aprivatedefinitionishiddenandonlyaccessiblefromothermembersofthesameclass.Changing,orevenremoving,aprivatedefinitiondoesn’tdirectlyimpactclientprogrammers.

privateclasses,top-levelfunctions,andtop-levelpropertiesareaccessibleonlyinsidethatfile:

//Visibility/RecordAnimals.kt

privatevarindex=0//[1]

privateclassAnimal(valname:String)//[2]

privatefunrecordAnimal(//[3]

animal:Animal

){

println("Animal#$index:${animal.name}")

index++

}

funrecordAnimals(){

recordAnimal(Animal("Tiger"))

recordAnimal(Animal("Antelope"))

}

funrecordAnimalsCount(){

println("$indexanimalsarehere!")

}

Youcanaccessprivatetop-levelproperties([1]),classes([2]),andfunctions([3])fromotherfunctionsandclasseswithinRecordAnimals.kt.Kotlinpreventsyoufromaccessingaprivatetop-levelelementfromwithinanotherfile,tellingyouit’sprivateinthefile:

//Visibility/ObserveAnimals.kt

funmain(){

//Can'taccessprivatemembers

//declaredinanotherfile.

//Classisprivate:

//valrabbit=Animal("Rabbit")

//Functionisprivate:

//recordAnimal(rabbit)

//Propertyisprivate:

//index++

recordAnimals()

recordAnimalsCount()

}

/*Output:

Animal#0:Tiger

Animal#1:Antelope

2animalsarehere!

*/

Privacyismostcommonlyusedformembersofaclass:

//Visibility/Cookie.kt

classCookie(

privatevarisReady:Boolean//[1]

){

privatefuncrumble()=//[2]

println("crumble")

publicfunbite()=//[3]

println("bite")

funeat(){//[4]

isReady=true//[5]

crumble()

bite()

}

}

funmain(){

valx=Cookie(false)

x.bite()

//Can'taccessprivatemembers:

//x.isReady

//x.crumble()

x.eat()

}

/*Output:

bite

crumble

bite

*/

[1]Aprivateproperty,notaccessibleoutsidethecontainingclass.[2]Aprivatememberfunction.[3]Apublicmemberfunction,accessibletoanyone.[4]Noaccessmodifiermeanspublic.[5]Onlymembersofthesameclasscanaccessprivatemembers.

Theprivatekeywordmeansnoonecanaccessthatmemberexceptothermembersofthatclass.Otherclassescannotaccessprivatemembers,soit’sasifyou’realsoinsulatingtheclassagainstyourselfandyourcollaborators.Withprivate,youcanfreelychangethatmemberwithoutworryingwhetheritaffectsanotherclassinthesamepackage.Asalibrarydesigneryou’lltypicallykeepthingsasprivateaspossible,andexposeonlyfunctionsandclassestoclientprogrammers.

Anymemberfunctionthatisahelperfunctionforaclasscanbemadeprivatetoensureyoudon’taccidentallyuseitelsewhereinthepackageandthusprohibityourselffromchangingorremovingthatfunction.

Thesameistrueforaprivatepropertyinsideaclass.Unlessyoumustexposetheunderlyingimplementation(whichislesslikelythanyoumightthink),makepropertiesprivate.However,justbecauseareferencetoanobjectisprivateinsideaclassdoesn’tmeansomeotherobjectcan’thaveapublicreferencetothesameobject:

//Visibility/MultipleRef.kt

classCounter(varstart:Int){

funincrement(){

start+=1

}

overridefuntoString()=start.toString()

}

classCounterHolder(counter:Counter){

privatevalctr=counter

overridefuntoString()=

"CounterHolder:"+ctr

}

funmain(){

valc=Counter(11)//[1]

valch=CounterHolder(c)//[2]

println(ch)

c.increment()//[3]

println(ch)

valch2=CounterHolder(Counter(9))//[4]

println(ch2)

}

/*Output:

CounterHolder:11

CounterHolder:12

CounterHolder:9

*/

[1]cisnowdefinedinthescopesurroundingthecreationoftheCounterHolderobjectonthefollowingline.[2]PassingcastheargumenttotheCounterHolderconstructormeansthatthenewCounterHoldernowreferstothesameCounterobjectthatcrefersto.[3]TheCounterthatissupposedlyprivateinsidechcanstillbemanipulatedviac.[4]Counter(9)hasnootherreferencesexceptwithinCounterHolder,soitcannotbeaccessedormodifiedbyanythingexceptch2.

Maintainingmultiplereferencestoasingleobjectiscalledaliasingandcanproducesurprisingbehavior.

Modules

Unlikethesmallexamplesinthisbook,realprogramsareoftenlarge.Itcanbehelpfultodividesuchprogramsintooneormoremodules.Amoduleisalogicallyindependentpartofacodebase.Thewayyoudivideaprojectintomodulesdependsonthebuildsystem(suchasGradleorMaven)andisbeyondthescopeofthisbook.

Aninternaldefinitionisaccessibleonlyinsidethemodulewhereitisdefined.internallandssomewherebetweenprivateandpublic—useitwhenprivateistoorestrictivebutyoudon’twantanelementtobeapartofthepublicAPI.Wedonotuseinternalinthebook’sexamplesorexercises.

Modulesareahigher-levelconcept.Thefollowingatomintroducespackages,whichenablefiner-grainedstructuring.Alibraryisoftenasinglemoduleconsistingofmultiplepackages,sointernalelementsareavailablewithinthelibrarybutarenotaccessiblebyconsumersofthatlibrary.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Packages

AfundamentalprincipleinprogrammingistheacronymDRY:Don’tRepeatYourself.

Multipleidenticalpiecesofcoderequiremaintenancewheneveryoumakefixesorimprovements.Soduplicatingcodeisnotjustextrawork—everyduplicationcreatesopportunitiesformistakes.

Theimportkeywordreusescodefromotherfiles.Onewaytouseimportistospecifyaclass,functionorpropertyname:

importpackagename.ClassName

importpackagename.functionName

importpackagename.propertyName

Apackageisanassociatedcollectionofcode.Eachpackageisusuallydesignedtosolveaparticularproblem,andoftencontainsmultiplefunctionsandclasses.Forexample,wecanimportmathematicalconstantsandfunctionsfromthekotlin.mathlibrary:

//Packages/ImportClass.kt

importkotlin.math.PI

importkotlin.math.cos//Cosine

funmain(){

println(PI)

println(cos(PI))

println(cos(2*PI))

}

/*Output:

3.141592653589793

-1.0

1.0

*/

Sometimesyouwanttousemultiplethird-partylibrariescontainingclassesorfunctionswiththesamename.Theaskeywordallowsyoutochangenameswhileimporting:

//Packages/ImportNameChange.kt

importkotlin.math.PIascircleRatio

importkotlin.math.cosascosine

funmain(){

println(circleRatio)

println(cosine(circleRatio))

println(cosine(2*circleRatio))

}

/*Output:

3.141592653589793

-1.0

1.0

*/

asisusefulifalibrarynameispoorlychosenorexcessivelylong.

Youcanfullyqualifyanimportinthebodyofyourcode.Inthefollowingexample,thecodemightbelessreadableduetotheexplicitpackagenames,buttheoriginofeachelementisabsolutelyclear:

//Packages/FullyQualify.kt

funmain(){

println(kotlin.math.PI)

println(kotlin.math.cos(kotlin.math.PI))

println(kotlin.math.cos(2*kotlin.math.PI))

}

/*Output:

3.141592653589793

-1.0

1.0

*/

Toimporteverythingfromapackage,useastar:

//Packages/ImportEverything.kt

importkotlin.math.*

funmain(){

println(E)

println(E.roundToInt())

println(E.toInt())

}

/*Output:

2.718281828459045

3

2

*/

Thekotlin.mathpackagecontainsaconvenientroundToInt()thatroundstheDoublevaluetothenearestinteger,unliketoInt()whichsimplytruncatesanythingafteradecimalpoint.

Toreuseyourcode,createapackageusingthepackagekeyword.Thepackagestatementmustbethefirstnon-commentstatementinthefile.packageisfollowedbythenameofyourpackage,whichbyconventionisalllowercase:

//Packages/PythagoreanTheorem.kt

packagepythagorean

importkotlin.math.sqrt

classRightTriangle(

vala:Double,

valb:Double

){

funhypotenuse()=sqrt(a*a+b*b)

funarea()=a*b/2

}

Youcannamethesource-codefileanythingyoulike,unlikeJavawhichrequiresthefilenametobethesameastheclassname.

Kotlinallowsyoutochooseanynameforyourpackage,butit’sconsideredgoodstyleforthepackagenametobeidenticaltothedirectorynamewherethepackagefilesarelocated(thiswillnotalwaysbethecasefortheexamplesinthisbook).

Theelementsinthepythagoreanpackagearenowavailableusingimport:

//Packages/ImportPythagorean.kt

importpythagorean.RightTriangle

funmain(){

valrt=RightTriangle(3.0,4.0)

println(rt.hypotenuse())

println(rt.area())

}

/*Output:

5.0

6.0

*/

Intheremainderofthisbookweusepackagestatementsforanyfilethatdefinesfunctions,classes,etc.,outsideofmain(),topreventnameclasheswithotherfilesinthebook,butweusuallywon’tputapackagestatementinafilethatonlycontainsamain().

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Testing

Constanttestingisessentialforrapidprogramdevelopment.

Ifchangingonepartofyourcodebreaksothercode,yourtestsrevealtheproblemrightaway.Ifyoudon’tfindoutimmediately,changesaccumulateandyoucannolongertellwhichchangecausedtheproblem.You’llspendalotlongertrackingitdown.

Testingisacrucialpractice,soweintroduceitearlyanduseitthroughouttherestofthebook.Thisway,youbecomeaccustomedtotestingasastandardpartoftheprogrammingprocess.

Usingprintln()toverifycodecorrectnessisaweakapproach—youmustscrutinizetheoutputeverytimeandconsciouslyensurethatit’scorrect.

Tosimplifyyourexperiencewhileusingthisbook,wecreatedourowntinytestingsystem.Thegoalisaminimalapproachthat:

1. Showstheexpectedresultofexpressions.2. Providesoutputsoyouknowtheprogramisrunning,evenwhenalltests

succeed.3. Ingrainstheconceptoftestingearlyinyourpractice.

Althoughusefulforthisbook,oursisnotatestingsystemfortheworkplace.Othershavetoiledlongandhardtocreatesuchtestsystems.Forexample:

JUnitisoneofthemostpopularJavatestframeworks,andiseasilyusedfromwithinKotlin.KotestisdesignedspecificallyforKotlin,andtakesadvantageofKotlinlanguagefeatures.TheSpekFrameworkproducesadifferentformoftesting,calledSpecificationTesting.

Touseourtestingframework,wemustfirstimportit.Thebasicelementsoftheframeworkareeq(equals)andneq(notequals):

//Testing/TestingExample.kt

importatomictest.*

funmain(){

valv1=11

valv2="Ontology"

//'eq'means"equals":

v1eq11

v2eq"Ontology"

//'neq'means"notequal"

v2neq"Epistimology"

//[Error]Epistimology!=Ontology

//v2eq"Epistimology"

}

/*Output:

11

Ontology

Ontology

*/

ThecodefortheatomictestpackageisinAppendixA:AtomicTest.Wedon’tintendthatyouunderstandeverythinginAtomicTest.ktrightnow,becauseitusessomefeaturesthatwon’tappearuntillaterinthebook.

Toproduceaclean,comfortableappearance,AtomicTestusesaKotlinfeatureyouhaven’tseenyet:theabilitytowriteafunctioncalla.function(b)inthetext-likeformafunctionb.Thisiscalledinfixnotation.Onlyfunctionsdefinedusingtheinfixkeywordcanbecalledthisway.AtomicTest.ktdefinestheinfixeqandnequsedinTestingExample.kt:

expressioneqexpected

expressionneqexpected

eqandneqareflexible—almostanythingworksasatestexpression.IfexpectedisaString,thenexpressionisconvertedtoaStringandthetwoStringsarecompared.Otherwise,expressionandexpectedarecompareddirectly(withoutconvertingthemfirst).Ineithercase,theresultofexpressionappearsontheconsolesoyouseesomethingwhentheprogramruns.Evenwhenthetestssucceed,youstillseetheresultontheleftofeqorneq.Ifexpressionandexpectedarenotequivalent,AtomicTestshowsanerrorwhentheprogramruns.

ThelasttestinTestingExample.ktintentionallyfailssoyouseeanexampleoffailureoutput.Ifthetwovaluesarenotequal,Kotlindisplaysthecorrespondingmessagestartingwith[Error].Ifyouuncommentthelastlineandruntheexampleabove,youwillsee,afterallthesuccessfultests:

[Error]Epistimology!=Ontology

Theactualvaluestoredinv2isnotwhatitisclaimedtobeinthe“expected”expression.AtomicTestdisplaystheStringrepresentationsforbothexpectedandactualvalues.

eqandneqarethebasic(infix)functionsdefinedforAtomicTest—ittrulyisaminimaltestingsystem.Whenyouputeqandneqexpressionsinyourexamples,you’llcreatebothatestandsomeconsoleoutput.Youverifythecorrectnessoftheprogrambyrunningit.

There’sasecondtoolinAtomicTest.Thetraceobjectcapturesoutputforlatercomparison:

//Testing/Trace1.kt

importatomictest.*

funmain(){

trace("line1")

trace(47)

trace("line2")

traceeq"""

line1

47

line2

"""

}

Addingresultstotracelookslikeafunctioncall,soyoucaneffectivelyreplaceprintln()withtrace().

Inpreviousatoms,wedisplayedoutputandreliedonhumanvisualinspectiontocatchanydiscrepancies.That’sunreliable;eveninabookwherewescrutinizethecodeoverandover,we’velearnedthatvisualinspectioncan’tbetrustedtofinderrors.FromnowonwerarelyusecommentedoutputblocksbecauseAtomicTestwilldoeverythingforus.However,sometimeswestillincludecommentedoutputblockswhenthatproducesamoreusefuleffect.

Seeingthebenefitsofusingtestingthroughouttherestofthebookshouldhelpyouincorporatetestingintoyourprogrammingprocess.You’llprobablystartfeelinguncomfortablewhenyouseecodethatdoesn’thavetests.Youmightevendecidethatcodewithouttestsisbrokenbydefinition.

TestingasPartofProgramming

Testingismosteffectivewhenit’sbuiltintoyoursoftwaredevelopmentprocess.Writingtestsensuresyougettheresultsyouexpect.Manypeopleadvocatewritingtestsbeforewritingtheimplementationcode—youfirstmakethetestfailbeforeyouwritethecodetomakeitpass.Thistechnique,calledTestDrivenDevelopment(TDD),isawaytoensurethatyou’rereallytestingwhatyouthinkyouare.You’llfindamorecompletedescriptionofTDDonWikipedia(searchfor“TestDrivenDevelopment”).

There’sanotherbenefittowritingtestably—itchangesthewayyoucraftyourcode.Youcouldjustdisplaytheresultsontheconsole.Butinthetestmindsetyouwonder,“HowwillItestthis?”Whenyoucreateafunction,youdecideyoushouldreturnsomethingfromthefunction,iffornootherreasonthantotestthatresult.Functionsthatdonothingbuttakeinputandproduceoutputtendtogeneratebetterdesigns,aswell.

Here’sasimplifiedexampleusingTDDtoimplementtheBMIcalculationfromNumberTypes.First,wewritethetests,alongwithaninitialimplementationthatfails(becausewehaven’tyetimplementedthefunctionality):

//Testing/TDDFail.kt

packagetesting1

importatomictest.eq

funmain(){

calculateBMI(160,68)eq"Normalweight"

//calculateBMI(100,68)eq"Underweight"

//calculateBMI(200,68)eq"Overweight"

}

funcalculateBMI(lbs:Int,height:Int)=

"Normalweight"

Onlythefirsttestpasses.Theothertestsfailandarecommented.Next,weaddcodetodeterminewhichweightsareinwhichcategories.Nowallthetestsfail:

//Testing/TDDStillFails.kt

packagetesting2

importatomictest.eq

funmain(){

//Everythingfails:

//calculateBMI(160,68)eq"Normalweight"

//calculateBMI(100,68)eq"Underweight"

//calculateBMI(200,68)eq"Overweight"

}

funcalculateBMI(

lbs:Int,

height:Int

):String{

valbmi=lbs/(height*height)*703.07

returnif(bmi<18.5)"Underweight"

elseif(bmi<25)"Normalweight"

else"Overweight"

}

We’reusingIntsinsteadofDoubles,producingazeroresult.Thetestsguideustothefix:

//Testing/TDDWorks.kt

packagetesting3

importatomictest.eq

funmain(){

calculateBMI(160.0,68.0)eq"Normalweight"

calculateBMI(100.0,68.0)eq"Underweight"

calculateBMI(200.0,68.0)eq"Overweight"

}

funcalculateBMI(

lbs:Double,

height:Double

):String{

valbmi=lbs/(height*height)*703.07

returnif(bmi<18.5)"Underweight"

elseif(bmi<25)"Normalweight"

else"Overweight"

}

Youmaychoosetoaddadditionaltestsfortheboundaryconditions.

Intheexercisesforthisbook,weincludeteststhatyourcodemustpass.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Exceptions

Theword“exception”isusedinthesamesenseasthephrase“Itakeexceptiontothat.”

Anexceptionalconditionpreventsthecontinuationofthecurrentfunctionorscope.Atthepointtheproblemoccurs,youmightnotknowwhattodowithit,butyoucannotcontinuewithinthecurrentcontext.Youdon’thaveenoughinformationtofixtheproblem.Soyoumuststopandhandtheproblemtoanothercontextthat’sabletotakeappropriateaction.

Thisatomcoversthebasicsofexceptionsasanerror-reportingmechanism.InSectionVI:PreventingFailure,welookatotherwaystodealwithproblems.

It’simportanttodistinguishanexceptionalconditionfromanormalproblem.Anormalproblemhasenoughinformationinthecurrentcontexttocopewiththeissue.Withanexceptionalcondition,youcannotcontinueprocessing.Allyoucandoisleave,relegatingtheproblemtoanexternalcontext.Thisiswhathappenswhenyouthrowanexception.Theexceptionistheobjectthatis“thrown”fromthesiteoftheerror.

ConsidertoInt(),whichconvertsaStringtoanInt.WhathappensifyoucallthisfunctionforaStringthatdoesn’tcontainanintegervalue?

//Exceptions/ToIntException.kt

packageexceptions

funerroneousCode(){

//Uncommentthislinetogetanexception:

//vali="1$".toInt()//[1]

}

funmain(){

erroneousCode()

}

Uncommentingline[1]producesanexception.Here,thefailinglineiscommentedsowedon’tstopthebook’sbuild,whichcheckswhethereachexamplecompilesandrunsasexpected.

Whenanexceptionisthrown,thepathofexecution—theonethatcan’tbecontinued—stops,andtheexceptionobjectejectsfromthecurrentcontext.Here,itexitsthecontextoferroneousCode()andgoesouttothecontextofmain().Inthiscase,Kotlinonlyreportstheerror;theprogrammerhaspresumablymadeamistakeandmustfixthecode.

Whenanexceptionisn’tcaught,theprogramabortsanddisplaysastacktracecontainingdetailedinformation.Uncommentingline[1]inToIntException.kt,producesthefollowingoutput:

Exceptioninthread"main"java.lang.NumberFormatException:Forinputs\

tring:"1$"

atjava.lang.NumberFormatException.forInputString(NumberFormatExcepti\

on.java:65)

atjava.lang.Integer.parseInt(Integer.java:580)

atjava.lang.Integer.parseInt(Integer.java:615)

atToIntExceptionKt.erroneousCode(atToIntException.kt:6)

atToIntExceptionKt.main(atToIntException.kt:10)

Thestacktracegivesdetailssuchasthefileandlinewheretheexceptionoccurred,soyoucanquicklydiscovertheissue.Thelasttwolinesshowtheproblem:inline10ofmain()wecallerroneousCode().Then,moreprecisely,inline6oferroneousCode()wecalltoInt().

Toavoidcommentinganduncommentingcodetodisplayexceptions,weusethecapture()functionfromtheAtomicTestpackage:

//Exceptions/IntroducingCapture.kt

importatomictest.*

funmain(){

capture{

"1$".toInt()

}eq"NumberFormatException:"+

"""Forinputstring:"1$""""

}

Usingcapture(),wecomparethegeneratedexceptiontotheexpectederrormessage.capture()isn’tveryhelpfulfornormalprogramming—it’sdesignedspecificallyforthisbook,soyoucanseetheexceptionandknowthattheoutputhasbeencheckedbythebook’sbuildsystem.

Anotherstrategywhenyoucan’tsuccessfullyproducetheexpectedresultistoreturnnull,whichisaspecialconstantdenoting“novalue.”Youcanreturnnullinsteadofavalueofanytype.LaterinNullableTypeswediscussthewaynullaffectsthetypeoftheresultingexpression.

TheKotlinstandardlibrarycontainsString.toIntOrNull()whichperformstheconversioniftheStringcontainsanintegernumber,orproducesnulliftheconversionisimpossible—nullisasimplewaytoindicatefailure:

//Exceptions/IntroducingNull.kt

importatomictest.eq

funmain(){

"1$".toIntOrNull()eqnull

}

Supposewecalculateaverageincomeoveraperiodofmonths:

//Exceptions/AverageIncome.kt

packagefirstversion

importatomictest.*

funaverageIncome(income:Int,months:Int)=

income/months

funmain(){

averageIncome(3300,3)eq1100

capture{

averageIncome(5000,0)

}eq"ArithmeticException:/byzero"

}

Ifmonthsiszero,thedivisioninaverageIncome()throwsanArithmeticException.Unfortunately,thisdoesn’ttellusanythingaboutwhytheerroroccurred,whatthedenominatormeansandwhetheritcanlegallybezerointhefirstplace.Thisisclearlyabuginthecode—averageIncome()

shouldcopewithamonthsof0inawaythatpreventsadivide-by-zeroerror.

Let’smodifyaverageIncome()toproducemoreinformationaboutthesourceoftheproblem.Ifmonthsiszero,wecan’treturnaregularintegervalueasaresult.Onestrategyistoreturnnull:

//Exceptions/AverageIncomeWithNull.kt

packagewithnull

importatomictest.eq

funaverageIncome(income:Int,months:Int)=

if(months==0)

null

else

income/months

funmain(){

averageIncome(3300,3)eq1100

averageIncome(5000,0)eqnull

}

Ifafunctioncanreturnnull,Kotlinrequiresthatyouchecktheresultbeforeusingit(thisiscoveredinNullableTypes).Evenifyouonlywanttodisplayoutputtotheuser,it’sbettertosay“Nofullmonthperiodshavepassed,”ratherthan“Youraverageincomefortheperiodis:null.”

InsteadofexecutingaverageIncome()withthewrongarguments,youcanthrowanexception—escapeandforcesomeotherpartoftheprogramtomanagetheissue.YoucouldjustallowthedefaultArithmeticException,butit’softenmoreusefultothrowaspecificexceptionwithadetailederrormessage.When,afteracoupleofyearsinproduction,yourapplicationsuddenlythrowsanexceptionbecauseanewfeaturecallsaverageIncome()withoutproperlycheckingthearguments,you’llbegratefulforthatmessage:

//Exceptions/AverageIncomeWithException.kt

packageproperexception

importatomictest.*

funaverageIncome(income:Int,months:Int)=

if(months==0)

throwIllegalArgumentException(//[1]

"Monthscan'tbezero")

else

income/months

funmain(){

averageIncome(3300,3)eq1100

capture{

averageIncome(5000,0)

}eq"IllegalArgumentException:"+

"Monthscan'tbezero"

}

[1]Whenthrowinganexception,thethrowkeywordisfollowedbytheexceptiontobethrown,alongwithanyargumentsitmightneed.HereweusethestandardexceptionclassIllegalArgumentException.

Yourgoalistogeneratethemostusefulmessagespossibletosimplifythesupportofyourapplicationinthefuture.Lateryou’lllearntodefineyourownexceptiontypesandmakethemspecifictoyourcircumstances.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Lists

AListisacontainer,whichisanobjectthatholdsotherobjects.

Containersarealsocalledcollections.Whenweneedabasiccontainerfortheexamplesinthisbook,wenormallyuseaList.

ListsarepartofthestandardKotlinpackagesotheydon’trequireanimport.

ThefollowingexamplecreatesaListpopulatedwithIntsbycallingthestandardlibraryfunctionlistOf()withinitializationvalues:

//Lists/Lists.kt

importatomictest.eq

funmain(){

valints=listOf(99,3,5,7,11,13)

intseq"[99,3,5,7,11,13]"//[1]

//SelecteachelementintheList:

varresult=""

for(iinints){//[2]

result+="$i"

}

resulteq"993571113"

//"Indexing"intotheList:

ints[4]eq11//[3]

}

[1]AListusessquarebracketswhendisplayingitself.[2]forloopsworkwellwithLists:for(iinints)meansireceiveseachvalueinints.Youdon’tdeclarevaliorgiveitstype;Kotlinknowsfromthecontextthatiisaforloopidentifier.[3]SquarebracketsindexintoaList.AListkeepsitselementsininitializationorder,andyouselectthemindividuallybynumber.Likemostprogramminglanguages,Kotlinstartsindexingatelementzero,whichinthiscaseproducesthevalue99.Thusanindexof4producesthevalue11.

Forgettingthatindexingstartsatzeroproducestheso-calledoff-by-oneerror.InalanguagelikeKotlinweoftendon’tselectelementsoneatatime,butinsteaditeratethroughanentirecontainerusingin.Thiseliminatesoff-by-oneerrors.

IfyouuseanindexbeyondthelastelementinaList,KotlinthrowsanArrayIndexOutOfBoundsException:

//Lists/OutOfBounds.kt

importatomictest.*

funmain(){

valints=listOf(1,2,3)

capture{

ints[3]

}contains

listOf("ArrayIndexOutOfBoundsException")

}

AListcanholdalldifferenttypes.Here’saListofDoublesandaListofStrings:

//Lists/ListUsefulFunction.kt

importatomictest.eq

funmain(){

valdoubles=

listOf(1.1,2.2,3.3,4.4)

doubles.sum()eq11.0

valstrings=listOf("Twas","Brillig",

"And","Slithy","Toves")

stringseqlistOf("Twas","Brillig",

"And","Slithy","Toves")

strings.sorted()eqlistOf("And",

"Brillig","Slithy","Toves","Twas")

strings.reversed()eqlistOf("Toves",

"Slithy","And","Brillig","Twas")

strings.first()eq"Twas"

strings.takeLast(2)eq

listOf("Slithy","Toves")

}

ThisshowssomeofList’soperations.Notethename“sorted”insteadof“sort.”Whenyoucallsorted()itproducesanewListcontainingthesameelementsastheold,insortedorder—butitleavestheoriginalListalone.Callingit“sort”impliesthattheoriginalListischangeddirectly(a.k.a.sortedinplace).ThroughoutKotlin,youseethistendencyof“leavingtheoriginalobjectaloneandproducinganewobject.”reversed()alsoproducesanewList.

ParameterizedTypesWeconsideritgoodpracticetousetypeinference—ittendstomakethecodecleanerandeasiertoread.Sometimes,however,Kotlincomplainsthatitcan’tfigureoutwhattypetouse,andinothercasesexplicitnessmakesthecodemoreunderstandable.Here’showwetellKotlinthetypecontainedbyaList:

//Lists/ParameterizedTypes.kt

importatomictest.eq

funmain(){

//Typeisinferred:

valnumbers=listOf(1,2,3)

valstrings=

listOf("one","two","three")

//Exactlythesame,butexplicitlytyped:

valnumbers2:List<Int>=listOf(1,2,3)

valstrings2:List<String>=

listOf("one","two","three")

numberseqnumbers2

stringseqstrings2

}

KotlinusestheinitializationvaluestoinferthatnumberscontainsaListofInts,whilestringscontainsaListofStrings.

numbers2andstrings2areexplicitly-typedversionsofnumbersandstrings,createdbyaddingthetypedeclarationsList<Int>andList<String>.Youhaven’tseenanglebracketsbefore—theydenoteatypeparameter,allowingyoutosay,“thiscontainerholds‘parameter’objects.”WepronounceList<Int>as“ListofInt.”

Typeparametersareusefulforcomponentsotherthancontainers,butyouoftenseethemwithcontainer-likeobjects.

Returnvaluescanalsohavetypeparameters:

//Lists/ParameterizedReturn.kt

packagelists

importatomictest.eq

//Returntypeisinferred:

funinferred(p:Char,q:Char)=

listOf(p,q)

//Explicitreturntype:

funexplicit(p:Char,q:Char):List<Char>=

listOf(p,q)

funmain(){

inferred('a','b')eq"[a,b]"

explicit('y','z')eq"[y,z]"

}

Kotlininfersthereturntypeforinferred(),whileexplicit()specifiesthefunctionreturntype.Youcan’tjustsayitreturnsaList;Kotlinwillcomplain,soyoumustgivethetypeparameteraswell.Whenyouspecifythereturntypeofafunction,Kotlinenforcesyourintention.

Read-OnlyandMutableListsIfyoudon’texplicitlysayyouwantamutableList,youwon’tgetone.listOf()producesaread-onlyListthathasnomutatingfunctions.

Ifyou’recreatingaListgradually(thatis,youdon’thavealltheelementsatcreationtime),usemutableListOf().ThisproducesaMutableListthatcanbemodified:

//Lists/MutableList.kt

importatomictest.eq

funmain(){

vallist=mutableListOf<Int>()

list.add(1)

list.addAll(listOf(2,3))

list+=4

list+=listOf(5,6)

listeqlistOf(1,2,3,4,5,6)

}

YoucanaddelementstoaMutableListusingadd()andaddAll(),ortheshortcut+=whichaddsasingleelementoranothercollection.Becauselisthasnoinitialelements,wemusttellKotlinwhattypeitisbyprovidingthe<Int>specificationinthecalltomutableListOf().

AMutableListcanbetreatedasaList,inwhichcaseitcannotbechanged.Youcan’t,however,treataread-onlyListasaMutableList:

//Lists/MutListIsList.kt

packagelists

importatomictest.eq

fungetList():List<Int>{

returnmutableListOf(1,2,3)

}

funmain(){

//getList()producesaread-onlyList:

vallist=getList()

//list+=3//Error

listeqlistOf(1,2,3)

}

NotethatlistlacksmutationfunctionsdespitebeingoriginallycreatedusingmutableListOf()insidegetList().Duringthereturn,theresulttypebecomesaList<Int>.TheoriginalobjectisstillaMutableList,butitisviewedthroughthelensofaList.

AListisread-only—youcanreaditscontentsbutnotwritetoit.IftheunderlyingimplementationisaMutableListandyouretainamutablereferencetothatimplementation,youcanstillmodifyitviathatmutablereference,andanyread-onlyreferenceswillseethosechanges.Thisisanotherexampleofaliasing,introducedinConstrainingVisibility:

//Lists/MultipleListRefs.kt

importatomictest.eq

funmain(){

valfirst=mutableListOf(1)

valsecond:List<Int>=first

secondeqlistOf(1)

first+=2

//secondseesthechange:

secondeqlistOf(1,2)

}

firstisanimmutablereference(val)tothemutableobjectproducedbymutableListOf(1).Thensecondisaliasedtofirst,soitisaviewofthatsameobject.secondisread-onlybecauseList<Int>doesnotincludemodificationfunctions.Notethat,withouttheexplicitList<Int>typedeclaration,Kotlinwouldinferthatsecondwasalsoareferencetoamutableobject.

We’reabletoaddanelement(2)totheobjectbecausefirstisareferencetoamutableList.Notethatsecondobservesthesechanges—itcannotchangetheListalthoughtheListchangesviafirst.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

VariableArgumentLists

Thevarargkeywordproducesaflexibly-sizedargumentlist.

InListsweintroducedlistOf(),whichtakesanynumberofparametersandproducesaList:

//Varargs/ListOf.kt

importatomictest.eq

funmain(){

listOf(1)eq"[1]"

listOf("a","b")eq"[a,b]"

}

Usingthevarargkeyword,youcandefineafunctionthattakesanynumberofarguments,justlikelistOf()does.varargisshortforvariableargumentlist:

//Varargs/VariableArgList.kt

packagevarargs

funv(s:String,varargd:Double){}

funmain(){

v("abc",1.0,2.0)

v("def",1.0,2.0,3.0,4.0)

v("ghi",1.0,2.0,3.0,4.0,5.0,6.0)

}

Afunctiondefinitionmayspecifyonlyoneparameterasvararg.Althoughit’spossibletospecifyanyitemintheparameterlistasvararg,it’susuallysimplesttodoitforthelastone.

varargallowsyoutopassanynumber(includingzero)ofarguments.Allargumentsmustbeofthespecifiedtype.varargargumentsareaccessedusingtheparametername,whichbecomesanArray:

//Varargs/VarargSum.kt

packagevarargs

importatomictest.eq

funsum(varargnumbers:Int):Int{

vartotal=0

for(ninnumbers){

total+=n

}

returntotal

}

funmain(){

sum(13,27,44)eq84

sum(1,3,5,7,9,11)eq36

sum()eq0

}

AlthoughArraysandListslooksimilar,theyareimplementeddifferently—ListisaregularlibraryclasswhileArrayhasspeciallow-levelsupport.ArraycomesfromKotlin’srequirementforcompatibilitywithotherlanguages,especiallyJava.

Inday-to-dayprogramming,useaListwhenyouneedasimplesequence.UseArraysonlywhenathird-partyAPIrequiresanArray,orwhenyou’redealingwithvarargs.

InmostcasesyoucanjustignorethefactthatvarargproducesanArrayandtreatitasifitwereaList:

//Varargs/VarargLikeList.kt

packagevarargs

importatomictest.eq

funevaluate(varargints:Int)=

"Size:${ints.size}\n"+

"Sum:${ints.sum()}\n"+

"Average:${ints.average()}"

funmain(){

evaluate(10,-3,8,1,9)eq"""

Size:5

Sum:25

Average:5.0

"""

}

YoucanpassanArrayofelementswhereveravarargisaccepted.TocreateanArray,usearrayOf()inthesamewayyouuselistOf().NotethatanArrayisalwaysmutable.ToconvertanArrayintoasequenceofarguments(notjustasingleelementoftypeArray),usethespreadoperator,*:

//Varargs/SpreadOperator.kt

importvarargs.sum

importatomictest.eq

funmain(){

valarray=intArrayOf(4,5)

sum(1,2,3,*array,6)eq21//[1]

//Doesn'tcompile:

//sum(1,2,3,array,6)

vallist=listOf(9,10,11)

sum(*list.toIntArray())eq30//[2]

}

IfyoupassanArrayofprimitivetypes(likeInt,DoubleorBoolean)asintheexampleabove,theArraycreationfunctionmustbespecificallytyped.IfyouusearrayOf(4,5)insteadofintArrayOf(4,5),line[1]willproduceanerrorcomplainingthatinferredtypeisArray<Int>butIntArraywasexpected.

Thespreadoperatoronlyworkswitharrays.IfyouhaveaListthatyouwanttopassasasequenceofarguments,firstconvertittoanArrayandthenapplythespreadoperator,asin[2].BecausetheresultisanArrayofaprimitivetype,wemustagainusethespecificconversionfunctiontoIntArray().

Thespreadoperatorisespeciallyhelpfulwhenyoumustpassvarargargumentstoanotherfunctionthatalsoexpectsvarargs:

//Varargs/TwoFunctionsWithVarargs.kt

packagevarargs

importatomictest.eq

funfirst(varargnumbers:Int):String{

varresult=""

for(iinnumbers){

result+="[$i]"

}

returnresult

}

funsecond(varargnumbers:Int)=

first(*numbers)

funmain(){

second(7,9,32)eq"[7][9][32]"

}

Command-LineArgumentsWheninvokingaprogramonthecommandline,youcanpassitavariablenumberofarguments.Tocapturecommand-linearguments,youmustprovideaparticularparametertomain():

//Varargs/MainArgs.kt

funmain(args:Array<String>){

for(ainargs){

println(a)

}

}

Theparameteristraditionallycalledargs(althoughyoucancallitanything),andthetypeforargscanonlybeArray<String>(ArrayofString).

IfyouareusingIntelliJIDEA,youcanpassprogramargumentsbyeditingthecorresponding“Runconfiguration,”asshowninthelastexerciseforthisatom.

Youcanalsousethekotlinccompilertoproduceacommand-lineprogram.Ifkotlincisn’tonyourcomputer,followtheinstructionsontheKotlinmainsite.Onceyou’veenteredandsavedthecodeforMainArgs.kt,typethefollowingatacommandprompt:

kotlincMainArgs.kt

Youprovidethecommand-lineargumentsfollowingtheprograminvocation,likethis:

kotlinMainArgsKthamster423.14159

You’llseethisoutput:

hamster

42

3.14159

IfyouwanttoturnaStringparameterintoaspecifictype,Kotlinprovidesconversionfunctions,suchasatoInt()forconvertingtoanInt,andtoFloat()forconvertingtoaFloat.Usingtheseassumesthatthecommand-lineargumentsappearinaparticularorder.Here,theprogramexpectsaString,followedbysomethingconvertibletoanInt,followedbysomethingconvertibletoaFloat:

//Varargs/MainArgConversion.kt

funmain(args:Array<String>){

if(args.size<3)return

valfirst=args[0]

valsecond=args[1].toInt()

valthird=args[2].toFloat()

println("$first$second$third")

}

Thefirstlineinmain()quitstheprogramiftherearen’tenougharguments.Ifyoudon’tprovidesomethingconvertibletoanIntandaFloatasthesecondandthirdcommand-linearguments,youwillseeruntimeerrors(tryittoseetheerrors).

CompileandrunMainArgConversion.ktwiththesamecommand-lineargumentsweusedbefore,andyou’llsee:

hamster423.14159

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Sets

ASetisacollectionthatallowsonlyoneelementofeachvalue.

ThemostcommonSetactivityistotestformembershipusinginorcontains():

//Sets/Sets.kt

importatomictest.eq

funmain(){

valintSet=setOf(1,1,2,3,9,9,4)

//Noduplicates:

intSeteqsetOf(1,2,3,4,9)

//Elementorderisunimportant:

setOf(1,2)eqsetOf(2,1)

//Setmembership:

(9inintSet)eqtrue

(99inintSet)eqfalse

intSet.contains(9)eqtrue

intSet.contains(99)eqfalse

//Doesthissetcontainanotherset?

intSet.containsAll(setOf(1,9,2))eqtrue

//Setunion:

intSet.union(setOf(3,4,5,6))eq

setOf(1,2,3,4,5,6,9)

//Setintersection:

intSetintersectsetOf(0,1,2,7,8)eq

setOf(1,2)

//Setdifference:

intSetsubtractsetOf(0,1,9,10)eq

setOf(2,3,4)

intSet-setOf(0,1,9,10)eq

setOf(2,3,4)

}

Thisexampleshows:

1. PlacingduplicateitemsintoaSetautomaticallyremovesthoseduplicates.2. Elementorderisnotimportantforsets.Twosetsareequaliftheycontain

thesameelements.3. Bothinandcontains()testformembership.

4. YoucanperformtheusualVenn-diagramoperationslikecheckingforsubset,union,intersectionanddifference,usingeitherdotnotation(set.union(other))orinfixnotation(setintersectother).Thefunctionsunion,intersectandsubtractcanbeusedwithinfixnotation.

5. Setdifferencecanbeexpressedwitheithersubtract()ortheminusoperator.

ToremoveduplicatesfromaList,convertittoaSet:

//Sets/RemoveDuplicates.kt

importatomictest.eq

funmain(){

vallist=listOf(3,3,2,1,2)

list.toSet()eqsetOf(1,2,3)

list.distinct()eqlistOf(3,2,1)

"abbcc".toSet()eqsetOf('a','b','c')

}

Youcanalsousedistinct(),whichreturnsaList.YoumaycalltoSet()onaStringtoconvertitintoasetofuniquecharacters.

AswithList,KotlinprovidestwocreationfunctionsforSet.TheresultofsetOf()isread-only.TocreateamutableSet,usemutableSetOf():

//Sets/MutableSet.kt

importatomictest.eq

funmain(){

valmutableSet=mutableSetOf<Int>()

mutableSet+=42

mutableSet+=42

mutableSeteqsetOf(42)

mutableSet-=42

mutableSeteqsetOf<Int>()

}

Theoperators+=and-=addandremoveelementstoSets,justaswithLists.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Maps

AMapconnectskeystovaluesandlooksupavaluewhengivenakey.

YoucreateaMapbyprovidingkey-valuepairstomapOf().Usingto,weseparateeachkeyfromitsassociatedvalue:

//Maps/Maps.kt

importatomictest.eq

funmain(){

valconstants=mapOf(

"Pi"to3.141,

"e"to2.718,

"phi"to1.618

)

constantseq

"{Pi=3.141,e=2.718,phi=1.618}"

//Lookupavaluefromakey:

constants["e"]eq2.718//[1]

constants.keyseqsetOf("Pi","e","phi")

constants.valueseq"[3.141,2.718,1.618]"

vars=""

//Iteratethroughkey-valuepairs:

for(entryinconstants){//[2]

s+="${entry.key}=${entry.value},"

}

seq"Pi=3.141,e=2.718,phi=1.618,"

s=""

//Unpackduringiteration:

for((key,value)inconstants)//[3]

s+="$key=$value,"

seq"Pi=3.141,e=2.718,phi=1.618,"

}

[1]The[]operatorlooksupavalueusingakey.Youcanproduceallthekeysusingkeysandallthevaluesusingvalues.CallingkeysproducesaSetbecauseallkeysinaMapmustbeunique,otherwiseyou’dhaveambiguityduringalookup.[2]IteratingthroughaMapproduceskey-valuepairsasmapentries.[3]Youcanunpackkeysandvaluesasyouiterate.

AplainMapisread-only.Here’saMutableMap:

//Maps/MutableMaps.kt

importatomictest.eq

funmain(){

valm=

mutableMapOf(5to"five",6to"six")

m[5]eq"five"

m[5]="5ive"

m[5]eq"5ive"

m+=4to"four"

meqmapOf(5to"5ive",

4to"four",6to"six")

}

map[key]=valueaddsorchangesthevalueassociatedwithkey.Youcanalsoexplicitlyaddapairbysayingmap+=keytovalue.

mapOf()andmutableMapOf()preservetheorderinwhichtheelementsareputintotheMap.ThisisnotguaranteedforothertypesofMap.

Aread-onlyMapdoesn’tallowmutations:

//Maps/ReadOnlyMaps.kt

importatomictest.eq

funmain(){

valm=mapOf(5to"five",6to"six")

m[5]eq"five"

//m[5]="5ive"//Fails

//m+=(4to"four")//Fails

m+(4to"four")//Doesn'tchangem

meqmapOf(5to"five",6to"six")

valm2=m+(4to"four")

m2eqmapOf(

5to"five",6to"six",4to"four")

}

ThedefinitionofmcreatesaMapassociatingIntswithStrings.IfwetrytoreplaceaString,Kotlinemitsanerror.

Anexpressionwith+createsanewMapthatincludesboththeoldelementsandthenewone,butdoesn’taffecttheoriginalMap.Theonlywayto“add”anelementtoaread-onlyMapisbycreatinganewMap.

AMapreturnsnullifitdoesn’tcontainanentryforagivenkey.Ifyouneedaresultthatcan’tbenull,usegetValue()andcatchNoSuchElementExceptionifthekeyismissing:

//Maps/GetValue.kt

importatomictest.*

funmain(){

valmap=mapOf('a'to"attempt")

map['b']eqnull

capture{

map.getValue('b')

}eq"NoSuchElementException:"+

"Keybismissinginthemap."

map.getOrDefault('a',"??")eq"attempt"

map.getOrDefault('b',"??")eq"??"

}

getOrDefault()isusuallyaniceralternativetonulloranexception.

YoucanstoreclassinstancesasvaluesinaMap.Here’samapthatretrievesaContactusinganumberString:

//Maps/ContactMap.kt

packagemaps

importatomictest.eq

classContact(

valname:String,

valphone:String

){

overridefuntoString():String{

return"Contact('$name','$phone')"

}

}

funmain(){

valmiffy=Contact("Miffy","1-234-567890")

valcleo=Contact("Cleo","098-765-4321")

valcontacts=mapOf(

miffy.phonetomiffy,

cleo.phonetocleo)

contacts["1-234-567890"]eqmiffy

contacts["1-111-111111"]eqnull

}

It’spossibletouseclassinstancesaskeysinaMap,butthat’strickiersowediscussitlaterinthebook.

-

Mapslooklikesimplelittledatabases.Theyaresometimescalledassociativearrays,becausetheyassociatekeyswithvalues.Althoughtheyarequitelimitedcomparedtoafull-featureddatabase,theyarenonethelessremarkablyuseful(andfarmoreefficientthanadatabase).

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

PropertyAccessors

Toreadaproperty,useitsname.Toassignavaluetoamutableproperty,usetheassignmentoperator=.

Thisreadsandwritesthepropertyi:

//PropertyAccessors/Data.kt

packagepropertyaccessors

importatomictest.eq

classData(vari:Int)

funmain(){

valdata=Data(10)

data.ieq10//Readthe'i'property

data.i=20//Writetothe'i'property

}

Thisappearstobestraightforwardaccesstothepieceofstoragenamedi.However,Kotlincallsfunctionstoperformthereadandwriteoperations.Asyouexpect,thedefaultbehaviorofthosefunctionsreadsandwritesthedatastoredini.Inthisatomyou’lllearntowriteyourownpropertyaccessorstocustomizethereadingandwritingactions.

Theaccessorusedtogetthevalueofapropertyiscalledagetter.Youcreateagetterbydefiningget()immediatelyafterthepropertydefinition.Theaccessorusedtomodifyamutablepropertyiscalledasetter.Youcreateasetterbydefiningset()immediatelyafterthepropertydefinition.

ThepropertyaccessorsdefinedinthefollowingexampleimitatethedefaultimplementationsgeneratedbyKotlin.Wedisplayadditionalinformationsoyoucanseethatthepropertyaccessorsareindeedcalledduringreadsandwrites.Weindentget()andset()tovisuallyassociatethemwiththeproperty,buttheactualassociationhappensbecauseget()andset()aredefinedimmediatelyafterthatproperty(Kotlindoesn’tcareabouttheindentation):

//PropertyAccessors/Default.kt

packagepropertyaccessors

importatomictest.*

classDefault{

vari:Int=0

get(){

trace("get()")

returnfield//[1]

}

set(value){

trace("set($value)")

field=value//[2]

}

}

funmain(){

vald=Default()

d.i=2

trace(d.i)

traceeq"""

set(2)

get()

2

"""

}

Thedefinitionorderforget()andset()isunimportant.Youcandefineget()withoutdefiningset(),andvice-versa.

Thedefaultbehaviorforapropertyreturnsitsstoredvaluefromagetterandmodifiesitwithasetter—theactionsof[1]and[2].Insidethegetterandsetter,thestoredvalueismanipulatedindirectlyusingthefieldkeyword,whichisonlyaccessiblewithinthesetwofunctions.

Thisnextexampleusesthedefaultimplementationofthegetterandaddsasettertotracechangestothepropertyn:

//PropertyAccessors/LogChanges.kt

packagepropertyaccessors

importatomictest.*

classLogChanges{

varn:Int=0

set(value){

trace("$fieldbecomes$value")

field=value

}

}

funmain(){

vallc=LogChanges()

lc.neq0

lc.n=2

lc.neq2

traceeq"0becomes2"

}

Ifyoudefineapropertyasprivate,bothaccessorsbecomeprivate.Youcanalsomakethesetterprivateandthegetterpublic.Thenyoucanreadthe

propertyoutsidetheclass,butonlychangeitsvalueinsidetheclass:

//PropertyAccessors/Counter.kt

packagepropertyaccessors

importatomictest.eq

classCounter{

varvalue:Int=0

privateset

funinc()=value++

}

funmain(){

valcounter=Counter()

repeat(10){

counter.inc()

}

counter.valueeq10

}

Usingprivateset,wecontrolthevaluepropertysoitcanonlybeincrementedbyone.

Normalpropertiesstoretheirdatainafield.Youcanalsocreateapropertythatdoesn’thaveafield:

//PropertyAccessors/Hamsters.kt

packagepropertyaccessors

importatomictest.eq

classHamster(valname:String)

classCage(privatevalmaxCapacity:Int){

privatevalhamsters=

mutableListOf<Hamster>()

valcapacity:Int

get()=maxCapacity-hamsters.size

valfull:Boolean

get()=hamsters.size==maxCapacity

funput(hamster:Hamster):Boolean=

if(full)

false

else{

hamsters+=hamster

true

}

funtake():Hamster=

hamsters.removeAt(0)

}

funmain(){

valcage=Cage(2)

cage.fulleqfalse

cage.capacityeq2

cage.put(Hamster("Alice"))eqtrue

cage.put(Hamster("Bob"))eqtrue

cage.fulleqtrue

cage.capacityeq0

cage.put(Hamster("Charlie"))eqfalse

cage.take()

cage.capacityeq1

}

Thepropertiescapacityandfullcontainnounderlyingstate—theyarecomputedatthetimeofeachaccess.Bothcapacityandfullaresimilartofunctions,andyoucandefinethemassuch:

//PropertyAccessors/Hamsters2.kt

packagepropertyaccessors

classCage2(privatevalmaxCapacity:Int){

privatevalhamsters=

mutableListOf<Hamster>()

funcapacity():Int=

maxCapacity-hamsters.size

funisFull():Boolean=

hamsters.size==maxCapacity

}

Inthiscase,usingpropertiesimprovesreadabilitybecausecapacityandfullnessarepropertiesofthecage.However,don’tjustconvertallyourfunctionstoproperties—first,seehowtheyread.

-

TheKotlinstyleguidepreferspropertiesoverfunctionswhenthevalueischeaptocalculateandthepropertyreturnsthesameresultforeachinvocationaslongastheobjectstatehasn’tchanged.

Propertyaccessorsprovideakindofprotectionforproperties.Manyobject-orientedlanguagesrelyonmakingaphysicalfieldprivatetocontrolaccesstothatproperty.Withpropertyaccessorsyoucanaddcodetocontrolormodifythataccess,whileallowinganyonetouseaproperty.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Summary2

ThisatomsummarizesandreviewstheatomsinSectionII,fromObjectsEverywherethroughPropertyAccessors.

Ifyou’reanexperiencedprogrammer,thisisyournextatomafterSummary1,andyouwillgothroughtheatomssequentiallyafterthis.

Newprogrammersshouldreadthisatomandperformtheexercisesasreview.Ifanyinformationhereisn’tcleartoyou,gobackandstudytheatomforthattopic.

Thetopicsappearinappropriateorderforexperiencedprogrammers,whichisnotthesameastheorderoftheatomsinthebook.Forexample,westartbyintroducingpackagesandimportssowecanuseourminimaltestframeworkfortherestoftheatom.

Packages&TestingAnynumberofreusablelibrarycomponentscanbebundledunderasinglelibrarynameusingthepackagekeyword:

//Summary2/ALibrary.kt

packagecom.yoururl.libraryname

//Componentstoreuse...

funf()="result"

Youcanputmultiplecomponentsinasinglefile,orspreadcomponentsoutamongmultiplefilesunderthesamepackagename.Herewe’vedefinedf()asthesolecomponent.

Tomakeitunique,thepackagenameconventionallybeginswithyourreverseddomainname.Inthisexample,thedomainnameisyoururl.com.

InKotlin,thepackagenamecanbeindependentfromthedirectorywhereitscontentsarelocated.Javarequiresthatthedirectorystructurecorrespondtothefully-qualifiedpackagename,sothepackagecom.yoururl.librarynameshouldbelocatedunderthecom/yoururl/librarynamedirectory.FormixedKotlinand

Javaprojects,Kotlin’sstyleguiderecommendsthesamepractice.ForpureKotlinprojects,putthedirectorylibrarynameatthetoplevelofyourproject’sdirectorystructure.

Animportstatementbringsoneormorenamesintothecurrentnamespace:

//Summary2/UseALibrary.kt

importcom.yoururl.libraryname.*

funmain(){

valx=f()

}

ThestarafterlibrarynametellsKotlintoimportallthecomponentsofalibrary.Youcanalsoselectcomponentsindividually;detailsareinPackages.

Intheremainderofthisbookweusepackagestatementsforanyfilethatdefinesfunctions,classes,etc.,outsideofmain().Thispreventsnameclasheswithotherfilesinthebook.Weusuallywon’tputapackagestatementinafilethatonlycontainsamain().

Animportantlibraryforthisbookisatomictest,oursimpletestingframework.atomictestisdefinedinAppendixA:AtomicTest,althoughituseslanguagefeaturesyouwillnotunderstandatthispointinthebook.

Afterimportingatomictest,youuseeq(equals)andneq(notequals)almostasiftheywerelanguagekeywords:

//Summary2/UsingAtomicTest.kt

importatomictest.*

funmain(){

valpi=3.14

valpie="Arounddessert"

pieq3.14

pieeq"Arounddessert"

pineqpie

}

/*Output:

3.14

Arounddessert

3.14

*/

Theabilitytouseeq/neqwithoutanydotsorparenthesesiscalledinfixnotation.Youcancallinfixfunctionseitherintheregularway:pi.eq(3.14),orusinginfixnotation:pieq3.14.Botheqandneqareassertionsoftruththatalsodisplaytheresultfromtheleftsideoftheeq/neqstatement,andanerrormessage

iftheexpressionontherightoftheeqisn’tequivalenttotheleft(orisequivalent,inthecaseofneq).Thiswayyouseeverifiedresultsinthesourcecode.

atomictest.traceusesfunction-callsyntaxforaddingresults,whichcanthenbevalidatedusingeq:

//Testing/UsingTrace.kt

importatomictest.*

funmain(){

trace("Hello,")

trace(47)

trace("World!")

traceeq"""

Hello,

47

World!

"""

}

Youcaneffectivelyreplaceprintln()withtrace().

ObjectsEverywhereKotlinisahybridobject-functionallanguage:itsupportsbothobject-orientedandfunctionalprogrammingparadigms.

Objectscontainvalsandvarstostoredata(thesearecalledproperties)andperformoperationsusingfunctionsdefinedwithinaclass,calledmemberfunctions(whenit’sunambiguous,wejustsay“functions”).Aclassdefinespropertiesandmemberfunctionsforwhatisessentiallyanew,user-defineddatatype.Whenyoucreateavalorvarofaclass,it’scalledcreatinganobjectorcreatinganinstance.

Anespeciallyusefultypeofobjectisthecontainer,alsocalledcollection.Acontainerisanobjectthatholdsotherobjects.Inthisbook,weoftenusetheListbecauseit’sthemostgeneral-purposesequence.HereweperformseveraloperationsonaListthatholdsDoubles.listOf()createsanewListfromitsarguments:

//Summary2/ListCollection.kt

importatomictest.eq

funmain(){

vallst=listOf(19.2,88.3,22.1)

lst[1]eq88.3//Indexing

lst.reversed()eqlistOf(22.1,88.3,19.2)

lst.sorted()eqlistOf(19.2,22.1,88.3)

lst.sum()eq129.6

}

NoimportstatementisrequiredtouseaList.

Kotlinusessquarebracketsforindexingintosequences.Indexingiszero-based.

ThisexamplealsoshowsafewofthemanystandardlibraryfunctionsavailableforLists:sorted(),reversed(),andsum().Tounderstandthesefunctions,consulttheonlineKotlindocumentation.

Whenyoucallsorted()orreversed(),lstisnotmodified.Instead,anewListiscreatedandreturned,containingthedesiredresult.ThisapproachofnevermodifyingtheoriginalobjectisconsistentthroughoutKotlinlibrariesandyoushouldendeavortofollowthispatternwhenwritingyourowncode.

CreatingClassesAclassdefinitionconsistsoftheclasskeyword,anamefortheclass,andanoptionalbody.Thebodycontainspropertydefinitions(valsandvars)andfunctiondefinitions.

ThisexampledefinesaNoBodyclasswithoutabody,andclasseswithvalproperties:

//Summary2/ClassBodies.kt

packagesummary2

classNoBody

classSomeBody{

valname="JanetDoe"

}

classEveryBody{

valall=listOf(SomeBody(),

SomeBody(),SomeBody())

}

funmain(){

valnb=NoBody()

valsb=SomeBody()

valeb=EveryBody()

}

Tocreateaninstanceofaclass,putparenthesesafteritsname,alongwithargumentsifthosearerequired.

Propertieswithinclassbodiescanbeanytype.SomeBodycontainsapropertyoftypeString,andEveryBody’spropertyisaListholdingSomeBodyobjects.

Here’saclasswithmemberfunctions:

//Summary2/Temperature.kt

packagesummary2

importatomictest.eq

classTemperature{

varcurrent=0.0

varscale="f"

funsetFahrenheit(now:Double){

current=now

scale="f"

}

funsetCelsius(now:Double){

current=now

scale="c"

}

fungetFahrenheit():Double=

if(scale=="f")

current

else

current*9.0/5.0+32.0

fungetCelsius():Double=

if(scale=="c")

current

else

(current-32.0)*5.0/9.0

}

funmain(){

valtemp=Temperature()//[1]

temp.setFahrenheit(98.6)

temp.getFahrenheit()eq98.6

temp.getCelsius()eq37.0

temp.setCelsius(100.0)

temp.getFahrenheit()eq212.0

}

Thesememberfunctionsarejustlikethetop-levelfunctionswe’vedefinedoutsideofclasses,excepttheybelongtotheclassandhaveunqualifiedaccesstotheothermembersoftheclass,suchascurrentandscale.Memberfunctionscanalsocallothermemberfunctionsinthesameclasswithoutqualification.

[1]Althoughtempisaval,welatermodifytheTemperatureobject.Thevaldefinitionpreventsthereferencetempfrombeingreassignedtoanewobject,butitdoesnotrestrictthebehavioroftheobjectitself.

Thefollowingtwoclassesarethefoundationofatic-tac-toegame:

//Summary2/TicTacToe.kt

packagesummary2

importatomictest.eq

classCell{

varentry=''//[1]

funsetValue(e:Char):String=//[2]

if(entry==''&&

(e=='X'||e=='O')){

entry=e

"Successfulmove"

}else

"Invalidmove"

}

classGrid{

valcells=listOf(

listOf(Cell(),Cell(),Cell()),

listOf(Cell(),Cell(),Cell()),

listOf(Cell(),Cell(),Cell())

)

funplay(e:Char,x:Int,y:Int):String=

if(x!in0..2||y!in0..2)

"Invalidmove"

else

cells[x][y].setValue(e)//[3]

}

funmain(){

valgrid=Grid()

grid.play('X',1,1)eq"Successfulmove"

grid.play('X',1,1)eq"Invalidmove"

grid.play('O',1,3)eq"Invalidmove"

}

TheGridclassholdsaListcontainingthreeLists,eachcontainingthreeCells—amatrix.

[1]TheentrypropertyinCellisavarsoitcanbemodified.ThesinglequotesintheinitializationproduceaChartype,soallassignmentstoentrymustalsobeChars.[2]setValue()teststhattheCellisavailableandthatyou’vepassedthecorrectcharacter.ItreturnsaStringresulttoindicatesuccessorfailure.[3]play()checkstoseeifthexandyargumentsarewithinrange,thenindexesintothematrix,relyingonthetestsperformedbysetValue().

ConstructorsConstructorscreatenewobjects.Youpassinformationtoaconstructorusingitsparameterlist,placedinparenthesesdirectlyaftertheclassname.Aconstructorcallthuslookslikeafunctioncall,exceptthattheinitialletterofthenameiscapitalized(followingtheKotlinstyleguide).Theconstructorreturnsanobjectoftheclass:

//Summary2/WildAnimals.kt

packagesummary2

importatomictest.eq

classBadger(id:String,years:Int){

valname=id

valage=years

overridefuntoString():String{

return"Badger:$name,age:$age"

}

}

classSnake(

vartype:String,

varlength:Double

){

overridefuntoString():String{

return"Snake:$type,length:$length"

}

}

classMoose(

valage:Int,

valheight:Double

){

overridefuntoString():String{

return"Moose,age:$age,height:$height"

}

}

funmain(){

Badger("Bob",11)eq"Badger:Bob,age:11"

Snake("Garden",2.4)eq

"Snake:Garden,length:2.4"

Moose(16,7.2)eq

"Moose,age:16,height:7.2"

}

TheparametersidandyearsinBadgerareonlyavailableintheconstructorbody.Theconstructorbodyconsistsofthelinesofcodeotherthanfunctiondefinitions;inthiscase,thedefinitionsfornameandage.

Oftenyouwanttheconstructorparameterstobeavailableinpartsoftheclassotherthantheconstructorbody,butwithoutthetroubleofexplicitlydefiningnewidentifiersaswedidwithnameandage.Ifyoudefineyourparametersasvarsorvals,theybecomespropertiesandareaccessibleeverywhereintheclass.BothSnakeandMooseusethisapproach,andyoucanseethattheconstructorparametersarenowavailableinsidetheirrespectivetoString()functions.

Constructorparametersdeclaredwithvalcannotbechanged,butthosedeclaredwithvarcan.

WheneveryouuseanobjectinasituationthatexpectsaString,KotlinproducesaStringrepresentationofthatobjectbycallingitstoString()memberfunction.TodefineatoString(),youmustunderstandanewkeyword:override.Thisisnecessary(Kotlininsistsonit)becausetoString()isalreadydefined.overridetellsKotlinthatwedoactuallywanttoreplacethedefaulttoString()withourowndefinition.Theexplicitnessofoverridemakesthiscleartothereaderandhelpspreventmistakes.

NoticetheformattingofthemultilineparameterlistforSnakeandMoose—thisistherecommendedstandardwhenyouhavetoomanyparameterstofitononeline,forbothconstructorsandfunctions.

ConstrainingVisibilityKotlinprovidesaccessmodifierssimilartothoseavailableinotherlanguageslikeC++orJava.Theseallowcomponentcreatorstodecidewhatisavailabletotheclientprogrammer.Kotlin’saccessmodifiersincludethepublic,private,protected,andinternalkeywords.protectedisexplainedlater.

Anaccessmodifierlikepublicorprivateappearsbeforethedefinitionforaclass,functionorproperty.Eachaccessmodifieronlycontrolstheaccessforthatparticulardefinition.

Apublicdefinitionisavailabletoeveryone,inparticulartotheclientprogrammerwhousesthatcomponent.Thus,anychangestoapublicdefinitionwillimpactclientcode.

Ifyoudon’tprovideamodifier,yourdefinitionisautomaticallypublic.Forclarityincertaincases,programmersstillsometimesredundantlyspecifypublic.

Ifyoudefineaclass,top-levelfunction,orpropertyasprivate,itisavailableonlywithinthatfile:

//Summary2/Boxes.kt

packagesummary2

importatomictest.*

privatevarcount=0//[1]

privateclassBox(valdimension:Int){//[2]

funvolume()=

dimension*dimension*dimension

overridefuntoString()=

"Boxvolume:${volume()}"

}

privatefuncountBox(box:Box){//[3]

trace("$box")

count++

}

funcountBoxes(){

countBox(Box(4))

countBox(Box(5))

}

funmain(){

countBoxes()

trace("$countboxes")

traceeq"""

Boxvolume:64

Boxvolume:125

2boxes

"""

}

Youcanaccessprivateproperties([1]),classes([2]),andfunctions([3])onlyfromotherfunctionsandclassesintheBoxes.ktfile.Kotlinpreventsyoufromaccessingprivatetop-levelelementsfromanotherfile.

Classmemberscanbeprivate:

//Summary2/JetPack.kt

packagesummary2

importatomictest.eq

classJetPack(

privatevarfuel:Double//[1]

){

privatevarwarning=false

privatefunburn()=//[2]

if(fuel-1<=0){

fuel=0.0

warning=true

}else

fuel-=1

publicfunfly()=burn()//[3]

funcheck()=//[4]

if(warning)//[5]

"Warning"

else

"OK"

}

funmain(){

valjetPack=JetPack(3.0)

while(jetPack.check()!="Warning"){

jetPack.check()eq"OK"

jetPack.fly()

}

jetPack.check()eq"Warning"

}

[1]fuelandwarningarebothprivatepropertiesandcan’tbeusedbynon-membersofJetPack.[2]burn()isprivate,andthusonlyaccessibleinsideJetPack.[3]fly()andcheck()arepublicandcanbeusedeverywhere.[4]Noaccessmodifiermeanspublicvisibility.[5]Onlymembersofthesameclasscanaccessprivatemembers.

Becauseaprivatedefinitionisnotavailabletoeveryone,youcangenerallychangeitwithoutconcernfortheclientprogrammer.Asalibrarydesigner,you’lltypicallykeepeverythingasprivateaspossible,andexposeonlyfunctionsandclassesyouwantclientprogrammerstouse.Tolimitthesizeandcomplexityofexamplelistingsinthisbook,weonlyuseprivateinspecialcases.

Anyfunctionyou’recertainisonlyahelperfunctioncanbemadeprivate,toensureyoudon’taccidentallyuseitelsewhereandthusprohibityourselffromchangingorremovingthefunction.

Itcanbeusefultodividelargeprogramsintomodules.Amoduleisalogicallyindependentpartofacodebase.Aninternaldefinitionisaccessibleonlyinsidethemodulewhereitisdefined.Thewayyoudivideaprojectintomodulesdependsonthebuildsystem(suchasGradleorMaven)andisbeyondthescopeofthisbook.

Modulesareahigher-levelconcept,whilepackagesenablefiner-grainedstructuring.

ExceptionsConsidertoDouble(),whichconvertsaStringtoaDouble.WhathappensifyoucallitforaStringthatdoesn’ttranslateintoaDouble?

//Summary2/ToDoubleException.kt

funmain(){

//vali="$1.9".toDouble()

}

Uncommentingthelineinmain()producesanexception.Here,thefailinglineiscommentedsowedon’tstopthebook’sbuild(whichcheckswhethereachexamplecompilesandrunsasexpected).

Whenanexceptionisthrown,thecurrentpathofexecutionstops,andtheexceptionobjectejectsfromthecurrentcontext.Whenanexceptionisn’tcaught,theprogramabortsanddisplaysastacktracecontainingdetailedinformation.

Toavoiddisplayingexceptionsbycommentinganduncommentingcode,atomictest.capture()storestheexceptionandcomparesittowhatweexpect:

//Summary2/AtomicTestCapture.kt

importatomictest.*

funmain(){

capture{

"$1.9".toDouble()

}eq"NumberFormatException:"+

"""Forinputstring:"$1.9""""

}

capture()isdesignedspecificallyforthisbook,soyoucanseetheexceptionandknowthattheoutputhasbeencheckedbythebook’sbuildsystem.

Anotherstrategywhenyourfunctioncan’tsuccessfullyproducetheexpectedresultistoreturnnull.LaterinNullableTypeswediscusshownullaffectsthetypeoftheresultingexpression.

Tothrowanexception,usethethrowkeywordfollowedbytheexceptionyouwanttothrow,alongwithanyargumentsitmightneed.quadraticZeroes()inthefollowingexamplesolvesthequadraticequationthatdefinesaparabola:

ax2+bx+c=0

Thesolutionisthequadraticformula:

TheQuadraticFormula

Theexamplefindsthezeroesoftheparabola,wherethelinescrossthex-axis.Wethrowexceptionsfortwolimitations:

1. acannotbezero.2. Forzeroestoexist,b2-4accannotbenegative.

Ifzeroesexist,therearetwoofthem,sowecreatetheRootsclasstoholdthereturnvalues:

//Summary2/Quadratic.kt

packagesummary2

importkotlin.math.sqrt

importatomictest.*

classRoots(

valroot1:Double,

valroot2:Double

)

funquadraticZeroes(

a:Double,

b:Double,

c:Double

):Roots{

if(a==0.0)

throwIllegalArgumentException(

"aiszero")

valunderRadical=b*b-4*a*c

if(underRadical<0)

throwIllegalArgumentException(

"NegativeunderRadical:$underRadical")

valsquareRoot=sqrt(underRadical)

valroot1=(-b-squareRoot)/2*a

valroot2=(-b+squareRoot)/2*a

returnRoots(root1,root2)

}

funmain(){

capture{

quadraticZeroes(0.0,4.0,5.0)

}eq"IllegalArgumentException:"+

"aiszero"

capture{

quadraticZeroes(3.0,4.0,5.0)

}eq"IllegalArgumentException:"+

"NegativeunderRadical:-44.0"

valroots=quadraticZeroes(3.0,8.0,5.0)

roots.root1eq-15.0

roots.root2eq-9.0

}

HereweusethestandardexceptionclassIllegalArgumentException.Lateryou’lllearntodefineyourownexceptiontypesandtomakethemspecifictoyourcircumstances.Yourgoalistogeneratethemostusefulmessagespossible,tosimplifythesupportofyourapplicationinthefuture.

ListsListsareKotlin’sbasicsequentialcontainertype.Youcreatearead-onlylistusinglistOf()andamutablelistusingmutableListOf():

//Summary2/ReadonlyVsMutableList.kt

importatomictest.*

funmain(){

valints=listOf(5,13,9)

//ints.add(11)//'add()'notavailable

for(iinints){

if(i>10){

trace(i)

}

}

valchars=mutableListOf('a','b','c')

chars.add('d')//'add()'available

chars+='e'

trace(chars)

traceeq"""

13

[a,b,c,d,e]

"""

}

AbasicListisread-only,anddoesnotincludemodificationfunctions.Thus,themodificationfunctionadd()doesn’tworkwithints.

forloopsworkwellwithLists:for(iinints)meansigetseachvalueinints.

charsiscreatedasaMutableList;itcanbemodifiedusingfunctionslikeadd()orremove().Youcanalsouse+=and-=toaddorremoveelements.

Aread-onlyListisnotthesameasanimmutableList,whichcan’tbemodifiedatall.Here,weassignfirst,amutableList,tosecond,aread-onlyListreference.Theread-onlycharacteristicofseconddoesn’tpreventtheListfromchangingviafirst:

//Summary2/MultipleListReferences.kt

importatomictest.eq

funmain(){

valfirst=mutableListOf(1)

valsecond:List<Int>=first

secondeqlistOf(1)

first+=2

//secondseesthechange:

secondeqlistOf(1,2)

}

firstandsecondrefertothesameobjectinmemory.WemutatetheListviathefirstreference,andthenobservethischangeinthesecondreference.

Here’saListofStringscreatedbybreakingupatriple-quotedparagraph.Thisshowsthepowerofsomeofthestandardlibraryfunctions.Noticehowthosefunctionscanbechained:

//Summary2/ListOfStrings.kt

importatomictest.*

funmain(){

valwocky="""

Twasbrillig,andtheslithytoves

Didgyreandgimbleinthewabe:

Allmimsyweretheborogoves,

Andthemomerathsoutgrabe.

""".trim().split(Regex("\\W+"))

trace(wocky.take(5))

trace(wocky.slice(6..12))

trace(wocky.slice(6..18step2))

trace(wocky.sorted().takeLast(5))

trace(wocky.sorted().distinct().takeLast(5))

traceeq"""

[Twas,brillig,and,the,slithy]

[Did,gyre,and,gimble,in,the,wabe]

[Did,and,in,wabe,mimsy,the,And]

[the,the,toves,wabe,were]

[slithy,the,toves,wabe,were]

"""

}

trim()producesanewStringwiththeleadingandtrailingwhitespacecharacters(includingnewlines)removed.split()dividestheStringaccordingtoitsargument.InthiscaseweuseaRegexobject,whichcreatesaregularexpression—apatternthatmatchesthepartstosplit.\Wisaspecialpatternthatmeans“notawordcharacter,”and+means“oneormoreofthepreceeding.”Thussplit()willbreakatoneormorenon-wordcharacters,andsodividestheblockoftextintoitscomponentwords.

InaStringliteral,\precedesaspecialcharacterandproduces,forexample,anewlinecharacter(\n),oratab(\t).Toproduceanactual\intheresultingStringyouneedtwobackslashes:"\\".Thusallregularexpressionsrequireanextra\toinsertabackslash,unlessyouuseatriple-quotedString:"""\W+""".

take(n)producesanewListcontainingthefirstnelements.slice()producesanewListcontainingtheelementsselectedbyitsRangeargument,andthisRangecanincludeastep.

Notethenamesorted()insteadofsort().Whenyoucallsorted()itproducesasortedList,leavingtheoriginalListalone.sort()onlyworkswithaMutableList,andthatlistissortedinplace—theoriginalListismodified.

Asthenameimplies,takeLast(n)producesanewListofthelastnelements.Youcanseefromtheoutputthat“the”isduplicated.Thisiseliminatedbyaddingthedistinct()functiontothecallchain.

ParameterizedTypesTypeparametersallowustodescribecompoundtypes,mostcommonlycontainers.Inparticular,typeparametersspecifywhatacontainerholds.Here,wetellKotlinthatnumberscontainaListofInt,whilestringscontainaListofString:

//Summary2/ExplicitTyping.kt

packagesummary2

importatomictest.eq

funmain(){

valnumbers:List<Int>=listOf(1,2,3)

valstrings:List<String>=

listOf("one","two","three")

numberseq"[1,2,3]"

stringseq"[one,two,three]"

toCharList("seven")eq"[s,e,v,e,n]"

}

funtoCharList(s:String):List<Char>=

s.toList()

Forboththenumbersandstringsdefinitions,weaddcolonsandthetypedeclarationsList<Int>andList<String>.Theanglebracketsdenoteatypeparameter,allowingustosay,“thecontainerholds‘parameter’objects.”YoutypicallypronounceList<Int>as“ListofInt.”

Areturnvaluecanalsohaveatypeparameter,asseenintoCharList().Youcan’tjustsayitreturnsaList—Kotlincomplains,soyoumustgivethetypeparameteraswell.

VariableArgumentListsThevarargkeywordisshortforvariableargumentlist,andallowsafunctiontoacceptanynumberofarguments(includingzero)ofthespecifiedtype.ThevarargbecomesanArray,whichissimilartoaList:

//Summary2/VarArgs.kt

packagesummary2

importatomictest.*

funvarargs(s:String,varargints:Int){

for(iinints){

trace("$i")

}

trace(s)

}

funmain(){

varargs("primes",5,7,11,13,17,19,23)

traceeq"571113171923primes"

}

Afunctiondefinitionmayspecifyonlyoneparameterasvararg.Anyparameterinthelistcanbethevararg,butthefinaloneisgenerallysimplest.

YoucanpassanArrayofelementswhereveravarargisaccepted.TocreateanArray,usearrayOf()inthesamewayyouuselistOf().NotethatanArrayisalwaysmutable.ToconvertanArrayintoasequenceofarguments(notjustasingleelementoftypeArray),usethespreadoperator*:

//Summary2/ArraySpread.kt

importsummary2.varargs

importatomictest.trace

funmain(){

valarray=intArrayOf(4,5)//[1]

varargs("x",1,2,3,*array,6)//[2]

vallist=listOf(9,10,11)

varargs(

"y",7,8,*list.toIntArray())//[3]

traceeq"123456x7891011y"

}

IfyoupassanArrayofprimitivetypesasintheexampleabove,theArraycreationfunctionmustbespecificallytyped.If[1]usesarrayOf(4,5)insteadofintArrayOf(4,5),[2]producesanerror:inferredtypeisArray<Int>butIntArraywasexpected.

Thespreadoperatoronlyworkswitharrays.IfyouhaveaListtopassasasequenceofarguments,firstconvertittoanArrayandthenapplythespreadoperator,asin[3].BecausetheresultisanArrayofaprimitivetype,wemustusethespecificconversionfunctiontoIntArray().

SetsSetsarecollectionsthatallowonlyoneelementofeachvalue.ASetautomaticallypreventsduplicates.

//Summary2/ColorSet.kt

packagesummary2

importatomictest.eq

valcolors=

"YellowGreenGreenBlue"

.split(Regex("""\W+""")).sorted()//[1]

funmain(){

colorseq

listOf("Blue","Green","Green","Yellow")

valcolorSet=colors.toSet()//[2]

colorSeteq

setOf("Yellow","Green","Blue")

(colorSet+colorSet)eqcolorSet//[3]

valmSet=colorSet.toMutableSet()//[4]

mSet-="Blue"

mSet+="Red"//[5]

mSeteq

setOf("Yellow","Green","Red")

//Setmembership:

("Green"incolorSet)eqtrue//[6]

colorSet.contains("Red")eqfalse

}

[1]TheStringissplit()usingaregularexpressionasdescribedearlierforListOfStrings.kt.[2]Whencolorsiscopiedintotheread-onlySetcolorSet,oneofthetwo"Green"Stringsisremoved,becauseitisaduplicate.[3]HerewecreateanddisplayanewSetusingthe+operator.PlacingduplicateitemsintoaSetautomaticallyremovesthoseduplicates.[4]toMutableSet()producesanewMutableSetfromaread-onlySet.[5]ForaMutableSet,theoperators+=and-=addandremoveelements,astheydowithMutableLists.[6]TestforSetmembershipusinginorcontains()

Thenormalmathematicalsetoperationssuchasunion,intersection,difference,etc.,areallavailable.

MapsAMapconnectskeystovaluesandlooksupavaluewhengivenakey.YoucreateaMapbyprovidingkey-valuepairstomapOf().Usingto,weseparateeachkeyfromitsassociatedvalue:

//Summary2/ASCIIMap.kt

importatomictest.eq

funmain(){

valascii=mapOf(

"A"to65,

"B"to66,

"C"to67,

"I"to73,

"J"to74,

"K"to75

)

asciieq

"{A=65,B=66,C=67,I=73,J=74,K=75}"

ascii["B"]eq66//[1]

ascii.keyseq"[A,B,C,I,J,K]"

ascii.valueseq

"[65,66,67,73,74,75]"

varkv=""

for(entryinascii){//[2]

kv+="${entry.key}:${entry.value},"

}

kveq"A:65,B:66,C:67,I:73,J:74,K:75,"

kv=""

for((key,value)inascii)//[3]

kv+="$key:$value,"

kveq"A:65,B:66,C:67,I:73,J:74,K:75,"

valmutable=ascii.toMutableMap()//[4]

mutable.remove("I")

mutableeq

"{A=65,B=66,C=67,J=74,K=75}"

mutable.put("Z",90)

mutableeq

"{A=65,B=66,C=67,J=74,K=75,Z=90}"

mutable.clear()

mutable["A"]=100

mutableeq"{A=100}"

}

[1]Akey("B")isusedtolookupavaluewiththe[]operator.Youcanproduceallthekeysusingkeysandallthevaluesusingvalues.AccessingkeysproducesaSetbecauseallkeysinaMapmustalreadybeunique(otherwiseyou’dhaveambiguityduringalookup).[2]IteratingthroughaMapproduceskey-valuepairsasmapentries.[3]Youcanunpackkey-valuepairsasyouiterate.[4]YoucancreateaMutableMapfromaread-onlyMapusingtoMutableMap().Nowwecanperformoperationsthatmodifymutable,suchasremove(),put(),andclear().Squarebracketscanassignanewkey-valuepairintomutable.Youcanalsoaddapairbysayingmap+=keytovalue.

PropertyAccessorsAccessingthepropertyiappearsstraightforward:

//Summary2/PropertyReadWrite.kt

packagesummary2

importatomictest.eq

classHolder(vari:Int)

funmain(){

valholder=Holder(10)

holder.ieq10//Readthe'i'property

holder.i=20//Writetothe'i'property

}

However,Kotlincallsfunctionstoperformthereadandwriteoperations.Thedefaultbehaviorofthosefunctionsistoreadandwritethedatastoredini.Bycreatingpropertyaccessors,youchangetheactionsthatoccurduringreadingandwriting.

Theaccessorusedtofetchthevalueofapropertyiscalledagetter.Tocreateyourowngetter,defineget()immediatelyafterthepropertydeclaration.Theaccessorusedtomodifyamutablepropertyiscalledasetter.Tocreateyourownsetter,defineset()immediatelyafterthepropertydeclaration.Theorderofdefinitionofgettersandsettersisunimportant,andyoucandefineonewithouttheother.

Thepropertyaccessorsinthefollowingexampleimitatethedefaultimplementationswhiledisplayingadditionalinformationsoyoucanseethatthepropertyaccessorsareindeedcalledduringreadsandwrites.Weindenttheget()andset()functionstovisuallyassociatethemwiththeproperty,buttheactualassociationhappensbecausetheyaredefinedimmediatelyafterthatproperty:

//Summary2/GetterAndSetter.kt

packagesummary2

importatomictest.*

classGetterAndSetter{

vari:Int=0

get(){

trace("get()")

returnfield

}

set(value){

trace("set($value)")

field=value

}

}

funmain(){

valgs=GetterAndSetter()

gs.i=2

trace(gs.i)

traceeq"""

set(2)

get()

2

"""

}

Insidethegetterandsetter,thestoredvalueismanipulatedindirectlyusingthefieldkeyword,whichisonlyaccessiblewithinthesetwofunctions.Youcanalsocreateapropertythatdoesn’thaveafield,butsimplycallsthegettertoproducearesult.

Ifyoudeclareaprivateproperty,bothaccessorsbecomeprivate.Youcanmakethesetterprivateandthegetterpublic.Thismeansyoucanreadthepropertyoutsidetheclass,butonlychangeitsvalueinsidetheclass.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SECTIONIII:USABILITY

Computerlanguagesdiffernotsomuchinwhattheymakepossible,butinwhattheymakeeasy—LarryWall,inventorofthePerllanguage

ExtensionFunctions

Supposeyoudiscoveralibrarythatdoeseverythingyouneed…almost.Ifitonlyhadoneortwoadditionalmemberfunctions,itwouldsolveyourproblemperfectly.

Butit’snotyourcode—eitheryoudon’thaveaccesstothesourcecodeoryoudon’tcontrolit.You’dhavetorepeatyourmodificationseverytimeanewversioncameout.

Kotlin’sextensionfunctionseffectivelyaddmemberfunctionstoexistingclasses.Thetypeyouextendiscalledthereceiver.Todefineanextensionfunction,youprecedethefunctionnamewiththereceivertype:

funReceiverType.extensionFunction(){...}

ThisaddstwoextensionfunctionstotheStringclass:

//ExtensionFunctions/Quoting.kt

packageextensionfunctions

importatomictest.eq

funString.singleQuote()="'$this'"

funString.doubleQuote()="\"$this\""

funmain(){

"Hi".singleQuote()eq"'Hi'"

"Hi".doubleQuote()eq"\"Hi\""

}

Youcallextensionfunctionsasiftheyweremembersoftheclass.

Touseextensionsfromanotherpackage,youmustimportthem:

//ExtensionFunctions/Quote.kt

packageother

importatomictest.eq

importextensionfunctions.doubleQuote

importextensionfunctions.singleQuote

funmain(){

"Single".singleQuote()eq"'Single'"

"Double".doubleQuote()eq"\"Double\""

}

Youcanaccessmemberfunctionsorotherextensionsusingthethiskeyword.thiscanalsobeomittedinthesamewayitcanbeomittedinsideaclass,soyoudon’tneedexplicitqualification:

//ExtensionFunctions/StrangeQuote.kt

packageextensionfunctions

importatomictest.eq

//Applytwosetsofsinglequotes:

funString.strangeQuote()=

this.singleQuote().singleQuote()//[1]

funString.tooManyQuotes()=

doubleQuote().doubleQuote()//[2]

funmain(){

"Hi".strangeQuote()eq"''Hi''"

"Hi".tooManyQuotes()eq"\"\"Hi\"\""

}

[1]thisreferstotheStringreceiver.[2]Weomitthereceiverobject(this)ofthefirstdoubleQuote()functioncall.

Creatingextensionstoyourownclassescansometimesproducesimplercode:

//ExtensionFunctions/BookExtensions.kt

packageextensionfunctions

importatomictest.eq

classBook(valtitle:String)

funBook.categorize(category:String)=

"""title:"$title",category:$category"""

funmain(){

Book("Dracula").categorize("Vampire")eq

"""title:"Dracula",category:Vampire"""

}

Insidecategorize(),weaccessthetitlepropertywithoutexplicitqualification.

-

Notethatextensionfunctionscanonlyaccesspublicelementsofthetypebeingextended.Thus,extensionscanonlyperformthesameactionsasregularfunctions.YoucanrewriteBook.categorize(String)ascategorize(Book,String).Theonlyreasonforusinganextensionfunctionisthesyntax,butthissyntaxsugarispowerful.Tothecallingcode,extensionslookthesameas

memberfunctions,andIDEsshowextensionswhenlistingthefunctionsthatyoucancallforanobject.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Named&DefaultArguments

Youcanprovideargumentnamesduringafunctioncall.

Namedargumentsimprovecodereadability.Thisisespeciallytrueforlongandcomplexargumentlists—namedargumentscanbeclearenoughthatthereadercanunderstandafunctioncallwithoutlookingatthedocumentation.

Inthisexample,allparametersareInt.Namedargumentsclarifytheirmeaning:

//NamedAndDefaultArgs/NamedArguments.kt

packagecolor1

importatomictest.eq

funcolor(red:Int,green:Int,blue:Int)=

"($red,$green,$blue)"

funmain(){

color(1,2,3)eq"(1,2,3)"//[1]

color(

red=76,//[2]

green=89,

blue=0

)eq"(76,89,0)"

color(52,34,blue=0)eq//[3]

"(52,34,0)"

}

[1]Thisdoesn’ttellyoumuch.You’llhavetolookatthedocumentationtoknowwhattheargumentsmean.[2]Themeaningofeveryargumentisclear.[3]Youaren’trequiredtonameallarguments.

Namedargumentsallowyoutochangetheorderofthecolors.Here,wespecifybluefirst:

//NamedAndDefaultArgs/ArgumentOrder.kt

importcolor1.color

importatomictest.eq

funmain(){

color(blue=0,red=99,green=52)eq

"(99,52,0)"

color(red=255,255,0)eq

"(255,255,0)"

}

Youcanmixnamedandregular(positional)arguments.Ifyouchangeargumentorder,youshouldusenamedargumentsthroughoutthecall—notonlyforreadability,butthecompileroftenneedstobetoldwheretheargumentsare.

Namedargumentsareevenmoreusefulwhencombinedwithdefaultarguments,whicharedefaultvaluesforarguments,specifiedinthefunctiondefinition:

//NamedAndDefaultArgs/Color2.kt

packagecolor2

importatomictest.eq

funcolor(

red:Int=0,

green:Int=0,

blue:Int=0,

)="($red,$green,$blue)"

funmain(){

color(139)eq"(139,0,0)"

color(blue=139)eq"(0,0,139)"

color(255,165)eq"(255,165,0)"

color(red=128,blue=128)eq

"(128,0,128)"

}

Anyargumentyoudon’tprovidegetsitsdefaultvalue,soyouonlyneedtoprovideargumentsthatdifferfromthedefaults.Ifyouhavealongargumentlist,thissimplifiestheresultingcode,makingiteasiertowriteand—moreimportantly—toread.

Thisexamplealsousesatrailingcommainthedefinitionforcolor().Thetrailingcommaistheextracommaafterthelastparameter(blue).Thisisusefulwhenyourparametersorvaluesarewrittenonmultiplelines.Withatrailingcomma,youcanaddnewitemsandchangetheirorderwithoutaddingorremovingcommas.

Namedanddefaultarguments(aswellastrailingcommas)alsoworkforconstructors:

//NamedAndDefaultArgs/Color3.kt

packagecolor3

importatomictest.eq

classColor(

valred:Int=0,

valgreen:Int=0,

valblue:Int=0,

){

overridefuntoString()=

"($red,$green,$blue)"

}

funmain(){

Color(red=77).toString()eq"(77,0,0)"

}

joinToString()isastandardlibraryfunctionthatusesdefaultarguments.Itcombinesthecontentsofaniterable(alist,setorrange)intoaString.Youcanspecifyaseparator,aprefixelementandapostfixelement:

//NamedAndDefaultArgs/CreateString.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,)

list.toString()eq"[1,2,3]"

list.joinToString()eq"1,2,3"

list.joinToString(prefix="(",

postfix=")")eq"(1,2,3)"

list.joinToString(separator=":")eq

"1:2:3"

}

ThedefaulttoString()foraListreturnsthecontentsinsquarebrackets,whichmightnotbewhatyouwant.ThedefaultvaluesforjoinToString()sparametersareacommaforseparatorandemptyStringsforprefixandpostfix.Intheaboveexample,weusenamedanddefaultargumentstospecifyonlytheargumentswewanttochange.

Theinitializerforlistincludesatrailingcomma.Normallyyou’llonlyuseatrailingcommawheneachelementisonitsownline.

Ifyouuseanobjectasadefaultargument,anewinstanceofthatobjectiscreatedforeachinvocation:

//NamedAndDefaultArgs/Evaluation.kt

packagenamedanddefault

classDefaultArg

funh(d:DefaultArg=DefaultArg())=

println(d)

funmain(){

h()

h()

}

/*Sampleoutput:

DefaultArg@28d93b30

DefaultArg@1b6d3586

*/

TheaddressesoftheDefaultobjectsaredifferentforthetwocallstoh(),showingthattherearetwodistinctobjects.

Specifyargumentnameswhentheyimprovereadability.ComparethefollowingtwocallstojoinToString():

//NamedAndDefaultArgs/CreateString2.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3)

list.joinToString(".","","!")eq

"1.2.3!"

list.joinToString(separator=".",

postfix="!")eq"1.2.3!"

}

It’shardtoguesswhether"."or""isaseparatorunlessyoumemorizetheparameterorder,whichisimpractical.

Asanotherexampleofdefaultarguments,trimMargin()isastandardlibraryfunctionthatformatsmulti-lineStrings.ItusesamarginprefixStringtoestablishthebeginningofeachline.trimMargin()trimsleadingwhitespacecharactersfollowedbythemarginprefixfromeverylineofthesourceString.Itremovesthefirstandlastlinesiftheyareblank:

//NamedAndDefaultArgs/TrimMargin.kt

importatomictest.eq

funmain(){

valpoem="""

|->LastnightIsawuponthestair

|->Alittlemanwhowasn'tthere

|->Hewasn'tthereagaintoday

|->Oh,howIwishhe'dgoaway."""

poem.trimMargin()eq

"""->LastnightIsawuponthestair

->Alittlemanwhowasn'tthere

->Hewasn'tthereagaintoday

->Oh,howIwishhe'dgoaway."""

poem.trimMargin(marginPrefix="|->")eq

"""LastnightIsawuponthestair

Alittlemanwhowasn'tthere

Hewasn'tthereagaintoday

Oh,howIwishhe'dgoaway."""

}

The|(“pipe”)isthedefaultargumentforthemarginprefix,andyoucanreplaceitwithaStringofyourchoosing.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Overloading

Languageswithoutsupportfordefaultargumentsoftenuseoverloadingtoimitatethatfeature.

Thetermoverloadreferstothenameofafunction:Youusethesamename(“overload”thatname)fordifferentfunctionsaslongastheparameterlistsdiffer.Here,weoverloadthememberfunctionf():

//Overloading/Overloading.kt

packageoverloading

importatomictest.eq

classOverloading{

funf()=0

funf(n:Int)=n+2

}

funmain(){

valo=Overloading()

o.f()eq0

o.f(11)eq13

}

InOverloading,youseetwofunctionswiththesamename,f().Thefunction’ssignatureconsistsofthename,parameterlistandreturntype.Kotlindistinguishesonefunctionfromanotherbycomparingsignatures.Whenoverloadingfunctions,theparameterlistsmustbeunique—youcannotoverloadonreturntypes.

Thecallsshowthattheyareindeeddifferentfunctions.Afunctionsignaturealsoincludesinformationabouttheenclosingclass(orthereceivertype,ifit’sanextensionfunction).

Notethatifaclassalreadyhasamemberfunctionwiththesamesignatureasanextensionfunction,Kotlinprefersthememberfunction.However,youcanoverloadthememberfunctionwithanextensionfunction:

//Overloading/MemberVsExtension.kt

packageoverloading

importatomictest.eq

classMy{

funfoo()=0

}

funMy.foo()=1//[1]

funMy.foo(i:Int)=i+2//[2]

funmain(){

My().foo()eq0

My().foo(1)eq3

}

[1]It’ssenselesstodeclareanextensionthatduplicatesamember,becauseitcanneverbecalled.[2]Youcanoverloadamemberfunctionusinganextensionfunctionbyprovidingadifferentparameterlist.

Don’tuseoverloadingtoimitatedefaultarguments.Thatis,don’tdothis:

//Overloading/WithoutDefaultArguments.kt

packagewithoutdefaultarguments

importatomictest.eq

funf(n:Int)=n+373

funf()=f(0)

funmain(){

f()eq373

}

Thefunctionwithoutparametersjustcallsthefirstfunction.Thetwofunctionscanbereplacedwithasinglefunctionbyusingadefaultargument:

//Overloading/WithDefaultArguments.kt

packagewithdefaultarguments

importatomictest.eq

funf(n:Int=0)=n+373

funmain(){

f()eq373

}

Inbothexamplesyoucancallthefunctioneitherwithoutanargumentorbypassinganintegervalue.PrefertheforminWithDefaultArguments.kt.

Whenusingoverloadedfunctionstogetherwithdefaultarguments,callingtheoverloadedfunctionsearchesforthe“closest”match.Inthefollowingexample,thefoo()callinmain()doesnotcallthefirstversionofthefunctionusingitsdefaultargumentof99,butinsteadcallsthesecondversion,theonewithoutparameters:

//Overloading/OverloadedVsDefaultArg.kt

packageoverloadingvsdefaultargs

importatomictest.*

funfoo(n:Int=99)=trace("foo-1-$n")

funfoo(){

trace("foo-2")

foo(14)

}

funmain(){

foo()

traceeq"""

foo-2

foo-1-14

"""

}

Youcanneverutilizethedefaultargumentof99,becausefoo()alwayscallsthesecondversionoff().

Whyisoverloadinguseful?Itallowsyoutoexpress“variationsonatheme”moreclearlythanifyouwereforcedtousedifferentfunctionnames.Supposeyouwantadditionfunctions:

//Overloading/OverloadingAdd.kt

packageoverloading

importatomictest.eq

funaddInt(i:Int,j:Int)=i+j

funaddDouble(i:Double,j:Double)=i+j

funadd(i:Int,j:Int)=i+j

funadd(i:Double,j:Double)=i+j

funmain(){

addInt(5,6)eqadd(5,6)

addDouble(56.23,44.77)eq

add(56.23,44.77)

}

addInt()takestwoIntsandreturnsanInt,whileaddDouble()takestwoDoublesandreturnsaDouble.Withoutoverloading,youcan’tjustnametheoperationadd(),soprogrammerstypicallyconflatewhatwithhowtoproduceuniquenames(youcanalsocreateuniquenamesusingrandomcharactersbutthetypicalpatternistousemeaningfulinformationlikeparametertypes).Incontrast,theoverloadedadd()ismuchclearer.

-

Thelackofoverloadinginalanguageisnotaterriblehardship,butthefeatureprovidesvaluablesimplification,producingmorereadablecode.Withoverloading,youjustsaywhat,whichraisesthelevelofabstractionandputslessmentalloadonthereader.Ifyouwanttoknowhow,lookattheparameters.Noticealsothatoverloadingreducesredundancy:IfwemustsayaddInt()andaddDouble(),thenweessentiallyrepeattheparameterinformationinthefunctionname.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

whenExpressions

Alargepartofcomputerprogrammingisperforminganactionwhenapatternmatches.

Anythingthatsimplifiesthistaskisaboonforprogrammers.Ifyouhavemorethantwoorthreechoicestomake,whenexpressionsaremuchnicerthanifexpressions.

Awhenexpressioncomparesavalueagainstaselectionofpossibilities.Itbeginswiththekeywordwhenandtheparenthesizedvaluetocompare.Thisisfollowedbyabodycontainingasetofpossiblematchesandtheirassociatedactions.Eachmatchisanexpressionfollowedbyarightarrow->.Thearrowisthetwoseparatecharacters-and>withnowhitespacebetweenthem.Theexpressionisevaluatedandcomparedtothetargetvalue.Ifitmatches,theexpressiontotherightofthe->producestheresultofthewhenexpression.

ordinal()inthefollowingexamplebuildstheGermanwordforanordinalnumberbasedonawordforthecardinalnumber.Itmatchesanintegertoafixedsetofnumberstocheckwhetheritappliestoageneralruleorisanexception(whichhappenspainfullyofteninGerman):

//WhenExpressions/GermanOrdinals.kt

packagewhenexpressions

importatomictest.eq

valnumbers=mapOf(

1to"eins",2to"zwei",3to"drei",

4to"vier",5to"fuenf",6to"sechs",

7to"sieben",8to"acht",9to"neun",

10to"zehn",11to"elf",12to"zwoelf",

13to"dreizehn",14to"vierzehn",

15to"fuenfzehn",16to"sechzehn",

17to"siebzehn",18to"achtzehn",

19to"neunzehn",20to"zwanzig"

)

funordinal(i:Int):String=

when(i){//[1]

1->"erste"//[2]

3->"dritte"

7->"siebte"

8->"achte"

20->"zwanzigste"

else->numbers.getValue(i)+"te"//[3]

}

funmain(){

ordinal(2)eq"zweite"

ordinal(3)eq"dritte"

ordinal(11)eq"elfte"

}

[1]Thewhenexpressioncomparesitothematchexpressionsinthebody.[2]Thefirstsuccessfulmatchcompletesexecutionofthewhenexpression—here,aStringisproducedwhichbecomesthereturnvalueofordinal().[3]Theelsekeywordprovidesa“fallthrough”whentherearenomatches.Theelsecasealwaysappearslastinthematchlist.Whenwetestagainst2,itdoesn’tmatch1,3,7,8or20,andsofallsthroughtotheelsecase.

Ifyouforgettheelsebranchintheexampleabove,thecompile-timeerroris:‘when’expressionmustbeexhaustive,addnecessary‘else’branch.Ifyoutreatawhenexpressionasastatement—thatis,youdon’tusetheresultofthewhen—youcanomittheelsebranch.Unmatchedvaluesarethenjustignored.

Inthefollowingexample,CoordinatesreportschangestoitspropertiesusingPropertyAccessors.Thewhenexpressionprocesseseachitemfrominputs:

//WhenExpressions/AnalyzeInput.kt

packagewhenexpressions

importatomictest.*

classCoordinates{

varx:Int=0

set(value){

trace("xgets$value")

field=value

}

vary:Int=0

set(value){

trace("ygets$value")

field=value

}

overridefuntoString()="($x,$y)"

}

funprocessInputs(inputs:List<String>){

valcoordinates=Coordinates()

for(inputininputs){

when(input){//[1]

"up","u"->coordinates.y--//[2]

"down","d"->coordinates.y++

"left","l"->coordinates.x--

"right","r"->{//[3]

trace("Movingright")

coordinates.x++

}

"nowhere"->{}//[4]

"exit"->return//[5]

else->trace("badinput:$input")

}

}

}

funmain(){

processInputs(listOf("up","d","nowhere",

"left","right","exit","r"))

traceeq"""

ygets-1

ygets0

xgets-1

Movingright

xgets0

"""

}

[1]inputismatchedagainstthedifferentoptions.[2]Youcanlistseveralvaluesinonebranchusingcommas.Here,iftheuserenterseither“up”or“u”weinterpretitasamoveup.[3]Multipleactionswithinabranchmustbeinablockbody.[4]“Doingnothing”isexpressedwithanemptyblock.[5]Returningfromtheouterfunctionisavalidactionwithinabranch.Inthiscase,thereturnterminatesthecalltoprocessInputs().

Anyexpressioncanbeanargumentforwhen,andthematchescanbeanyvalues(notjustconstants):

//WhenExpressions/MatchingAgainstVals.kt

importatomictest.*

funmain(){

valyes="A"

valno="B"

for(choiceinlistOf(yes,no,yes)){

when(choice){

yes->trace("Hooray!")

no->trace("Toobad!")

}

//Thesamelogicusing'if':

if(choice==yes)trace("Hooray!")

elseif(choice==no)trace("Toobad!")

}

traceeq"""

Hooray!

Hooray!

Toobad!

Toobad!

Hooray!

Hooray!

"""

}

whenexpressionscanoverlapthefunctionalityofifexpressions.whenismoreflexible,sopreferitoverifwhenthere’sachoice.

WecanmatchaSetofvaluesagainstanotherSetofvalues:

//WhenExpressions/MixColors.kt

packagewhenexpressions

importatomictest.eq

funmixColors(first:String,second:String)=

when(setOf(first,second)){

setOf("red","blue")->"purple"

setOf("red","yellow")->"orange"

setOf("blue","yellow")->"green"

else->"unknown"

}

funmain(){

mixColors("red","blue")eq"purple"

mixColors("blue","red")eq"purple"

mixColors("blue","purple")eq"unknown"

}

InsidemixColors()weuseaSetasawhenargumentandcompareitwithdifferentSets.WeuseaSetbecausetheorderofelementsisunimportant—weneedthesameresultwhenwemix“red”and“blue”aswhenwemix“blue”and“red.”

whenhasaspecialformthattakesnoargument.OmittingtheargumentmeansthebranchescancheckdifferentBooleanconditions.YoucanuseanyBooleanexpressionasabranchcondition.Asanexample,werewritebmiMetric()introducedinNumberTypes,firstshowingtheoriginalsolution,thenusingwheninsteadofif:

//WhenExpressions/BmiWhen.kt

packagewhenexpressions

importatomictest.eq

funbmiMetricOld(

kg:Double,

heightM:Double

):String{

valbmi=kg/(heightM*heightM)

returnif(bmi<18.5)"Underweight"

elseif(bmi<25)"Normalweight"

else"Overweight"

}

funbmiMetricWithWhen(

kg:Double,

heightM:Double

):String{

valbmi=kg/(heightM*heightM)

returnwhen{

bmi<18.5->"Underweight"

bmi<25->"Normalweight"

else->"Overweight"

}

}

funmain(){

bmiMetricOld(72.57,1.727)eq

bmiMetricWithWhen(72.57,1.727)

}

Thesolutionusingwhenisamoreelegantwaytochoosebetweenseveraloptions.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Enumerations

Anenumerationisacollectionofnames.

Kotlin’senumclassisaconvenientwaytomanagethesenames:

//Enumerations/Level.kt

packageenumerations

importatomictest.eq

enumclassLevel{

Overflow,High,Medium,Low,Empty

}

funmain(){

Level.Mediumeq"Medium"

}

CreatinganenumgeneratestoString()sfortheenumnames.

Youmustqualifyeachreferencetoanenumerationname,aswithLevel.Mediuminmain().Youcaneliminatethisqualificationusinganimporttobringallnamesfromtheenumerationintothecurrentnamespace(namespaceskeepnamesfromcollidingwitheachother):

//Enumerations/EnumImport.kt

importatomictest.eq

importenumerations.Level.*//[1]

funmain(){

Overfloweq"Overflow"

Higheq"High"

}

[1]The*importsallthenamesinsidetheLevelenumeration,butdoesnotimportthenameLevel.

Youcanimportenumvaluesintothesamefilewheretheenumclassisdefined:

//Enumerations/RecursiveEnumImport.kt

packageenumerations

importatomictest.eq

importenumerations.Size.*//[1]

enumclassSize{

Tiny,Small,Medium,Large,Huge,Gigantic

}

funmain(){

Giganticeq"Gigantic"//[2]

Size.values().toList()eq//[3]

listOf(Tiny,Small,Medium,

Large,Huge,Gigantic)

Tiny.ordinaleq0//[4]

Huge.ordinaleq4

}

[1]WeimportthevaluesofSizebeforetheSizedefinitionappearsinthefile.[2]Aftertheimport,wenolongerneedtoqualifyaccesstotheenumerationnames.[3]Youcaniteratethroughtheenumerationnamesusingvalues().values()returnsanArray,sowecalltoList()toconvertittoaList.[4]Thefirstdeclaredconstantofanenumhasanordinalvalueofzero.Eachsubsequentconstantreceivesthenextintegervalue.

Youcanperformdifferentactionsfordifferentenumentriesusingawhenexpression.HereweimportthenameLevel,aswellasallitsentries:

//Enumerations/CheckingOptions.kt

packagecheckingoptions

importatomictest.*

importenumerations.Level

importenumerations.Level.*

funcheckLevel(level:Level){

when(level){

Overflow->trace(">>>Overflow!")

Empty->trace("Alert:Empty")

else->trace("Level$levelOK")

}

}

funmain(){

checkLevel(Empty)

checkLevel(Low)

checkLevel(Overflow)

traceeq"""

Alert:Empty

LevelLowOK

>>>Overflow!

"""

}

checkLevel()performsspecificactionsforonlytwooftheconstants,whilebehavingordinarily(theelsecase)forallotheroptions.

Anenumerationisaspecialkindofclasswithafixednumberofinstances,alllistedwithintheclassbody.Inotherways,anenumclassbehaveslikearegularclass,soyoucandefinememberpropertiesandfunctions.Ifyouincludeadditionalelements,youmustaddasemicolonafterthelastenumerationvalue:

//Enumerations/Direction.kt

packageenumerations

importatomictest.eq

importenumerations.Direction.*

enumclassDirection(valnotation:String){

North("N"),South("S"),

East("E"),West("W");//Semicolonrequired

valopposite:Direction

get()=when(this){

North->South

South->North

West->East

East->West

}

}

funmain(){

North.notationeq"N"

North.oppositeeqSouth

West.opposite.oppositeeqWest

North.opposite.notationeq"S"

}

TheDirectionclasscontainsanotationpropertyholdingadifferentvalueforeachinstance.Youpassvaluesforthenotationconstructorparameterinparentheses(North("N")),justlikeyouconstructaninstanceofaregularclass.

Thegetterfortheoppositepropertydynamicallycomputestheresultwhenitisaccessed.

Noticethatwhendoesn’trequireanelsebranchinthisexample,becauseallpossibleenumentriesarecovered.

-

Enumerationscanmakeyourcodemorereadable,whichisalwaysdesirable.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

DataClasses

Kotlinreducesrepetitivecoding.

Theclassmechanismperformsafairamountofworkforyou.However,creatingclassesthatprimarilyholddatastillrequiresasignificantamountofrepetitivecode.Whenyouneedaclassthat’sessentiallyadataholder,dataclassessimplifyyourcodeandperformcommontasks.

Youdefineadataclassusingthedatakeyword,whichtellsKotlintogenerateadditionalfunctionality.Eachconstructorparametermustbeprecededbyvarorval:

//DataClasses/Simple.kt

packagedataclasses

importatomictest.eq

dataclassSimple(

valarg1:String,

vararg2:Int

)

funmain(){

vals1=Simple("Hi",29)

vals2=Simple("Hi",29)

s1eq"Simple(arg1=Hi,arg2=29)"

s1eqs2

}

Thisexamplerevealstwofeaturesofdataclasses:

1. TheStringproducedbys1isdifferentthanwhatweusuallysee;itincludestheparameternamesandvaluesofthedataheldbytheobject.dataclassesdisplayobjectsinanice,readableformatwithoutrequiringanyadditionalcode.

2. Ifyoucreatetwoinstancesofthesamedataclasscontainingidenticaldata(equalvaluesforproperties),youprobablyalsowantthosetwoinstancestobeequal.Toachievethatbehaviorforaregularclass,youmustdefineaspecialfunctionequals()tocompareinstances.Indataclasses,thisfunctionisautomaticallygenerated;itcomparesthevaluesofallpropertiesspecifiedasconstructorparameters.

Here’sanordinaryclassPersonandadataclassContact:

//DataClasses/DataClasses.kt

packagedataclasses

importatomictest.*

classPerson(valname:String)

dataclassContact(

valname:String,

valnumber:String

)

funmain(){

//Theseseemthesame,butthey'renot:

Person("Cleo")neqPerson("Cleo")

//Adataclassdefinesequalitysensibly:

Contact("Miffy","1-234-567890")eq

Contact("Miffy","1-234-567890")

}

/*Sampleoutput:

dataclasses.Person@54bedef2

Contact(name=Miffy,number=1-234-567890)

*/

BecausethePersonclassisdefinedwithoutthedatakeyword,twoinstancescontainingthesamenamearenotequal.Fortunately,creatingContactasadataclassproducesareasonableresult.

Noticethedifferencebetweenthedisplayformatofthedataclass,andPerson,whichjustshowsdefaultobjectinformation.

Anotherusefulfunctiongeneratedforeverydataclassiscopy(),whichcreatesanewobjectcontainingthedatafromthecurrentobject.However,italsoallowsyoutochangeselectedvaluesintheprocess:

//DataClasses/CopyDataClass.kt

packagedataclasses

importatomictest.eq

dataclassDetailedContact(

valname:String,

valsurname:String,

valnumber:String,

valaddress:String

)

funmain(){

valcontact=DetailedContact(

"Miffy",

"Miller",

"1-234-567890",

"1600AmphitheatreParkway")

valnewContact=contact.copy(

number="098-765-4321",

address="Brandschenkestrasse110")

newContacteqDetailedContact(

"Miffy",

"Miller",

"098-765-4321",

"Brandschenkestrasse110")

}

Theparameternamesforcopy()areidenticaltotheconstructorparameters.Allargumentshavedefaultvaluesthatareequaltothecurrentvalues,soyouprovideonlytheonesyouwanttoreplace.

HashMapandHashSetCreatingadataclassalsogeneratesanappropriatehashfunctionsothatobjectscanbeusedaskeysinHashMapsandHashSets:

//DataClasses/HashCode.kt

packagedataclasses

importatomictest.eq

dataclassKey(valname:String,valid:Int)

funmain(){

valkorvo:Key=Key("Korvo",19)

korvo.hashCode()eq-2041757108

valmap=HashMap<Key,String>()

map[korvo]="Alien"

map[korvo]eq"Alien"

valset=HashSet<Key>()

set.add(korvo)

set.contains(korvo)eqtrue

}

hashCode()isusedinconjunctionwithequals()torapidlylookupaKeyinaHashMaporaHashSet.CreatingacorrecthashCode()byhandistrickyanderror-prone,soitisquitebeneficialtohavethedataclassdoitforyou.OperatorOverloadingcoversequals()andhashCode()inmoredetail.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

DestructuringDeclarations

Supposeyouwanttoreturnmorethanoneitemfromafunction,suchasaresultalongwithsomeinformationaboutthatresult.

ThePairclass,whichispartofthestandardlibrary,allowsyoutoreturntwovalues:

//Destructuring/Pairs.kt

packagedestructuring

importatomictest.eq

funcompute(input:Int):Pair<Int,String>=

if(input>5)

Pair(input*2,"High")

else

Pair(input*2,"Low")

funmain(){

compute(7)eqPair(14,"High")

compute(4)eqPair(8,"Low")

valresult=compute(5)

result.firsteq10

result.secondeq"Low"

}

Wespecifythereturntypeofcompute()asPair<Int,String>.APairisaparameterizedtype,likeListorSet.

Returningmultiplevaluesishelpful,butwe’dalsolikeaconvenientwaytounpacktheresults.Asshownabove,youcanaccessthecomponentsofaPairusingitsfirstandsecondproperties,butyoucanalsodeclareandinitializeseveralidentifierssimultaneouslyusingadestructuringdeclaration:

val(a,b,c)=composedValue

Thisdestructuresacomposedvalueandpositionallyassignsitscomponents.Thesyntaxdiffersfromdefiningasingleidentifier—fordestructuring,youputthenamesoftheidentifiersinsideparentheses.

Here’sadestructuringdeclarationforthePairreturnedfromcompute():

//Destructuring/PairDestructuring.kt

importdestructuring.compute

importatomictest.eq

funmain(){

val(value,description)=compute(7)

valueeq14

descriptioneq"High"

}

TheTripleclasscombinesthreevalues,butthat’sasfarasitgoes.Thisisintentional:ifyouneedtostoremorevalues,orifyoufindyourselfusingmanyPairsorTriples,considercreatingspecialclassesinstead.

dataClassesautomaticallyallowdestructuringdeclarations:

//Destructuring/Computation.kt

packagedestructuring

importatomictest.eq

dataclassComputation(

valdata:Int,

valinfo:String

)

funevaluate(input:Int)=

if(input>5)

Computation(input*2,"High")

else

Computation(input*2,"Low")

funmain(){

val(value,description)=evaluate(7)

valueeq14

descriptioneq"High"

}

It’sclearertoreturnaComputationinsteadofaPair<Int,String>.Choosingagoodnamefortheresultisalmostasimportantaschoosingagoodself-explanatorynameforthefunctionitself.AddingorremovingComputationinformationissimplerifit’saseparateclassratherthanaPair.

Whenyouunpackaninstanceofadataclass,youmustassignvaluestothenewidentifiersinthesameorderyoudefinethepropertiesintheclass:

//Destructuring/Tuple.kt

packagedestructuring

importatomictest.eq

dataclassTuple(

vali:Int,

vald:Double,

vals:String,

valb:Boolean,

vall:List<Int>

)

funmain(){

valtuple=Tuple(

1,3.14,"Mouse",false,listOf())

val(i,d,s,b,l)=tuple

ieq1

deq3.14

seq"Mouse"

beqfalse

leqlistOf()

val(_,_,animal)=tuple//[1]

animaleq"Mouse"

}

[1]Ifyoudon’tneedsomeoftheidentifiers,youmayuseunderscoresinsteadoftheirnames,oromitthemcompletelyiftheyappearattheend.Here,theunpackedvalues1and3.14arediscardedusingunderscores,"Mouse"iscapturedintoanimal,andfalseandtheemptyListarediscardedbecausetheyareattheendofthelist.

Thepropertiesofadataclassareassignedbyorder,notbyname.Ifyoudestructureanobjectandlateraddapropertyanywhereexcepttheendofitsdataclass,thatnewpropertywillbedestructuredontopofyourpreviousidentifier,producingunexpectedresults(seeExercise3).Ifyourcustomdataclasshaspropertieswithidenticaltypes,thecompilercan’tdetectmisusesoyoumaywanttoavoiddestructuringit.DestructuringlibrarydataclasseslikePairorTripleissafe,becausetheydon’tchange.

Usingaforloop,youcaniterateoveraMaporaListofpairs(orotherdataclasses)anddestructureeachelement:

//Destructuring/ForLoop.kt

importatomictest.eq

funmain(){

varresult=""

valmap=mapOf(1to"one",2to"two")

for((key,value)inmap){

result+="$key=$value,"

}

resulteq"1=one,2=two,"

result=""

vallistOfPairs=

listOf(Pair(1,"one"),Pair(2,"two"))

for((i,s)inlistOfPairs){

result+="($i,$s),"

}

resulteq"(1,one),(2,two),"

}

withIndex()isastandardlibraryextensionfunctionforList.ItreturnsacollectionofIndexedValues,whichcanbedestructured:

//Destructuring/LoopWithIndex.kt

importatomictest.trace

funmain(){

vallist=listOf('a','b','c')

for((index,value)inlist.withIndex()){

trace("$index:$value")

}

traceeq"0:a1:b2:c"

}

Destructuringdeclarationsareonlyallowedforlocalvarsandvals,andcannotbeusedtocreateclassproperties.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

NullableTypes

Considerafunctionthatsometimesproduces“noresult.”Whenthishappens,thefunctiondoesn’tproduceanerrorperse.Nothingwentwrong,there’sjust“noanswer.”

AgoodexampleisretrievingavaluefromaMap.IftheMapdoesn’tcontainavalueforagivenkey,itcan’tgiveyouananswerandreturnsanullreferencetoindicate“novalue”:

//NullableTypes/NullInMaps.kt

importatomictest.eq

funmain(){

valmap=mapOf(0to"yes",1to"no")

map[2]eqnull

}

LanguageslikeJavaallowaresulttobeeithernullorameaningfulvalue.Unfortunately,ifyoutreatnullthesamewayyoutreatameaningfulvalue,yougetadramaticfailure(InJava,thisproducesaNullPointerException;inamoreprimitivelanguagelikeC,anullpointercancrashtheprocessoreventheoperatingsystemormachine).Thecreatorofthenullreference,TonyHoare,referstoitas“mybillion-dollarmistake”(althoughithasarguablycostmuchmorethanthat).

Onepossiblesolutiontothisproblemisforalanguagetoneverallownullsinthefirstplace,andinsteadintroduceaspecial“novalue”indicator.Kotlinmighthavedonethis,exceptthatitmustinteractwithJava,andJavausesnulls.

Kotlin’ssolutionisarguablythebestcompromise:typesdefaulttonon-nullable.However,ifsomethingcanproduceanullresult,youmustappendaquestionmarktothetypenametoexplicitlytagthatresultasnullable:

//NullableTypes/NullableTypes.kt

importatomictest.eq

funmain(){

vals1="abc"//[1]

//Compile-timeerror:

//vals2:String=null//[2]

//Nullabledefinitions:

vals3:String?=null//[3]

vals4:String?=s1//[4]

//Compile-timeerror:

//vals5:String=s4//[5]

vals6=s4//[6]

s1eq"abc"

s3eqnull

s4eq"abc"

s6eq"abc"

}

[1]s1can’tcontainanullreference.Allthevarsandvalswe’vecreatedinthebooksofarareautomaticallynon-nullable.[2]Theerrormessageis:nullcannotbeavalueofanon-nulltypeString.[3]Todefineanidentifierthatcancontainanullreference,youputa?attheendofthetypename.Suchanidentifiercancontaineithernulloraregularvalue.[4]Bothnullsandregularnon-nullablevaluescanbestoredinanullabletype.[5]Youcan’tassignanidentifierofanullabletypetoanidentifierofanon-nulltype.Kotlinemits:Typemismatch:inferredtypeisString?butStringwasexpected.Eveniftheactualvalueisnon-nullasinthiscase(weknowit’s"abc"),Kotlinwon’tallowitbecausetheyaretwodifferenttypes.[6]Ifyouusetypeinference,Kotlinproducestheappropriatetype.Here,s6isnullablebecauses4isnullable.

Eventhoughitlookslikewejustmodifyanexistingtypebyaddinga?attheend,we’reactuallyspecifyingadifferenttype.Forexample,StringandString?aretwodifferenttypes.TheString?typeforbidstheoperationsinlines[2]and[5],thusguaranteeingthatavalueofanon-nullabletypeisnevernull.

RetrievingavaluefromaMapusingsquarebracketsproducesanullableresult,becausetheunderlyingMapimplementationcomesfromJava:

//NullableTypes/NullableInMap.kt

importatomictest.eq

funmain(){

valmap=mapOf(0to"yes",1to"no")

valfirst:String?=map[0]

valsecond:String?=map[2]

firsteq"yes"

secondeqnull

}

Whyisitimportanttoknowthatavaluecan’tbenull?Manyoperationsimplicitlyassumeanon-nullableresult.Forexample,callingamemberfunctionwillfailwithanexceptionifthereceivervalueisnull.InJavasuchacallwillfailwithaNullPointerException(oftenabbreviatedNPE).BecausealmostanyvaluecanbenullinJava,anyfunctioninvocationcanfailthisway.Inthesecasesyoumustwritecodetocheckfornullresults,orrelyonotherpartsofthecodetoguardagainstnulls.

InKotlinyoucan’tsimplydereference(callamemberfunctionoraccessamemberproperty)avalueofanullabletype:

//NullableTypes/Dereference.kt

importatomictest.eq

funmain(){

vals1:String="abc"

vals2:String?=s1

s1.lengtheq3//[1]

//Doesn'tcompile:

//s2.length//[2]

}

Youcanaccessmembersofanon-nullabletypeasin[1].Ifyoureferencemembersofanullabletype,asin[2],Kotlinemitsanerror.

Valuesofmosttypesarestoredasreferencestotheobjectsinmemory.That’sthemeaningofthetermdereference—toaccessanobject,youretrieveitsvaluefrommemory.

Themoststraightforwardwaytoensurethatdereferencinganullabletypewon’tthrowaNullPointerExceptionistoexplicitlycheckthatthereferenceisnotnull:

//NullableTypes/ExplicitCheck.kt

importatomictest.eq

funmain(){

vals:String?="abc"

if(s!=null)

s.lengtheq3

}

Aftertheexplicitif-check,Kotlinallowsyoutodereferenceanullable.Butwritingthisifwheneveryouworkwithnullabletypesistoonoisyforsuchacommonoperation.Kotlinhasconcisesyntaxtoalleviatethisproblem,whichyou’lllearnaboutinsubsequentatoms.

Wheneveryoucreateanewclass,Kotlinautomaticallyincludesnullableandnon-nullabletypes:

//NullableTypes/Amphibian.kt

packagenullabletypes

classAmphibian

enumclassSpecies{

Frog,Toad,Salamander,Caecilian

}

funmain(){

vala1:Amphibian=Amphibian()

vala2:Amphibian?=null

valat1:Species=Species.Toad

valat2:Species?=null

}

Asyoucansee,wedidn’tdoanythingspecialtoproducethecomplementarynullabletypes—they’reavailablebydefault.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SafeCalls&theElvisOperator

Kotlinprovidesconvenientoperationsforhandlingnullability.

Nullabletypescomewithnumerousrestrictions.Youcan’tsimplydereferenceanidentifierofanullabletype:

//SafeCallsAndElvis/DereferenceNull.kt

funmain(){

vals:String?=null

//Doesn'tcompile:

//s.length//[1]

}

Uncommenting[1]producesacompile-timeerror:Onlysafe(?.)ornon-nullasserted(!!.)callsareallowedonanullablereceiveroftypeString?.

Asafecallreplacesthedot(.)inaregularcallwithaquestionmarkandadot(?.),withoutinterveningspace.Safecallsaccessmembersofanullableinawaythatensuresnoexceptionsarethrown.Theyonlyperformanoperationwhenthereceiverisnotnull:

//SafeCallsAndElvis/SafeOperation.kt

packagesafecalls

importatomictest.*

funString.echo(){

trace(toUpperCase())

trace(this)

trace(toLowerCase())

}

funmain(){

vals1:String?="Howdy!"

s1?.echo()//[1]

vals2:String?=null

s2?.echo()//[2]

traceeq"""

HOWDY!

Howdy!

howdy!

"""

}

Line[1]callsecho()andproducesresultsinthetrace,whileline[2]doesnothingbecausethereceivers2isnull.

Safecallsareacleanwaytocaptureresults:

//SafeCallsAndElvis/SafeCall.kt

packagesafecalls

importatomictest.eq

funcheckLength(s:String?,expected:Int?){

vallength1=

if(s!=null)s.lengthelsenull//[1]

vallength2=s?.length//[2]

length1eqexpected

length2eqexpected

}

funmain(){

checkLength("abc",3)

checkLength(null,null)

}

Line[2]achievesthesameeffectasline[1].Ifthereceiverisnotnullitperformsanormalaccess(s.length).Ifthereceiverisnullitdoesn’tperformthes.lengthcall(whichwouldcauseanexception),butproducesnullfortheexpression.

Whatifyouneedsomethingmorethanthenullproducedby?.?TheElvisoperatorprovidesanalternative.Thisoperatorisaquestionmarkfollowedbyacolon(?:),withnointerveningspace.ItisnamedforanemoticonofthemusicianElvisPresley,andisalsoaplayonthewords“else-if”(whichsoundsvaguelylike“Elvis”).

AnumberofprogramminglanguagesprovideanullcoalescingoperatorthatperformsthesameactionasKotlin’sElvisoperator.

Iftheexpressionontheleftof?:isnotnull,thatexpressionbecomestheresult.Iftheleft-handexpressionisnull,thentheexpressionontherightofthe?:becomestheresult:

//SafeCallsAndElvis/ElvisOperator.kt

importatomictest.eq

funmain(){

vals1:String?="abc"

(s1?:"---")eq"abc"

vals2:String?=null

(s2?:"---")eq"---"

}

s1isnotnull,sotheElvisoperatorproduces"abc"astheresult.Becauses2isnull,theElvisoperatorproducesthealternateresultof"---".

TheElvisoperatoristypicallyusedafterasafecall,toproduceameaningfulvalueinsteadofthedefaultnull,asyouseein[2]:

//SafeCallsAndElvis/ElvisCall.kt

packagesafecalls

importatomictest.eq

funcheckLength(s:String?,expected:Int){

vallength1=

if(s!=null)s.lengthelse0//[1]

vallength2=s?.length?:0//[2]

length1eqexpected

length2eqexpected

}

funmain(){

checkLength("abc",3)

checkLength(null,0)

}

ThischeckLength()functionisquitesimilartotheoneinSafeCall.ktabove.Theexpectedparametertypeisnownon-nullable.[1]and[2]producezeroinsteadofnull.

Safecallsallowyoutowritechainedcallsconcisely,whensomeelementsinthechainmightbenullandyou’reonlyinterestedinthefinalresult:

//SafeCallsAndElvis/ChainedCalls.kt

packagesafecalls

importatomictest.eq

classPerson(

valname:String,

varfriend:Person?=null

)

funmain(){

valalice=Person("Alice")

alice.friend?.friend?.nameeqnull//[1]

valbob=Person("Bob")

valcharlie=Person("Charlie",bob)

bob.friend=charlie

bob.friend?.friend?.nameeq"Bob"//[2]

(alice.friend?.friend?.name

?:"Unknown")eq"Unknown"//[3]

}

Whenyouchainaccesstoseveralmembersusingsafecalls,theresultisnullifanyintermediateexpressionsarenull.

[1]Thepropertyalice.friendisnull,sotherestofthecallsreturnnull.[2]Allintermediatecallsproducemeaningfulvalues.[3]AnElvisoperatorafterthechainofsafecallsprovidesanalternatevalueifanyintermediateelementisnull.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Non-NullAssertions

Asecondapproachtotheproblemofnullabletypesistohavespecialknowledgethatthereferenceinquestionisn’tnull.

Tomakethisclaim,usethedoubleexclamationpoint,!!,calledthenon-nullassertion.Ifthislooksalarming,itshould:believingthatsomethingcan’tbenullisthesourceofmostnull-relatedprogramfailures(therestcomefromnotrealizingthatanullcanhappen).

x!!means“forgetthefactthatxmightbenull—Iguaranteethatit’snotnull.”x!!producesxifxisn’tnull,otherwiseitthrowsanexception:

//NonNullAssertions/NonNullAssert.kt

importatomictest.*

funmain(){

varx:String?="abc"

x!!eq"abc"

x=null

capture{

vals:String=x!!

}eq"NullPointerException"

}

Thedefinitionvals:String=x!!tellsKotlintoignorewhatitthinksitknowsaboutxandjustassignittos,whichisanon-nullablereference.Fortunately,there’srun-timesupportthatthrowsaNullPointerExceptionwhenxisnull.

Ordinarilyyouwon’tusethe!!byitself,butinsteadinconjunctionwitha.dereference:

//NonNullAssertions/NonNullAssertCall.kt

importatomictest.eq

funmain(){

vals:String?="abc"

s!!.lengtheq3

}

Ifyoulimityourselftoasinglenon-nullassertedcallperline,it’seasiertolocateafailurewhentheexceptiongivesyoualinenumber.

Thesafecall?.isasingleoperator,butanon-nullassertedcallconsistsoftwooperators:thenon-nullassertion(!!)andadereference(.).AsyousawinNonNullAssert.kt,youcanuseanon-nullassertionbyitself.

Avoidnon-nullassertionsandprefersafecallsorexplicitchecks.Non-nullassertionswereintroducedtoenableinteractionbetweenKotlinandJava,andfortherarecaseswhenKotlinisn’tsmartenoughtoensurethenecessarychecksareperformed.

Ifyoufrequentlyusenon-nullassertionsinyourcodeforthesameoperation,it’sbettertouseaseparatefunctionwithaspecificassertiondescribingtheproblem.Asanexample,supposeyourprogramlogicrequiresaparticularkeytobepresentinaMap,andyouprefergettinganexceptioninsteadofsilentlydoingnothingifthekeyisabsent.Insteadofextractingthevaluewiththeusualapproach(squarebrackets),getValue()throwsNoSuchElementExceptionifakeyismissing:

//NonNullAssertions/ValueFromMap.kt

importatomictest.*

funmain(){

valmap=mapOf(1to"one")

map[1]!!.toUpperCase()eq"ONE"

map.getValue(1).toUpperCase()eq"ONE"

capture{

map[2]!!.toUpperCase()

}eq"NullPointerException"

capture{

map.getValue(2).toUpperCase()

}eq"NoSuchElementException:"+

"Key2ismissinginthemap."

}

ThrowingthespecificNoSuchElementExceptiongivesyoumoreusefuldetailswhensomethinggoeswrong.

-

Optimalcodeusesonlysafecallsandspecialfunctionsthatthrowdetailedexceptions.Onlyusenon-nullassertedcallswhenyouabsolutelymust.Althoughnon-nullassertionswereincludedtosupportinteractionwithJava

code,therearebetterwaystointeractwithJava,whichyoucanlearnaboutinAppendixB:JavaInteroperability.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ExtensionsforNullableTypes

Sometimesit’snotwhatitlookslike.

s?.f()impliesthatsisnullable—otherwiseyoucouldsimplycalls.f().Similarly,t.f()seemstoimplythattisnon-nullablebecauseKotlindoesn’trequireasafecallorprogrammaticcheck.However,tisnotnecessarilynon-nullable.

TheKotlinstandardlibraryprovidesStringextensionfunctions,including:

isNullOrEmpty():TestswhetherthereceiverStringisnullorempty.isNullOrBlank():PerformsthesamecheckasisNullOrEmpty()andallowsthereceiverStringtoconsistsolelyofwhitespacecharacters,includingtabs(\t)andnewlines(\n).

Here’sabasictestofthesefunctions:

//NullableExtensions/StringIsNullOr.kt

importatomictest.eq

funmain(){

vals1:String?=null

s1.isNullOrEmpty()eqtrue

s1.isNullOrBlank()eqtrue

vals2=""

s2.isNullOrEmpty()eqtrue

s2.isNullOrBlank()eqtrue

vals3:String="\t\n"

s3.isNullOrEmpty()eqfalse

s3.isNullOrBlank()eqtrue

}

Thefunctionnamessuggesttheyarefornullabletypes.However,eventhoughs1isnullable,youcancallisNullOrEmpty()orisNullOrBlank()withoutasafecallorexplicitcheck.That’sbecausetheseareextensionfunctionsonthenullabletypeString?.

WecanrewriteisNullOrEmpty()asanon-extensionfunctionthattakesthenullableStringsasaparameter:

//NullableExtensions/NullableParameter.kt

packagenullableextensions

importatomictest.eq

funisNullOrEmpty(s:String?):Boolean=

s==null||s.isEmpty()

funmain(){

isNullOrEmpty(null)eqtrue

isNullOrEmpty("")eqtrue

}

Becausesisnullable,weexplicitlycheckfornullorempty.Theexpressions==null||s.isEmpty()usesshort-circuiting:ifthefirstpartoftheexpressionistrue,therestoftheexpressionisnotevaluated,thuspreventinganullpointerexception.

Extensionfunctionsusethistorepresentthereceiver(theobjectofthetypebeingextended).Tomakethereceivernullable,add?tothetypebeingextended:

//NullableExtensions/NullableExtension.kt

packagenullableextensions

importatomictest.eq

funString?.isNullOrEmpty():Boolean=

this==null||isEmpty()

funmain(){

"".isNullOrEmpty()eqtrue

}

isNullOrEmpty()ismorereadableasanextensionfunction.

-

Takecarewhenusingextensionsfornullabletypes.TheyaregreatforsimplecaseslikeisNullOrEmpty()andisNullOrBlank(),especiallywithself-explanatorynamesthatimplythereceivermightbenull.Ingeneral,it’sbettertodeclareregular(non-nullable)extensions.Safecallsandexplicitchecksclarifythereceiver’snullability,whileextensionsfornullabletypesmayconcealnullabilityandconfusethereaderofyourcode(probably,“futureyou”).

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

IntroductiontoGenerics

Genericscreateparameterizedtypes:componentsthatworkacrossmultipletypes.

Theterm“generic”means“pertainingorappropriatetolargegroupsofclasses.”Theoriginalintentofgenericsinprogramminglanguageswastoprovidetheprogrammermaximumexpressivenesswhenwritingclassesorfunctions,bylooseningtypeconstraintsonthoseclassesorfunctions.

Oneofthemostcompellinginitialmotivationsforgenericsistocreatecollectionclasses,whichyou’veseenintheLists,SetsandMapsusedfortheexamplesinthisbook.Acollectionisanobjectthatholdsotherobjects.Manyprogramsrequireyoutoholdagroupofobjectswhileyouusethem,socollectionsareoneofthemostreusableofclasslibraries.

Let’slookataclassthatholdsasingleobject.Thisclassspecifiestheexacttypeofthatobject:

//IntroGenerics/RigidHolder.kt

packageintrogenerics

importatomictest.eq

dataclassAutomobile(valbrand:String)

classRigidHolder(privatevala:Automobile){

fungetValue()=a

}

funmain(){

valholder=RigidHolder(Automobile("BMW"))

holder.getValue()eq

"Automobile(brand=BMW)"

}

RigidHolderisnotaparticularlyreusabletool;itcan’tholdanythingbutanAutomobile.Wewouldprefernottowriteanewtypeofholderforeverydifferenttype.Toachievethis,weuseatypeparameterinsteadofAutomobile.

Todefineagenerictype,addanglebrackets(<>)containingoneormoregenericplaceholdersandputthisgenericspecificationaftertheclassname.Here,the

genericplaceholderTrepresentstheunknowntypeandisusedwithintheclassasifitwerearegulartype:

//IntroGenerics/GenericHolder.kt

packageintrogenerics

importatomictest.eq

classGenericHolder<T>(//[1]

privatevalvalue:T

){

fungetValue():T=value

}

funmain(){

valh1=GenericHolder(Automobile("Ford"))

vala:Automobile=h1.getValue()//[2]

aeq"Automobile(brand=Ford)"

valh2=GenericHolder(1)

vali:Int=h2.getValue()//[3]

ieq1

valh3=GenericHolder("Chartreuse")

vals:String=h3.getValue()//[4]

seq"Chartreuse"

}

[1]GenericHolderstoresaT,anditsmemberfunctiongetValue()returnsaT.

WhenyoucallgetValue()asin[2],[3]or[4],theresultisautomaticallytherighttype.

Itseemslikewemightbeabletosolvethisproblemwitha“universaltype”—atypethatistheparentofallothertypes.InKotlin,thisuniversaltypeiscalledAny.Asthenameimplies,Anyallowsanytypeofargument.Ifyouwanttopassavarietyoftypestoafunctionandtheyhavenothingincommon,Anysolvestheproblem.

Ataglance,itlookslikewemightbeabletouseAnyinsteadofTinGenericHolder.kt:

//IntroGenerics/AnyInstead.kt

packageintrogenerics

importatomictest.eq

classAnyHolder(privatevalvalue:Any){

fungetValue():Any=value

}

classDog{

funbark()="Ruff!"

}

funmain(){

valholder=AnyHolder(Dog())

valany=holder.getValue()

//Doesn'tcompile:

//any.bark()

valgenericHolder=GenericHolder(Dog())

valdog=genericHolder.getValue()

dog.bark()eq"Ruff!"

}

Anydoesinfactworkforsimplecases,butassoonasweneedthespecifictype—tocallbark()fortheDog—itdoesn’tworkbecausewelosetrackofthefactthatit’saDogwhenitisassignedtotheAny.WhenwepassaDogasanAny,theresultisjustanAny,whichhasnobark().

Usinggenericsretainstheinformationthat,inthiscase,weactuallyhaveaDog,whichmeanswecanperformDogoperationsontheobjectreturnedbygetValue().

GenericFunctionsTodefineagenericfunction,specifyagenerictypeparameterinanglebracketsbeforethefunctionname:

//IntroGenerics/GenericFunction.kt

packageintrogenerics

importatomictest.eq

fun<T>identity(arg:T):T=arg

funmain(){

identity("Yellow")eq"Yellow"

identity(1)eq1

vald:Dog=identity(Dog())

d.bark()eq"Ruff!"

}

dhastypeDogbecauseidentity()isagenericfunctionandreturnsaT.

TheKotlinstandardlibrarycontainsmanygenericextensionfunctionsforcollections.Towriteagenericextensionfunction,putthegenericspecificationbeforethereceiver.Forexample,noticehowfirst()andfirstOrNull()aredefined:

//IntroGenerics/GenericListExtensions.kt

packageintrogenerics

importatomictest.eq

fun<T>List<T>.first():T{

if(isEmpty())

throwNoSuchElementException("EmptyList")

returnthis[0]

}

fun<T>List<T>.firstOrNull():T?=

if(isEmpty())nullelsethis[0]

funmain(){

listOf(1,2,3).first()eq1

vali:Int?=//[1]

listOf(1,2,3).firstOrNull()

ieq1

vals:String?=//[2]

listOf<String>().firstOrNull()

seqnull

}

first()andfirstOrNull()workwithanykindofList.ToreturnaT,theymustbegenericfunctions.

NoticehowfirstOrNull()specifiesanullablereturntype.Line[1]showsthatcallingthefunctiononList<Int>returnsthenullabletypeInt?.Line[2]showsthatcallingfirstOrNull()onList<String>returnsString?.Kotlinrequiresthe?onlines[1]and[2]—takethemoutandseetheerrormessages.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ExtensionProperties

Justasfunctionscanbeextensionfunctions,propertiescanbeextensionproperties.

Thereceivertypespecificationforextensionpropertiesissimilartothesyntaxforextensionfunctions—theextendedtypecomesrightbeforethefunctionorpropertyname:

funReceiverType.extensionFunction(){...}

valReceiverType.extensionProperty:PropType

get(){...}

Anextensionpropertyrequiresacustomgetter.Thepropertyvalueiscomputedforeachaccess:

//ExtensionProperties/StringIndices.kt

packageextensionproperties

importatomictest.eq

valString.indices:IntRange

get()=0untillength

funmain(){

"abc".indiceseq0..2

}

Althoughyoucanconvertanyextensionfunctionwithoutparametersintoaproperty,werecommendthinkingaboutitfirst.ThereasonsdescribedinPropertyAccessorsforchoosingbetweenpropertiesandfunctionsalsoapplytoextensionproperties.Preferringapropertyoverafunctionmakessenseonlyifit’ssimpleenoughandimprovesreadability.

Youcandefineagenericextensionproperty.Here,weconvertfirstOrNull()fromIntroductiontoGenericstoanextensionproperty:

//ExtensionProperties/GenericListExt.kt

packageextensionproperties

importatomictest.eq

val<T>List<T>.firstOrNull:T?

get()=if(isEmpty())nullelsethis[0]

funmain(){

listOf(1,2,3).firstOrNulleq1

listOf<String>().firstOrNulleqnull

}

TheKotlinStyleGuiderecommendsafunctionoverapropertyifthefunctionthrowsanexception.

Whenthegenericargumenttypeisn’tused,youmayreplaceitwith*.Thisiscalledastarprojection:

//ExtensionProperties/ListOfStar.kt

packageextensionproperties

importatomictest.eq

valList<*>.indices:IntRange

get()=0untilsize

funmain(){

listOf(1).indiceseq0..0

listOf('a','b','c','d').indiceseq0..3

emptyList<Int>().indiceseqIntRange.EMPTY

}

WhenyouuseList<*>,youloseallspecificinformationaboutthetypecontainedintheList.Forexample,anelementofaList<*>canonlybeassignedtoAny?:

//ExtensionProperties/AnyFromListOfStar.kt

importatomictest.eq

funmain(){

vallist:List<*>=listOf(1,2)

valany:Any?=list[0]

anyeq1

}

WehavenoinformationwhetheravaluestoredinaList<*>isnullableornot,whichiswhyitcanbeonlyassignedtoanullableAny?type.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

break&continue

breakandcontinueallowyouto“jump”withinaloop.

Earlyprogrammerswrotedirectlytotheprocessor,usingeithernumericalopcodesasinstructions,orassemblylanguage,whichtranslatesintoopcodes.Thiskindofprogrammingisaslow-levelasyoucanget.Forexample,manycodingdecisionswerefacilitatedby“jumping”directlytootherplacesinthecode.Earlyhigher-levellanguages(includingFORTRAN,ALGOL,Pascal,CandC++)duplicatedthispracticebyimplementingagotokeyword.

gotomadeassembly-languageprogrammersmorecomfortableastheytransitionedtohigher-levellanguages.Asweaccumulatedmoreexperience,however,theprogrammingcommunitydiscoveredthatunconditionaljumpsproducecomplicatedandun-maintainablecode.Thisgeneratedalargebacklashagainstgoto,andmostsubsequentlanguageshaveavoidedanykindofunconditionaljump.

Kotlinprovidesaconstrainedjumpintheformofbreakandcontinue.Thesearetiedtotheloopingconstructsfor,whileanddo-while—youcanonlyusebreakandcontinuefromwithinsuchloops.Inaddition,continuecanonlyjumptothebeginningofaloop,andbreakcanonlyjumptotheendofaloop.

InpracticeyourarelyusebreakandcontinuewhenwritingnewKotlincode.Thesefeaturesareartifactsfromearlierlanguages.Althoughtheyareoccasionallyuseful,you’lllearninthisbookthatKotlinprovidessuperiormechanisms.

Here’sanexamplewithaforloopcontainingbothacontinueandabreak:

//BreakAndContinue/ForControl.kt

importatomictest.eq

funmain(){

valnums=mutableListOf(0)

for(iin4until100step4){//[1]

if(i==8)continue//[2]

if(i==40)break//[3]

nums.add(i)

}//[4]

numseq"[0,4,12,16,20,24,28,32,36]"

}

TheexampleaggregatesIntsintoamutableList.Thecontinueat[2]jumpsbacktothebeginningoftheloop,whichistheopeningbraceat[1].It“continues”executionstartingwiththenextiterationoftheloop.Notethatthecodefollowingcontinueinsidetheforloopbodyisnotexecuted:nums.add(i)isnotcalledwheni==8soyoudon’tseeitintheresultingnums.

Wheni==40,breakisexecutedat[3],which“breaksout”oftheforloopbyjumpingtotheendofitsscopeat[4].Thenumbersbeginningat40arenotaddedtotheresultingListbecausetheforloopstopsexecuting.

Lines[2]and[3]areinterchangeablebecausetheirlogicdoesn’toverlap.Tryswappingthelinesandverifythattheoutputdoesn’tchange.

WecanrewriteForControl.ktusingawhileloop:

//BreakAndContinue/WhileControl.kt

importatomictest.eq

funmain(){

valnums=mutableListOf(0)

vari=0

while(i<100){

i+=4

if(i==8)continue

if(i==40)break

nums.add(i)

}

numseq"[0,4,12,16,20,24,28,32,36]"

}

Thebreakandcontinuebehaviorremainsthesame,asitdoesforado-whileloop:

//BreakAndContinue/DoWhileControl.kt

importatomictest.eq

funmain(){

valnums=mutableListOf(0)

vari=0

do{

i+=4

if(i==8)continue

if(i==40)break

nums.add(i)

}while(i<100)

numseq"[0,4,12,16,20,24,28,32,36]"

}

Ado-whileloopalwaysexecutesatleastonce,becausethewhiletestisattheendoftheloop.

LabelsPlainbreakandcontinuecanjumpnofurtherthantheboundariesoftheirlocalloop.Labelsallowbreakandcontinuetojumptotheboundariesofenclosingloops,soyouaren’tlimitedtothescopeofthecurrentloop.

Youcreatealabelbyusinglabel@,wherelabelcanbeanyname.Here,thelabelisouter:

//BreakAndContinue/ForLabeled.kt

importatomictest.eq

funmain(){

valstrings=mutableListOf<String>()

outer@for(cin'a'..'e'){

for(iin1..9){

if(i==5)continue@outer

if("$c$i"=="c3")break@outer

strings.add("$c$i")

}

}

stringseqlistOf("a1","a2","a3","a4",

"b1","b2","b3","b4","c1","c2")

}

Thelabeledcontinueexpressioncontinue@outercontinuesbacktothelabelouter@.Thelabeledbreakexpressionbreak@outerfindstheendoftheblocknamedouter@,andproceedsfromthere.

Labelsworkwithwhileanddo-while:

//BreakAndContinue/WhileLabeled.kt

importatomictest.eq

funmain(){

valstrings=mutableListOf<String>()

varc='a'-1

outer@while(c<'f'){

c+=1

vari=0

do{

i++

if(i==5)continue@outer

if("$c$i"=="c3")break@outer

strings.add("$c$i")

}while(i<10)

}

stringseqlistOf("a1","a2","a3","a4",

"b1","b2","b3","b4","c1","c2")

}

WhileLabeled.ktcanberewrittenas:

//BreakAndContinue/Improved.kt

importatomictest.eq

funmain(){

valstrings=mutableListOf<String>()

for(cin'a'..'c'){

for(iin1..4){

valvalue="$c$i"

if(value<"c3"){//[1]

strings.add(value)

}

}

}

stringseqlistOf("a1","a2","a3","a4",

"b1","b2","b3","b4","c1","c2")

}

Thisisfarmorecomprehensible.Inline[1],weonlyaddStringsthatoccur(alphabetically)before"c3".Thisproducesthesamebehaviorasusingbreakwhenreaching"c3"inthepreviousversionsoftheexample.

-

breakandcontinuetendtocreatecomplicatedandun-maintainablecode.Althoughthesejumpsaresomewhatmorecivilizedthan“goto,”theystillinterruptprogramflow.Codewithoutjumpsisalmostalwayseasiertounderstand.

Insomecases,youcanwritetheconditionsforiterationexplicitlyinsteadofusingbreakandcontinue,aswedidintheexampleabove.Inothercases,youcanrestructureyourcodeandintroducenewfunctions.Bothbreakandcontinuecanbereplacedwithreturnifyouextractthewholelooportheloopbodyintonewfunctions.Inthenextsection,FunctionalProgramming,you’lllearntowriteclearcodewithoutusingbreakandcontinue.

Consideralternativeapproaches,andchoosethesimplerandmorereadablesolution.Thistypicallywon’tincludebreakandcontinue.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SECTIONIV:FUNCTIONALPROGRAMMING

Theunavoidablepriceofreliabilityissimplicity—C.A.R.Hoare

Lambdas

Lambdasproducecompactcodethat’seasiertounderstand.

Alambda(alsocalledafunctionliteral)isalow-ceremonyfunction:ithasnoname,requiresaminimalamountofcodetocreate,andyoucaninsertitdirectlyintoothercode.

Asastartingpoint,considermap(),whichworkswithcollectionslikeList.Theparameterformap()isatransformationfunctionwhichisappliedtoeachelementinacollection.map()returnsanewListcontainingallthetransformedelements.Here,wetransformeachListitemtoaStringsurroundedwith[]:

//Lambdas/BasicLambda.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4)

valresult=list.map({n:Int->"[$n]"})

resulteqlistOf("[1]","[2]","[3]","[4]")

}

Thelambdaisthecodewithinthecurlybracesusedintheinitializationofresult.Theparameterlistisseparatedfromthefunctionbodybyanarrow->(thesamearrowusedinwhenexpressions).

Thefunctionbodycanbeoneormoreexpressions.Thefinalexpressionbecomesthereturnvalueofthelambda.

BasicLambda.ktshowsthefulllambdasyntax,butthiscanoftenbesimplified.Wetypicallycreateandusealambdainplace,whichmeansKotlincanusuallyinfertypeinformation.Here,thetypeofnisinferred:

//Lambdas/LambdaTypeInference.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4)

valresult=list.map({n->"[$n]"})

resulteqlistOf("[1]","[2]","[3]","[4]")

}

KotlincantellnisanIntbecausethelambdaisbeingusedwithaList<Int>.

Ifthere’sonlyasingleparameter,Kotlingeneratesthenameitforthatparameter,whichmeanswenolongerneedthen->:

//Lambdas/LambdaIt.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4)

valresult=list.map({"[$it]"})

resulteqlistOf("[1]","[2]","[3]","[4]")

}

map()workswithaListofanytype.Here,KotlininfersthetypeofthelambdaargumentittobeChar:

//Lambdas/Mapping.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c','d')

valresult=

list.map({"[${it.toUpperCase()}]"})

resulteqlistOf("[A]","[B]","[C]","[D]")

}

Ifthelambdaistheonlyfunctionargument,orthelastargument,youcanremovetheparenthesesaroundthecurlybraces,producingcleanersyntax:

//Lambdas/OmittingParentheses.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c','d')

valresult=

list.map{"[${it.toUpperCase()}]"}

resulteqlistOf("[A]","[B]","[C]","[D]")

}

Ifthefunctiontakesmorethanoneargument,allexceptthelastlambdaargumentmustbeinparentheses.Forexample,youcanspecifythelastargumentforjoinToString()asalambda.ThelambdaisusedtotransformeachelementtoaString,thenalltheelementsarejoined:

//Lambdas/JoinToString.kt

importatomictest.eq

funmain(){

vallist=listOf(9,11,23,32)

list.joinToString(""){"[$it]"}eq

"[9][11][23][32]"

}

Ifyouwanttoprovidethelambdaasanamedargument,youmustplacethelambdainsidetheparenthesesoftheargumentlist:

//Lambdas/LambdaAndNamedArgs.kt

importatomictest.eq

funmain(){

vallist=listOf(9,11,23,32)

list.joinToString(

separator="",

transform={"[$it]"}

)eq"[9][11][23][32]"

}

Here’sthesyntaxforalambdawithmorethanoneparameter:

//Lambdas/TwoArgLambda.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c')

list.mapIndexed{index,element->

"[$index:$element]"

}eqlistOf("[0:a]","[1:b]","[2:c]")

}

ThisusesthemapIndexed()libraryfunction,whichtakeseachelementinlistandproducestheindexofthatelementtogetherwiththeelement.ThelambdathatweapplyaftermapIndexed()requirestwoargumentstomatchtheindexandtheelement(whichisacharacter,inthecaseofList<Char>).

Ifyouaren’tusingaparticularargument,youcanignoreitusinganunderscoretoeliminatecompilerwarningsaboutunusedidentifiers:

//Lambdas/Underscore.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c')

list.mapIndexed{index,_->

"[$index]"

}eqlistOf("[0]","[1]","[2]")

}

NotethatUnderscore.ktcanberewrittenusinglist.indices:

//Lambdas/ListIndicesMap.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c')

list.indices.map{

"[$it]"

}eqlistOf("[0]","[1]","[2]")

}

Lambdascanhavezeroparameters,inwhichcaseyoucanleavethearrowforemphasis,buttheKotlinstyleguiderecommendsomittingthearrow:

//Lambdas/ZeroArguments.kt

importatomictest.*

funmain(){

run{->trace("ALambda")}

run{trace("Withoutargs")}

traceeq"""

ALambda

Withoutargs

"""

}

Thestandardlibraryrun()simplycallsitslambdaargument.

-

Youcanusealambdaanywhereyouusearegularfunction,butifthelambdabecomestoocomplexit’softenbettertodefineanamedfunction,forclarity,evenifyou’reonlygoingtouseitonce.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

TheImportanceofLambdas

Lambdasmayseemlikesyntaxsugar,buttheyprovideimportantpowertoyourprogramming.

Codeoftenmanipulatesthecontentsofacollection,andtypicallyrepeatsthesemanipulationswithminormodifications.Considerselectingelementsfromacollection,suchaspeopleunderagivenage,employeeswithaspecificrole,citizensofaparticularcity,orunfinishedorders.Here’sanexamplethatselectsevennumbersfromalist.Supposewedon’thavearichlibraryoffunctionsforworkingwithcollections—we’dhavetoimplementourownfilterEven()operation:

//ImportanceOfLambdas/FilterEven.kt

packageimportanceoflambdas

importatomictest.eq

funfilterEven(nums:List<Int>):List<Int>{

valresult=mutableListOf<Int>()

for(iinnums){

if(i%2==0){//[1]

result+=i

}

}

returnresult

}

funmain(){

filterEven(listOf(1,2,3,4))eq

listOf(2,4)

}

Ifanelementhasaremainderof0whendividedby2,it’sappendedtotheresult.

Imagineyouneedsomethingsimilar,butfornumbersthataregreaterthan2.YoucancopyfilterEven()andmodifythesmallpartthatchoosestheelementsincludedintheresult:

//ImportanceOfLambdas/GreaterThan2.kt

packageimportanceoflambdas

importatomictest.eq

fungreaterThan2(nums:List<Int>):List<Int>{

valresult=mutableListOf<Int>()

for(iinnums){

if(i>2){//[1]

result+=i

}

}

returnresult

}

funmain(){

greaterThan2(listOf(1,2,3,4))eq

listOf(3,4)

}

Theonlynotabledifferencebetweentheprevioustwoexamplesisthelineofcode([1]inbothcases)specifyingthedesiredelements.

Withlambdas,wecanusethesamefunctionforbothcases.Thestandardlibraryfunctionfilter()takesapredicatespecifyingtheelementsyouwanttopreserve,andthispredicatecanbealambda:

//ImportanceOfLambdas/Filter.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4)

valeven=list.filter{it%2==0}

valgreaterThan2=list.filter{it>2}

eveneqlistOf(2,4)

greaterThan2eqlistOf(3,4)

}

Nowwehaveclear,concisecodethatavoidsrepetition.BothevenandgreaterThan2usefilter()anddifferonlyinthepredicate.filter()hasbeenheavilytested,soyou’relesslikelytointroduceabug.

Noticethatfilter()handlestheiterationthatwouldotherwiserequirehandwrittencode.Althoughmanagingtheiterationyourselfmightnotseemlikemucheffort,it’sonemoreerror-pronedetailandonemoreplacetomakeamistake.Becausethey’reso“obvious,”suchmistakesareparticularlyhardtofind.

Thisisoneofthehallmarksoffunctionalprogramming,ofwhichmap()andfilter()areexamples.Functionalprogrammingsolvesproblemsinsmallsteps.Thefunctionsoftendothingsthatseemtrivial—it’snotthathardtowriteyourowncoderatherthanusingmap()andfilter().However,onceyouhaveacollectionofthesesmall,debuggedsolutions,youcaneasilycombinethemwithoutdebuggingateverylevel.Thisallowsyoutocreatemorerobustcode,morequickly.

Youcanstorealambdainavarorval.Thisallowsreuseofthatlambda’slogic,bypassingitasanargumenttodifferentfunctions:

//ImportanceOfLambdas/StoringLambda.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4)

valisEven={e:Int->e%2==0}

list.filter(isEven)eqlistOf(2,4)

list.any(isEven)eqtrue

}

isEvencheckswhetheranumberiseven,andthisreferenceispassedasanargumenttobothfilter()andany().Thelibraryfunctionany()checkswhetherthere’satleastoneelementintheListsatisfyingagivenpredicate.WhenwedefineisEvenwemustspecifytheparametertypebecausethereisnocontextforthetypeinferencer.

Anotherimportantqualityoflambdasistheabilitytorefertoelementsoutsidetheirscope.Whenafunction“closesover”or“captures”theelementsinitsenvironment,wecallitaclosure.Unfortunately,somelanguagesconflatetheterm“closure”withtheideaofalambda.Thetwoconceptsarecompletelydistinct:youcanhavelambdaswithoutclosures,andclosureswithoutlambdas.

Whenalanguagesupportsclosures,it“justworks”thewayyouexpect:

//ImportanceOfLambdas/Closures.kt

importatomictest.eq

funmain(){

vallist=listOf(1,5,7,10)

valdivider=5

list.filter{it%divider==0}eq

listOf(5,10)

}

Here,thelambda“captures”thevaldividerthatisdefinedoutsidethelambda.Thelambdanotonlyreadscapturedelements,itcanalsomodifythem:

//ImportanceOfLambdas/Closures2.kt

importatomictest.eq

funmain(){

vallist=listOf(1,5,7,10)

varsum=0

valdivider=5

list.filter{it%divider==0}

.forEach{sum+=it}

sumeq15

}

TheforEach()libraryfunctionappliesthespecifiedactiontoeachelementofthecollection.

AlthoughyoucancapturethemutablevariablesumasinClosures2.kt,youcanusuallychangeyourcodeandavoidmodifyingthestateofyourenvironment:

//ImportanceOfLambdas/Sum.kt

importatomictest.eq

funmain(){

vallist=listOf(1,5,7,10)

valdivider=5

list.filter{it%divider==0}

.sum()eq15

}

sum()worksonalistofnumbers,addingalltheelementsinthelist.

Anordinaryfunctioncanalsocloseoversurroundingelements:

//ImportanceOfLambdas/FunctionClosure.kt

packageimportanceoflambdas

importatomictest.eq

varx=100

funuseX(){

x++

}

funmain(){

useX()

xeq101

}

useX()capturesandmodifiesxfromitssurroundings.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

OperationsonCollections

Anessentialaspectoffunctionallanguagesistheabilitytoeasilyperformbatchoperationsoncollectionsofobjects.

Mostfunctionallanguagesprovidepowerfulsupportforworkingwithcollections,andKotlinisnoexception.You’vealreadyseenmap(),filter(),any()andforEach().ThisatomshowsadditionaloperationsavailableforListsandothercollections.

WestartbylookingatvariouswaystomanufactureLists.Here,weinitializeListsusinglambdas:

//OperationsOnCollections/CreatingLists.kt

importatomictest.eq

funmain(){

//Thelambdaargumentistheelementindex:

vallist1=List(10){it}

list1eq"[0,1,2,3,4,5,6,7,8,9]"

//Alistofasinglevalue:

vallist2=List(10){0}

list2eq"[0,0,0,0,0,0,0,0,0,0]"

//Alistofletters:

vallist3=List(10){'a'+it}

list3eq"[a,b,c,d,e,f,g,h,i,j]"

//Cyclethroughasequence:

vallist4=List(10){list3[it%3]}

list4eq"[a,b,c,a,b,c,a,b,c,a]"

}

ThisversionoftheListconstructorhastwoparameters:thesizeoftheListandalambdathatinitializeseachListelement(theelementindexispassedinastheitargument).Rememberthatifalambdaisthelastargument,itcanbeseparatedfromtheargumentlist.

MutableListscanbeinitializedinthesameway.Hereweseetheinitializationlambdabothinsidetheargumentlist(mutableList1)andseparatedfromtheargumentlist(mutableList2):

//OperationsOnCollections/ListInit.kt

importatomictest.eq

funmain(){

valmutableList1=

MutableList(5,{10*(it+1)})

mutableList1eq"[10,20,30,40,50]"

valmutableList2=

MutableList(5){10*(it+1)}

mutableList2eq"[10,20,30,40,50]"

}

NotethatList()andMutableList()arenotconstructors,butfunctions.Theirnamesintentionallybeginwithanupper-caselettertomakethemlooklikeconstructors.

Manycollectionfunctionstakeapredicateandtestitagainsttheelementsofacollection,someofwhichwe’vealreadyseen:

filter()producesalistcontainingallelementsmatchingthegivenpredicate.any()returnstrueifatleastoneelementmatchesthepredicate.all()checkswhetherallelementsmatchthepredicate.none()checksthatnoelementsmatchthepredicate.find()andfirstOrNull()bothreturnthefirstelementmatchingthepredicate,ornullifnosuchelementwasfound.lastOrNull()returnsthelastelementmatchingthepredicate,ornull.count()returnsthenumberofelementsmatchingthepredicate.

Herearesimpleexamplesforeachfunction:

//OperationsOnCollections/Predicates.kt

importatomictest.eq

funmain(){

vallist=listOf(-3,-1,5,7,10)

list.filter{it>0}eqlistOf(5,7,10)

list.count{it>0}eq3

list.find{it>0}eq5

list.firstOrNull{it>0}eq5

list.lastOrNull{it<0}eq-1

list.any{it>0}eqtrue

list.any{it!=0}eqtrue

list.all{it>0}eqfalse

list.all{it!=0}eqtrue

list.none{it>0}eqfalse

list.none{it==0}eqtrue

}

filter()andcount()applythepredicateagainsteachelement,whileany()orfind()stopwhenthefirstmatchingresultisfound.Forexample,ifthefirstelementsatisfiesthepredicate,any()returnstruerightaway,whilefind()returnsthefirstmatchingelement.Theonlytimealltheelementsareprocessedisifthelistcontainsnoelementsmatchingthegivenpredicate.

filter()returnsagroupofelementssatisfyingthegivenpredicate.Sometimesyoumaybeinterestedintheremaininggroup—theelementsthatdon’tsatisfythepredicate.filterNot()producesthisremaininggroup,butpartition()canbemoreusefulbecauseitsimultaneouslyproducesbothlists:

//OperationsOnCollections/Partition.kt

importatomictest.eq

funmain(){

vallist=listOf(-3,-1,5,7,10)

valisPositive={i:Int->i>0}

list.filter(isPositive)eq"[5,7,10]"

list.filterNot(isPositive)eq"[-3,-1]"

val(pos,neg)=list.partition{it>0}

poseq"[5,7,10]"

negeq"[-3,-1]"

}

partition()producesaPairobjectcontainingLists.UsingDestructuringDeclarations,youcanassigntheelementsofthePairtoaparenthesizedgroupofvarsorvals.Destructuringmeansdefiningmultiplevarsorvalsandinitializingthemsimultaneously,fromtheexpressionontherightsideoftheassignment.Here,destructuringisusedwithacustomfunction:

//OperationsOnCollections/PairOfLists.kt

packageoperationsoncollections

importatomictest.eq

funcreatePair()=Pair(1,"one")

funmain(){

val(i,s)=createPair()

ieq1

seq"one"

}

filterNotNull()producesanewListwiththenullsremoved:

//OperationsOnCollections/FilterNotNull.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,null)

list.filterNotNull()eq"[1,2]"

}

InLists,wesawfunctionssuchassum()orsorted()appliedtoalistofcomparableelements.Thesefunctionscan’tbecalledonlistsofnon-summableornon-comparableelements,buttheyhavecounterpartsnamedsumBy()andsortedBy().Youpassafunction(oftenalambda)asanargument,whichspecifiestheattributetousefortheoperation:

//OperationsOnCollections/ByOperations.kt

packageoperationsoncollections

importatomictest.eq

dataclassProduct(

valdescription:String,

valprice:Double

)

funmain(){

valproducts=listOf(

Product("bread",2.0),

Product("wine",5.0)

)

products.sumByDouble{it.price}eq7.0

products.sortedByDescending{it.price}eq

"[Product(description=wine,price=5.0),"+

"Product(description=bread,price=2.0)]"

products.minByOrNull{it.price}eq

Product("bread",2.0)

}

NotethatwehavetwofunctionssumBy()andsumByDouble()tosumintegeranddoublevalues,respectively.sorted()andsortedBy()sortthecollectioninascendingorder,whilesortedDescending()andsortedByDescending()sortthecollectionindescendingorder.

minByOrNullreturnsaminimumvaluebasedonagivencriteriaornullifthelistisempty.

take()anddrop()produceorremove(respectively)thefirstelement,whiletakeLast()anddropLast()produceorremovethelastelement.Thesehavecounterpartsthatacceptapredicatespecifyingtheelementstotakeordrop:

//OperationsOnCollections/TakeOrDrop.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c','X','Z')

list.takeLast(3)eq"[c,X,Z]"

list.takeLastWhile{it.isUpperCase()}eq

"[X,Z]"

list.drop(1)eq"[b,c,X,Z]"

list.dropWhile{it.isLowerCase()}eq

"[X,Z]"

}

Operationslikethoseyou’veseenforListsarealsoavailableforSets:

//OperationsOnCollections/SetOperations.kt

importatomictest.eq

funmain(){

valset=setOf("a","ab","ac")

set.maxByOrNull{it.length}?.lengtheq2

set.filter{

it.contains('b')

}eqlistOf("ab")

set.map{it.length}eqlistOf(1,2,2)

}

maxByOrNull()returnsnullifacollectionisempty,soitsresultisnullable.

Notethatfilter()andmap(),whenappliedtoaSet,returntheirresultsinaList.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

MemberReferences

Youcanpassamemberreferenceasafunctionargument.

Memberreferences—forfunctions,propertiesandconstructors—canreplacetriviallambdasthatsimplycallthecorrespondingfunction,propertyorconstructor.

Amemberreferenceusesadoublecolontoseparatetheclassnamefromthefunctionorproperty.Here,Message::isReadisamemberreference:

//MemberReferences/PropertyReference.kt

packagememberreferences1

importatomictest.eq

dataclassMessage(

valsender:String,

valtext:String,

valisRead:Boolean

)

funmain(){

valmessages=listOf(

Message("Kitty","Hey!",true),

Message("Kitty","Whereareyou?",false))

valunread=

messages.filterNot(Message::isRead)

unread.sizeeq1

unread.single().texteq"Whereareyou?"

}

Tofilterforunreadmessages,weusethelibraryfunctionfilterNot(),whichtakesapredicate.Inourcase,thepredicateindicateswhetheramessageisalreadyread.Wecouldpassalambda,butinsteadwepassthepropertyreferenceMessage::isRead.

Propertyreferencesareusefulwhenspecifyinganon-trivialsortorder:

//MemberReferences/SortWith.kt

importmemberreferences1.Message

importatomictest.eq

funmain(){

valmessages=listOf(

Message("Kitty","Hey!",true),

Message("Kitty","Whereareyou?",false),

Message("Boss","Meetingtoday",false))

messages.sortedWith(compareBy(

Message::isRead,Message::sender))eq

listOf(

//Firstunread,sortedbysender:

Message("Boss","Meetingtoday",false),

Message("Kitty",

"Whereareyou?",false),

//Thenread,alsosortedbysender:

Message("Kitty","Hey!",true))

}

ThelibraryfunctionsortedWith()sortsalistusingacomparator,whichisanobjectusedtocomparetwoelements.ThelibraryfunctioncompareBy()buildsacomparatorbasedonitsparameters,whicharealistofpredicates.UsingcompareBy()withasingleargumentisequivalenttocallingsortedBy().

FunctionReferencesSupposeyouwanttocheckwhetheraListcontainsanyimportantmessages,notjustunreadmessages.Youmighthaveanumberofcomplicatedcriteriatodecidewhat“important”means.Youcanputthislogicintoalambda,butthatlambdacouldeasilybecomelargeandcomplex.Thecodeismoreunderstandableifyouextractitintoaseparatefunction.InKotlinyoucan’tpassafunctionwhereafunctiontypeisexpected,butyoucanpassareferencetothatfunction:

//MemberReferences/FunctionReference.kt

packagememberreferences2

importatomictest.eq

dataclassMessage(

valsender:String,

valtext:String,

valisRead:Boolean,

valattachments:List<Attachment>

)

dataclassAttachment(

valtype:String,

valname:String

)

funMessage.isImportant():Boolean=

text.contains("Salaryincrease")||

attachments.any{

it.type=="image"&&

it.name.contains("cat")

}

funmain(){

valmessages=listOf(Message(

"Boss","Let'sdiscussgoals"+

"fornextyear",false,

listOf(Attachment("image","cutecats"))))

messages.any(Message::isImportant)eqtrue

}

ThisnewMessageclassaddsanattachmentsproperty,andtheextensionfunctionMessage.isImportant()usesthisinformation.Inthecalltomessages.any(),wecreateareferencetoanextensionfunction—referencesarenotlimitedtomemberfunctions.

Ifyouhaveatop-levelfunctiontakingMessageasitsonlyparameter,youcanpassitasareference.Whenyoucreateareferencetoatop-levelfunction,there’snoclassname,soit’swritten::function:

//MemberReferences/TopLevelFunctionRef.kt

packagememberreferences2

importatomictest.eq

funignore(message:Message)=

!message.isImportant()&&

message.senderinsetOf("Boss","Mom")

funmain(){

valtext="Let'sdiscussgoals"+

"forthenextyear"

valmsgs=listOf(

Message("Boss",text,false,listOf()),

Message("Boss",text,false,listOf(

Attachment("image","cutecats"))))

msgs.filter(::ignore).sizeeq1

msgs.filterNot(::ignore).sizeeq1

}

ConstructorReferencesYoucancreateareferencetoaconstructorusingtheclassname.

Here,names.mapIndexed()takestheconstructorreference::Student:

//MemberReferences/ConstructorReference.kt

packagememberreferences3

importatomictest.eq

dataclassStudent(

valid:Int,

valname:String

)

funmain(){

valnames=listOf("Alice","Bob")

valstudents=

names.mapIndexed{index,name->

Student(index,name)

}

studentseqlistOf(Student(0,"Alice"),

Student(1,"Bob"))

names.mapIndexed(::Student)eqstudents

}

mapIndexed()wasintroducedinLambdas.Itturnseachelementinnamesintotheindexofthatelementalongwiththeelement.Inthedefinitionofstudents,theseareexplicitlymappedintotheconstructor,buttheidenticaleffectisachievedwithnames.mapIndexed(::Student).Thus,functionandconstructorreferencescaneliminatespecifyingalonglistofparametersthataresimplypassedintoalambda.Functionandconstructorreferencesareoftenmorereadablethanlambdas.

ExtensionFunctionReferencesToproduceareferencetoanextensionfunction,prefixthereferencewiththenameoftheextendedtype:

//MemberReferences/ExtensionReference.kt

packagememberreferences

importatomictest.eq

funInt.times47()=times(47)

classFrog

funFrog.speak()="Ribbit!"

fungoInt(n:Int,g:(Int)->Int)=g(n)

fungoFrog(frog:Frog,g:(Frog)->String)=

g(frog)

funmain(){

goInt(12,Int::times47)eq564

goFrog(Frog(),Frog::speak)eq"Ribbit!"

}

IngoInt(),gisafunctionthatexpectsanIntargumentandproducesanInt.IngoFrog(),gexpectsaFrogandproducesaString.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Higher-OrderFunctions

Alanguagesupportshigher-orderfunctionsifitsfunctionscanacceptotherfunctionsasargumentsandproducefunctionsasreturnvalues.

Higher-orderfunctionsareanessentialpartoffunctionalprogramminglanguages.Inpreviousatoms,we’veseenhigher-orderfunctionssuchasfilter(),map(),andany().

Youcanstorealambdainareference.Let’slookatthetypeofthisstorage:

//HigherOrderFunctions/IsPlus.kt

packagehigherorderfunctions

importatomictest.eq

valisPlus:(Int)->Boolean={it>0}

funmain(){

listOf(1,2,-3).any(isPlus)eqtrue

}

(Int)->Booleanisthefunctiontype:itstartswithparenthesessurroundingzeroormoreparametertypes,thenanarrow(->),followedbythereturntype:

(Arg1Type,Arg2Type...ArgNType)->ReturnType

Thesyntaxforcallingafunctionthroughareferenceisidenticaltoanordinaryfunctioncall:

//HigherOrderFunctions/CallingReference.kt

packagehigherorderfunctions

importatomictest.eq

valhelloWorld:()->String=

{"Hello,world!"}

valsum:(Int,Int)->Int=

{x,y->x+y}

funmain(){

helloWorld()eq"Hello,world!"

sum(1,2)eq3

}

Whenafunctionacceptsafunctionparameter,youcaneitherpassitafunctionreferenceoralambda.Considerhowyoumightdefineany()fromthestandardlibrary:

//HigherOrderFunctions/Any.kt

packagehigherorderfunctions

importatomictest.eq

fun<T>List<T>.any(//[1]

predicate:(T)->Boolean//[2]

):Boolean{

for(elementinthis){

if(predicate(element))//[3]

returntrue

}

returnfalse

}

funmain(){

valints=listOf(1,2,-3)

ints.any{it>0}eqtrue//[4]

valstrings=listOf("abc","")

strings.any{it.isBlank()}eqtrue//[5]

strings.any(String::isNotBlank)eq//[6]

true

}

[1]any()shouldbeusablewithListsofdifferenttypessowedefineitasanextensiontothegenericList<T>.[2]ThepredicatefunctioniscallablewithaparameteroftypeTsowecanapplyittotheListelements.[3]Applyingpredicate()tellswhetherthatelementfitsourcriteria.Thetypeofthelambdadiffers:it’sIntin[4]andStringin[5].[6]Amemberreferenceisanotherwaytopassafunctionreference.

repeat()fromthestandardlibrarytakesafunctionasitssecondparameter.ItrepeatsanactionanIntnumberoftimes:

//HigherOrderFunctions/RepeatByInt.kt

importatomictest.*

funmain(){

repeat(4){trace("hi!")}

traceeq"hi!hi!hi!hi!"

}

Considerhowrepeat()mightbedefined:

//HigherOrderFunctions/Repeat.kt

packagehigherorderfunctions

importatomictest.*

funrepeat(

times:Int,

action:(Int)->Unit//[1]

){

for(indexin0untiltimes){

action(index)//[2]

}

}

funmain(){

repeat(3){trace("#$it")}//[3]

traceeq"#0#1#2"

}

[1]repeat()takesaparameteractionofthefunctiontype(Int)->Unit.[2]Whenaction()iscalled,itispassedthecurrentrepetitionindex.[3]Whencallingrepeat(),youaccesstherepetitionindexusingitinsidethelambda.

Afunctionreturntypecanbenullable:

//HigherOrderFunctions/NullableReturn.kt

importatomictest.eq

funmain(){

valtransform:(String)->Int?=

{s:String->s.toIntOrNull()}

transform("112")eq112

transform("abc")eqnull

valx=listOf("112","abc")

x.mapNotNull(transform)eq"[112]"

x.mapNotNull{it.toIntOrNull()}eq"[112]"

}

toIntOrNull()mightreturnnull,sotransform()acceptsaStringandreturnsanullableInt?.mapNotNull()convertseachelementinaListintoanullablevalueandremovesallnullsfromtheresult.Ithasthesameeffectasfirstcallingmap(),thenapplyingfilterNotNull()totheresultinglist.

Notethedifferencebetweenmakingthereturntypenullableversusmakingthewholefunctiontypenullable:

//HigherOrderFunctions/NullableFunction.kt

importatomictest.eq

funmain(){

valreturnTypeNullable:(String)->Int?=

{null}

valmightBeNull:((String)->Int)?=null

returnTypeNullable("abc")eqnull

//Doesn'tcompilewithoutanullcheck:

//mightBeNull("abc")

if(mightBeNull!=null){

mightBeNull("abc")

}

}

BeforecallingthefunctionstoredinmightBeNull,wemustensurethatthefunctionreferenceitselfisnotnull.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ManipulatingLists

ZippingandflatteningaretwocommonoperationsthatmanipulateLists.

Zippingzip()combinestwoListsbymimickingthebehaviorofthezipperonyourjacket,pairingadjacentListelements:

//ManipulatingLists/Zipper.kt

importatomictest.eq

funmain(){

valleft=listOf("a","b","c","d")

valright=listOf("q","r","s","t")

left.zip(right)eq//[1]

"[(a,q),(b,r),(c,s),(d,t)]"

left.zip(0..4)eq//[2]

"[(a,0),(b,1),(c,2),(d,3)]"

(10..100).zip(right)eq//[3]

"[(10,q),(11,r),(12,s),(13,t)]"

}

[1]ZippingleftwithrightresultsinaListofPairs,combiningeachelementinleftwithitscorrespondingelementinright.[2]Youcanalsozip()aListwitharange.[3]Therange10..100ismuchlargerthanright,butthezippingprocessstopswhenonesequencerunsout.

zip()canalsoperformanoperationoneachPairitcreates:

//ManipulatingLists/ZipAndTransform.kt

packagemanipulatinglists

importatomictest.eq

dataclassPerson(

valname:String,

valid:Int

)

funmain(){

valnames=listOf("Bob","Jill","Jim")

valids=listOf(1731,9274,8378)

names.zip(ids){name,id->

Person(name,id)

}eq"[Person(name=Bob,id=1731),"+

"Person(name=Jill,id=9274),"+

"Person(name=Jim,id=8378)]"

}

names.zip(ids){...}producesasequenceofname-idPairs,andappliesthelambdatoeachPair.TheresultisaListofinitializedPersonobjects.

ToziptwoadjacentelementsfromasingleList,usezipWithNext():

//ManipulatingLists/ZippingWithNext.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c','d')

list.zipWithNext()eqlistOf(

Pair('a','b'),

Pair('b','c'),

Pair('c','d'))

list.zipWithNext{a,b->"$a$b"}eq

"[ab,bc,cd]"

}

ThesecondcalltozipWithNext()performsanadditionaloperationafterzipping.

Flatteningflatten()takesaListcontainingelementsthatarethemselvesLists—aListofLists—andflattensitintoaListofsingleelements:

//ManipulatingLists/Flatten.kt

importatomictest.eq

funmain(){

vallist=listOf(

listOf(1,2),

listOf(4,5),

listOf(7,8),

)

list.flatten()eq"[1,2,4,5,7,8]"

}

flatten()helpsusunderstandanotherimportantoperationoncollections:flatMap().Let’sproduceallpossiblePairsofarangeofInts:

//ManipulatingLists/FlattenAndFlatMap.kt

importatomictest.eq

funmain(){

valintRange=1..3

intRange.map{a->//[1]

intRange.map{b->atob}

}eq"["+

"[(1,1),(1,2),(1,3)],"+

"[(2,1),(2,2),(2,3)],"+

"[(3,1),(3,2),(3,3)]"+

"]"

intRange.map{a->//[2]

intRange.map{b->atob}

}.flatten()eq"["+

"(1,1),(1,2),(1,3),"+

"(2,1),(2,2),(2,3),"+

"(3,1),(3,2),(3,3)"+

"]"

intRange.flatMap{a->//[3]

intRange.map{b->atob}

}eq"["+

"(1,1),(1,2),(1,3),"+

"(2,1),(2,2),(2,3),"+

"(3,1),(3,2),(3,3)"+

"]"

}

Thelambdaineachcaseisidentical:everyintRangeelementiscombinedwitheveryintRangeelementtoproduceallpossibleatobPairs.Butin[1],map()helpfullypreservestheextrainformationthatwehaveproducedthreeLists,oneforeachelementinintRange.Therearesituationswherethisextrainformationisessential,butherewedon’twantit—wejustneedasingleflatListofallcombinations,withnoadditionalstructure.

Therearetwooptions.[2]showstheapplicationoftheflatten()functiontoremovethisadditionalstructureandflattentheresultintoasingleList,whichisanacceptableapproach.However,thisissuchacommontaskthatKotlinprovidesacombinedoperationcalledflatMap(),whichperformsbothmap()andflatten()withasinglecall.[3]showsflatMap()inaction.You’llfindflatMap()inmostlanguagesthatsupportfunctionalprogramming.

Here’sasecondexampleofflatMap():

//ManipulatingLists/WhyFlatMap.kt

packagemanipulatinglists

importatomictest.eq

classBook(

valtitle:String,

valauthors:List<String>

)

funmain(){

valbooks=listOf(

Book("1984",listOf("GeorgeOrwell")),

Book("Ulysses",listOf("JamesJoyce"))

)

books.map{it.authors}.flatten()eq

listOf("GeorgeOrwell","JamesJoyce")

books.flatMap{it.authors}eq

listOf("GeorgeOrwell","JamesJoyce")

}

We’dlikeaListofauthors.map()producesaListofListofauthors,whichisn’tveryconvenient.flatten()takesthatandproducesasimpleList.flatMap()producesthesameresultsinasinglestep.

Here,weusemap()andflatMap()tocombinetheenumsSuitandRank,producingadeckofCards:

//ManipulatingLists/PlayingCards.kt

packagemanipulatinglists

importkotlin.random.Random

importatomictest.*

enumclassSuit{

Spade,Club,Heart,Diamond

}

enumclassRank(valfaceValue:Int){

Ace(1),Two(2),Three(3),Four(4),Five(5),

Six(6),Seven(7),Eight(8),Nine(9),

Ten(10),Jack(10),Queen(10),King(10)

}

classCard(valrank:Rank,valsuit:Suit){

overridefuntoString()=

"$rankof${suit}s"

}

valdeck:List<Card>=

Suit.values().flatMap{suit->

Rank.values().map{rank->

Card(rank,suit)

}

}

funmain(){

valrand=Random(26)

repeat(7){

trace("'${deck.random(rand)}'")

}

traceeq"""

'JackofHearts''FourofHearts'

'FiveofClubs''SevenofClubs'

'JackofDiamonds''TenofSpades'

'SevenofSpades'

"""

}

Intheinitializationofdeck,theinnerRank.values().mapproducesfourLists,oneforeachSuit,soweuseflatMap()ontheouterlooptoproduceaList<Card>fordeck.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

BuildingMaps

Mapsareextremelyusefulprogrammingtools,andtherearenumerouswaystoconstructthem.

Tocreatearepeatablesetofdata,weusethetechniqueshowninManipulatingLists,wheretwoListsarezippedandtheresultisusedinalambdatocallaconstructor,producingaList<Person>:

//BuildingMaps/People.kt

packagebuildingmaps

dataclassPerson(

valname:String,

valage:Int

)

valnames=listOf("Alice","Arthricia",

"Bob","Bill","Birdperson","Charlie",

"Crocubot","Franz","Revolio")

valages=listOf(21,15,25,25,42,21,

42,21,33)

funpeople():List<Person>=

names.zip(ages){name,age->

Person(name,age)

}

AMapuseskeystoprovidefastaccesstoitsvalues.BybuildingaMapwithageasthekey,wecanquicklylookupgroupsofpeoplebyage.ThelibraryfunctiongroupBy()isonewaytocreatesuchaMap:

//BuildingMaps/GroupBy.kt

importbuildingmaps.*

importatomictest.eq

funmain(){

valmap:Map<Int,List<Person>>=

people().groupBy(Person::age)

map[15]eqlistOf(Person("Arthricia",15))

map[21]eqlistOf(

Person("Alice",21),

Person("Charlie",21),

Person("Franz",21))

map[22]eqnull

map[25]eqlistOf(

Person("Bob",25),

Person("Bill",25))

map[33]eqlistOf(Person("Revolio",33))

map[42]eqlistOf(

Person("Birdperson",42),

Person("Crocubot",42))

}

groupBy()’sparameterproducesaMapwhereeachkeyconnectstoaListofelements.Here,allpeopleofthesameageareselectedbytheagekey.

Youcanproducethesamegroupsusingthefilter()function,butgroupBy()ispreferablebecauseitonlyperformsthegroupingonce.Withfilter()youmustrepeatthegroupingforeachnewkey:

//BuildingMaps/GroupByVsFilter.kt

importbuildingmaps.*

importatomictest.eq

funmain(){

valgroups=

people().groupBy{it.name.first()}

//groupBy()producesmap-speedaccess:

groups['A']eqlistOf(Person("Alice",21),

Person("Arthricia",15))

groups['Z']eqnull

//Mustrepeatfilter()foreachcharacter:

people().filter{

it.name.first()=='A'

}eqlistOf(Person("Alice",21),

Person("Arthricia",15))

people().filter{

it.name.first()=='F'

}eqlistOf(Person("Franz",21))

people().partition{

it.name.first()=='A'

}eqPair(

listOf(Person("Alice",21),

Person("Arthricia",15)),

listOf(Person("Bob",25),

Person("Bill",25),

Person("Birdperson",42),

Person("Charlie",21),

Person("Crocubot",42),

Person("Franz",21),

Person("Revolio",33)))

}

Here,groupBy()groupspeople()bytheirfirstcharacter,selectedbyfirst().Wecanalsousefilter()toproducethesameresultbyrepeatingthelambdacodeforeachcharacter.

Ifyouonlyneedtwogroups,thepartition()functionismoredirectbecauseitdividesthecontentsintotwolistsbasedonapredicate.groupBy()isappropriate

whenyouneedmorethantworesultinggroups.

associateWith()allowsyoutotakealistofkeysandbuildaMapbyassociatingeachofthesekeyswithavaluecreatedbyitsparameter(here,thelambda):

//BuildingMaps/AssociateWith.kt

importbuildingmaps.*

importatomictest.eq

funmain(){

valmap:Map<Person,String>=

people().associateWith{it.name}

mapeqmapOf(

Person("Alice",21)to"Alice",

Person("Arthricia",15)to"Arthricia",

Person("Bob",25)to"Bob",

Person("Bill",25)to"Bill",

Person("Birdperson",42)to"Birdperson",

Person("Charlie",21)to"Charlie",

Person("Crocubot",42)to"Crocubot",

Person("Franz",21)to"Franz",

Person("Revolio",33)to"Revolio")

}

associateBy()reversestheorderofassociationproducedbyassociateWith()—theselector(thelambdainthefollowingexample)becomesthekey:

//BuildingMaps/AssociateBy.kt

importbuildingmaps.*

importatomictest.eq

funmain(){

valmap:Map<String,Person>=

people().associateBy{it.name}

mapeqmapOf(

"Alice"toPerson("Alice",21),

"Arthricia"toPerson("Arthricia",15),

"Bob"toPerson("Bob",25),

"Bill"toPerson("Bill",25),

"Birdperson"toPerson("Birdperson",42),

"Charlie"toPerson("Charlie",21),

"Crocubot"toPerson("Crocubot",42),

"Franz"toPerson("Franz",21),

"Revolio"toPerson("Revolio",33))

}

associateBy()mustbeusedwithauniqueselectionkeyandreturnsaMapthatpairseachuniquekeytothesingleelementselectedbythatkey.

//BuildingMaps/AssociateByUnique.kt

importbuildingmaps.*

importatomictest.eq

funmain(){

//associateBy()failswhenthekeyisn't

//unique--valuesdisappear:

valages=people().associateBy{it.age}

ageseqmapOf(

21toPerson("Franz",21),

15toPerson("Arthricia",15),

25toPerson("Bill",25),

42toPerson("Crocubot",42),

33toPerson("Revolio",33))

}

Ifmultiplevaluesareselectedbythepredicate,asinages,onlythelastoneappearsinthegeneratedMap.

getOrElse()triestolookupavalueinaMap.Itsassociatedlambdacomputesadefaultvaluewhenakeyisnotpresent.Becauseit’salambda,wecomputethedefaultkeyonlywhennecessary:

//BuildingMaps/GetOrPut.kt

importatomictest.eq

funmain(){

valmap=mapOf(1to"one",2to"two")

map.getOrElse(0){"zero"}eq"zero"

valmutableMap=map.toMutableMap()

mutableMap.getOrPut(0){"zero"}eq

"zero"

mutableMapeq"{1=one,2=two,0=zero}"

}

getOrPut()worksonaMutableMap.Ifakeyispresentitsimplyreturnstheassociatedvalue.Ifthekeyisn’tfound,itcomputesthevalue,putsitintothemapandreturnsthatvalue.

ManyMapoperationsduplicateonesinList.Forexample,youcanfilter()ormap()thecontentsofaMap.Youcanfilterkeysandvaluesseparately:

//BuildingMaps/FilterMap.kt

importatomictest.eq

funmain(){

valmap=mapOf(1to"one",

2to"two",3to"three",4to"four")

map.filterKeys{it%2==1}eq

"{1=one,3=three}"

map.filterValues{it.contains('o')}eq

"{1=one,2=two,4=four}"

map.filter{entry->

entry.key%2==1&&

entry.value.contains('o')

}eq"{1=one}"

}

Allthreefunctionsfilter(),filterKeys()andfilterValues()produceanewmapcontainingonlytheelementsthatsatisfythepredicate.filterKeys()appliesitspredicatetothekeys,andfilterValues()appliesitspredicatetothevalues.

ApplyingOperationstoMapsTomap()aMapsoundslikeatautology,likesaying“saltissalty.”Thewordmaprepresentstwodistinctideas:

TransformingacollectionThekey-valuedatastructure

Inmanyprogramminglanguages,thewordmapisusedforbothconcepts.Forclarity,wesaytransformamapwhenapplyingmap()toaMap.

Herewedemonstratemap(),mapKeys()andmapValues():

//BuildingMaps/TransformingMap.kt

importatomictest.eq

funmain(){

valeven=mapOf(2to"two",4to"four")

even.map{//[1]

"${it.key}=${it.value}"

}eqlistOf("2=two","4=four")

even.map{(key,value)->//[2]

"$key=$value"

}eqlistOf("2=two","4=four")

even.mapKeys{(num,_)->-num}//[3]

.mapValues{(_,str)->"minus$str"}eq

mapOf(-2to"minustwo",

-4to"minusfour")

even.map{(key,value)->

-keyto"minus$value"

}.toMap()eqmapOf(-2to"minustwo",//[4]

-4to"minusfour")

}

[1]Here,map()takesapredicatewithaMap.Entryargument.Weaccessitscontentsasit.keyandit.value.[2]Youcanalsouseadestructuringdeclarationtoplacetheentrycontentsintokeyandvalue.

[3]Ifaparameterisn’tused,anunderscore(_)avoidscompilercomplaints.mapKeys()andmapValues()returnanewmap,withallkeysorvaluestransformedaccordingly.[4],map()returnsalistofpairs,sotoproduceaMapweusetheexplicitconversiontoMap().

Functionslikeany()andall()canalsobeappliedtoMaps:

//BuildingMaps/SimilarOperation.kt

importatomictest.eq

funmain(){

valmap=mapOf(1to"one",

-2to"minustwo")

map.any{(key,_)->key<0}eqtrue

map.all{(key,_)->key<0}eqfalse

map.maxByOrNull{it.key}?.valueeq"one"

}

any()checkswhetheranyoftheentriesinaMapsatisfythegivenpredicate,whileall()istrueonlyifallentriesintheMapsatisfythepredicate.

maxByOrNull()findsthemaximumentrybasedonthegivencriteria.Theremaynotbeamaximumentry,sotheresultisnullable.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Sequences

AKotlinSequenceislikeaList,butyoucanonlyiteratethroughaSequence—youcannotindexintoaSequence.Thisrestrictionproducesveryefficientchainedoperations.

KotlinSequencesaretermedstreamsinotherfunctionallanguages.KotlinhadtochooseadifferentnametomaintaininteroperabilitywiththeJava8Streamlibrary.

OperationsonListsareperformedeagerly—theyalwayshappenrightaway.WhenchainingListoperations,thefirstresultmustbeproducedbeforestartingthenextoperation.Here,eachfilter(),map()andany()operationisappliedtoeveryelementinlist:

//Sequences/EagerEvaluation.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4)

list.filter{it%2==0}

.map{it*it}

.any{it<10}eqtrue

//Equivalentto:

valmid1=list.filter{it%2==0}

mid1eqlistOf(2,4)

valmid2=mid1.map{it*it}

mid2eqlistOf(4,16)

mid2.any{it<10}eqtrue

}

Eagerevaluationisintuitiveandstraightforward,butcanbesuboptimal.InEagerEvaluation.kt,itwouldmakemoresensetostopafterencounteringthefirstelementthatsatisfiestheany().Foralongsequence,thisoptimizationmightbemuchfasterthanevaluatingeveryelementandthensearchingforasinglematch.

Eagerevaluationissometimescalledhorizontalevaluation:

HorizontalEvaluation

Thefirstlinecontainstheinitiallistcontents.Eachfollowinglineshowstheresultsfromthepreviousoperation.Beforethenextoperationisperformed,allelementsonthecurrenthorizontallevelareprocessed.

Thealternativetoeagerevaluationislazyevaluation:aresultiscomputedonlywhenneeded.Performinglazyoperationsonsequencesissometimescalledverticalevaluation:

VerticalEvaluation

Withlazyevaluation,anoperationisperformedonanelementonlywhenthatelement’sassociatedresultisrequested.Ifthefinalresultofacalculationisfoundbeforeprocessingthelastelement,nofurtherelementsareprocessed.

ConvertingListstoSequencesusingasSequence()enableslazyevaluation.AllListoperationsexceptindexingarealsoavailableforSequences,soyoucan

usuallymakethissinglechangeandgainthebenefitsoflazyevaluation.

Thefollowingexampleshowstheabovediagramsconvertedintocode.Weperformtheidenticalchainofoperations,firstonaList,thenonaSequence.Theoutputshowswhereeachoperationiscalled:

//Sequences/EagerVsLazyEvaluation.kt

packagesequences

importatomictest.*

funInt.isEven():Boolean{

trace("$this.isEven()")

returnthis%2==0

}

funInt.square():Int{

trace("$this.square()")

returnthis*this

}

funInt.lessThanTen():Boolean{

trace("$this.lessThanTen()")

returnthis<10

}

funmain(){

vallist=listOf(1,2,3,4)

trace(">>>List:")

trace(

list

.filter(Int::isEven)

.map(Int::square)

.any(Int::lessThanTen)

)

trace(">>>Sequence:")

trace(

list.asSequence()

.filter(Int::isEven)

.map(Int::square)

.any(Int::lessThanTen)

)

traceeq"""

>>>List:

1.isEven()

2.isEven()

3.isEven()

4.isEven()

2.square()

4.square()

4.lessThanTen()

true

>>>Sequence:

1.isEven()

2.isEven()

2.square()

4.lessThanTen()

true

"""

}

TheonlydifferencebetweenthetwoapproachesistheadditionoftheasSequence()call,butmoreelementsareprocessedfortheListcodethantheSequencecode.

Callingeitherfilter()ormap()onaSequenceproducesanotherSequence.Nothinghappensuntilyouaskforaresultfromacalculation.Instead,thenewSequencestoresallinformationaboutpostponedoperationsandwillperformthoseoperationsonlywhenneeded:

//Sequences/NoComputationYet.kt

importatomictest.eq

importsequences.*

funmain(){

valr=listOf(1,2,3,4)

.asSequence()

.filter(Int::isEven)

.map(Int::square)

r.toString().substringBefore("@")eq

"kotlin.sequences.TransformingSequence"

}

ConvertingrtoaStringdoesnotproducetheresultswewant,butjusttheidentifierfortheobject(includingthe@addressoftheobjectinmemory,whichweremoveusingthestandardlibrarysubstringBefore()).TheTransformingSequencejustholdstheoperationsbutdoesnotperformthem.

TherearetwocategoriesofSequenceoperations:intermediateandterminal.IntermediateoperationsreturnanotherSequenceasaresult.filter()andmap()areintermediateoperations.Terminaloperationsreturnanon-Sequence.Todothis,aterminaloperationexecutesallstoredcomputations.Inthepreviousexamples,any()isaterminaloperationbecauseittakesaSequenceandreturnsaBoolean.Inthefollowingexample,toList()isterminalbecauseitconvertstheSequencetoaList,runningallstoredoperationsintheprocess:

//Sequences/TerminalOperations.kt

importsequences.*

importatomictest.*

funmain(){

vallist=listOf(1,2,3,4)

trace(list.asSequence()

.filter(Int::isEven)

.map(Int::square)

.toList())

traceeq"""

1.isEven()

2.isEven()

2.square()

3.isEven()

4.isEven()

4.square()

[4,16]

"""

}

BecauseaSequencestorestheoperations,itcancallthoseoperationsinanyorder,resultinginlazyevaluation.

ThefollowingexampleusesthestandardlibraryfunctiongenerateSequence()toproduceaninfinitesequenceofnaturalnumbers.Thefirstargumentistheinitialelementinthesequence,followedbyalambdadefininghowthenextelementiscalculatedfromthepreviouselement:

//Sequences/GenerateSequence1.kt

importatomictest.eq

funmain(){

valnaturalNumbers=

generateSequence(1){it+1}

naturalNumbers.take(3).toList()eq

listOf(1,2,3)

naturalNumbers.take(10).sum()eq55

}

Collectionsareaknownsize,discoverablethroughtheirsizeproperty.Sequencesaretreatedasiftheyareinfinite.Here,wedecidehowmanyelementswewantusingtake(),followedbyaterminaloperation(toList()orsum()).

There’sanoverloadedversionofgenerateSequence()thatdoesn’trequirethefirstparameter,onlyalambdathatreturnsthenextelementintheSequence.Whentherearenomoreelements,itreturnsnull.ThefollowingexamplegeneratesaSequenceuntilthe“terminationflag”XXXappearsinitsinput:

//Sequences/GenerateSequence2.kt

importatomictest.*

funmain(){

valitems=mutableListOf(

"first","second","third","XXX","4th"

)

valseq=generateSequence{

items.removeAt(0).takeIf{it!="XXX"}

}

seq.toList()eq"[first,second,third]"

capture{

seq.toList()

}eq"IllegalStateException:This"+

"sequencecanbeconsumedonlyonce."

}

removeAt(0)removesandproducesthezeroethelementfromtheList.takeIf()returnsthereceiver(theStringproducedbyremoveAt(0))ifitsatisfiesthegivenpredicate,andnullifthepredicatefails(whentheStringis"XXX").

YoucanonlyiterateoncethroughaSequence.Furtherattemptsproduceanexception.TomakemultiplepassesthroughaSequence,firstconvertittosometypeofCollection.

Here’sanimplementationfortakeIf(),definedusingagenericTsoitcanworkwithanytypeofargument:

//Sequences/DefineTakeIf.kt

packagesequences

importatomictest.eq

fun<T>T.takeIf(

predicate:(T)->Boolean

):T?{

returnif(predicate(this))thiselsenull

}

funmain(){

"abc".takeIf{it!="XXX"}eq"abc"

"XXX".takeIf{it!="XXX"}eqnull

}

Here,generateSequence()andtakeIf()produceadecreasingsequenceofnumbers:

//Sequences/NumberSequence2.kt

importatomictest.eq

funmain(){

generateSequence(6){

(it-1).takeIf{it>0}

}.toList()eqlistOf(6,5,4,3,2,1)

}

AnordinaryifexpressioncanalwaysbeusedinsteadoftakeIf(),butintroducinganextraidentifiercanmaketheifexpressionclumsy.ThetakeIf()versionismorefunctional,especiallyifit’susedasapartofachainofcalls.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

LocalFunctions

Youcandefinefunctionsanywhere—eveninsideotherfunctions.

Namedfunctionsdefinedwithinotherfunctionsarecalledlocalfunctions.Localfunctionsreduceduplicationbyextractingrepetitivecode.Atthesametime,theyareonlyvisiblewithinthesurroundingfunction,sotheydon’t“polluteyournamespace.”Here,eventhoughlog()isdefinedjustlikeanyotherfunction,it’snestedinsidemain():

//LocalFunctions/LocalFunctions.kt

importatomictest.eq

funmain(){

vallogMsg=StringBuilder()

funlog(message:String)=

logMsg.appendLine(message)

log("Startingcomputation")

valx=42//Imitatecomputation

log("Computationresult:$x")

logMsg.toString()eq"""

Startingcomputation

Computationresult:42

"""

}

Localfunctionsareclosures:theycapturevarsorvalsfromthesurroundingenvironmentthatwouldotherwisehavetobepassedasadditionalparameters.log()useslogMsg,whichisdefinedinitsouterscope.Thisway,youdon’trepeatedlypasslogMsgintolog().

Youcancreatelocalextensionfunctions:

//LocalFunctions/LocalExtensions.kt

importatomictest.eq

funmain(){

funString.exclaim()="$this!"

"Hello".exclaim()eq"Hello!"

"Hallo".exclaim()eq"Hallo!"

"Bonjour".exclaim()eq"Bonjour!"

"Ciao".exclaim()eq"Ciao!"

}

exclaim()isavailableonlyinsidemain().

Hereisademonstrationclassandexamplevaluesforuseinthisatom:

//LocalFunctions/Session.kt

packagelocalfunctions

classSession(

valtitle:String,

valspeaker:String

)

valsessions=listOf(Session(

"KotlinCoroutines","RomanElizarov"))

valfavoriteSpeakers=setOf("RomanElizarov")

Youcanrefertoalocalfunctionusingafunctionreference:

//LocalFunctions/LocalFunctionReference.kt

importlocalfunctions.*

importatomictest.eq

funmain(){

funinteresting(session:Session):Boolean{

if(session.title.contains("Kotlin")&&

session.speakerinfavoriteSpeakers){

returntrue

}

//...morechecks

returnfalse

}

sessions.any(::interesting)eqtrue

}

interesting()isonlyusedonce,sowemightbeinclinedtodefineitasalambda.Asyouwillseelaterinthisatom,thereturnexpressionswithininteresting()complicatethetaskofturningitintoalambda.Wecanavoidthiscomplicationwithananonymousfunction.Likelocalfunctions,anonymousfunctionsaredefinedwithinotherfunctions—however,ananonymousfunctionhasnoname.Anonymousfunctionsareconceptuallysimilartolambdasbutusethefunkeyword.Here’sLocalFunctionReference.ktrewrittenusingananonymousfunction:

//LocalFunctions/InterestingSessions.kt

importlocalfunctions.*

importatomictest.eq

funmain(){

sessions.any(

fun(session:Session):Boolean{//[1]

if(session.title.contains("Kotlin")&&

session.speakerinfavoriteSpeakers){

returntrue

}

//...morechecks

returnfalse

})eqtrue

}

[1]Ananonymousfunctionlookslikearegularfunctionwithoutafunctionname.Here,theanonymousfunctionispassedasanargumenttosessions.any().

Ifalambdabecomestoocomplicatedandhardtoread,replaceitwithalocalfunctionorananonymousfunction.

LabelsHere,forEach()actsuponalambdacontainingareturn:

//LocalFunctions/ReturnFromFun.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4,5)

valvalue=3

varresult=""

list.forEach{

result+="$it"

if(it==value){

resulteq"123"

return//[1]

}

}

resulteq"Nevergetshere"//[2]

}

Areturnexpressionexitsafunctiondefinedusingfun(thatis,notalambda).Inline[1]thismeansreturningfrommain().Line[2]isnevercalledandyouseenooutput.

Toreturnonlyfromalambda,andnotfromthesurroundingfunction,usealabeledreturn:

//LocalFunctions/LabeledReturn.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4,5)

valvalue=3

varresult=""

list.forEach{

result+="$it"

if(it==value)return@forEach

}

resulteq"12345"

}

Here,thelabelisthenameofthefunctionthatcalledthelambda.Thelabeledreturnexpressionreturn@forEachtellsittoreturnonlytothenameforEach.

Youcancreatealabelbypreceedingthelambdawithlabel@,wherelabelcanbeanyname:

//LocalFunctions/CustomLabel.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3,4,5)

valvalue=3

varresult=""

list.forEachtag@{//[1]

result+="$it"

if(it==value)return@tag//[2]

}

resulteq"12345"

}

[1]Thislambdaislabeledtag.[2]return@tagreturnsfromthelambda,notfrommain().

Let’sreplacetheanonymousfunctioninInterestingSessions.ktwithalambda:

//LocalFunctions/ReturnInsideLambda.kt

importlocalfunctions.*

importatomictest.eq

funmain(){

sessions.any{session->

if(session.title.contains("Kotlin")&&

session.speakerinfavoriteSpeakers){

return@anytrue

}

//...morechecks

false

}eqtrue

}

Wemustreturntoalabelsoitexitsonlythelambdaandnotmain().

ManipulatingLocalFunctionsYoucanstorealambdaorananonymousfunctioninavarorval,thenusethatidentifiertocallthefunction.Tostorealocalfunction,useafunctionreference(seeMemberReferences).

Inthefollowingexample,first()createsananonymousfunction,second()usesalambda,andthird()returnsareferencetoalocalfunction.fourth()achievesthesameeffectasthird()butusesamorecompactexpressionbody.fifth()producesthesameeffectusingalambda:

//LocalFunctions/ReturningFunc.kt

packagelocalfunctions

importatomictest.eq

funfirst():(Int)->Int{

valfunc=fun(i:Int)=i+1

func(1)eq2

returnfunc

}

funsecond():(String)->String{

valfunc2={s:String->"$s!"}

func2("abc")eq"abc!"

returnfunc2

}

funthird():()->String{

fungreet()="Hi!"

return::greet

}

funfourth()=fun()="Hi!"

funfifth()={"Hi!"}

funmain(){

valfunRef1:(Int)->Int=first()

valfunRef2:(String)->String=second()

valfunRef3:()->String=third()

valfunRef4:()->String=fourth()

valfunRef5:()->String=fifth()

funRef1(42)eq43

funRef2("xyz")eq"xyz!"

funRef3()eq"Hi!"

funRef4()eq"Hi!"

funRef5()eq"Hi!"

first()(42)eq43

second()("xyz")eq"xyz!"

third()()eq"Hi!"

fourth()()eq"Hi!"

fifth()()eq"Hi!"

}

main()firstverifiesthatcallingeachfunctiondoesindeedreturnafunctionreferenceoftheexpectedtype.EachfunRefisthencalledwithanappropriateargument.Finally,eachfunctioniscalledandthenthereturnedfunctionreferenceisimmediatelycalledbyaddinganappropriateargumentlist.Forexample,callingfirst()returnsafunction,sowecallthatfunctionbyappendingtheargumentlist(42).

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

FoldingLists

fold()combinesallelementsofalist,inorder,togenerateasingleresult.

Acommonexerciseistoimplementoperationssuchassum()orreverse()usingfold().Here,fold()sumsasequence:

//FoldingLists/SumViaFold.kt

importatomictest.eq

funmain(){

vallist=listOf(1,10,100,1000)

list.fold(0){sum,n->

sum+n

}eq1111

}

fold()takestheinitialvalue(itsargument,0inthiscase)andsuccessivelyappliestheoperation(expressedhereasalambda)tocombinethecurrentaccumulatedvaluewitheachelement.fold()firstadds0(theinitialvalue)and1toget1.Thatbecomesthesum,whichisthenaddedtothe10toget11,whichbecomesthenewsum.Theoperationisrepeatedfortwomoreelements:100and1000.Thisproduces111and1111.Thefold()willstopwhenthereisnothingelseinthelist,returningthefinalsumof1111.Ofcourse,fold()doesn’treallyknowit’sdoinga“sum”—thechoiceofidentifiernamewasours,tomakeiteasiertounderstand.

Toilluminatethestepsinafold(),here’sSumViaFold.ktusinganordinaryforloop:

//FoldingLists/FoldVsForLoop.kt

importatomictest.eq

funmain(){

vallist=listOf(1,10,100,1000)

varaccumulator=0

valoperation=

{sum:Int,i:Int->sum+i}

for(iinlist){

accumulator=operation(accumulator,i)

}

accumulatoreq1111

}

fold()accumulatesvaluesbysuccessivelyapplyingoperationtocombinethecurrentelementwiththeaccumulatorvalue.

Althoughfold()isanimportantconceptandtheonlywaytoaccumulatevaluesinpurefunctionallanguages,youmaysometimesstilluseanordinaryforloopinKotlin.

foldRight()processeselementsstartingfromrighttoleft,asopposedtofold()whichprocessestheelementsfromlefttoright.Thisexampledemonstratesthedifference:

//FoldingLists/FoldRight.kt

importatomictest.eq

funmain(){

vallist=listOf('a','b','c','d')

list.fold("*"){acc,elem->

"($acc)+$elem"

}eq"((((*)+a)+b)+c)+d"

list.foldRight("*"){elem,acc->

"$elem+($acc)"

}eq"a+(b+(c+(d+(*))))"

}

fold()firstappliestheoperationtoa,aswecanseein(*)+a,whilefoldRight()firstprocessestheright-handelementd,andprocessesalast.

fold()andfoldRight()takeanexplicitaccumulatorvalueasthefirstargument.Sometimesthefirstelementcanactasaninitialvalue.reduce()andreduceRight()behavelikefold()andfoldRight()butusethefirstandlastelement,respectively,astheinitialvalue:

//FoldingLists/ReduceAndReduceRight.kt

importatomictest.eq

funmain(){

valchars="ABCDEFGHI".split("")

chars.fold("X"){a,e->"$a$e"}eq

"XABCDEFGHI"

chars.foldRight("X"){a,e->"$a$e"}eq

"ABCDEFGHIX"

chars.reduce{a,e->"$a$e"}eq

"ABCDEFGHI"

chars.reduceRight{a,e->"$a$e"}eq

"ABCDEFGHI"

}

runningFold()andrunningReduce()produceaListcontainingalltheintermediatestepsoftheprocess.ThefinalvalueintheLististheresultofthefold()orreduce():

//FoldingLists/RunningFold.kt

importatomictest.eq

funmain(){

vallist=listOf(11,13,17,19)

list.fold(7){sum,n->

sum+n

}eq67

list.runningFold(7){sum,n->

sum+n

}eq"[7,18,31,48,67]"

list.reduce{sum,n->

sum+n

}eq60

list.runningReduce{sum,n->

sum+n

}eq"[11,24,41,60]"

}

runningFold()firststorestheinitialvalue(7),thenstoreseachintermediateresult.runningReduce()keepstrackofeachsumvalue.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Recursion

Recursionistheprogrammingtechniqueofcallingafunctionwithinthatsamefunction.Tailrecursionisanoptimizationthatcanbeexplicitlyappliedtosomerecursivefunctions.

Arecursivefunctionusestheresultofthepreviousrecursivecall.Factorialsareacommonexample—factorial(n)multipliesallnumbersfrom1ton,andcanbedefinedlikethis:

factorial(1)is1factorial(n)isn*factorial(n-1)

factorial()isrecursivebecauseitusestheresultfromthesamefunctionappliedtoitsmodifiedargument.Here’sarecursiveimplementationoffactorial():

//Recursion/Factorial.kt

packagerecursion

importatomictest.eq

funfactorial(n:Long):Long{

if(n<=1)return1

returnn*factorial(n-1)

}

funmain(){

factorial(5)eq120

factorial(17)eq355687428096000

}

Whilethisiseasytoread,it’sexpensive.Whencallingafunction,theinformationaboutthatfunctionanditsargumentsarestoredinacallstack.YouseethecallstackwhenanexceptionisthrownandKotlindisplaysthestacktrace:

//Recursion/CallStack.kt

packagerecursion

funillegalState(){

//throwIllegalStateException()

}

funfail()=illegalState()

funmain(){

fail()

}

Ifyouuncommentthelinecontainingtheexception,you’llseethefollowing:

Exceptioninthread"main"java.lang.IllegalStateException

atrecursion.CallStackKt.illegalState(CallStack.kt:5)

atrecursion.CallStackKt.fail(CallStack.kt:8)

atrecursion.CallStackKt.main(CallStack.kt:11)

Thestacktracedisplaysthestateofthecallstackatthemomenttheexceptionisthrown.ForCallStack.kt,thecallstackconsistsofonlythreefunctions:

TheCallStack

Westartinmain(),whichcallsfail().Thefail()callisaddedtothecallstackalongwithitsarguments.Next,fail()callsillegalState(),whichisalsoaddedtothecallstack.

Whenyoucallarecursivefunction,eachrecursiveinvocationaddsaframetothecallstack.ThiscaneasilyproduceaStackOverflowError,whichmeansthatyourcallstackbecametoolargeandexhaustedtheavailablememory.

ProgrammerscommonlycauseStackOverflowErrorsbyforgettingtoterminatethechainofrecursivecalls—thisisinfiniterecursion:

//Recursion/InfiniteRecursion.kt

packagerecursion

funrecurse(i:Int):Int=recurse(i+1)

funmain(){

//println(recurse(1))

}

Ifyouuncommentthelineinmain(),you’llseeastacktracewithmanyduplicatecalls:

Exceptioninthread"main"java.lang.StackOverflowError

atrecursion.InfiniteRecursionKt.recurse(InfiniteRecursion.kt:4)

atrecursion.InfiniteRecursionKt.recurse(InfiniteRecursion.kt:4)

...

atrecursion.InfiniteRecursionKt.recurse(InfiniteRecursion.kt:4)

Therecursivefunctionkeepscallingitself(withadifferentargumenteachtime),andfillsupthecallstack:

InfiniteRecursion

InfiniterecursionalwaysendswithaStackOverflowError,butyoucanproducethesameresultwithoutinfiniterecursion,simplybymakingenoughrecursivefunctioncalls.Forexample,let’ssumtheintegersuptoagivennumber,recursivelydefiningsum(n)asn+sum(n-1):

//Recursion/RecursionLimits.kt

packagerecursion

importatomictest.eq

funsum(n:Long):Long{

if(n==0L)return0

returnn+sum(n-1)

}

funmain(){

sum(2)eq3

sum(1000)eq500500

//sum(100_000)eq500050000//[1]

(1..100_000L).sum()eq5000050000//[2]

}

Thisrecursionquicklybecomesexpensive.Ifyouuncommentline[1],you’lldiscoverthatittakesfartoolongtocomplete,andallthoserecursivecallsoverflowthestack.Ifsum(100_000)stillworksonyourmachine,tryabiggernumber.

Callingsum(100_000)causesaStackOverflowErrorbyadding100_000sum()functioncallstothecallstack.Forcomparison,line[2]usesthesum()libraryfunctiontoaddthenumberswithintherange,andthisdoesnotfail.

ToavoidaStackOverflowError,youcanuseaniterativesolutioninsteadofrecursion:

//Recursion/Iteration.kt

packageiteration

importatomictest.eq

funsum(n:Long):Long{

varaccumulator=0L

for(iin1..n){

accumulator+=i

}

returnaccumulator

}

funmain(){

sum(10000)eq50005000

sum(100000)eq5000050000

}

There’snoriskofaStackOverflowErrorbecauseweonlymakeasinglesum()callandtheresultiscalculatedinaforloop.Althoughtheiterativesolutionisstraightforward,itmustusethemutablestatevariableaccumulatortostorethechangingvalue,andfunctionalprogrammingattemptstoavoidmutation.

Topreventcallstackoverflows,functionallanguages(includingKotlin)useatechniquecalledtailrecursion.Thegoaloftailrecursionistoreducethesizeofthecallstack.Inthesum()example,thecallstackbecomesasinglefunctioncall,justasitdidinIteration.kt:

RegularRecursionvs.TailRecursion

Toproducetailrecursion,usethetailreckeyword.Undertherightconditions,thisconvertsrecursivecallsintoiteration,eliminatingcall-stackoverhead.Thisisacompileroptimization,butitwon’tworkforjustanyrecursivecall.

Tousetailrecsuccessfully,recursionmustbethefinaloperation,whichmeanstherecanbenoextracalculationsontheresultoftherecursivecallbeforeitisreturned.Forexample,ifwesimplyputtailrecbeforethefunforsum()inRecursionLimits.kt,Kotlinproducesthefollowingwarningmessages:

Afunctionismarkedastail-recursivebutnotailcallsarefoundRecursivecallisnotatailcall

Theproblemisthatniscombinedwiththeresultoftherecursivesum()callbeforereturningthatresult.Fortailrectobesuccessful,theresultoftherecursivecallmustbereturnedwithoutdoinganythingtoitduringthereturn.Thisoftenrequiressomeworkinrearrangingthefunction.Forsum(),asuccessfultailreclookslikethis:

//Recursion/TailRecursiveSum.kt

packagetailrecursion

importatomictest.eq

privatetailrecfunsum(

n:Long,

accumulator:Long

):Long=

if(n==0L)accumulator

elsesum(n-1,accumulator+n)

funsum(n:Long)=sum(n,0)

funmain(){

sum(2)eq3

sum(10000)eq50005000

sum(100000)eq5000050000

}

Byincludingtheaccumulatorparameter,theadditionhappensduringtherecursivecallandyoudon’tdoanythingtotheresultexceptreturnit.Thetailreckeywordisnowsuccessful,becausethecodewasrewrittentodelegateallactivitiestotherecursivecall.Inaddition,accumulatorbecomesanimmutablevalue,eliminatingthecomplaintwehadforIteration.kt.

factorial()isacommonexamplefordemonstratingtailrecursion,andisoneoftheexercisesforthisatom.AnotherexampleistheFibonaccisequence,whereeachnewFibonaccinumberisthesumoftheprevioustwo.Thefirsttwonumbersare0and1,whichproducesthefollowingsequence:0,1,1,2,3,5,8,13,21...Thiscanbeexpressedrecursively:

//Recursion/VerySlowFibonacci.kt

packageslowfibonacci

importatomictest.eq

funfibonacci(n:Long):Long{

returnwhen(n){

0L->0

1L->1

else->

fibonacci(n-1)+fibonacci(n-2)

}

}

funmain(){

fibonacci(0)eq0

fibonacci(22)eq17711

//Verytime-consuming:

//fibonacci(50)eq12586269025

}

Thisimplementationisterriblyinefficientbecausethepreviously-calculatedresultsarenotreused.Thus,thenumberofoperationsgrowsexponentially:

InefficientComputationofFibonacciNumbers

Whencomputingthe50thFibonaccinumber,wefirstcomputethe49thand48thnumbersindependently,whichmeanswecomputethe48thnumbertwice.The46thnumberiscomputedasmanyas4times,andsoon.

Usingtailrecursion,thecalculationsbecomedramaticallymoreefficient:

//Recursion/Fibonacci.kt

packagerecursion

importatomictest.eq

funfibonacci(n:Int):Long{

tailrecfunfibonacci(

n:Int,

current:Long,

next:Long

):Long{

if(n==0)returncurrent

returnfibonacci(

n-1,next,current+next)

}

returnfibonacci(n,0L,1L)

}

funmain(){

(0..8).map{fibonacci(it)}eq

"[0,1,1,2,3,5,8,13,21]"

fibonacci(22)eq17711

fibonacci(50)eq12586269025

}

Wecouldavoidthelocalfibonacci()functionusingdefaultarguments.However,defaultargumentsimplythattheusercanputothervaluesinthosedefaults,whichproduceincorrectresults.Becausetheauxiliaryfibonacci()functionisalocalfunction,wedon’texposetheadditionalparameters,andyoucanonlycallfibonacci(n).

main()showsthefirsteightelementsoftheFibonaccisequence,theresultfor22,andfinallythe50thFibonaccinumberthatisnowproducedveryquickly.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SECTIONV:OBJECT-ORIENTED

PROGRAMMING

…inheritanceisaveryflexiblemechanism.It’spossibleandinfactfairlycommontomisuseit,butthat’snotareasontodistrustitsystematicallyasseemstohavebecomethefashion.—BertrandMeyer

Interfaces

Aninterfacedescribestheconceptofatype.Itisaprototypeforallclassesthatimplementtheinterface.

Itdescribeswhataclassshoulddo,butnothowitshoulddoit.Aninterfaceprovidesaform,butgenerallynoimplementation.Itspecifiesanobject’sactionswithoutdetailinghowthoseactionsareperformed.Theinterfacedescribesthemissionorgoalofanentity,versusaclassthatcontainsimplementationdetails.

Onedictionarydefinitionsaysthataninterfaceis“Theplaceatwhichindependentandoftenunrelatedsystemsmeetandactonorcommunicatewitheachother.”Thus,aninterfaceisameansofcommunicationbetweendifferentpartsofasystem.

AnApplicationProgrammingInterface(API)isasetofclearlydefinedcommunicationpathsbetweenvarioussoftwarecomponents.Inobject-orientedprogramming,theAPIofanobjectisthesetofpublicmembersitusestointeractwithotherobjects.

Codeusingaparticularinterfaceonlyknowswhatfunctionscanbecalledforthatinterface.Theinterfaceestablishesa“protocol”betweenclasses.(Someobject-orientedlanguageshaveakeywordcalledprotocoltodothesamething.)

Tocreateaninterface,usetheinterfacekeywordinsteadoftheclasskeyword.Whendefiningaclassthatimplementsaninterface,followtheclassnamewitha:(colon)andthenameoftheinterface:

//Interfaces/Computer.kt

packageinterfaces

importatomictest.*

interfaceComputer{

funprompt():String

funcalculateAnswer():Int

}

classDesktop:Computer{

overridefunprompt()="Hello!"

overridefuncalculateAnswer()=11

}

classDeepThought:Computer{

overridefunprompt()="Thinking..."

overridefuncalculateAnswer()=42

}

classQuantum:Computer{

overridefunprompt()="Probably..."

overridefuncalculateAnswer()=-1

}

funmain(){

valcomputers=listOf(

Desktop(),DeepThought(),Quantum()

)

computers.map{it.calculateAnswer()}eq

"[11,42,-1]"

computers.map{it.prompt()}eq

"[Hello!,Thinking...,Probably...]"

}

Computerdeclaresprompt()andcalculateAnswer()butprovidesnoimplementations.Aclassthatimplementstheinterfacemustprovidebodiesforallthedeclaredfunctions,makingthosefunctionsconcrete.Inmain()youseethatdifferentimplementationsofaninterfaceexpressdifferentbehaviorsviatheirfunctiondefinitions.

Whenimplementingamemberofaninterface,youmustusetheoverridemodifier.overridetellsKotlinyouareintentionallyusingthesamenamethatappearsintheinterface(orbaseclass)—thatis,youaren’taccidentallyoverriding.

Aninterfacecandeclareproperties.Thesemustbeoverriddeninallclassesimplementingthatinterface:

//Interfaces/PlayerInterface.kt

packageinterfaces

importatomictest.eq

interfacePlayer{

valsymbol:Char

}

classFood:Player{

overridevalsymbol='.'

}

classRobot:Player{

overridevalsymbolget()='R'

}

classWall(overridevalsymbol:Char):Player

funmain(){

listOf(Food(),Robot(),Wall('|')).map{

it.symbol

}eq"[.,R,|]"

}

Eachsubclassoverridesthesymbolpropertyinadifferentway:

Fooddirectlyreplacesthesymbolvalue.Robothasacustomgetterthatreturnsthevalue(seePropertyAccessors).Walloverridessymbolinsidetheconstructorargumentlist(seeConstructors)

Anenumerationcanimplementaninterface:

//Interfaces/Hotness.kt

packageinterfaces

importatomictest.*

interfaceHotness{

funfeedback():String

}

enumclassSpiceLevel:Hotness{

Mild{

overridefunfeedback()=

"Itaddsflavor!"

},

Medium{

overridefunfeedback()=

"Isitwarminhere?"

},

Hot{

overridefunfeedback()=

"I'msuddenlysweatingalot."

},

Flaming{

overridefunfeedback()=

"I'minpain.Iamsuffering."

}

}

funmain(){

SpiceLevel.values().map{it.feedback()}eq

"[Itaddsflavor!,"+

"Isitwarminhere?,"+

"I'msuddenlysweatingalot.,"+

"I'minpain.Iamsuffering.]"

}

Thecompilerensuresthateachenumelementprovidesadefinitionforfeedback().

SAMConversions

TheSingleAbstractMethod(SAM)interfacecomesfromJava,wheretheycallmemberfunctions“methods.”KotlinhasaspecialsyntaxfordefiningSAMinterfaces:funinterface.HereweshowSAMinterfaceswithdifferentparameterlists:

//Interfaces/SAM.kt

packageinterfaces

funinterfaceZeroArg{

funf():Int

}

funinterfaceOneArg{

fung(n:Int):Int

}

funinterfaceTwoArg{

funh(i:Int,j:Int):Int

}

Whenyousayfuninterface,thecompilerensuresthereisonlyasinglememberfunction.

YoucanimplementaSAMinterfaceintheordinaryverboseway,orbypassingitalambda;thelatteriscalledaSAMconversion.InaSAMconversion,thelambdabecomestheimplementationforthesinglemethodintheinterface.Hereweshowbothwaystoimplementthethreeinterfaces:

//Interfaces/SAMImplementation.kt

packageinterfaces

importatomictest.eq

classVerboseZero:ZeroArg{

overridefunf()=11

}

valverboseZero=VerboseZero()

valsamZero=ZeroArg{11}

classVerboseOne:OneArg{

overridefung(n:Int)=n+47

}

valverboseOne=VerboseOne()

valsamOne=OneArg{it+47}

classVerboseTwo:TwoArg{

overridefunh(i:Int,j:Int)=i+j

}

valverboseTwo=VerboseTwo()

valsamTwo=TwoArg{i,j->i+j}

funmain(){

verboseZero.f()eq11

samZero.f()eq11

verboseOne.g(92)eq139

samOne.g(92)eq139

verboseTwo.h(11,47)eq58

samTwo.h(11,47)eq58

}

Comparingthe“verbose”implementationstothe“sam”implementationsyoucanseethatSAMconversionsproducemuchmoresuccinctsyntaxforacommonly-usedidiom,andyouaren’tforcedtodefineaclasstocreateasingleobject.

YoucanpassalambdawhereaSAMinterfaceisexpected,withoutfirstwrappingitintoanobject:

//Interfaces/SAMConversion.kt

packageinterfaces

importatomictest.trace

funinterfaceAction{

funact()

}

fundelayAction(action:Action){

trace("Delaying...")

action.act()

}

funmain(){

delayAction{trace("Hey!")}

traceeq"Delaying...Hey!"

}

Inmain()wepassalambdainsteadofanobjectthatimplementstheActioninterface.KotlinautomaticallycreatesanActionobjectfromthislambda.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ComplexConstructors

Forcodetoworkcorrectly,objectsmustbeproperlyinitialized.

Aconstructorisaspecialfunctionthatcreatesanewobject.InConstructors,wesawsimpleconstructorsthatonlyinitializetheirarguments.Usingvarorvalintheparameterlistmakesthoseparametersproperties,accessiblefromoutsidetheobject:

//ComplexConstructors/SimpleConstructor.kt

packagecomplexconstructors

importatomictest.eq

classAlien(valname:String)

funmain(){

valalien=Alien("Pencilvester")

alien.nameeq"Pencilvester"

}

Inthesecases,wedon’twriteconstructorcode—Kotlindoesitforus.Formorecustomization,addconstructorcodeintheclassbody.Codeinsidetheinitsectionisexecutedduringobjectcreation:

//ComplexConstructors/InitSection.kt

packagecomplexconstructors

importatomictest.eq

privatevarcounter=0

classMessage(text:String){

privatevalcontent:String

init{

counter+=10

content="[$counter]$text"

}

overridefuntoString()=content

}

funmain(){

valm1=Message("Bigba-daboom!")

m1eq"[10]Bigba-daboom!"

valm2=Message("Bzzzzt!")

m2eq"[20]Bzzzzt!"

}

Constructorparametersareaccessibleinsidetheinitsectioneveniftheyaren’tmarkedaspropertiesusingvarorval.

Althoughdefinedasval,contentisnotinitializedatthepointofdefinition.Inthiscase,Kotlinensuresthatinitializationoccursatone(andonlyone)pointduringconstruction.Eitherreassigningcontentorforgettingtoinitializeitproducesanerrormessage.

-

Aconstructoristhecombinationofitsconstructorparameterlist—initializedbeforeenteringtheclassbody—andtheinitsection(s),executedduringobjectcreation.Kotlinallowsmultipleinitsections,whichareexecutedindefinitionorder.However,inalargeandcomplexclass,spreadingouttheinitsectionsmayproducemaintenanceissuesforprogrammerswhoareaccustomedtoasingleinitsection.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SecondaryConstructors

Whenyourequireseveralwaystoconstructanobject,namedanddefaultargumentsareusuallytheeasiestapproach.Sometimes,however,youmustcreatemultipleoverloadedconstructors.

Theconstructoris“overloaded”becauseyou’remakingdifferentwaystocreateobjectsofthesameclass.InKotlin,overloadedconstructorsarecalledsecondaryconstructors.Theconstructorparameterlist(directlyaftertheclassname)combinedwithpropertyinitializationsandtheinitblockiscalledtheprimaryconstructor.

Tocreateasecondaryconstructor,usetheconstructorkeywordfollowedbyaparameterlistthat’sdistinctfromallotherprimaryandsecondaryparameterlists.Withinasecondaryconstructor,thethiskeywordcallseithertheprimaryconstructororanothersecondaryconstructor:

//SecondaryConstructors/WithSecondary.kt

packagesecondaryconstructors

importatomictest.*

classWithSecondary(i:Int){

init{

trace("Primary:$i")

}

constructor(c:Char):this(c-'A'){

trace("Secondary:'$c'")

}

constructor(s:String):

this(s.first()){//[1]

trace("Secondary:\"$s\"")

}

/*Doesn'tcompilewithoutacall

totheprimaryconstructor:

constructor(f:Float){//[2]

trace("Secondary:$f")

}

*/

}

funmain(){

funsep()=trace("-".repeat(10))

WithSecondary(1)

sep()

WithSecondary('D')

sep()

WithSecondary("LastConstructor")

traceeq"""

Primary:1

----------

Primary:3

Secondary:'D'

----------

Primary:11

Secondary:'L'

Secondary:"LastConstructor"

"""

}

Callinganotherconstructorfromasecondaryconstructor(usingthis)musthappenbeforeadditionalconstructorlogic,becausetheconstructorbodymaydependonthoseotherinitializations.Thusitprecedestheconstructorbody.

Theargumentlistdeterminestheconstructortocall.WithSecondary(1)matchestheprimaryconstructor,WithSecondary('D')matchesthefirstsecondaryconstructor,andWithSecondary("LastConstructor")matchesthesecondsecondaryconstructor.Thethis()callin[1]matchesthefirstsecondaryconstructor,andyoucanseethechainofcallsintheoutput.

Theprimaryconstructormustalwaysbecalled,eitherdirectlyorthroughacalltoasecondaryconstructor.Otherwise,Kotlingeneratesacompile-timeerror,asin[2].Thus,allcommoninitializationlogicthatcanbesharedbetweenconstructorsshouldbeplacedintheprimaryconstructor.

Aninitsectionisnotrequiredwhenusingsecondaryconstructors:

//SecondaryConstructors/GardenItem.kt

packagesecondaryconstructors

importatomictest.eq

importsecondaryconstructors.Material.*

enumclassMaterial{

Ceramic,Metal,Plastic

}

classGardenItem(valname:String){

varmaterial:Material=Plastic

constructor(

name:String,material:Material//[1]

):this(name){//[2]

this.material=material//[3]

}

constructor(

material:Material

):this("StrangeThing",material)//[4]

overridefuntoString()="$material$name"

}

funmain(){

GardenItem("Elf").materialeqPlastic

GardenItem("Snowman").nameeq"Snowman"

GardenItem("GazingBall",Metal)eq//[5]

"MetalGazingBall"

GardenItem(material=Ceramic)eq

"CeramicStrangeThing"

}

[1]Onlytheparametersoftheprimaryconstructorcanbedeclaredaspropertiesviavalorvar.[2]Youcannotdeclareareturntypeforasecondaryconstructor.[3]Thematerialparameterhasthesamenameasaproperty,sowedisambiguateitusingthis.[4]Thesecondaryconstructorbodyisoptional(althoughyoumuststillincludeanexplicitthis()call).

Whencallingthefirstsecondaryconstructorinline[5],thepropertymaterialisassignedtwice.First,thePlasticvalueisassignedduringthecalltotheprimaryconstructor(in[2])andinitializationofalltheclassproperties,thenit’schangedtothematerialparameterat[3].

TheGardenItemclasscanbesimplifiedusingdefaultarguments,replacingthesecondaryconstructorswithasingleprimaryconstructor.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Inheritance

Inheritanceisamechanismforcreatinganewclassbyreusingandmodifyinganexistingclass.

Objectsstoredatainpropertiesandperformactionsviamemberfunctions.Eachobjectoccupiesauniqueplaceinstoragesooneobject’spropertiescanhavedifferentvaluesfromeveryotherobject.Anobjectalsobelongstoacategorycalledaclass,whichdeterminestheform(propertiesandfunctions)foritsobjects.Thus,anobjectlooksliketheclassthatformedit.

Creatinganddebuggingaclasscanrequireextensivework.Whatifyouwanttomakeaclassthat’ssimilartoanexistingclass,butwithsomevariations?Itseemswastefultobuildanewclassfromscratch.Object-orientedlanguagesprovideamechanismforreusecalledinheritance.

Inheritancefollowstheconceptofbiologicalinheritance.Yousay,“Iwanttomakeanewclassfromanexistingclass,butwithsomeadditionsandmodifications.”

Thesyntaxforinheritanceissimilartoimplementinganinterface.ToinheritanewclassDerivedfromanexistingclassBase,usea:(colon):

//Inheritance/BasicInheritance.kt

packageinheritance

openclassBase

classDerived:Base()

ThesubsequentatomexplainsthereasonfortheparenthesesafterBaseduringinheritance.

Thetermsbaseclassandderivedclass(orparentclassandchildclass,orsuperclassandsubclass)areoftenusedtodescribetheinheritancerelationship.

Thebaseclassmustbeopen.Anon-openclassdoesn’tallowinheritance—itisclosedbydefault.Thisdiffersfrommostotherobject-orientedlanguages.In

Java,forexample,aclassisautomaticallyinheritableunlessyouexplicitlyforbidinheritancebydeclaringthatclasstobefinal.AlthoughKotlinallowsit,thefinalmodifierisredundantbecauseeveryclassiseffectivelyfinalbydefault:

//Inheritance/OpenAndFinalClasses.kt

packageinheritance

//Thisclasscanbeinherited:

openclassParent

classChild:Parent()

//Childisnotopen,sothisfails:

//classGrandChild:Child()

//Thisclasscan'tbeinherited:

finalclassSingle

//Thesameasusing'final':

classAnotherSingle

Kotlinforcesyoutoclarifyyourintentbyusingtheopenkeywordtospecifythataclassisdesignedforinheritance.

Inthefollowingexample,GreatApeisabaseclass,andhastwopropertieswithfixedvalues.ThederivedclassesBonobo,ChimpanzeeandBonoboBarenewtypesthatareidenticaltotheirparentclass:

//Inheritance/GreatApe.kt

packageinheritance.ape1

importatomictest.eq

openclassGreatApe{

valweight=100.0

valage=12

}

openclassBonobo:GreatApe()

classChimpanzee:GreatApe()

classBonoboB:Bonobo()

funGreatApe.info()="wt:$weightage:$age"

funmain(){

GreatApe().info()eq"wt:100.0age:12"

Bonobo().info()eq"wt:100.0age:12"

Chimpanzee().info()eq"wt:100.0age:12"

BonoboB().info()eq"wt:100.0age:12"

}

info()isanextensionforGreatApe,sonaturallyyoucancallitonaGreatApe.Butnoticethatyoucanalsocallinfo()onaBonobo,aChimpanzee,oraBonoboB!Eventhoughthelatterthreearedistincttypes,Kotlinhappilyaccepts

themasiftheywerethesametypeasGreatApe.Thisworksatanylevelofinheritance—BonoboBistwoinheritancelevelsawayfromGreatApe.

InheritanceguaranteesthatanythinginheritingfromGreatApeisaGreatApe.AllcodethatactsuponobjectsofthederivedclassesknowsthatGreatApeisattheircore,soanyfunctionsandpropertiesinGreatApewillalsobeavailableinitschildclasses.

Inheritanceenablesyoutowriteasinglepieceofcode(theinfo()function)thatworksnotjustwithoneclass,butalsowitheveryclassthatinheritsthatclass.Thus,inheritancecreatesopportunitiesforcodesimplificationandreuse.

GreatApe.ktisabittoosimplebecausealltheclassesareidentical.Inheritancegetsinterestingwhenyoustartoverridingfunctions,whichmeansredefiningafunctionfromabaseclasstodosomethingdifferentinaderivedclass.

Let’slookatanotherversionofGreatApe.kt.Thistimeweincludememberfunctionsthataremodifiedinthesubclasses:

//Inheritance/GreatApe2.kt

packageinheritance.ape2

importatomictest.eq

openclassGreatApe{

protectedvarenergy=0

openfuncall()="Hoo!"

openfuneat(){

energy+=10

}

funclimb(x:Int){

energy-=x

}

funenergyLevel()="Energy:$energy"

}

classBonobo:GreatApe(){

overridefuncall()="Eep!"

overridefuneat(){

//Modifythebase-classvar:

energy+=10

//Callthebase-classversion:

super.eat()

}

//Addafunction:

funrun()="Bonoborun"

}

classChimpanzee:GreatApe(){

//Newproperty:

valadditionalEnergy=20

overridefuncall()="Yawp!"

overridefuneat(){

energy+=additionalEnergy

super.eat()

}

//Addafunction:

funjump()="Chimpjump"

}

funtalk(ape:GreatApe):String{

//ape.run()//Notanapefunction

//ape.jump()//Northis

ape.eat()

ape.climb(10)

return"${ape.call()}${ape.energyLevel()}"

}

funmain(){

//Cannotaccess'energy':

//GreatApe().energy

talk(GreatApe())eq"Hoo!Energy:0"

talk(Bonobo())eq"Eep!Energy:10"

talk(Chimpanzee())eq"Yawp!Energy:20"

}

EveryGreatApehasacall().Theystoreenergywhentheyeat()andtheyexpendenergywhentheyclimb().

AsdescribedinConstrainingVisibility,thederivedclasscan’taccesstheprivatemembersofthebaseclass.Sometimesthecreatorofthebaseclasswouldliketotakeaparticularmemberandgrantaccesstoderivedclassesbutnottotheworldingeneral.That’swhatprotecteddoes:protectedmembersareclosedtotheoutsideworld,butcanbeaccessedoroverriddeninsubclasses.

Ifwedeclareenergyasprivate,itwon’tbepossibletochangeitwheneverGreatApeisused,whichisgood,butwealsocan’taccessitinsubclasses.Makingitprotectedallowsustokeepitaccessibletosubclassesbutinvisibletotheoutsideworld.

call()isdefinedthesamewayinBonoboandChimpanzeeasitisinGreatApe.IthasnoparametersandtypeinferencedeterminesthatitreturnsaString.

BothBonoboandChimpanzeeshouldhavedifferentbehaviorsforcall()thanGreatApe,sowewanttochangetheirdefinitionsofcall().Ifyoucreateanidenticalfunctionsignatureinaderivedclassasinabaseclass,yousubstitutethebehaviordefinedinthebaseclasswithyournewbehavior.Thisiscalledoverriding.

WhenKotlinseesanidenticalfunctionsignatureinthederivedclassasinthebaseclass,itdecidesthatyou’vemadeamistake,calledanaccidentaloverride.Ifyouwriteafunctionthathasthesamenameasafunctioninthebaseclass,

yougetanerrormessagesayingyouforgottheoverridekeyword.Kotlinassumesyou’veunintentionallychosenthesamename,parametersandreturntypeunlessyouusetheoverridekeyword(whichyoufirstsawinConstructors)tosay“yes,Imeantodothis.”Theoverridekeywordalsohelpswhenreadingthecode,soyoudon’thavetocomparesignaturestonoticetheoverrides.

Kotlinimposesanadditionalconstraintwhenoverridingfunctions.Justasyoucannotinheritfromabaseclassunlessthatbaseclassisopen,youcannotoverrideafunctionfromabaseclassunlessthatfunctionisdefinedasopeninthebaseclass.Notethatclimb()andenergyLevel()arenotopen,sotheycannotbeoverridden.InheritanceandoverridingcannotbeaccomplishedinKotlinwithoutclearintentions.

It’sespeciallyinterestingtotakeaBonobooraChimpanzeeandtreatitasanordinaryGreatApe.Insidetalk(),call()producesthecorrectbehaviorineachcase.talk()somehowknowstheexacttypeoftheobjectandproducestheappropriatevariationofcall().Thisispolymorphism.

Insidetalk(),youcanonlycallGreatApememberfunctionsbecausetalk()’sparameterisaGreatApe.EventhoughBonobodefinesrun()andChimpanzeedefinesjump(),neitherfunctionispartofGreatApe.

Oftenwhenyouoverrideafunction,youwanttocallthebase-classversionofthatfunction(foronething,toreusethecode),asseenintheoverridesforeat().Thisproducesaconundrum:Ifyousimplycalleat(),youcallthesamefunctionyou’recurrentlyinside(aswe’veseeninRecursion).Tocallthebase-classversionofeat(),usethesuperkeyword,shortfor“superclass.”

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

BaseClassInitialization

Whenaclassinheritsanotherclass,Kotlinguaranteesthatbothclassesareproperlyinitialized.

Kotlincreatesvalidobjectsbyensuringthatconstructorsarecalled:

Constructorsformemberobjects.Constructorsfornewobjectsaddedinthederivedclass.Theconstructorforthebaseclass.

IntheInheritanceexamples,thebaseclassesdidn’thaveconstructorparameters.Ifabaseclassdoeshaveconstructorparameters,aderivedclassmustprovidethoseargumentsduringconstruction.

Here’sthefirstGreatApeexample,rewrittenwithconstructorparameters:

//BaseClassInit/GreatApe3.kt

packagebaseclassinit

importatomictest.eq

openclassGreatApe(

valweight:Double,

valage:Int

)

openclassBonobo(weight:Double,age:Int):

GreatApe(weight,age)

classChimpanzee(weight:Double,age:Int):

GreatApe(weight,age)

classBonoboB(weight:Double,age:Int):

Bonobo(weight,age)

funGreatApe.info()="wt:$weightage:$age"

funmain(){

GreatApe(100.0,12).info()eq

"wt:100.0age:12"

Bonobo(110.0,13).info()eq

"wt:110.0age:13"

Chimpanzee(120.0,14).info()eq

"wt:120.0age:14"

BonoboB(130.0,15).info()eq

"wt:130.0age:15"

}

WheninheritingfromGreatApe,youmustpassthenecessaryconstructorargumentstotheGreatApebaseclass,otherwiseyou’llgetacompile-timeerrormessage.

AfterKotlincreatesmemoryforyourobject,itcallsthebase-classconstructorfirst,thentheconstructorforthenext-derivedclass,andsoonuntilitreachesthemost-derivedconstructor.Thisway,allconstructorcallscanrelyonthevalidityofallthesub-objectscreatedbeforethem.Indeed,thosearetheonlythingsitknowsabout;aBonoboknowsitinheritsfromGreatApeandtheBonoboconstructorcancallfunctionsintheGreatApeclass,butaGreatApecannotknowwhetherit’saBonobooraChimpanzee,orcallfunctionsspecifictothosesubclasses.

Wheninheritingfromaclassyoumustprovideargumentstothebase-classconstructorafterthebaseclassname.Thiscallsthebase-classconstructorduringobjectconstruction:

//BaseClassInit/NoArgConstructor.kt

packagebaseclassinit

openclassSuperClass1(vali:Int)

classSubClass1(i:Int):SuperClass1(i)

openclassSuperClass2

classSubClass2:SuperClass2()

Whentherearenobase-classconstructorparameters,Kotlinstillrequiresemptyparenthesesafterthebaseclassname,tocallthatconstructorwithoutarguments.

Iftherearesecondaryconstructorsinthebaseclassyoumaycalloneofthoseinstead:

//BaseClassInit/House.kt

packagebaseclassinit

importatomictest.eq

openclassHouse(

valaddress:String,

valstate:String,

valzip:String

){

constructor(fullAddress:String):

this(fullAddress.substringBefore(","),

fullAddress.substringAfter(",")

.substringBefore(""),

fullAddress.substringAfterLast(""))

valfullAddress:String

get()="$address,$state$zip"

}

classVacationHouse(

address:String,

state:String,

zip:String,

valstartMonth:String,

valendMonth:String

):House(address,state,zip){

overridefuntoString()=

"Vacationhouseat$fullAddress"+

"from$startMonthto$endMonth"

}

classTreeHouse(

valname:String

):House("TreeStreet,TR00000"){

overridefuntoString()=

"$nametreehouseat$fullAddress"

}

funmain(){

valvacationHouse=VacationHouse(

address="8TargetSt.",

state="KS",

zip="66632",

startMonth="May",

endMonth="September")

vacationHouseeq

"Vacationhouseat8TargetSt.,"+

"KS66632fromMaytoSeptember"

TreeHouse("Oak")eq

"OaktreehouseatTreeStreet,TR00000"

}

WhenVacationHouseinheritsfromHouseitpassestheappropriateargumentstotheprimaryHouseconstructor.ItalsoaddsitsownparametersstartMonthandendMonth—youaren’tlimitedbythenumber,typeororderoftheparametersinthebaseclass.Youronlyresponsibilityistoprovidethecorrectargumentsinthecalltothebase-classconstructor.

Youcallanoverloadedbase-classconstructorbypassingthematchingconstructorargumentsinthebase-classconstructorcall.YouseethisinthedefinitionsofVacationHouseandTreeHouse.Eachcallsadifferentbase-classconstructor.

Insideasecondaryconstructorofaderivedclassyoucaneithercallthebase-classconstructororadifferentderived-classconstructor:

//BaseClassInit/OtherConstructors.kt

packagebaseclassinit

importatomictest.eq

openclassBase(vali:Int)

classDerived:Base{

constructor(i:Int):super(i)

constructor():this(9)

}

funmain(){

vald1=Derived(11)

d1.ieq11

vald2=Derived()

d2.ieq9

}

Tocallthebase-classconstructor,usethesuperkeyword,passingtheconstructorargumentsasifitisafunctioncall.Usethistocallanotherconstructorofthesameclass.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

AbstractClasses

Anabstractclassislikeanordinaryclassexceptoneormorefunctionsorpropertiesisincomplete:afunctionlacksadefinitionorapropertyisdeclaredwithoutinitialization.Aninterfaceislikeanabstractclassbutwithoutstate.

Youmustusetheabstractmodifiertomarkclassmembersthathavemissingdefinitions.Aclasscontainingabstractfunctionsorpropertiesmustalsobemarkedabstract.Tryremovinganyoftheabstractmodifiersbelowandseewhatmessageyouget:

//Abstract/AbstractKeyword.kt

packageabstractclasses

abstractclassWithProperty{

abstractvalx:Int

}

abstractclassWithFunctions{

abstractfunf():Int

abstractfung(n:Double)

}

WithPropertydeclaresxwithnoinitializationvalue(adeclarationdescribessomethingwithoutprovidingadefinitiontocreatestorageforavalueorcodeforafunction).Ifthereisn’taninitializer,Kotlinrequiresreferencestobeabstract,andexpectstheabstractmodifierontheclass.Withoutaninitializer,Kotlincannotinferthetype,soitalsorequirestypeinformationforanabstractreference.

WithFunctionsdeclaresf()andg()butprovidesnofunctiondefinitions,againforcingyoutoaddtheabstractmodifiertothefunctionsandthecontainingclass.Ifyoudon’tgiveareturntypeforthefunction,aswithg(),KotlinassumesitreturnsUnit.

Abstractfunctionsandpropertiesmustsomehowexist(bemadeconcrete)intheclassthatyouultimatelycreatefromtheabstractclass.

Allfunctionsandpropertiesdeclaredinaninterfaceareabstractbydefault,whichmakesaninterfacesimilartoanabstractclass.Whenaninterfacecontainsafunctionorpropertydeclaration,theabstractmodifierisredundantandcanberemoved.Thesetwointerfacesareequivalent:

//Abstract/Redundant.kt

packageabstractclasses

interfaceRedundant{

abstractvalx:Int

abstractfunf():Int

abstractfung(n:Double)

}

interfaceRemoved{

valx:Int

funf():Int

fung(n:Double)

}

Thedifferencebetweeninterfacesandabstractclassesisthatanabstractclasscancontainstate,whileaninterfacecannot.Stateisthedatastoredinsideproperties.Inthefollowing,thestateofIntListconsistsofthevaluesstoredinthepropertiesnameandlist.

//Abstract/StateOfAClass.kt

packageabstractstate

importatomictest.eq

classIntList(valname:String){

vallist=mutableListOf<Int>()

}

funmain(){

valints=IntList("numbers")

ints.nameeq"numbers"

ints.list+=7

ints.listeqlistOf(7)

}

Aninterfacemaydeclareproperties,butactualdataisonlystoredinclassesthatimplementtheinterface.Aninterfaceisn’tallowedtostorevaluesinitsproperties:

//Abstract/NoStateInInterfaces.kt

packageabstractclasses

interfaceIntList{

valname:String

//Doesn'tcompile:

//vallist=listOf(0)

}

Bothinterfacesandabstractclassescancontainfunctionswithimplementations.Youcancallotherabstractmembersfromsuchfunctions:

//Abstract/Implementations.kt

packageabstractclasses

importatomictest.eq

interfaceParent{

valch:Char

funf():Int

fung()="ch=$ch;f()=${f()}"

}

classActual(

overridevalch:Char//[1]

):Parent{

overridefunf()=17//[2]

}

classOther:Parent{

overridevalch:Char//[3]

get()='B'

overridefunf()=34//[4]

}

funmain(){

Actual('A').g()eq"ch=A;f()=17"//[5]

Other().g()eq"ch=B;f()=34"//[6]

}

Parentdeclaresanabstractpropertychandanabstractfunctionf()thatmustbeoverriddeninanyimplementingclasses.Lines[1]-[4]showdifferentimplementationsofthesemembersinsubclasses.

Parent.g()usesabstractmembersthathavenodefinitionsatthepointwhereg()isdefined.Interfacesandabstractclassesguaranteethatallabstractpropertiesandfunctionsareimplementedbeforeanyobjectscanbecreated—andyoucan’tcallamemberfunctionunlessyou’vegotanobject.Lines[5]and[6]calldifferentimplementationsofchandf().

Becauseaninterfacecancontainfunctionimplementations,itcanalsocontaincustompropertyaccessorsifthecorrespondingpropertydoesn’tchangestate:

//Abstract/PropertyAccessor.kt

packageabstractclasses

importatomictest.eq

interfacePropertyAccessor{

vala:Int

get()=11

}

classImpl:PropertyAccessor

funmain(){

Impl().aeq11

}

Youmightwonderwhyweneedinterfaceswhenabstractclassesaremorepowerful.Tounderstandtheimportanceof“aclasswithoutstate,”let’slookattheconceptofmultipleinheritance,whichKotlindoesn’tsupport.InKotlin,aclasscanonlyinheritfromasinglebaseclass:

//Abstract/NoMultipleInheritance.kt

packagemultipleinheritance1

openclassAnimal

openclassMammal:Animal()

openclassAquaticAnimal:Animal()

//Morethanonebaseclassdoesn'tcompile:

//classDolphin:Mammal(),AquaticAnimal()

Tryingtocompilethecommentedcodeproducesanerror:Onlyoneclassmayappearinasupertypelist.

Javaworksthesameway.TheoriginalJavadesignersdecidedthatC++multipleinheritancewasabadidea.Themaincomplexityanddissatisfactionatthattimecamefrommultiplestateinheritance.Therulesmanaginginheritanceofmultiplestatesarecomplicatedandcaneasilycauseconfusionandsurprisingbehavior.Javaaddedanelegantsolutiontothisproblembyintroducinginterfaces,whichcan’tcontainstate.Javaforbidsmultiplestateinheritance,butallowsmultipleinterfaceinheritance,andKotlinfollowsthisdesign:

//Abstract/MultipleInterfaceInheritance.kt

packagemultipleinheritance2

interfaceAnimal

interfaceMammal:Animal

interfaceAquaticAnimal:Animal

classDolphin:Mammal,AquaticAnimal

Notethat,justlikeclasses,interfacescaninheritfromeachother.

Wheninheritingfromseveralinterfaces,it’spossibletosimultaneouslyoverridetwoormorefunctionswiththesamesignature(thenamecombinedwiththeparametersandreturntype).Iffunctionorpropertysignaturescollide,youmustresolvethecollisionsbyhand,asseeninclassC:

//Abstract/InterfaceCollision.kt

packagecollision

importatomictest.eq

interfaceA{

funf()=1

fung()="A.g"

valn:Double

get()=1.1

}

interfaceB{

funf()=2

fung()="B.g"

valn:Double

get()=2.2

}

classC:A,B{

overridefunf()=0

overridefung()=super<A>.g()

overridevaln:Double

get()=super<A>.n+super<B>.n

}

funmain(){

valc=C()

c.f()eq0

c.g()eq"A.g"

c.neq3.3

}

Thefunctionsf()andg()andthepropertynhaveidenticalsignaturesininterfacesAandB,soKotlindoesn’tknowwhattodoandproducesanerrormessageifyoudon’tresolvetheissue(tryindividuallycommentingthedefinitionsinC).Memberfunctionsandpropertiescanbeoverriddenwithnewdefinitionsasinf(),butfunctionscanalsoaccessthebaseversionsofthemselvesusingthesuperkeyword,specifyingthebaseclassinanglebrackets,asinthedefinitionofC.g()andC.n.

CollisionswheretheidentifieristhesamebutthetypeisdifferentarenotallowedinKotlinandcannotberesolved.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Upcasting

Takinganobjectreferenceandtreatingitasareferencetoitsbasetypeiscalledupcasting.Thetermupcastreferstothewayinheritancehierarchiesaretraditionallyrepresentedwiththebaseclassatthetopandderivedclassesfanningoutbelow.

InheritingandaddingnewmemberfunctionsisthepracticeinSmalltalk,oneofthefirstsuccessfulobject-orientedlanguages.InSmalltalk,everythingisanobjectandtheonlywaytocreateaclassistoinheritfromanexistingclass,oftenaddingnewmemberfunctions.SmalltalkheavilyinfluencedJava,whichalsorequireseverythingtobeanobject.

Kotlinfreesusfromtheseconstraints.Wehavestand-alonefunctionssoeverythingdoesn’tneedtobecontainedwithinclasses.Extensionfunctionsallowustoaddfunctionalitywithoutinheritance.Indeed,requiringtheopenkeywordforinheritancemakesitaveryconsciousandintentionalchoice,notsomethingtouseallthetime.

Moreprecisely,itnarrowsinheritancetoaveryspecificuse,anabstractionthatallowsustowritecodethatcanbereusedacrossmultipleclasseswithinasinglehierarchy.ThePolymorphismatomexploresthesemechanics,butfirstyoumustunderstandupcasting.

ConsidersomeShapesthatcanbedrawnanderased:

//Upcasting/Shapes.kt

packageupcasting

interfaceShape{

fundraw():String

funerase():String

}

classCircle:Shape{

overridefundraw()="Circle.draw"

overridefunerase()="Circle.erase"

}

classSquare:Shape{

overridefundraw()="Square.draw"

overridefunerase()="Square.erase"

funcolor()="Square.color"

}

classTriangle:Shape{

overridefundraw()="Triangle.draw"

overridefunerase()="Triangle.erase"

funrotate()="Triangle.rotate"

}

Theshow()functionacceptsanyShape:

//Upcasting/Drawing.kt

packageupcasting

importatomictest.*

funshow(shape:Shape){

trace("Show:${shape.draw()}")

}

funmain(){

listOf(Circle(),Square(),Triangle())

.forEach(::show)

traceeq"""

Show:Circle.draw

Show:Square.draw

Show:Triangle.draw

"""

}

Inmain(),show()iscalledwiththreedifferenttypes:Circle,Square,andTriangle.Theshow()parameterisofthebaseclassShape,soshow()acceptsallthreetypes.EachofthosetypesistreatedasabasicShape—wesaythatthespecifictypesareupcasttothebasictype.

Wetypicallydrawadiagramforthishierarchywiththebaseclassatthetop:

ShapeHierarchy

WhenwepassaCircle,Square,orTriangleasanargumentoftypeShapeinshow(),wecastupthisinheritancehierarchy.Intheprocessofupcasting,welosethespecificinformationaboutwhetheranobjectisoftypeCircle,Square,orTriangle.Ineachcase,itbecomesnothingmorethanaShapeobject.

Treatingaspecifictypeasamoregeneraltypeistheentirepointofinheritance.Themechanicsofinheritanceexistsolelytofulfillthegoalofupcastingtothebasetype.Becauseofthisabstraction(“everythingisaShape”),wecanwriteasingleshow()functioninsteadofwritingoneforeverytypeofelement.Upcastingisawaytoreusecodeforobjects.

Indeed,invirtuallyeverycasewherethere’sinheritancewithoutupcasting,inheritanceisbeingmisused—it’sunnecessary,anditmakesthecodeneedlesslycomplicated.Thismisuseisthereasonforthemaxim:

Prefercompositiontoinheritance.

Ifthepointofinheritanceistheabilitytosubstituteaderivedtypeforabasetype,whathappenstotheextramemberfunctions:color()inSquareandrotate()inTriangle?

Substitutability,alsocalledtheLiskovSubstitutionPrinciple,saysthat,afterupcasting,thederivedtypecanbetreatedexactlylikethebasetype—nomoreandnoless.Thismeansthatanymemberfunctionsaddedtothederivedclassare,ineffect,“trimmedoff.”Theystillexist,butbecausetheyarenotpartofthebase-classinterface,theyareunavailablewithinshow():

//Upcasting/TrimmedMembers.kt

packageupcasting

importatomictest.*

funtrim(shape:Shape){

trace(shape.draw())

trace(shape.erase())

//Doesn'tcompile:

//shape.color()//[1]

//shape.rotate()//[2]

}

funmain(){

trim(Square())

trim(Triangle())

traceeq"""

Square.draw

Square.erase

Triangle.draw

Triangle.erase

"""

}

Youcan’tcallcolor()inline[1]becausetheSquareinstancewasupcasttoaShape,andyoucan’tcallrotate()inline[2]becausetheTriangleinstanceis

alsoupcasttoaShape.TheonlymemberfunctionsavailablearetheonesthatarecommontoallShapes—thosedefinedinthebasetypeShape.

NotethatthesameapplieswhenyoudirectlyassignasubtypeofShapetoageneralShape.Thespecifiedtypedeterminestheavailablemembers:

//Upcasting/Assignment.kt

importupcasting.*

funmain(){

valshape1:Shape=Square()

valshape2:Shape=Triangle()

//Doesn'tcompile:

//shape1.color()

//shape2.rotate()

}

Afteranupcast,youcanonlycallmembersofthebasetype.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Polymorphism

PolymorphismisanancientGreektermmeaning“manyforms.”Inprogramming,polymorphismmeansanobjectoritsmembershavemultipleimplementations.

ConsiderasimplehierarchyofPettypes.ThePetclasssaysthatallPetscanspeak().DogandCatoverridethespeak()memberfunction:

//Polymorphism/Pet.kt

packagepolymorphism

importatomictest.eq

openclassPet{

openfunspeak()="Pet"

}

classDog:Pet(){

overridefunspeak()="Bark!"

}

classCat:Pet(){

overridefunspeak()="Meow"

}

funtalk(pet:Pet)=pet.speak()

funmain(){

talk(Dog())eq"Bark!"//[1]

talk(Cat())eq"Meow"//[2]

}

Noticethetalk()functionparameter.WhenpassingaDogoraCattotalk(),thespecifictypeisforgottenandbecomesaplainPet—bothDogsandCatsareupcasttoPet.TheobjectsarenowtreatedasplainPetssoshouldn’ttheoutputforbothlines[1]and[2]be"Pet"?

talk()doesn’tknowtheexacttypeofPetitreceives.Despitethat,whenyoucallspeak()throughareferencetothebase-classPet,thecorrectsubclassimplementationiscalled,andyougetthedesiredbehavior.

Polymorphismoccurswhenaparentclassreferencecontainsachildclassinstance.Whenyoucallamemberonthatparentclassreference,polymorphismproducesthecorrectoverriddenmemberfromthechildclass.

Connectingafunctioncalltoafunctionbodyiscalledbinding.Ordinarily,youdon’tthinkmuchaboutbindingbecauseithappensstatically,atcompiletime.Withpolymorphism,thesameoperationmustbehavedifferentlyfordifferenttypes—butthecompilercannotknowinadvancewhichfunctionbodytouse.Thefunctionbodymustbedetermineddynamically,atruntime,usingdynamicbinding.Dynamicbindingisalsocalledlatebindingordynamicdispatch.OnlyatruntimecanKotlindeterminetheexactspeak()functiontocall.Thuswesaythatthebindingforthepolymorphiccallpet.speak()occursdynamically.

Considerafantasygame.EachCharacterinthegamehasanameandcanplay().WecombineFighterandMagiciantobuildspecificcharacters:

//Polymorphism/FantasyGame.kt

packagepolymorphism

importatomictest.*

abstractclassCharacter(valname:String){

abstractfunplay():String

}

interfaceFighter{

funfight()="Fight!"

}

interfaceMagician{

fundoMagic()="Magic!"

}

classWarrior:

Character("Warrior"),Fighter{

overridefunplay()=fight()

}

openclassElf(name:String="Elf"):

Character(name),Magician{

overridefunplay()=doMagic()

}

classFightingElf:

Elf("FightingElf"),Fighter{

overridefunplay()=

super.play()+fight()

}

funCharacter.playTurn()=//[1]

trace(name+":"+play())//[2]

funmain(){

valcharacters:List<Character>=listOf(

Warrior(),Elf(),FightingElf()

)

characters.forEach{it.playTurn()}//[3]

traceeq"""

Warrior:Fight!

Elf:Magic!

FightingElf:Magic!Fight!

"""

}

Inmain(),eachobjectisupcasttoCharacterasitisplacedintotheList.ThetraceshowsthatcallingplayTurn()oneachCharacterintheListproducesdifferentoutput.

playTurn()isanextensionfunctiononthebasetypeCharacter.Whencalledinline[3],itisstaticallybound,whichmeanstheexactfunctiontobecalledisdeterminedatcompiletime.Inline[3],thecompilerdeterminesthatthereisonlyoneplayTurn()functionimplementation—theonedefinedonline[1].

Whenthecompileranalyzestheplay()functioncallonline[2],itdoesn’tknowwhichfunctionimplementationtouse.IftheCharacterisanElf,itmustcallElf’splay().IftheCharacterisaFightingElf,itmustcallFightingElf’splay().Itmightalsoneedtocallafunctionfromanas-yet-undefinedsubclass.Thefunctionbindingdiffersfrominvocationtoinvocation.Atcompiletime,theonlycertaintyisthatplay()online[2]isamemberfunctionofoneoftheCharactersubclasses.Thespecificsubclasscanonlybeknownatruntime,basedontheactualCharactertype.

-

Dynamicbindingisn’tfree.Theadditionallogicthatdeterminestheruntimetypeslightlyimpactsperformancecomparedtostaticbinding.Toforceclarity,Kotlindefaultstoclosedclassesandmemberfunctions.Toinheritandoverride,youmustbeexplicit.

Alanguagefeaturesuchasthewhenstatementcanbelearnedinisolation.Polymorphismcannot—itonlyworksinconcert,aspartofthelargerpictureofclassrelationships.Touseobject-orientedtechniqueseffectively,youmustexpandyourperspectivetoincludenotjustmembersofanindividualclass,butalsothecommonalityamongclassesandtheirrelationshipswitheachother.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Composition

Oneofthemostcompellingargumentsforobject-orientedprogrammingiscodereuse.

Youmayfirstthinkof“reuse”as“copyingcode.”Copyingseemslikeaneasysolution,butitdoesn’tworkverywell.Astimepasses,yourneedsevolve.Applyingchangestocodethat’sbeencopiedisamaintenancenightmare.Didyoufindallthecopies?Didyoumakethechangesthesamewayforeachcopy?Reusedcodecanbechangedinjustoneplace.

Inobject-orientedprogrammingyoureusecodebycreatingnewclasses,butinsteadofcreatingthemfromscratch,youuseexistingclassesthatsomeonehasalreadybuiltanddebugged.Thetrickistousetheclasseswithoutsoilingtheexistingcode.

Inheritanceisonewaytoachievethis.Inheritancecreatesanewclassasatypeofanexistingclass.Youaddcodetotheformoftheexistingclasswithoutmodifyingtheoriginal.Inheritanceisacornerstoneofobject-orientedprogramming.

Youcanalsochooseamorestraightforwardapproach,bycreatingobjectsofexistingclassesinsideyournewclass.Thisiscalledcomposition,becausethenewclassiscomposedofobjectsofexistingclasses.You’rereusingthefunctionalityofthecode,notitsform.

Compositionisusedfrequentlyinthisbook.Compositionisoftenoverlookedbecauseitseemssosimple—youjustputanobjectinsideaclass.

Compositionisahas-arelationship.“Ahouseisabuildingandhasakitchen”canbeexpressedlikethis:

//Composition/House1.kt

packagecomposition1

interfaceBuilding

interfaceKitchen

interfaceHouse:Building{

valkitchen:Kitchen

}

Inheritancedescribesanis-arelationship,andit’softenhelpfultoreadthedescriptionaloud:“Ahouseisabuilding.”Thatsoundsright,doesn’tit?Whentheis-arelationshipmakessense,inheritanceusuallymakessense.

Ifyourhousehastwokitchens,compositionyieldsaneasysolution:

//Composition/House2.kt

packagecomposition2

interfaceBuilding

interfaceKitchen

interfaceHouse:Building{

valkitchen1:Kitchen

valkitchen2:Kitchen

}

Toallowanynumberofkitchens,usecompositionwithacollection:

//Composition/House3.kt

packagecomposition3

interfaceBuilding

interfaceKitchen

interfaceHouse:Building{

valkitchens:List<Kitchen>

}

Wespendtimeandeffortunderstandinginheritancebecauseit’smorecomplex,andthatcomplexitymightgivetheimpressionthatit’ssomehowmoreimportant.Onthecontrary:

Prefercompositiontoinheritance.

Compositionproducessimplerdesignsandimplementations.Thisdoesn’tmeanyoushouldavoidinheritance.It’sjustthatwetendtogetboundupinmorecomplicatedrelationships.Themaximprefercompositiontoinheritanceisaremindertostepback,lookatyourdesign,andwonderwhetheryoucansimplifyitwithcomposition.Theultimategoalistoproperlyapplyyourtoolsandproduceagooddesign.

Compositionappearstrivial,butispowerful.Whenaclassgrowsandbecomesresponsiblefordifferentunrelatedthings,compositionhelpspullthemapart.Usecompositiontosimplifythecomplicatedlogicofaclass.

ChoosingBetweenCompositionandInheritanceBothcompositionandinheritanceputsubobjectsinsideyournewclass—compositionhasexplicitsubobjectswhileinheritancehasimplicitsubjobjects.Whendoyouchooseoneovertheother?

Compositionprovidesthefunctionalityofanexistingclass,butnotitsinterface.Youembedanobjecttouseitsfeaturesinyournewclass,buttheuserseestheinterfaceyou’vedefinedforthatnewclassratherthantheinterfaceoftheembeddedobject.Tohidetheobjectcompletely,embeditprivately:

//Composition/Embedding.kt

packagecomposition

classFeatures{

funf1()="feature1"

funf2()="feature2"

}

classForm{

privatevalfeatures=Features()

funoperation1()=

features.f2()+features.f1()

funoperation2()=

features.f1()+features.f2()

}

TheFeaturesclassprovidesimplementationsfortheoperationsofForm,buttheclientprogrammerwhousesFormhasnoaccesstofeatures—indeed,theuseriseffectivelyunawareofhowFormisimplemented.ThismeansthatifyoufindabetterwaytoimplementForm,youcanremovefeaturesandchangetothenewapproachwithoutanyimpactoncodethatcallsForm.

IfForminheritedFeatures,theclientprogrammercouldexpecttoupcastFormtoFeatures.TheinheritancerelationshipisthenpartofForm—theconnectionisexplicit.Ifyouchangethis,you’llbreakcodethatreliesuponthatconnection.

Sometimesitmakessensetoallowtheclassusertodirectlyaccessthecompositionofyournewclass;thatis,tomakethememberobjectspublic.Thisisrelativelysafe,assumingthememberobjectsuseappropriateimplementation

hiding.Forsomesystems,thisapproachcanmaketheinterfaceeasiertounderstand.ConsideraCar:

//Composition/Car.kt

packagecomposition

importatomictest.*

classEngine{

funstart()=trace("Enginestart")

funstop()=trace("Enginestop")

}

classWheel{

funinflate(psi:Int)=

trace("Wheelinflate($psi)")

}

classWindow(valside:String){

funrollUp()=

trace("$sideWindowrollup")

funrollDown()=

trace("$sideWindowrolldown")

}

classDoor(valside:String){

valwindow=Window(side)

funopen()=trace("$sideDooropen")

funclose()=trace("$sideDoorclose")

}

classCar{

valengine=Engine()

valwheel=List(4){Wheel()}

//Twodoor:

valleftDoor=Door("left")

valrightDoor=Door("right")

}

funmain(){

valcar=Car()

car.leftDoor.open()

car.rightDoor.window.rollUp()

car.wheel[0].inflate(72)

car.engine.start()

traceeq"""

leftDooropen

rightWindowrollup

Wheelinflate(72)

Enginestart

"""

}

ThecompositionofaCarispartoftheanalysisoftheproblem,andnotsimplypartoftheunderlyingimplementation.Thisassiststheclientprogrammer’sunderstandingofhowtousetheclassandrequireslesscodecomplexityforthecreatoroftheclass.

Whenyouinherit,youcreateacustomversionofanexistingclass.Thistakesageneral-purposeclassandspecializesitforaparticularneed.Inthisexample,itwouldmakenosensetocomposeaCarusinganobjectofaVehicleclass—aCardoesn’tcontainaVehicle,itisaVehicle.Theis-arelationshipisexpressedwithinheritance,andthehas-arelationshipisexpressedwithcomposition.

Theclevernessofpolymorphismcanmakeitcanseemthateverythingoughttobeinherited.Thiswillburdenyourdesigns.Infact,ifyouchooseinheritancefirstwhenyou’reusinganexistingclasstobuildanewclass,thingscanbecomeneedlesslycomplicated.Abetterapproachistotrycompositionfirst,especiallywhenit’snotobviouswhichapproachworksbest.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Inheritance&Extensions

Inheritanceissometimesusedtoaddfunctionstoaclassasawaytoreuseitforanewpurpose.Thiscanleadtocodethatisdifficulttounderstandandmaintain.

SupposesomeonehascreatedaHeaterclassalongwithfunctionsthatactuponaHeater:

//InheritanceExtensions/Heater.kt

packageinheritanceextensions

importatomictest.eq

openclassHeater{

funheat(temperature:Int)=

"heatingto$temperature"

}

funwarm(heater:Heater){

heater.heat(70)eq"heatingto70"

}

Forthesakeofargument,imaginethatHeaterisfarmorecomplexthanthis,andthattherearemanyadjunctfunctionssuchaswarm().Wedon’twanttomodifythislibrary—wewanttoreuseitas-is.

IfwhatweactuallywantisanHVAC(Heating,VentilationandAirConditioning)system,wecaninheritHeaterandaddacool()function.Theexistingwarm()function,andallotherfunctionsthatactuponaHeater,stillworkwithournewHVACtype—whichwouldnotbetrueifwehadusedcomposition:

//InheritanceExtensions/InheritAdd.kt

packageinheritanceextensions

importatomictest.eq

classHVAC:Heater(){

funcool(temperature:Int)=

"coolingto$temperature"

}

funwarmAndCool(hvac:HVAC){

hvac.heat(70)eq"heatingto70"

hvac.cool(60)eq"coolingto60"

}

funmain(){

valheater=Heater()

valhvac=HVAC()

warm(heater)

warm(hvac)

warmAndCool(hvac)

}

Thisseemspractical:Heaterdidn’tdoeverythingwewanted,soweinheritedHVACfromHeaterandtackedonanotherfunction.

AsyousawinUpcasting,object-orientedlanguageshaveamechanismtodealwithmemberfunctionsaddedduringinheritance:theaddedfunctionsaretrimmedoffduringupcastingandareunavailabletothebaseclass.ThisistheLiskovSubstitutionPrinciple,aka“Substitutability,”whichsaysfunctionsthatacceptabaseclassmustbeabletouseobjectsofderivedclasseswithoutknowingit.Substitutabilityiswhywarm()stillworksonanHVAC.

AlthoughmodernOOprogrammingallowstheadditionoffunctionsduringinheritance,thiscanbea“codesmell”—itappearstobereasonableandexpedientbutcanleadyouintotrouble.Justbecauseitseemstoworkdoesn’tmeanit’sagoodidea.Inparticular,itmightnegativelyimpactalatermaintainerofthecode(whichmightbeyou).Thiskindofproblemiscalledtechnicaldebt.

Addingfunctionsduringinheritancecanbeusefulwhenthenewclassisrigorouslytreatedasabaseclassthroughoutyoursystem,ignoringthefactthatithasitsownbases.InTypeCheckingyou’llseemoreexampleswhereaddingfunctionsduringinheritancecanbeaviabletechnique.

WhatwereallywantedwhencreatingtheHVACclasswasaHeaterclasswithanaddedcool()functionsoitworkswithwarmAndCool().Thisisexactlywhatanextensionfunctiondoes,withoutinheritance:

//InheritanceExtensions/ExtensionFuncs.kt

packageinheritanceextensions2

importinheritanceextensions.Heater

importatomictest.eq

funHeater.cool(temperature:Int)=

"coolingto$temperature"

funwarmAndCool(heater:Heater){

heater.heat(70)eq"heatingto70"

heater.cool(60)eq"coolingto60"

}

funmain(){

valheater=Heater()

warmAndCool(heater)

}

Insteadofinheritingtoextendthebaseclassinterface,extensionfunctionsextendthebaseclassinterfacedirectly,withoutinheritance.

IfwehadcontrolovertheHeaterlibrary,wecoulddesignitdifferently,tobemoreflexible:

//InheritanceExtensions/TemperatureDelta.kt

packageinheritanceextensions

importatomictest.*

classTemperatureDelta(

valcurrent:Double,

valtarget:Double

)

funTemperatureDelta.heat(){

if(current<target)

trace("heatingto$target")

}

funTemperatureDelta.cool(){

if(current>target)

trace("coolingto$target")

}

funadjust(deltaT:TemperatureDelta){

deltaT.heat()

deltaT.cool()

}

funmain(){

adjust(TemperatureDelta(60.0,70.0))

adjust(TemperatureDelta(80.0,60.0))

traceeq"""

heatingto70.0

coolingto60.0

"""

}

Inthisapproach,wecontrolthetemperaturebychoosingamongmultiplestrategies.Wecouldalsohavemadeheat()andcool()memberfunctionsinsteadofextensionfunctions.

InterfacebyConventionAnextensionfunctioncanbethoughtofascreatinganinterfacecontainingasinglefunction:

//InheritanceExtensions/Convention.kt

packageinheritanceextensions

classX

funX.f(){}

classY

funY.f(){}

funcallF(x:X)=x.f()

funcallF(y:Y)=y.f()

funmain(){

valx=X()

valy=Y()

x.f()

y.f()

callF(x)

callF(y)

}

BothXandYnowappeartohaveamemberfunctioncalledf(),butwedon’tgetpolymorphicbehaviorsowemustoverloadcallF()tomakeitworkforbothtypes.

This“interfacebyconvention”isusedextensivelyintheKotlinlibraries,especiallywhendealingwithcollections.AlthoughthesearepredominantlyJavacollections,theKotlinlibraryturnsthemintofunctional-stylecollectionsbyaddingalargenumberofextensionfunctions.Forexample,onvirtuallyanycollection-likeobject,youcanexpecttofindmap()andreduce(),amongmanyothers.Becausetheprogrammercomestoexpectthisconvention,itmakesprogrammingeasier.

TheKotlinstandardlibrarySequenceinterfacecontainsasinglememberfunction.TheotherSequencefunctionsareallextensions—therearewelloveronehundred.Initially,thisapproachwasusedforcompatibilitywithJavacollections,butnowit’spartoftheKotlinphilosophy:Createasimpleinterfacecontainingonlythemethodsthatdefineitsessence,thencreateallauxiliaryoperationsasextensions.

TheAdapterPatternAlibraryoftendefinesatypeandprovidesfunctionsthatacceptparametersofthattypeand/orreturnthattype:

//InheritanceExtensions/UsefulLibrary.kt

packageusefullibrary

interfaceLibType{

funf1()

funf2()

}

funutility1(lt:LibType){

lt.f1()

lt.f2()

}

funutility2(lt:LibType){

lt.f2()

lt.f1()

}

Tousethislibrary,youmustsomehowconvertyourexistingclasstoLibType.Here,weinheritfromanexistingMyClasstoproduceMyClassAdaptedForLib,whichimplementsLibTypeandcanthusbepassedtothefunctionsinUsefulLibrary.kt:

//InheritanceExtensions/Adapter.kt

packageinheritanceextensions

importusefullibrary.*

importatomictest.*

openclassMyClass{

fung()=trace("g()")

funh()=trace("h()")

}

funuseMyClass(mc:MyClass){

mc.g()

mc.h()

}

classMyClassAdaptedForLib:

MyClass(),LibType{

overridefunf1()=h()

overridefunf2()=g()

}

funmain(){

valmc=MyClassAdaptedForLib()

utility1(mc)

utility2(mc)

useMyClass(mc)

traceeq"h()g()g()h()g()h()"

}

Althoughthisdoesextendaclassduringinheritance,thenewmemberfunctionsareusedonlyforthepurposeofadaptingtoUsefulLibrary.Notethateverywhereelse,objectsofMyClassAdaptedForLibcanbetreatedasMyClassobjects,asinthecalltouseMyClass().There’snocodethatusestheextendedMyClassAdaptedForLibwhereusersofthebaseclassmustknowaboutthederivedclass.

Adapter.ktreliesonMyClassbeingopenforinheritance.Whatifyoudon’tcontrolMyClassandit’snotopen?Fortunately,adapterscanalsobebuiltusingcomposition.Here,weaddaMyClassfieldinsideMyClassAdaptedForLib:

//InheritanceExtensions/ComposeAdapter.kt

packageinheritanceextensions2

importusefullibrary.*

importatomictest.*

classMyClass{//Notopen

fung()=trace("g()")

funh()=trace("h()")

}

funuseMyClass(mc:MyClass){

mc.g()

mc.h()

}

classMyClassAdaptedForLib:LibType{

valfield=MyClass()

overridefunf1()=field.h()

overridefunf2()=field.g()

}

funmain(){

valmc=MyClassAdaptedForLib()

utility1(mc)

utility2(mc)

useMyClass(mc.field)

traceeq"h()g()g()h()g()h()"

}

ThisisnotquiteascleanasAdapter.kt—youmustexplicitlyaccesstheMyClassobjectasseeninthecalltouseMyClass(mc.field).Butitstillhandilysolvestheproblemofadaptingtoalibrary.

Extensionfunctionsseemliketheymightbeveryusefulforcreatingadapters.Unfortunately,youcannotimplementaninterfacebycollectingextensionfunctions.

MembersversusExtensionsTherearecaseswhereyouareforcedtousememberfunctionsratherthanextensions.Ifafunctionmustaccessaprivatemember,youhavenochoicebuttomakeitamemberfunction:

//InheritanceExtensions/PrivateAccess.kt

packageinheritanceextensions

importatomictest.eq

classZ(vari:Int=0){

privatevarj=0

funincrement(){

i++

j++

}

}

funZ.decrement(){

i--

//j--//Cannotaccess

}

Thememberfunctionincrement()canmanipulatej,buttheextensionfunctiondecrement()doesn’thaveaccesstojbecausejisprivate.

Themostsignificantlimitationtoextensionfunctionsisthattheycannotbeoverridden:

//InheritanceExtensions/NoExtOverride.kt

packageinheritanceextensions

importatomictest.*

openclassBase{

openfunf()="Base.f()"

}

classDerived:Base(){

overridefunf()="Derived.f()"

}

funBase.g()="Base.g()"

funDerived.g()="Derived.g()"

funuseBase(b:Base){

trace("Received${b::class.simpleName}")

trace(b.f())

trace(b.g())

}

funmain(){

useBase(Base())

useBase(Derived())

traceeq"""

ReceivedBase

Base.f()

Base.g()

ReceivedDerived

Derived.f()

Base.g()

"""

}

Thetraceoutputshowsthatpolymorphismworkswiththememberfunctionf()butnottheextensionfunctiong().

Whenafunctiondoesn’tneedoverridingandyouhaveadequateaccesstothemembersofaclass,youcandefineitaseitheramemberfunctionoran

extensionfunction—astylisticchoicethatshouldmaximizecodeclarity.

Amemberfunctionreflectstheessenceofatype;youcan’timaginethetypewithoutthatfunction.Extensionfunctionsindicate“auxiliary”or“convenience”operationsthatsupportorutilizethetype,butarenotnecessarilyessentialtothattype’sexistence.Includingauxiliaryfunctionsinsideatypemakesithardertoreasonabout,whiledefiningsomefunctionsasextensionskeepsthetypecleanandsimple.

ConsideraDeviceinterface.ThemodelandproductionYearpropertiesareintrinsictoDevicebecausetheydescribekeyfeatures.Functionslikeoverpriced()andoutdated()canbedefinedeitherasmembersoftheinterfaceorasextensionfunctions.Heretheyarememberfunctions:

//InheritanceExtensions/DeviceMembers.kt

packageinheritanceextensions1

importatomictest.eq

interfaceDevice{

valmodel:String

valproductionYear:Int

funoverpriced()=model.startsWith("i")

funoutdated()=productionYear<2050

}

classMyDevice(

overridevalmodel:String,

overridevalproductionYear:Int

):Device

funmain(){

valgadget:Device=

MyDevice("myfirstphone",2000)

gadget.outdated()eqtrue

gadget.overpriced()eqfalse

}

Ifweassumeoverpriced()andoutdated()willnotbeoverriddeninsubclasses,theycanbedefinedasextensions:

//InheritanceExtensions/DeviceExtensions.kt

packageinheritanceextensions2

importatomictest.eq

interfaceDevice{

valmodel:String

valproductionYear:Int

}

funDevice.overpriced()=

model.startsWith("i")

funDevice.outdated()=

productionYear<2050

classMyDevice(

overridevalmodel:String,

overridevalproductionYear:Int

):Device

funmain(){

valgadget:Device=

MyDevice("myfirstphone",2000)

gadget.outdated()eqtrue

gadget.overpriced()eqfalse

}

Interfacesthatonlycontaindescriptivemembersareeasiertocomprehendandreasonabout,sotheDeviceinterfaceinthesecondexampleisprobablyabetterchoice.Ultimately,however,it’sadesigndecision.

-

LanguageslikeC++andJavaallowinheritanceunlessyouspecificallydisallowit.Kotlinassumesthatyouwon’tbeusinginheritance—itactivelypreventsinheritanceandpolymorphismunlesstheyareintentionallyallowedusingtheopenkeyword.ThisprovidesinsightintoKotlin’sorientation:

Often,functionsareallyouneed.Sometimesobjectsareveryuseful.Objectsareonetoolamongmany,butthey’renotforeverything.

Ifyou’reponderinghowtouseinheritanceinaparticularsituation,considerwhetheryouneedinheritanceatall,andapplythemaximPreferextensionfunctionsandcompositiontoinheritance(modifiedfromthebookDesignPatterns).

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ClassDelegation

Bothcompositionandinheritanceplacesubobjectsinsideyournewclass.Withcompositionthesubobjectisexplicitandwithinheritanceitisimplicit.

Compositionusesthefunctionalityofanembeddedobjectbutdoesnotexposeitsinterface.Foraclasstoreuseanexistingimplementationandimplementitsinterface,youhavetwooptions:inheritanceandclassdelegation.

Classdelegationismidwaybetweeninheritanceandcomposition.Likecomposition,youplaceamemberobjectintheclassyou’rebuilding.Likeinheritance,classdelegationexposestheinterfaceofthesubobject.Inaddition,youcanupcasttothemembertype.Forcodereuse,classdelegationmakescompositionaspowerfulasinheritance.

Howwouldyouachievethiswithoutlanguagesupport?Here,aspaceshipneedsacontrolmodule:

//ClassDelegation/SpaceShipControls.kt

packageclassdelegation

interfaceControls{

funup(velocity:Int):String

fundown(velocity:Int):String

funleft(velocity:Int):String

funright(velocity:Int):String

funforward(velocity:Int):String

funback(velocity:Int):String

funturboBoost():String

}

classSpaceShipControls:Controls{

overridefunup(velocity:Int)=

"up$velocity"

overridefundown(velocity:Int)=

"down$velocity"

overridefunleft(velocity:Int)=

"left$velocity"

overridefunright(velocity:Int)=

"right$velocity"

overridefunforward(velocity:Int)=

"forward$velocity"

overridefunback(velocity:Int)=

"back$velocity"

overridefunturboBoost()="turboboost"

}

Ifwewanttoexpandthefunctionalityofthecontrolsoradjustsomecommands,wemighttryinheritingfromSpaceShipControls.Thisdoesn’tworkbecauseSpaceShipControlsisnotopen.

ToexposethememberfunctionsinControls,youcancreateaninstanceofSpaceShipControlsasapropertyandexplicitlydelegatealltheexposedmemberfunctionstothatinstance:

//ClassDelegation/ExplicitDelegation.kt

packageclassdelegation

importatomictest.eq

classExplicitControls:Controls{

privatevalcontrols=SpaceShipControls()

//Delegationbyhand:

overridefunup(velocity:Int)=

controls.up(velocity)

overridefunback(velocity:Int)=

controls.back(velocity)

overridefundown(velocity:Int)=

controls.down(velocity)

overridefunforward(velocity:Int)=

controls.forward(velocity)

overridefunleft(velocity:Int)=

controls.left(velocity)

overridefunright(velocity:Int)=

controls.right(velocity)

//Modifiedimplementation:

overridefunturboBoost():String=

controls.turboBoost()+"...boooooost!"

}

funmain(){

valcontrols=ExplicitControls()

controls.forward(100)eq"forward100"

controls.turboBoost()eq

"turboboost...boooooost!"

}

Thefunctionsareforwardedtotheunderlyingcontrolsobject,andtheresultinginterfaceisthesameasifyouhadusedregularinheritance.Youcanalsoprovideimplementationchanges,aswithturboBoost().

Kotlinautomatestheprocessofclassdelegation,soinsteadofwritingexplicitfunctionimplementationsasinExplicitDelegation.kt,youspecifyanobjecttouseasadelegate.

Todelegatetoaclass,placethebykeywordaftertheinterfacename,followedbythememberpropertytouseasthedelegate:

//ClassDelegation/BasicDelegation.kt

packageclassdelegation

interfaceAI

classA:AI

classB(vala:A):AIbya

Readthisas“classBimplementsinterfaceAIbyusingtheamemberobject.”Youcanonlydelegatetointerfaces,soyoucan’tsayAbya.Thedelegateobject(a)mustbeaconstructorargument.

ExplicitDelegation.ktcannowberewrittenusingby:

//ClassDelegation/DelegatedControls.kt

packageclassdelegation

importatomictest.eq

classDelegatedControls(

privatevalcontrols:SpaceShipControls=

SpaceShipControls()

):Controlsbycontrols{

overridefunturboBoost():String=

"${controls.turboBoost()}...boooooost!"

}

funmain(){

valcontrols=DelegatedControls()

controls.forward(100)eq"forward100"

controls.turboBoost()eq

"turboboost...boooooost!"

}

WhenKotlinseesthebykeyword,itgeneratescodesimilartowhatwewroteforExplicitDelegation.kt.Afterdelegation,thefunctionsofthememberobjectareaccessibleviatheouterobject,butwithoutwritingallthatextracode.

Kotlindoesn’tsupportmultipleclassinheritance,butyoucansimulateitusingclassdelegation.Ingeneral,multipleinheritanceisusedtocombineclassesthathavecompletelydifferentfunctionality.Forexample,supposeyouwanttoproduceabuttonbycombiningaclassthatdrawsarectangleonthescreenwithaclassthatmanagesmouseevents:

//ClassDelegation/ModelingMI.kt

packageclassdelegation

importatomictest.eq

interfaceRectangle{

funpaint():String

}

classButtonImage(

valwidth:Int,

valheight:Int

):Rectangle{

overridefunpaint()=

"paintingButtonImage($width,$height)"

}

interfaceMouseManager{

funclicked():Boolean

funhovering():Boolean

}

classUserInput:MouseManager{

overridefunclicked()=true

overridefunhovering()=true

}

//Evenifwemaketheclassesopen,we

//getanerrorbecauseonlyoneclassmay

//appearinasupertypelist:

//classButton:ButtonImage(),UserInput()

classButton(

valwidth:Int,

valheight:Int,

varimage:Rectangle=

ButtonImage(width,height),

privatevarinput:MouseManager=UserInput()

):Rectanglebyimage,MouseManagerbyinput

funmain(){

valbutton=Button(10,5)

button.paint()eq

"paintingButtonImage(10,5)"

button.clicked()eqtrue

button.hovering()eqtrue

//Canupcasttobothdelegatedtypes:

valrectangle:Rectangle=button

valmouseManager:MouseManager=button

}

TheclassButtonimplementstwointerfaces:RectangleandMouseManager.Itcan’tinheritfromimplementationsofbothButtonImageandUserInput,butitcandelegatetobothofthem.

Noticethatthedefinitionforimageintheconstructorargumentlistisbothpublicandavar.ThisallowstheclientprogrammertodynamicallyreplacetheButtonImage.

Thelasttwolinesinmain()showthataButtoncanbeupcasttobothofitsdelegatedtypes.Thiswasthegoalofmultipleinheritance,sodelegationeffectivelysolvestheneedformultipleinheritance.

-

Inheritancecanbeconstraining.Forexample,youcannotinheritaclasswhenthesuperclassisnotopen,orifyournewclassisalreadyextendinganotherclass.Classdelegationreleasesyoufromtheseandotherlimitations.

Useclassdelegationwithcare.Amongthethreechoices—inheritance,compositionandclassdelegation—trycompositionfirst.It’sthesimplestapproachandsolvesthemajorityofusecases.Inheritanceisnecessarywhenyouneedahierarchyoftypes,tocreaterelationshipsbetweenthosetypes.Classdelegationcanworkwhenthoseoptionsdon’t.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Downcasting

Downcastingdiscoversthespecifictypeofapreviously-upcastobject.

Upcastsarealwayssafebecausethebaseclasscannothaveabiggerinterfacethanthederivedclass.Everybase-classmemberisguaranteedtoexistandisthereforesafetocall.Althoughobject-orientedprogrammingisprimarilyfocusedonupcasting,therearesituationswheredowncastingcanbeausefulandexpedientapproach.

Downcastinghappensatruntime,andisalsocalledrun-timetypeidentification(RTTI).

Consideraclasshierarchywherethebasetypehasanarrowerinterfacethanthederivedtypes.Ifyouupcastanobjecttothebasetype,thecompilernolongerknowsthespecifictype.Inparticular,itcannotknowwhatextendedfunctionsaresafetocall:

//DownCasting/NarrowingUpcast.kt

packagedowncasting

interfaceBase{

funf()

}

classDerived1:Base{

overridefunf(){}

fung(){}

}

classDerived2:Base{

overridefunf(){}

funh(){}

}

funmain(){

valb1:Base=Derived1()//Upcast

b1.f()//PartofBase

//b1.g()//NotpartofBase

valb2:Base=Derived2()//Upcast

b2.f()//PartofBase

//b2.h()//NotpartofBase

}

Tosolvethisproblem,theremustbesomewaytoguaranteethatadowncastiscorrect,soyoudon’taccidentallycasttothewrongtypeandcallanon-existentmember.

SmartCastsSmartcastsinKotlinareautomaticdowncasts.Theiskeywordcheckswhetheranobjectisaparticulartype.Anycodewithinthescopeofthatcheckassumesthatitisthattype:

//DownCasting/IsKeyword.kt

importdowncasting.*

funmain(){

valb1:Base=Derived1()//Upcast

if(b1isDerived1)

b1.g()//Withinscopeof"is"check

valb2:Base=Derived2()//Upcast

if(b2isDerived2)

b2.h()//Withinscopeof"is"check

}

Ifb1isoftypeDerived1,youcancallg().Ifb2isoftypeDerived2,youcancallh().

Smartcastsareespeciallyusefulinsidewhenexpressionsthatuseistosearchforthetypeofthewhenargument.Notethat,inmain(),eachspecifictypeisfirstupcasttoaCreature,thenpassedtowhat():

//DownCasting/Creature.kt

packagedowncasting

importatomictest.eq

interfaceCreature

classHuman:Creature{

fungreeting()="I'mHuman"

}

classDog:Creature{

funbark()="Yip!"

}

classAlien:Creature{

funmobility()="Threelegs"

}

funwhat(c:Creature):String=

when(c){

isHuman->c.greeting()

isDog->c.bark()

isAlien->c.mobility()

else->"Somethingelse"

}

funmain(){

valc:Creature=Human()

what(c)eq"I'mHuman"

what(Dog())eq"Yip!"

what(Alien())eq"Threelegs"

classWho:Creature

what(Who())eq"Somethingelse"

}

Inmain(),upcastinghappenswhenassigningaHumantoCreature,passingaDogtowhat(),passinganAlientowhat(),andpassingaWhotowhat().

Classhierarchiesaretraditionallydrawnwiththebaseclassatthetopandderivedclassesfanningdownbelowit.what()takesapreviously-upcastCreatureanddiscoversitsexacttype,thuscastingthatCreatureobjectdowntheinheritancehierarchy,fromthemore-generalbaseclasstoamore-specificderivedclass.

Awhenexpressionthatproducesavaluerequiresanelsebranchtocaptureallremainingpossibilities.Inmain(),theelsebranchistestedusinganinstanceofthelocalclassWho.

Eachbranchofthewhenusescasifitisthetypewecheckedfor:callinggreeting()ifcisHuman,bark()ifit’saDogandmobility()ifit’sanAlien.

TheModifiableReferenceAutomaticdowncastsaresubjecttoaspecialconstraint.Ifthebase-classreferencetotheobjectismodifiable(avar),thenthere’sapossibilitythatthisreferencecouldbeassignedtoadifferentobjectbetweentheinstantthatthetypeisdetectedandtheinstantwhenyoucallspecificfunctionsonthedowncastobject.Thatis,thespecifictypeoftheobjectmightchangebetweentypedetectionanduse.

Inthefollowing,cistheargumenttowhen,andKotlininsiststhatthisargumentbeimmutablesothatitcannotchangebetweentheisexpressionandthecallmadeafterthe->:

//DownCasting/MutableSmartCast.kt

packagedowncasting

classSmartCast1(valc:Creature){

funcontact(){

when(c){

isHuman->c.greeting()

isDog->c.bark()

isAlien->c.mobility()

}

}

}

classSmartCast2(varc:Creature){

funcontact(){

when(valc=c){//[1]

isHuman->c.greeting()//[2]

isDog->c.bark()

isAlien->c.mobility()

}

}

}

ThecconstructorargumentisavalinSmartCast1andavarinSmartCast2.Inbothcasescispassedintothewhenexpression,whichusesaseriesofsmartcasts.

In[1],theexpressionvalc=clooksodd,andonlyusedhereforconvenience—wedon’trecommend“shadowing”identifiernamesinnormalcode.valccreatesanewlocalidentifiercthatcapturesthevalueofthepropertyc.However,thepropertycisavarwhilethelocal(shadowed)cisaval.Tryremovingthevalc=.Thismeansthatthecwillnowbetheproperty,whichisavar.Thisproducesanerrormessageforline[2]:

Smartcastto‘Human’isimpossible,because‘c’isamutablepropertythatcouldhavebeenchangedbythistime

isDogandisAlienproducesimilarmessages.Thisisnotlimitedtowhileexpressions;thereareothersituationsthatcanproducethesameerrormessage.

Thechangedescribedintheerrormessagetypicallyhappensthroughconcurrency,whenmultipleindependenttaskshavetheopportunitytochangecatunpredictabletimes.(Concurrencyisanadvancedtopicthatwedonotcoverinthisbook).

Kotlinforcesustoensurethatcwillnotchangefromthetimethattheischeckisperformedandthetimethatcisusedasthedowncasttype.SmartCast1doesthisbymakingthecpropertyaval,andSmartCast2doesitbyintroducingthelocalvalc.

Similarly,complexexpressionscannotbesmart-castbecausetheexpressionmightbere-evaluated.Propertiesthatareopenforinheritancecan’tbesmart-

castbecausetheirvaluemightbeoverriddeninsubclasses,sothere’snoguaranteethevaluewillbethesameonthenextaccess.

TheasKeywordTheaskeywordforcefullycastsageneraltypetoaspecifictype:

//DownCasting/Unsafe.kt

packagedowncasting

importatomictest.*

fundogBarkUnsafe(c:Creature)=

(casDog).bark()

fundogBarkUnsafe2(c:Creature):String{

casDog

c.bark()

returnc.bark()+c.bark()

}

funmain(){

dogBarkUnsafe(Dog())eq"Yip!"

dogBarkUnsafe2(Dog())eq"Yip!Yip!"

(capture{

dogBarkUnsafe(Human())

})containslistOf("ClassCastException")

}

dogBarkUnsafe2()showsasecondformofas:ifyousaycasDog,thencistreatedasaDogthroughouttherestofthescope.

AfailingascastthrowsaClassCastException.Aplainasiscalledanunsafecast.

Whenasafecastas?fails,itdoesn’tthrowanexception,butinsteadreturnsnull.YoumustthendosomethingreasonablewiththatnulltopreventalaterNullPointerException.TheElvisoperator(describedinSafeCalls&theElvisOperator)isusuallythemoststraightforwardapproach:

//DownCasting/Safe.kt

packagedowncasting

importatomictest.eq

fundogBarkSafe(c:Creature)=

(cas?Dog)?.bark()?:"NotaDog"

funmain(){

dogBarkSafe(Dog())eq"Yip!"

dogBarkSafe(Human())eq"NotaDog"

}

IfcisnotaDog,as?producesanull.Thus,(cas?Dog)isanullableexpressionandwemustusethesafecalloperator?.tocallbark().Ifas?producesanull,thenthewholeexpression(cas?Dog)?.bark()willalsoproduceanull,whichtheElvisoperatorhandlesbyproducing"NotaDog".

DiscoveringTypesinListsWhenusedinapredicate,isfindsobjectsofagiventypewithinaList,oranyiterable(somethingyoucaniteratethrough):

//DownCasting/FindType.kt

packagedowncasting

importatomictest.eq

valgroup:List<Creature>=listOf(

Human(),Human(),Dog(),Alien(),Dog()

)

funmain(){

valdog=group

.find{itisDog}asDog?//[1]

dog?.bark()eq"Yip!"//[2]

}

BecausegroupcontainsCreatures,find()returnsaCreature.WewanttotreatitasaDog,soweexplicitlycastitattheendofline[1].TheremightbezeroDogsingroup,inwhichcasefind()returnsanullsowemustcasttheresulttoanullableDog?.Becausedogisnullable,weusethesafecalloperatorinline[2].

Youcanusuallyavoidthecodeinline[1]byusingfilterIsInstance(),whichproducesallelementsofaspecifictype:

//DownCasting/FilterIsInstance.kt

importdowncasting.*

importatomictest.eq

funmain(){

valhumans1:List<Creature>=

group.filter{itisHuman}

humans1.sizeeq2

valhumans2:List<Human>=

group.filterIsInstance<Human>()

humans2eqhumans1

}

filterIsInstance()isamorereadablewaytoproducethesameresultasfilter().However,theresulttypesaredifferent:whilefilter()returnsaListofCreature(eventhoughalltheresultingelementsareHuman),

filterIsInstance()returnsalistofthetargettypeHuman.We’vealsoeliminatedthenullabilityissuesseeninFindType.kt.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SealedClasses

Toconstrainaclasshierarchy,declarethesuperclasssealed.

Consideratriptakenbytravelersusingdifferentmodesoftransportation:

//SealedClasses/UnSealed.kt

packagewithoutsealedclasses

importatomictest.eq

openclassTransport

dataclassTrain(

valline:String

):Transport()

dataclassBus(

valnumber:String,

valcapacity:Int

):Transport()

funtravel(transport:Transport)=

when(transport){

isTrain->

"Train${transport.line}"

isBus->

"Bus${transport.number}:"+

"size${transport.capacity}"

else->"$transportisinlimbo!"

}

funmain(){

listOf(Train("S1"),Bus("11",90))

.map(::travel)eq

"[TrainS1,Bus11:size90]"

}

TrainandBuseachcontaindifferentdetailsabouttheirTransportmode.

travel()containsawhenexpressionthatdiscoverstheexacttypeofthetransportparameter.Kotlinrequiresthedefaultelsebranch,becausetheremightbeothersubclassesofTransport.

travel()showsdowncasting’sinherenttroublespot.SupposeyouinheritTramasanewtypeofTransport.Ifyoudothis,travel()continuestocompileandrun,givingyounocluethatyoushouldmodifyittodetectTram.Ifyouhave

manyinstancesofdowncastingscatteredthroughoutyourcode,thisbecomesamaintenancechallenge.

Wecanimprovethesituationusingthesealedkeyword.WhendefiningTransport,replaceopenclasswithsealedclass:

//SealedClasses/SealedClasses.kt

packagesealedclasses

importatomictest.eq

sealedclassTransport

dataclassTrain(

valline:String

):Transport()

dataclassBus(

valnumber:String,

valcapacity:Int

):Transport()

funtravel(transport:Transport)=

when(transport){

isTrain->

"Train${transport.line}"

isBus->

"Bus${transport.number}:"+

"size${transport.capacity}"

}

funmain(){

listOf(Train("S1"),Bus("11",90))

.map(::travel)eq

"[TrainS1,Bus11:size90]"

}

Alldirectsubclassesofasealedclassmustbelocatedinthesamefileasthebaseclass.

AlthoughKotlinforcesyoutoexhaustivelycheckallpossibletypesinawhenexpression,thewhenintravel()nolongerrequiresanelsebranch.BecauseTransportissealed,KotlinknowsthatnoadditionalsubclassesofTransportexistotherthantheonespresentinthisfile.Thewhenexpressionisnowexhaustivewithoutanelsebranch.

sealedhierarchiesdiscovererrorswhenaddingnewsubclasses.Whenyouintroduceanewsubclass,youmustupdateallthecodethatusestheexistinghierarchy.Thetravel()functioninUnSealed.ktwillcontinuetoworkbecausetheelsebranchproduces"$transportisinlimbo!"onunknowntypesoftransportation.However,that’sprobablynotthebehavioryouwant.

AsealedclassrevealsalltheplacestomodifywhenweaddanewsubclasssuchasTram.Thetravel()functioninSealedClasses.ktwon’tcompileifweintroducetheTramclasswithoutmakingadditionalchanges.Thesealedkeywordmakesitimpossibletoignoretheproblem,becauseyougetacompilationerror.

Thesealedkeywordmakesdowncastingmorepalatable,butyoushouldstillbesuspiciousofdesignsthatmakeexcessiveuseofdowncasting.Thereisoftenabetterandcleanerwaytowritethatcodeusingpolymorphism.

sealedvs.abstractHereweshowthatbothabstractandsealedclassesallowidenticaltypesoffunctions,properties,andconstructors:

//SealedClasses/SealedVsAbstract.kt

packagesealedclasses

abstractclassAbstract(valav:String){

openfunconcreteFunction(){}

openvalconcreteProperty=""

abstractfunabstractFunction():String

abstractvalabstractProperty:String

init{}

constructor(c:Char):this(c.toString())

}

openclassConcrete():Abstract(""){

overridefunconcreteFunction(){}

overridevalconcreteProperty=""

overridefunabstractFunction()=""

overridevalabstractProperty=""

}

sealedclassSealed(valav:String){

openfunconcreteFunction(){}

openvalconcreteProperty=""

abstractfunabstractFunction():String

abstractvalabstractProperty:String

init{}

constructor(c:Char):this(c.toString())

}

openclassSealedSubclass():Sealed(""){

overridefunconcreteFunction(){}

overridevalconcreteProperty=""

overridefunabstractFunction()=""

overridevalabstractProperty=""

}

funmain(){

Concrete()

SealedSubclass()

}

Asealedclassisbasicallyanabstractclasswiththeextraconstraintthatalldirectsubclassesmustbedefinedwithinthesamefile.

Indirectsubclassesofasealedclasscanbedefinedinaseparatefile:

//SealedClasses/ThirdLevelSealed.kt

packagesealedclasses

classThirdLevel:SealedSubclass()

ThirdLeveldoesn’tdirectlyinheritfromSealedsoitdoesn’tneedtoresideinSealedVsAbstract.kt.

Althoughasealedinterfaceseemslikeitwouldbeausefulconstruct,Kotlindoesn’tprovideitbecauseJavaclassescannotbepreventedfromimplementingthesameinterface.

EnumeratingSubclassesWhenaclassissealed,youcaneasilyiteratethroughitssubclasses:

//SealedClasses/SealedSubclasses.kt

packagesealedclasses

importatomictest.eq

sealedclassTop

classMiddle1:Top()

classMiddle2:Top()

openclassMiddle3:Top()

classBottom3:Middle3()

funmain(){

Top::class.sealedSubclasses

.map{it.simpleName}eq

"[Middle1,Middle2,Middle3]"

}

Creatingaclassgeneratesaclassobject.Youcanaccesspropertiesandmemberfunctionsofthatclassobjecttodiscoverinformation,andtocreateandmanipulateobjectsofthatclass.::classproducesaclassobject,soTop::classproducestheclassobjectforTop.

OneofthepropertiesofclassobjectsissealedSubclasses,whichexpectsthatTopisasealedclass(otherwiseitproducesanemptylist).sealedSubclassesproducesalltheclassobjectsofthosesubclasses.NoticethatonlytheimmediatesubclassesofTopappearintheresult.

ThetoString()foraclassobjectisslightlyverbose.WeproducetheclassnamealonebyusingthesimpleNameproperty.

sealedSubclassesusesreflection,whichrequiresthatthedependencykotlin-reflection.jarbeintheclasspath.Reflectionisawaytodynamicallydiscoverandusecharacteristicsofaclass.

sealedSubclassescanbeanimportanttoolwhenbuildingpolymorphicsystems.Itcanensurethatnewclasseswillautomaticallybeincludedinallappropriateoperations.Becauseitdiscoversthesubclassesatruntime,however,itmayhaveaperformanceimpactonyoursystem.Ifyouarehavingspeedissues,besuretouseaprofilertodiscoverwhethersealedSubclassesmightbetheproblem(asyoulearntouseaprofiler,you’lldiscoverthatperformanceproblemsareusuallynotwhereyouguessthemtobe).

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

TypeChecking

InKotlinyoucaneasilyactonanobjectbasedonitstype.Normallythisactivityisthedomainofpolymorphism,sotypecheckingenablesinterestingdesignchoices.

Traditionally,typecheckingisusedforspecialcases.Forexample,themajorityofinsectscanfly,butthereareatinynumberthatcannot.Itdoesn’tmakesensetoburdentheInsectinterfacewiththefewinsectsthatareunabletofly,soinbasic()weusetypecheckingtopickthoseout:

//TypeChecking/Insects.kt

packagetypechecking

importatomictest.eq

interfaceInsect{

funwalk()="$name:walk"

funfly()="$name:fly"

}

classHouseFly:Insect

classFlea:Insect{

overridefunfly()=

throwException("Fleacannotfly")

funcrawl()="Flea:crawl"

}

funInsect.basic()=

walk()+""+

if(thisisFlea)

crawl()

else

fly()

interfaceSwimmingInsect:Insect{

funswim()="$name:swim"

}

interfaceWaterWalker:Insect{

funwalkWater()=

"$name:walkonwater"

}

classWaterBeetle:SwimmingInsect

classWaterStrider:WaterWalker

classWhirligigBeetle:

SwimmingInsect,WaterWalker

funInsect.water()=

when(this){

isSwimmingInsect->swim()

isWaterWalker->walkWater()

else->"$name:drown"

}

funmain(){

valinsects=listOf(

HouseFly(),Flea(),WaterStrider(),

WaterBeetle(),WhirligigBeetle()

)

insects.map{it.basic()}eq

"[HouseFly:walkHouseFly:fly,"+

"Flea:walkFlea:crawl,"+

"WaterStrider:walkWaterStrider:fly,"+

"WaterBeetle:walkWaterBeetle:fly,"+

"WhirligigBeetle:walk"+

"WhirligigBeetle:fly]"

insects.map{it.water()}eq

"[HouseFly:drown,Flea:drown,"+

"WaterStrider:walkonwater,"+

"WaterBeetle:swim,"+

"WhirligigBeetle:swim]"

}

Therearealsoaverysmallnumberofinsectsthatcanwalkonwaterorswimunderwater.Again,itdoesn’tmakesensetoputthosespecial-casebehaviorsinthebaseclasstosupportsuchasmallfractionoftypes.Instead,Insect.water()containsawhenexpressionthatselectsthosesubtypesforspecialbehaviorandassumesstandardbehaviorforeverythingelse.

Selectingafewisolatedtypesforspecialtreatmentisthetypicalusecasefortypechecking.Noticethataddingnewtypestothesystemdoesn’timpacttheexistingcode(unlessanewtypealsorequiresspecialtreatment).

Tosimplifythecode,nameproducesthetypeoftheobjectpointedtobythethisunderquestion:

//TypeChecking/AnyName.kt

packagetypechecking

valAny.name

get()=this::class.simpleName

nametakesanAnyandgetstheassociatedclassreferenceusing::class,thenproducesthesimpleNameofthatclass.

Nowconsideravariationofthe“shape”example:

//TypeChecking/TypeCheck1.kt

packagetypechecking

importatomictest.eq

interfaceShape{

fundraw():String

}

classCircle:Shape{

overridefundraw()="Circle:Draw"

}

classSquare:Shape{

overridefundraw()="Square:Draw"

funrotate()="Square:Rotate"

}

funturn(s:Shape)=when(s){

isSquare->s.rotate()

else->""

}

funmain(){

valshapes=listOf(Circle(),Square())

shapes.map{it.draw()}eq

"[Circle:Draw,Square:Draw]"

shapes.map{turn(it)}eq

"[,Square:Rotate]"

}

Thereareseveralreasonswhyyoumightaddrotate()toSquareinsteadofShape:

TheShapeinterfaceisoutofyourcontrol,soyoucannotmodifyit.RotatingSquareseemslikeaspecialcasethatshouldn’tburdenand/orcomplicatetheShapeinterface.You’rejusttryingtoquicklysolveaproblembyaddingSquareandyoudon’twanttotakethetroubleofputtingrotate()inShapeandimplementingitinallthesubtypes.

Therearecertainlysituationswhenthissolutiondoesn’tnegativelyimpactyourdesign,andKotlin’swhenproducescleanandstraightforwardcode.

If,however,youmustevolveyoursystembyaddingmoretypes,itbeginstogetmessy:

//TypeChecking/TypeCheck2.kt

packagetypechecking

importatomictest.eq

classTriangle:Shape{

overridefundraw()="Triangle:Draw"

funrotate()="Triangle:Rotate"

}

funturn2(s:Shape)=when(s){

isSquare->s.rotate()

isTriangle->s.rotate()

else->""

}

funmain(){

valshapes=

listOf(Circle(),Square(),Triangle())

shapes.map{it.draw()}eq

"[Circle:Draw,Square:Draw,"+

"Triangle:Draw]"

shapes.map{turn(it)}eq

"[,Square:Rotate,]"

shapes.map{turn2(it)}eq

"[,Square:Rotate,Triangle:Rotate]"

}

Thepolymorphiccallinshapes.map{it.draw()}adaptstothenewTriangleclasswithoutanychangesorerrors.Also,KotlindisallowsTriangleunlessitimplementsdraw().

Theoriginalturn()doesn’tbreakwhenweaddTriangle,butitalsodoesn’tproducetheresultwewant.turn()mustbecometurn2()togeneratethedesiredbehavior.

Supposeyoursystembeginstoaccumulatemorefunctionsliketurn().TheShapelogicisnowdistributedacrossallthesefunctions,ratherthanbeingcentralizedwithintheShapehierarchy.IfyouaddmorenewtypesofShape,youmustsearchforeveryfunctioncontainingawhenthatswitchesonaShapetype,andmodifyittoincludethenewcase.Ifyoumissanyofthesefunctions,thecompilerwon’tcatchit.

turn()andturn2()exhibitwhatisoftencalledtype-checkcoding,whichmeanstestingforeverytypeinyoursystem.(Ifyouareonlylookingforoneorafewspecialtypesitisnotusuallyconsideredtype-checkcoding).

Intraditionalobject-orientedlanguages,type-checkcodingisusuallyconsideredanantipatternbecauseitinvitesthecreationofoneormorepiecesofcodethatmustbevigilantlymaintainedandupdatedwheneveryouaddorchangetypesinyoursystem.Polymorphism,ontheotherhand,encapsulatesthosechangesintothetypesthatyouaddormodify,andthosechangesarethentransparentlypropagatedthroughyoursystem.

NotethattheproblemonlyoccurswhenthesystemneedstoevolvebyaddingmoreShapetypes.Ifthat’snothowyoursystemevolves,youwon’tencountertheissue.Ifitisaproblemitdoesn’tusuallyhappensuddenly,butbecomessteadilymoredifficultasyoursystemevolves.

WeshallseethatKotlinsignificantlymitigatesthisproblemthroughtheuseofsealedclasses.Thesolutionisn’tperfect,buttypecheckingbecomesamuchmorereasonabledesignchoice.

TypeCheckinginAuxiliaryFunctionsTheessenceofaBeverageContaineristhatitholdsanddeliversbeverages.Itseemstomakesensetotreatrecyclingasanauxiliaryfunction:

//TypeChecking/BeverageContainer.kt

packagetypechecking

importatomictest.eq

interfaceBeverageContainer{

funopen():String

funpour():String

}

classCan:BeverageContainer{

overridefunopen()="PopTop"

overridefunpour()="Can:Pour"

}

openclassBottle:BeverageContainer{

overridefunopen()="RemoveCap"

overridefunpour()="Bottle:Pour"

}

classGlassBottle:Bottle()

classPlasticBottle:Bottle()

funBeverageContainer.recycle()=

when(this){

isCan->"RecycleCan"

isGlassBottle->"RecycleGlass"

else->"Landfill"

}

funmain(){

valrefrigerator=listOf(

Can(),GlassBottle(),PlasticBottle()

)

refrigerator.map{it.open()}eq

"[PopTop,RemoveCap,RemoveCap]"

refrigerator.map{it.recycle()}eq

"[RecycleCan,RecycleGlass,"+

"Landfill]"

}

Bydefiningrecycle()asanauxiliaryfunctionitcapturesthedifferentrecyclingbehaviorsinasingleplace,ratherthanhavingthemdistributedthroughouttheBeverageContainerhierarchybymakingrecycle()amemberfunction.

Actingontypeswithwheniscleanandstraightforward,butthedesignisstillproblematic.Whenyouaddanewtype,recycle()quietlyusestheelseclause.

Becauseofthis,necessarychangestotype-checkingfunctionslikerecycle()mightbemissed.Whatwe’dlikeisforthecompilertotellusthatwe’veforgottenatypecheck,justasitdoeswhenweimplementaninterfaceorinheritanabstractclassandittellsuswe’veforgottentooverrideafunction.

sealedclassesprovideasignificantimprovementhere.MakingShapeasealedclassmeansthatthewheninturn()(afterremovingtheelse)requiresthateachtypebechecked.InterfacescannotbesealedsowemustrewriteShapeintoaclass:

//TypeChecking/TypeCheck3.kt

packagetypechecking3

importatomictest.eq

importtypechecking.name

sealedclassShape{

fundraw()="$name:Draw"

}

classCircle:Shape()

classSquare:Shape(){

funrotate()="Square:Rotate"

}

classTriangle:Shape(){

funrotate()="Triangle:Rotate"

}

funturn(s:Shape)=when(s){

isCircle->""

isSquare->s.rotate()

isTriangle->s.rotate()

}

funmain(){

valshapes=listOf(Circle(),Square())

shapes.map{it.draw()}eq

"[Circle:Draw,Square:Draw]"

shapes.map{turn(it)}eq

"[,Square:Rotate]"

}

IfweaddanewShape,thecompilertellsustoaddanewtype-checkpathinturn().

Butlet’slookatwhathappenswhenwetrytoapplysealedtotheBeverageContainerproblem.Intheprocess,wecreateadditionalCanandBottlesubtypes:

//TypeChecking/BeverageContainer2.kt

packagetypechecking2

importatomictest.eq

sealedclassBeverageContainer{

abstractfunopen():String

abstractfunpour():String

}

sealedclassCan:BeverageContainer(){

overridefunopen()="PopTop"

overridefunpour()="Can:Pour"

}

classSteelCan:Can()

classAluminumCan:Can()

sealedclassBottle:BeverageContainer(){

overridefunopen()="RemoveCap"

overridefunpour()="Bottle:Pour"

}

classGlassBottle:Bottle()

sealedclassPlasticBottle:Bottle()

classPETBottle:PlasticBottle()

classHDPEBottle:PlasticBottle()

funBeverageContainer.recycle()=

when(this){

isCan->"RecycleCan"

isBottle->"RecycleBottle"

}

funBeverageContainer.recycle2()=

when(this){

isCan->when(this){

isSteelCan->"RecycleSteel"

isAluminumCan->"RecycleAluminum"

}

isBottle->when(this){

isGlassBottle->"RecycleGlass"

isPlasticBottle->when(this){

isPETBottle->"RecyclePET"

isHDPEBottle->"RecycleHDPE"

}

}

}

funmain(){

valrefrigerator=listOf(

SteelCan(),AluminumCan(),

GlassBottle(),

PETBottle(),HDPEBottle()

)

refrigerator.map{it.open()}eq

"[PopTop,PopTop,RemoveCap,"+

"RemoveCap,RemoveCap]"

refrigerator.map{it.recycle()}eq

"[RecycleCan,RecycleCan,"+

"RecycleBottle,RecycleBottle,"+

"RecycleBottle]"

refrigerator.map{it.recycle2()}eq

"[RecycleSteel,RecycleAluminum,"+

"RecycleGlass,"+

"RecyclePET,RecycleHDPE]"

}

NotethattheintermediateclassesCanandBottlemustalsobesealedforthisapproachtowork.

AslongastheclassesaredirectsubclassesofBeverageContainer,thecompilerguaranteesthatthewheninrecycle()isexhaustive.ButsubclasseslikeGlassBottleandAluminumCanarenotchecked.Tosolvetheproblemwemustexplicitlyincludethenestedwhenexpressionsseeninrecycle2(),atwhichpointthecompilerdoesrequireexhaustivetypechecks(trycommentingoneofthespecificCanorBottletypestoverifythis).

Tocreatearobusttype-checkingsolutionyoumustrigorouslyusesealedateachintermediateleveloftheclasshierarchy,whileensuringthateachlevelofsubclasseshasacorrespondingnestedwhen.Inthiscase,ifyouaddanewsubtypeofCanorBottlethecompilerensuresthatrecycle2()testsforeachsubtype.

Althoughnotascleanaspolymorphism,thisisasignificantimprovementoverpriorobject-orientedlanguages,andallowsyoutochoosewhethertowriteapolymorphicmemberfunctionorauxiliaryfunction.Noticethatthisproblemonlyoccurswhenyouhavemultiplelevelsofinheritance.

Forcomparison,let’srewriteBeverageContainer2.ktbybringingrecycle()intoBeverageContainer,whichcanagainbeaninterface:

//TypeChecking/BeverageContainer3.kt

packagetypechecking3

importatomictest.eq

importtypechecking.name

interfaceBeverageContainer{

funopen():String

funpour()="$name:Pour"

funrecycle():String

}

abstractclassCan:BeverageContainer{

overridefunopen()="PopTop"

}

classSteelCan:Can(){

overridefunrecycle()="RecycleSteel"

}

classAluminumCan:Can(){

overridefunrecycle()="RecycleAluminum"

}

abstractclassBottle:BeverageContainer{

overridefunopen()="RemoveCap"

}

classGlassBottle:Bottle(){

overridefunrecycle()="RecycleGlass"

}

abstractclassPlasticBottle:Bottle()

classPETBottle:PlasticBottle(){

overridefunrecycle()="RecyclePET"

}

classHDPEBottle:PlasticBottle(){

overridefunrecycle()="RecycleHDPE"

}

funmain(){

valrefrigerator=listOf(

SteelCan(),AluminumCan(),

GlassBottle(),

PETBottle(),HDPEBottle()

)

refrigerator.map{it.open()}eq

"[PopTop,PopTop,RemoveCap,"+

"RemoveCap,RemoveCap]"

refrigerator.map{it.recycle()}eq

"[RecycleSteel,RecycleAluminum,"+

"RecycleGlass,"+

"RecyclePET,RecycleHDPE]"

}

BymakingCanandBottleabstractclasses,weforcetheirsubclassestooverriderecycle()inthesamewaythatthecompilerforceseachtypetobecheckedinsiderecycle2()inBeverageContainer2.kt.

Nowthebehaviorofrecycle()isdistributedamongtheclasses,whichmightbefine—it’sadesigndecision.Ifyoudecidethatrecyclingbehaviorchangesoftenandyou’dliketohaveitallinoneplace,thenusingtheauxiliarytype-checkedrecycle2()fromBeverageContainer2.ktmightbeabetterchoiceforyourneeds,andKotlin’sfeaturesmakethatreasonable.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

NestedClasses

Nestedclassesenablemorerefinedstructureswithinyourobjects.

Anestedclassissimplyaclasswithinthenamespaceoftheouterclass.Theimplicationisthattheouterclass“owns”thenestedclass.Thisfeatureisnotessential,butnestingaclasscanclarifyyourcode.Here,PlaneisnestedwithinAirport:

//NestedClasses/Airport.kt

packagenestedclasses

importatomictest.eq

importnestedclasses.Airport.Plane

classAirport(privatevalcode:String){

openclassPlane{

//Canaccessprivateproperties:

funcontact(airport:Airport)=

"Contacting${airport.code}"

}

privateclassPrivatePlane:Plane()

funprivatePlane():Plane=PrivatePlane()

}

funmain(){

valdenver=Airport("DEN")

varplane=Plane()//[1]

plane.contact(denver)eq"ContactingDEN"

//Can'tdothis:

//valprivatePlane=Airport.PrivatePlane()

valfrankfurt=Airport("FRA")

plane=frankfurt.privatePlane()

//Can'tdothis:

//valp=planeasPrivatePlane//[2]

plane.contact(frankfurt)eq"ContactingFRA"

}

Incontact(),thenestedclassPlanehasaccesstotheprivatepropertycodeintheairportargument,whereasanordinaryclasswouldnothavethisaccess.Otherthanthat,PlaneissimplyaclassinsidetheAirportnamespace.

CreatingaPlaneobjectdoesnotrequireanAirportobject,butifyoucreateitoutsidetheAirportclassbody,youmustordinarilyqualifytheconstructorcallin[1].Byimportingnestedclasses.Airport.Planeweavoidthisqualification.

Anestedclasscanbeprivate,aswithPrivatePlane.MakingitprivatemeansthatPrivatePlaneiscompletelyinvisibleoutsidethebodyofAirport,soyoucannotcallthePrivatePlaneconstructoroutsideofAirport.IfyoudefineandreturnaPrivatePlanefromamemberfunction,asseeninprivatePlane(),theresultmustbeupcasttoapublictype(assumingitextendsapublictype),andcannotbedowncasttotheprivatetype,asseenin[2].

Here’sanexampleofnestingwhereCleanableisabaseclassforboththeenclosingclassHouseandallthenestedclasses.clean()goesthroughaListofpartsandcallsclean()foreachone,producingakindofrecursion:

//NestedClasses/NestedHouse.kt

packagenestedclasses

importatomictest.*

abstractclassCleanable(valid:String){

openvalparts:List<Cleanable>=listOf()

funclean():String{

valtext="$idclean"

if(parts.isEmpty())returntext

return"${parts.joinToString(

"","(",")",

transform=Cleanable::clean)}$text\n"

}

}

classHouse:Cleanable("House"){

overridevalparts=listOf(

Bedroom("MasterBedroom"),

Bedroom("GuestBedroom")

)

classBedroom(id:String):Cleanable(id){

overridevalparts=

listOf(Closet(),Bathroom())

classCloset:Cleanable("Closet"){

overridevalparts=

listOf(Shelf(),Shelf())

classShelf:Cleanable("Shelf")

}

classBathroom:Cleanable("Bathroom"){

overridevalparts=

listOf(Toilet(),Sink())

classToilet:Cleanable("Toilet")

classSink:Cleanable("Sink")

}

}

}

funmain(){

House().clean()eq"""

(((ShelfcleanShelfclean)Closetclean

(ToiletcleanSinkclean)Bathroomclean

)MasterBedroomclean

((ShelfcleanShelfclean)Closetclean

(ToiletcleanSinkclean)Bathroomclean

)GuestBedroomclean

)Houseclean

"""

}

Noticethemultiplelevelsofnesting.Forexample,BedroomcontainsBathroomwhichcontainsToiletandSink.

LocalClassesClassesthatarenestedinsidefunctionsarecalledlocalclasses:

//NestedClasses/LocalClasses.kt

packagenestedclasses

funlocalClasses(){

openclassAmphibian

classFrog:Amphibian()

valamphibian:Amphibian=Frog()

}

Amphibianlookslikeacandidatetobeaninterfaceratherthananopenclass.However,localinterfacesarenotallowed.

Localopenclassesshouldberare;ifyouneedone,whatyou’retryingtomakeisprobablysignificantenoughtocreatearegularclass.

AmphibianandFrogareinvisibleoutsidelocalClasses(),soyoucan’treturnthemfromthefunction.Toreturnobjectsoflocalclasses,youmustupcastthemtoaclassorinterfacedefinedoutsidethefunction:

//NestedClasses/ReturnLocal.kt

packagenestedclasses

interfaceAmphibian

funcreateAmphibian():Amphibian{

classFrog:Amphibian

returnFrog()

}

funmain(){

valamphibian=createAmphibian()

//amphibianasFrog

}

FrogisstillinvisibleoutsidecreateAmphibian()—inmain(),youcannotcastamphibiantoaFrogbecauseFrogisn’tavailable,soKotlinreportstheattempttouseFrogasan“unresolvedreference.”

ClassesInsideInterfaces

Classescanbenestedwithininterfaces:

//NestedClasses/WithinInterface.kt

packagenestedclasses

importatomictest.eq

interfaceItem{

valtype:Type

dataclassType(valtype:String)

}

classBolt(type:String):Item{

overridevaltype=Item.Type(type)

}

funmain(){

valitems=listOf(

Bolt("Slotted"),Bolt("Hex")

)

items.map(Item::type)eq

"[Type(type=Slotted),Type(type=Hex)]"

}

InBolt,thevaltypemustbeoverriddenandassignedusingthequalifiedclassnameItem.Type.

NestedEnumerationsEnumerationsareclasses,sotheycanbenestedinsideotherclasses:

//NestedClasses/Ticket.kt

packagenestedclasses

importatomictest.eq

importnestedclasses.Ticket.Seat.*

classTicket(

valname:String,

valseat:Seat=Coach

){

enumclassSeat{

Coach,

Premium,

Business,

First

}

funupgrade():Ticket{

valnewSeat=values()[

(seat.ordinal+1)

.coerceAtMost(First.ordinal)

]

returnTicket(name,newSeat)

}

funmeal()=when(seat){

Coach->"BagMeal"

Premium->"BagMealwithCookie"

Business->"HotMeal"

First->"PrivateChef"

}

overridefuntoString()="$seat"

}

funmain(){

valtickets=listOf(

Ticket("Jerry"),

Ticket("Summer",Premium),

Ticket("Squanchy",Business),

Ticket("Beth",First)

)

tickets.map(Ticket::meal)eq

"[BagMeal,BagMealwithCookie,"+

"HotMeal,PrivateChef]"

tickets.map(Ticket::upgrade)eq

"[Premium,Business,First,First]"

ticketseq

"[Coach,Premium,Business,First]"

tickets.map(Ticket::meal)eq

"[BagMeal,BagMealwithCookie,"+

"HotMeal,PrivateChef]"

}

upgrade()addsonetotheordinalvalueoftheseat,thenusesthelibraryfunctioncoerceAtMost()toensurethenewvaluedoesnotexceedFirst.ordinalbeforeindexingintovalues()toproducethenewSeattype.Followingfunctionalprogrammingprinciples,upgradingaTicketproducesanewTicketratherthanmodifyingtheoldone.

meal()useswhentotesteverytypeofSeatandthissuggestswecouldusepolymorphisminstead.

Enumerationscannotbenestedwithinfunctions,andcannotinheritfromotherclasses(includingotherenumerations).

Interfacescancontainnestedenumerations.FillItisagame-likesimulationthatfillsasquaregridwithrandomly-chosenXandOmarks:

//NestedClasses/FillIt.kt

packagenestedclasses

importnestedclasses.Game.State.*

importnestedclasses.Game.Mark.*

importkotlin.random.Random

importatomictest.*

interfaceGame{

enumclassState{Playing,Finished}

enumclassMark{Blank,X,O}

}

classFillIt(

valside:Int=3,randomSeed:Int=0

):Game{

valrand=Random(randomSeed)

privatevarstate=Playing

privatevalgrid=

MutableList(side*side){Blank}

privatevarplayer=X

funturn(){

valblanks=grid.withIndex()

.filter{it.value==Blank}

if(blanks.isEmpty()){

state=Finished

}else{

grid[blanks.random(rand).index]=player

player=if(player==X)OelseX

}

}

funplay(){

while(state!=Finished)

turn()

}

overridefuntoString()=

grid.chunked(side).joinToString("\n")

}

funmain(){

valgame=FillIt(8,17)

game.play()

gameeq"""

[O,X,O,X,O,X,X,X]

[X,O,O,O,O,O,X,X]

[O,O,X,O,O,O,X,X]

[X,O,O,O,O,O,X,O]

[X,X,O,O,X,X,X,O]

[X,X,O,O,X,X,O,X]

[O,X,X,O,O,O,X,O]

[X,O,X,X,X,O,X,X]

"""

}

Fortestability,weseedaRandomobjectwithrandomSeedtoproduceidenticaloutputeachtimetheprogramruns.EachelementofgridisinitializedwithBlank.Inturn(),wefirstfindallcellscontainingBlank,alongwiththeirindices.IftherearenomoreBlankcellsthenthesimulationiscomplete.Otherwise,weuserandom()withourseededgeneratortoselectoneoftheBlankcells.BecauseweusedwithIndex()earlier,wemustselecttheindexpropertytoproducethelocationofthecellwewanttochange.

TodisplaytheListintheformofatwo-dimensionalgrid,toString()usesthechunked()libraryfunctiontobreaktheListintopieces,eachoflengthside,thenjoinsthesetogetherwithnewlines.

TryexperimentingwithFillItusingdifferentsidesandrandomSeeds.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Objects

Theobjectkeyworddefinessomethingthatlooksroughlylikeaclass.However,youcan’tcreateinstancesofanobject—there’sonlyone.ThisissometimescalledtheSingletonpattern.

Anobjectisawaytocombinefunctionsandpropertiesthatlogicallybelongtogether,butthiscombinationeitherdoesn’trequiremultipleinstances,oryouwanttoexplicitlypreventmultipleinstances.Younevercreateaninstanceofanobject—there’sonlyoneandit’savailableoncetheobjecthasbeendefined:

//Objects/ObjectKeyword.kt

packageobjects

importatomictest.eq

objectJustOne{

valn=2

funf()=n*10

fung()=this.n*20//[1]

}

funmain(){

//valx=JustOne()//Error

JustOne.neq2

JustOne.f()eq20

JustOne.g()eq40

}

Here,youcan’tsayJustOne()tocreateanewinstanceofaclassJustOne.That’sbecausetheobjectkeyworddefinesthestructureandcreatestheobjectatthesametime.Inaddition,itplacestheelementsinsidetheobject’snamespace.Ifyouonlywanttheobjecttobevisiblewithinthecurrentfile,youcanmakeitprivate.

[1]Thethiskeywordreferstothesingleobjectinstance.

Youcannotprovideaparameterlistforanobject.

Namingconventionsareslightlydifferentwhenusingobject.Typically,whenwecreateaninstanceofaclass,welower-casethefirstletteroftheinstancename.Whenyoucreateanobject,however,Kotlindefinestheclassandcreates

asingleinstanceofthatclass.Wecapitalizethefirstletteroftheobjectnamebecauseitalsorepresentsaclass.

Anobjectcaninheritfromaregularclassorinterface:

//Objects/ObjectInheritance.kt

packageobjects

importatomictest.eq

openclassPaint(valcolor:String){

openfunapply()="Applying$color"

}

objectAcrylic:Paint("Blue"){

overridefunapply()=

"Acrylic,${super.apply()}"

}

interfacePaintPreparation{

funprepare():String

}

objectPrepare:PaintPreparation{

overridefunprepare()="Scrape"

}

funmain(){

Prepare.prepare()eq"Scrape"

Paint("Green").apply()eq"ApplyingGreen"

Acrylic.apply()eq"Acrylic,ApplyingBlue"

}

There’sonlyasingleinstanceofanobject,sothatinstanceissharedacrossallcodethatusesit.Here’sanobjectinitsownpackage:

//Objects/GlobalSharing.kt

packageobjectsharing

objectShared{

vari:Int=0

}

WecannowuseSharedinadifferentpackage:

//Objects/Share1.kt

packageobjectshare1

importobjectsharing.Shared

funf(){

Shared.i+=5

}

Andwithinathirdpackage:

//Objects/Share2.kt

packageobjectshare2

importobjectsharing.Shared

importobjectshare1.f

importatomictest.eq

fung(){

Shared.i+=7

}

funmain(){

f()

g()

Shared.ieq12

}

YoucanseefromtheresultsthatSharedisthesameobjectinallpackages,whichmakessensebecauseobjectcreatesasingleinstance.IfyoumakeSharedprivate,it’snotavailableintheotherfiles.

objectscan’tbeplacedinsidefunctions,buttheycanbenestedinsideotherobjectsorclasses(aslongasthoseclassesarenotthemselvesnestedwithinotherclasses):

//Objects/ObjectNesting.kt

packageobjects

importatomictest.eq

objectOuter{

objectNested{

vala="Outer.Nested.a"

}

}

classHasObject{

objectNested{

vala="HasObject.Nested.a"

}

}

funmain(){

Outer.Nested.aeq"Outer.Nested.a"

HasObject.Nested.aeq"HasObject.Nested.a"

}

There’sanotherwaytoputanobjectinsideaclass:acompanionobject,whichyou’llseeintheCompanionObjectsatom.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

InnerClasses

Innerclassesarelikenestedclasses,butanobjectofaninnerclassmaintainsareferencetotheouterclass.

Aninnerclasshasanimplicitlinktotheouterclass.Inthefollowingexample,HotelislikeAirportfromNestedClasses,butitusesinnerclasses.NotethatreceptionispartofHotel,butcallReception(),whichisamemberofthenestedclassRoom,accessesreceptionwithoutqualification:

//InnerClasses/Hotel.kt

packageinnerclasses

importatomictest.eq

classHotel(privatevalreception:String){

openinnerclassRoom(valid:Int=0){

//Uses'reception'fromouterclass:

funcallReception()=

"Room$idCalling$reception"

}

privateinnerclassCloset:Room()

funcloset():Room=Closet()

}

funmain(){

valnycHotel=Hotel("311")

//Youneedanouterobjectto

//createaninstanceoftheinnerclass:

valroom=nycHotel.Room(319)

room.callReception()eq

"Room319Calling311"

valsfHotel=Hotel("0")

valcloset=sfHotel.closet()

closet.callReception()eq"Room0Calling0"

}

BecauseClosetinheritstheinnerclassRoom,Closetmustalsobeaninnerclass.Nestedclassescannotinheritfrominnerclasses.

Closetisprivate,soitisonlyvisiblewithinthescopeofHotel.

Aninnerobjectkeepsareferencetoitsassociatedouterobject.Thus,whencreatinganinnerobjectyoumustfirsthaveanouterobject.YoucannotcreateaRoomobjectwithoutaHotelobject,asyouseewithnycHotel.Room().

innerdataclassesarenotallowed.

QualifiedthisOneofthebenefitsofclassesisthethisreference.Youdon’thavetoexplicitlysay“thecurrentobject”whenyouaccessapropertyormemberfunction.

Withasimpleclass,themeaningofthisisobvious,butwithaninnerclass,thiscouldrefertoeithertheinnerobjectoranouterobject.Toresolvethisissue,Kotlinprovidesthequalifiedthissyntax:thisfollowedby@andthenameofthetargetclass.

Considerthreelevelsofclasses:anouterclassFruitcontaininganinnerclassSeed,whichitselfcontainsaninnerclassDNA:

//InnerClasses/QualifiedThis.kt

packageinnerclasses

importatomictest.eq

importtypechecking.name

classFruit{//Implicitlabel@Fruit

funchangeColor(color:String)=

"Fruit$color"

funabsorbWater(amount:Int){}

innerclassSeed{//Implicitlabel@Seed

funchangeColor(color:String)=

"Seed$color"

fungerminate(){}

funwhichThis(){

//Defaultstothecurrentclass:

this.nameeq"Seed"

//Toclarify,youcanredundantly

//qualifythedefaultthis:

[email protected]"Seed"

//MustexplicitlyaccessFruit:

[email protected]"Fruit"

//Cannotaccessafurther-innerclass:

//[email protected]

}

innerclassDNA{//Implicitlabel@DNA

funchangeColor(color:String){

//changeColor(color)//Recursive

[email protected](color)

[email protected](color)

}

funplant(){

//Callouter-classfunctions

//Withoutqualification:

germinate()

absorbWater(10)

}

//Extensionfunction:

funInt.grow(){//Implicitlabel@grow

//DefaultistheInt.grow()receiver:

this.nameeq"Int"

//Redundantqualification:

[email protected]"Int"

//Youcanstillaccesseverything:

[email protected]"DNA"

[email protected]"Seed"

[email protected]"Fruit"

}

//Extensionfunctionsonouterclasses:

funSeed.plant(){}

funFruit.plant(){}

funwhichThis(){

//Defaultstothecurrentclass:

this.nameeq"DNA"

//Redundantqualification:

[email protected]"DNA"

//Theothersmustbeexplicit:

[email protected]"Seed"

[email protected]"Fruit"

}

}

}

}

//Extensionfunction:

funFruit.grow(amount:Int){

absorbWater(amount)

//CallsFruit'sversionofchangeColor():

changeColor("Red")eq"FruitRed"

}

//Inner-classextensionfunction:

funFruit.Seed.grow(n:Int){

germinate()

//CallsSeed'sversionofchangeColor():

changeColor("Green")eq"SeedGreen"

}

//Inner-classextensionfunction:

funFruit.Seed.DNA.grow(n:Int)=n.grow()

funmain(){

valfruit=Fruit()

fruit.grow(4)

valseed=fruit.Seed()

seed.grow(9)

seed.whichThis()

valdna=seed.DNA()

dna.plant()

dna.grow(5)

dna.whichThis()

dna.changeColor("Purple")

}

Fruit,SeedandDNAallhavefunctionscalledchangeColor(),butthere’snooverriding—thisisnotaninheritancerelationship.Becausetheyhavethesamenameandsignature,theonlywaytodistinguishthemiswithaqualifiedthis,asyouseeinDNA’schangeColor().Insideplant(),functionsineitherofthetwoouterclassescanbecalledwithoutqualificationiftherearenonamecollisions.

Eventhoughit’sanextensionfunction,grow()canstillaccessalltheobjectsintheouterclass.grow()canbecalledanywheretheFruit.Seed.DNAimplicitreceiverisavailable;forexample,insideanextensionfunctionforDNA.

InnerClassInheritanceAninnerclasscaninheritanotherinnerclassfromadifferentouterclass.Here,YolkinBigEggisderivedfromYolkinEgg:

//InnerClasses/InnerClassInheritance.kt

packageinnerclasses

importatomictest.*

openclassEgg{

privatevaryolk=Yolk()

openinnerclassYolk{

init{trace("Egg.Yolk()")}

openfunf(){trace("Egg.Yolk.f()")}

}

init{trace("NewEgg()")}

funinsertYolk(y:Yolk){yolk=y}

fung(){yolk.f()}

}

classBigEgg:Egg(){

innerclassYolk:Egg.Yolk(){

init{trace("BigEgg.Yolk()")}

overridefunf(){

trace("BigEgg.Yolk.f()")

}

}

init{insertYolk(Yolk())}

}

funmain(){

BigEgg().g()

traceeq"""

Egg.Yolk()

NewEgg()

Egg.Yolk()

BigEgg.Yolk()

BigEgg.Yolk.f()

"""

}

BigEgg.YolkexplicitlynamesEgg.Yolkasitsbaseclass,andoverridesitsf()memberfunction.ThefunctioninsertYolk()allowsBigEggtoupcastoneofitsownYolkobjectsintotheyolkreferenceinEgg,sowheng()callsyolk.f(),theoverriddenversionoff()isused.ThesecondcalltoEgg.Yolk()isthebase-classconstructorcalloftheBigEgg.Yolkconstructor.Youcanseethattheoverriddenversionoff()isusedwheng()iscalled.

Asareviewofobjectconstruction,studythetraceoutputuntilitmakessense.

Local&AnonymousInnerClassesClassesdefinedinsidememberfunctionsarecalledlocalinnerclasses.Thesecanalsobecreatedanonymously,usinganobjectexpression,orusingaSAMconversion.Inallcases,theinnerkeywordisnotused,butisimplied:

//InnerClasses/LocalInnerClasses.kt

packageinnerclasses

importatomictest.eq

funinterfacePet{

funspeak():String

}

objectCreatePet{

funhome()="home!"

fundog():Pet{

valsay="Bark"

//Localinnerclass:

classDog:Pet{

overridefunspeak()=say+home()

}

returnDog()

}

funcat():Pet{

valemit="Meow"

//Anonymousinnerclass:

returnobject:Pet{

overridefunspeak()=emit+home()

}

}

funhamster():Pet{

valsqueak="Squeak"

//SAMconversion:

returnPet{squeak+home()}

}

}

funmain(){

CreatePet.dog().speak()eq"Barkhome!"

CreatePet.cat().speak()eq"Meowhome!"

CreatePet.hamster().speak()eq"Squeakhome!"

}

Alocalinnerclasshasaccesstootherelementsinthefunctionaswellaselementsintheouter-classobject,thussay,emit,squeakandhome()areavailablewithinspeak().

Youcanidentifyananonymousinnerclassbecauseitusesanobjectexpression,whichyouseeincat().ItreturnsanobjectofaclassinheritedfromPetthatoverridesspeak().Anonymousinnerclassesaresmallerandmorestraightforwardanddonotcreateanamedclassthatwillonlybeusedinoneplace.EvenmorecompactisaSAMconversion,asseeninhamster().

Becauseinnerclasseskeepareferencetotheouter-classobject,localinnerclassescanaccessallmembersoftheenclosingclass:

//InnerClasses/CounterFactory.kt

packageinnerclasses

importatomictest.*

funinterfaceCounter{

funnext():Int

}

objectCounterFactory{

privatevarcount=0

funnew(name:String):Counter{

//Localinnerclass:

classLocal:Counter{

init{trace("Local()")}

overridefunnext():Int{

//Accesslocalidentifiers:

trace("$name$count")

returncount++

}

}

returnLocal()

}

funnew2(name:String):Counter{

//Instanceofananonymousinnerclass:

returnobject:Counter{

init{trace("Counter()")}

overridefunnext():Int{

trace("$name$count")

returncount++

}

}

}

funnew3(name:String):Counter{

trace("Counter()")

returnCounter{//SAMconversion

trace("$name$count")

count++

}

}

}

funmain(){

funtest(counter:Counter){

(0..3).forEach{counter.next()}

}

test(CounterFactory.new("Local"))

test(CounterFactory.new2("Anon"))

test(CounterFactory.new3("SAM"))

traceeq"""

Local()Local0Local1Local2Local3

Counter()Anon4Anon5Anon6Anon7

Counter()SAM8SAM9SAM10SAM11

"""

}

ACounterkeepstrackofacountandreturnsthenextIntvalue.new(),new2()andnew3()eachcreateadifferentimplementationoftheCounterinterface.

new()returnsaninstanceofanamedinnerclass,new2()returnsaninstanceofananonymousinnerclass,andnew3()usesaSAMconversiontocreateananonymousobject.AlltheresultingCounterobjectshaveimplicitaccesstotheelementsoftheouterobject,thustheyareinnerclassesandnotjustnestedclasses.YoucanseefromtheoutputthatcountinCounterFactoryissharedbyallCounterobjects.

SAMconversionsarelimited—forexample,theydonotsupportinitclauses.

-

InKotlin,filescancontainmultipletop-levelclassesandfunctions.Becauseofthis,there’srarelyaneedforlocalclasses,soifyoudoneedthemtheyshouldbebasicandstraightforward.Forexample,it’sreasonabletocreateasimpledataclassthat’sonlyusedinsideafunction.Ifalocalclassbecomescomplex,youshouldprobablytakeitoutofthefunctionandmakeitaregularclass.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

CompanionObjects

Memberfunctionsactonparticularinstancesofaclass.Somefunctionsaren’t“about”anobject,sotheydon’tneedtobetiedtothatobject.

Functionsandfieldsinsidecompanionobjectsareabouttheclass.Regularclasselementscanaccesstheelementsofthecompanionobject,butthecompanionobjectelementscannotaccesstheregularclasselements.

AsyousawinObjects,it’spossibletodefinearegularobjectinsideaclass,butthatdoesn’tprovideanassociationbetweentheobjectandtheclass.Inparticular,you’reforcedtoexplicitlynamethenestedobjectwhenyourefertoitsmembers.Ifyoudefineacompanionobjectinsideaclass,itselementsbecometransparentlyavailabletothatclass:

//CompanionObjects/CompanionObject.kt

packagecompanionobjects

importatomictest.eq

classWithCompanion{

companionobject{

vali=3

funf()=i*3

}

fung()=i+f()

}

funWithCompanion.Companion.h()=f()*i

funmain(){

valwc=WithCompanion()

wc.g()eq12

WithCompanion.ieq3

WithCompanion.f()eq9

WithCompanion.h()eq27

}

Outsidetheclass,youaccessmembersofthecompanionobjectusingtheclassname,asinWithCompanion.iandWithCompanion.f().Othermembersoftheclasscanaccessthecompanionobjectelementswithoutqualification,asyouseeinthedefinitionofg().

h()isanextensionfunctiontothecompanionobject.

Ifafunctiondoesn’trequireaccesstoprivateclassmembers,youcanchoosetodefineitatfilescoperatherthanputtingitinacompanionobject.

Onlyonecompanionobjectisallowedperclass.Forclarity,youcangivethecompanionobjectaname:

//CompanionObjects/NamingCompanionObjects.kt

packagecompanionobjects

importatomictest.eq

classWithNamed{

companionobjectNamed{

funs()="fromNamed"

}

}

classWithDefault{

companionobject{

funs()="fromDefault"

}

}

funmain(){

WithNamed.s()eq"fromNamed"

WithNamed.Named.s()eq"fromNamed"

WithDefault.s()eq"fromDefault"

//Thedefaultnameis"Companion":

WithDefault.Companion.s()eq"fromDefault"

}

Evenwhenyounamethecompanionobjectyoucanstillaccessitselementswithoutusingthename.Ifyoudon’tgivethecompanionobjectaname,KotlinassignsitthenameCompanion.

Ifyoucreateapropertyinsideacompanionobject,itproducesasinglepieceofstorageforthatfield,sharedwithallinstancesoftheassociatedclass:

//CompanionObjects/ObjectProperty.kt

packagecompanionobjects

importatomictest.eq

classWithObjectProperty{

companionobject{

privatevarn:Int=0//Onlyone

}

funincrement()=++n

}

funmain(){

vala=WithObjectProperty()

valb=WithObjectProperty()

a.increment()eq1

b.increment()eq2

a.increment()eq3

}

Thetestsinmain()showthatnhasonlyasinglepieceofstorage,nomatterhowmanyinstancesofWithObjectPropertyarecreated.aandbbothaccessthesamememoryforn.

increment()showsthatyoucanaccessprivatemembersofthecompanionobjectfromitssurroundingclass.

Whenafunctionisonlyaccessingpropertiesinthecompanionobject,itmakessensetomovethatfunctioninsidethecompanionobject:

//CompanionObjects/ObjectFunctions.kt

packagecompanionobjects

importatomictest.eq

classCompanionObjectFunction{

companionobject{

privatevarn:Int=0

funincrement()=++n

}

}

funmain(){

CompanionObjectFunction.increment()eq1

CompanionObjectFunction.increment()eq2

}

YounolongerneedaCompanionObjectFunctioninstancetocallincrement().

Supposeyou’dliketokeepacountofeveryobjectyoucreate,togiveeachoneauniquereadableidentifier:

//CompanionObjects/ObjectCounter.kt

packagecompanionobjects

importatomictest.eq

classCounted{

companionobject{

privatevarcount=0

}

privatevalid=count++

overridefuntoString()="#$id"

}

funmain(){

List(4){Counted()}eq"[#0,#1,#2,#3]"

}

Acompanionobjectcanbeaninstanceofaclassdefinedelsewhere:

//CompanionObjects/CompanionInstance.kt

packagecompanionobjects

importatomictest.*

interfaceZI{

funf():String

fung():String

}

openclassZIOpen:ZI{

overridefunf()="ZIOpen.f()"

overridefung()="ZIOpen.g()"

}

classZICompanion{

companionobject:ZIOpen()

funu()=trace("${f()}${g()}")

}

classZICompanionInheritance{

companionobject:ZIOpen(){

overridefung()=

"ZICompanionInheritance.g()"

funh()="ZICompanionInheritance.h()"

}

funu()=trace("${f()}${g()}${h()}")

}

classZIClass{

companionobject:ZI{

overridefunf()="ZIClass.f()"

overridefung()="ZIClass.g()"

}

funu()=trace("${f()}${g()}")

}

funmain(){

ZIClass.f()

ZIClass.g()

ZIClass().u()

ZICompanion.f()

ZICompanion.g()

ZICompanion().u()

ZICompanionInheritance.f()

ZICompanionInheritance.g()

ZICompanionInheritance().u()

traceeq"""

ZIClass.f()ZIClass.g()

ZIOpen.f()ZIOpen.g()

ZIOpen.f()

ZICompanionInheritance.g()

ZICompanionInheritance.h()

"""

}

ZICompanionusesaZIOpenobjectasitscompanionobject,andZICompanionInheritancecreatesaZIOpenobjectwhileoverridingandextendingZIOpen.ZIClassshowsthatyoucanimplementaninterfacewhilecreatingthecompanionobject.

Iftheclassyouwanttouseasacompanionobjectisnotopen,youcannotuseitdirectlyaswedidabove.However,ifthatclassimplementsaninterfaceyoucan

stilluseitviaClassDelegation:

//CompanionObjects/CompanionDelegation.kt

packagecompanionobjects

importatomictest.*

classZIClosed:ZI{

overridefunf()="ZIClosed.f()"

overridefung()="ZIClosed.g()"

}

classZIDelegation{

companionobject:ZIbyZIClosed()

funu()=trace("${f()}${g()}")

}

classZIDelegationInheritance{

companionobject:ZIbyZIClosed(){

overridefung()=

"ZIDelegationInheritance.g()"

funh()=

"ZIDelegationInheritance.h()"

}

funu()=trace("${f()}${g()}${h()}")

}

funmain(){

ZIDelegation.f()

ZIDelegation.g()

ZIDelegation().u()

ZIDelegationInheritance.f()

ZIDelegationInheritance.g()

ZIDelegationInheritance().u()

traceeq"""

ZIClosed.f()ZIClosed.g()

ZIClosed.f()

ZIDelegationInheritance.g()

ZIDelegationInheritance.h()

"""

}

ZIDelegationInheritanceshowsthatyoucantakethenon-openclassZIClosed,delegateit,thenoverrideandextendthatdelegate.Delegationforwardsthemethodsofaninterfacetotheinstancethatprovidesanimplementation.Eveniftheclassofthatinstanceisfinal,wecanstilloverrideandaddmethodstothedelegationreceiver.

Here’sasmallbrain-teaser:

//CompanionObjects/DelegateAndExtend.kt

packagecompanionobjects

importatomictest.eq

interfaceExtended:ZI{

funu():String

}

classExtend:ZIbyCompanion,Extended{

companionobject:ZI{

overridefunf()="Extend.f()"

overridefung()="Extend.g()"

}

overridefunu()="${f()}${g()}"

}

privatefuntest(e:Extended):String{

e.f()

e.g()

returne.u()

}

funmain(){

test(Extend())eq"Extend.f()Extend.g()"

}

InExtend,theZIinterfaceisimplementedusingitsowncompanionobject,whichhasthedefaultnameCompanion.ButwearealsoimplementingtheExtendedinterface,whichistheZIinterfaceplusanextrafunctionu().TheZIportionofExtendedisalreadyimplemented,viaCompanion,soweonlyneedtooverridetheadditionalfunctionu()tocompleteExtend.NowanExtendobjectcanbeupcasttoExtendedastheargumenttotest().

Acommonuseforacompanionobjectiscontrollingobjectcreation—thisistheFactoryMethodpattern.Supposeyou’dliketoonlyallowthecreationofListsofNumbered2objects,andnotindividualNumbered2objects:

//CompanionObjects/CompanionFactory.kt

packagecompanionobjects

importatomictest.eq

classNumbered2

privateconstructor(privatevalid:Int){

overridefuntoString():String="#$id"

companionobjectFactory{

funcreate(size:Int)=

List(size){Numbered2(it)}

}

}

funmain(){

Numbered2.create(0)eq"[]"

Numbered2.create(5)eq

"[#0,#1,#2,#3,#4]"

}

TheNumbered2constructorisprivate.Thismeansthere’sonlyonewaytocreateaninstance—viathecreate()factoryfunction.Afactoryfunctioncansometimessolveproblemsthatregularconstructorscannot.

Constructorsincompanionobjectsareinitializedwhentheenclosingclassisinstantiatedforthefirsttimeinaprogram:

//CompanionObjects/Initialization.kt

packagecompanionobjects

importatomictest.*

classCompanionInit{

companionobject{

init{

trace("CompanionConstructor")

}

}

}

funmain(){

trace("Before")

CompanionInit()

trace("After1")

CompanionInit()

trace("After2")

CompanionInit()

trace("After3")

traceeq"""

Before

CompanionConstructor

After1

After2

After3

"""

}

Youcanseefromtheoutputthatthecompanionobjectisconstructedonlyonce,thefirsttimeaCompanionInit()objectiscreated.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SECTIONVI:PREVENTINGFAILUREIfdebuggingistheprocessofremovingsoftwarebugs,thenprogrammingmustbetheprocessofputtingthemin.—EdsgerDijkstra

ExceptionHandling

Failureisalwaysapossibility.

Kotlinfindsbasicerrorswhenitanalyzesyourprogram.Errorsthatcannotbedetectedatcompiletimemustbedealtwithatruntime.InExceptions,youlearnedtothrowexceptions.Inthisatom,wecatchexceptions.

Historically,failureswereoftendisastrous.Forexample,programswrittenintheClanguagewouldsimplystopworking,losetheirdata,andpotentiallycrashtheoperatingsystem.

Improvederrorhandlingisapowerfulwaytoincreasecodereliability.Errorhandlingisespeciallyimportantwhencreatingreusableprogramcomponents.Tocreatearobustsystem,eachcomponentmustberobust.Withconsistenterrorhandling,componentscanreliablycommunicateproblemstoclientcode.

Modernapplicationsoftenuseconcurrency,andaconcurrentprogrammustsurvivenon-criticalexceptions.Aserver,forexample,shouldrecoverwhenanopensessionisterminatedviaanexception.

Exceptionsconflatethreeactivities:

1. Errorreporting2. Recovery3. Resourcecleanup

Let’sconsidereachone.

ReportingStandardlibraryexceptionsareoftenadequate.Formorespecificexceptionhandling,youcaninheritnewexceptiontypesfromExceptionorasubtype:

//ExceptionHandling/DefiningExceptions.kt

packageexceptionhandling

importatomictest.*

classException1(

valvalue:Int

):Exception("wrongvalue:$value")

openclassException2(

description:String

):Exception(description)

classException3(

description:String

):Exception2(description)

funmain(){

capture{

throwException1(13)

}eq"Exception1:wrongvalue:13"

capture{

throwException3("error")

}eq"Exception3:error"

}

Athrowexpression,asinmain(),requiresaninstanceofaThrowablesubtype.Todefinenewexceptiontypes,inheritException(whichextendsThrowable).BothException1andException2inheritException,whileException3inheritsException2.

RecoveryTheambitionofexceptionhandlingisrecovery.Thismeansthatyoufixtheproblem,returntheprogramtoastablestate,andresumeexecution.Recoveryoftenincludeslogginginformationabouttheerror.

Quiteoften,recoveryisn’tpossible.Anexceptionmightrepresentanunrecoverableprogramfailure,eitheracodingerrororsomethinguncontrollableintheenvironment.

Whenanexceptionisthrown,theexception-handlingmechanismlooksforanappropriateplacetocontinueexecution.Anexceptionkeepsmovingouttohigherlevels,fromfunction1()thatthrewtheexception,tofunction2()thatcallsfunction1(),tofunction3()thatcallsfunction2(),andsoonuntilreachingmain().Amatchinghandlercatchestheexception.Thisstopsthesearchandrunsthathandler.Iftheprogramneverfindsamatchinghandler,itterminateswithaconsolestacktrace.

//ExceptionHandling/Stacktrace.kt

packagestacktrace

importexceptionhandling.Exception1

funfunction1():Int=

throwException1(-52)

funfunction2()=function1()

funfunction3()=function2()

funmain(){

//function3()

}

Uncommentingthecalltofunction3()producesthefollowingstacktrace:

Exceptioninthread"main"exceptionhandling.Exception1:wrongvalue:-\

52

atstacktrace.StacktraceKt.function1(Stacktrace.kt:6)

atstacktrace.StacktraceKt.function2(Stacktrace.kt:8)

atstacktrace.StacktraceKt.function3(Stacktrace.kt:10)

atstacktrace.StacktraceKt.main(Stacktrace.kt:13)

atstacktrace.StacktraceKt.main(Stacktrace.kt)

Anyoffunction1(),function2()orfunction3()cancatchtheexceptionandhandleit,preventingtheexceptionfromterminatingtheprogram.

Anexceptionhandleristhecatchkeywordfollowedbyaparameterlistcontainingtheexceptionyou’rehandling.Thisisfollowedbyablockofcodeimplementingtherecovery.

Inthefollowingexample,thefunctiontoss()producesdifferentexceptionsforarguments1-3,otherwiseitreturns“OK”.test()containsacompletesetofhandlersforthetoss()function:

//ExceptionHandling/Handlers.kt

packageexceptionhandling

importatomictest.eq

funtoss(which:Int)=when(which){

1->throwException1(1)

2->throwException2("Exception2")

3->throwException3("Exception3")

else->"OK"

}

funtest(which:Int):Any?=

try{

toss(which)

}catch(e:Exception1){

e.value

}catch(e:Exception3){

e.message

}catch(e:Exception2){

e.message

}

funmain(){

test(0)eq"OK"

test(1)eq1

test(2)eq"Exception2"

test(3)eq"Exception3"

}

Whenyoucalltoss()youmustcatchallrelevanttoss()exceptions,allowingnon-relevantexceptionsto“bubbleup”andbecaughtelsewhere.

Theentiretry-catchintest()isasingleexpression:itreturnseitherthelastexpressionofthetrybodyorthelastexpressionofthecatchclausematchinganexception.Ifnocatchhandlestheexception,thatexceptionisthrownfurtherupthestack.Ifuncaught,itgeneratesastacktrace.

BecauseException3extendsException2,anException3ishandledasanException2ifException2’scatchappearsinthesequenceofhandlersbeforeException3’scatch:

//ExceptionHandling/Hierarchy.kt

packageexceptionhandling

importatomictest.eq

funtestCatchOrder(which:Int)=

try{

toss(which)

}catch(e:Exception2){//[1]

"HandlerforException2got${e.message}"

}catch(e:Exception3){//[2]

"HandlerforException3got${e.message}"

}

funmain(){

testCatchOrder(2)eq

"HandlerforException2gotException2"

testCatchOrder(3)eq

"HandlerforException2gotException3"

}

Thecatch-clauseordermeansanException3iscaughtbyline[1],despitethemorespecifictypeofexceptionhandlerinline[2].

ExceptionSubtypesIntestCode(),anincorrectcodeargumentthrowsanIllegalArgumentException:

//ExceptionHandling/LibraryException.kt

packageexceptionhandling

importatomictest.*

funtestCode(code:Int){

if(code<=1000){

throwIllegalArgumentException(

"'code'mustbe>1000:$code")

}

}

funmain(){

try{

//A1is161inbase-16(hex)notation:

testCode("A1".toInt(16))

}catch(e:IllegalArgumentException){

e.messageeq

"'code'mustbe>1000:161"

}

try{

testCode("0".toInt(1))

}catch(e:IllegalArgumentException){

e.messageeq

"radix1wasnotinvalidrange2..36"

}

}

AnIllegalArgumentExceptionisthrownbybothtestCode()andthelibraryfunctiontoInt(radix).Thisresultsinthesomewhatconfusingerrormessagesinmain().Theproblemisthatweareusingthesameexceptiontorepresenttwodifferentissues.WesolveitbythrowinganewexceptiontypecalledIncorrectInputExceptionforourerror:

//ExceptionHandling/NewException.kt

packageexceptionhandling

importatomictest.eq

classIncorrectInputException(

message:String

):Exception(message)

funcheckCode(code:Int){

if(code<=1000){

throwIncorrectInputException(

"Codemustbe>1000:$code")

}

}

funmain(){

try{

checkCode("A1".toInt(16))

}catch(e:IncorrectInputException){

e.messageeq"Codemustbe>1000:161"

}catch(e:IllegalArgumentException){

"Produceserror"eq"ifitgetshere"

}

try{

checkCode("1".toInt(1))

}catch(e:IncorrectInputException){

"Produceserror"eq"ifitgetshere"

}catch(e:IllegalArgumentException){

e.messageeq

"radix1wasnotinvalidrange2..36"

}

}

Noweachissuehasitsownhandler.

Resistcreatingtoomanyexceptiontypes.Asaruleofthumb,usedifferentexceptiontypestodistinguishdifferenthandlingschemes,andusedifferentconstructorparameterstoprovidedetailsforaparticularhandlingscheme.

ResourceCleanupWhenfailureisinevitable,automaticresourcecleanuphelpsotherpartsoftheprogramtocontinuerunningsafely.

finallyensuresresourcecleanupduringexceptionhandling.Afinallyclausealwaysruns,regardlessofwhetheryouleaveatryblocknormallyorexceptionally:

//ExceptionHandling/TryFinally.kt

packageexceptionhandling

importatomictest.*

funcheckValue(value:Int){

try{

trace(value)

if(value<=0)

throwIllegalArgumentException(

"valuemustbepositive:$value")

}finally{

trace("Infinallyclausefor$value")

}

}

funmain(){

listOf(10,-10).forEach{

try{

checkValue(it)

}catch(e:IllegalArgumentException){

trace("Incatchclauseformain()")

trace(e.message)

}

}

traceeq"""

10

Infinallyclausefor10

-10

Infinallyclausefor-10

Incatchclauseformain()

valuemustbepositive:-10

"""

}

finallyworksevenwithintermediatecatchclauses.Forexample,supposeaswitchmustbeturnedoffwhenyou’redonewithit:

//ExceptionHandling/GuaranteedCleanup.kt

packageexceptionhandling

importatomictest.eq

dataclassSwitch(

varon:Boolean=false,

varresult:String="OK"

)

funtestFinally(i:Int):Switch{

valsw=Switch()

try{

sw.on=true

when(i){

0->throwIllegalStateException()

1->returnsw//[1]

}

}catch(e:IllegalStateException){

sw.result="exception"

}finally{

sw.on=false

}

returnsw

}

funmain(){

testFinally(0)eq

"Switch(on=false,result=exception)"

testFinally(1)eq

"Switch(on=false,result=OK)"//[2]

testFinally(2)eq

"Switch(on=false,result=OK)"

}

Evenifwereturninsideatry([1]),thefinallyclausestillruns([2]).WhethertestFinally()completesnormallyorwithanexception,thefinallyclausealwaysexecutes.

ExceptionHandlinginAtomicTestThisbookusesAtomicTest’scapture()toensurethatexpectedexceptionsarethrown.capture()takesafunctionargumentandreturnsaCapturedExceptionobjectcontainingtheexceptionclassanderrormessage:

//ExceptionHandling/CaptureImplementation.kt

packageexceptionhandling

importatomictest.CapturedException

funcapture(f:()->Unit):CapturedException=

try{//[1]

f()

CapturedException(null,

"<Error>:Expectedanexception")//[2]

}catch(e:Throwable){//[3]

CapturedException(e::class,//[4]

if(e.message!=null)":${e.message}"

else"")

}

funmain(){

capture{

throwException("!!!")

}eq"Exception:!!!"//[5]

capture{

1

}eq"<Error>:Expectedanexception"

}

capture()callsitsfunctionargumentfwithinatryblock([1]),handlingallpossibleexceptionsbycatchingThrowable([3]).Ifnoexceptionisthrown,theCapturedExceptionmessageindicatesthatanexceptionwasexpected([2]).Ifanexceptioniscaught,thereturnedCapturedExceptioncontainstheexceptionclassandamessage([4]).ACapturedExceptioncanbecomparedtoaStringusingeq([5]).

Ordinarilyyouwon’tcatchThrowable,butwillprocesseachspecificexceptiontype.

GuidelinesRecoveringfromexceptionsturnsouttoberemarkablyrare,consideringthatrecoverywastheoriginalintent.TheprimarypurposeofexceptionsinKotlinistodiscoverprogrambugs,notrecovery.CatchingexceptionsinordinaryKotlincodeisthusa“codesmell.”

HereareguidelinesforprogrammingwithexceptionsinKotlin:

1. LogicErrors:Thesearebugsinyourcode.Eitherdon’tcatchthematall(andproduceastacktrace),orcatchthematthetoplevelofyourapplicationandreportthebugs,possiblyrestartingtheaffectedoperation.

2. DataErrors:Theseareerrorsfrombaddatathattheprogrammercannotcontrol.Theapplicationmustsomehowdealwiththeproblemwithoutblamingitonprogramlogic.Forexample,we’veusedString.toInt()thisatom,whichthrowsanexceptionforaninappropriateString.ItalsohasacompanionString.toIntOrNull()thatproducesanulluponfailuresoyoucanuseitinanexpressionsuchasvaln=string.toIntOrNull()?:default.

TheKotlinlibraryisdesignedarounddealingwithabadresultbyreturninganullinsteadofthrowinganexception.Operationsthatareexpectedtooccasionallyfailwillusuallyhavean“OrNull”versionthatyoucanuseinsteadoftheexceptionversion.

3. Checkinstructionstestforlogicerrors.Theseproduceexceptionswhentheyfindabug,buttheylooklikefunctioncallssoyoudon’texplicitlythrowexceptionsinyourcode.

4. Input/OutputErrors:Theseareexternalconditionsthatyoucan’tcontrolandyoucan’tignore.However,usingthe“OrNull”approachrapidlyobscurestheunderstandabilityofthecode.Moreimportantly,youoftencanrecoverfromI/Oerrors,typicallybyretryingtheoperation.Thus,I/OoperationsinKotlinthrowexceptions,soyou’llhavecodeinyourapplicationsthathandlethoseandattempttorecoverfromthem.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

CheckInstructions

Checkinstructionsassertthatconstraintsaresatisfied.Theyarecommonlyusedtovalidatefunctionargumentsandresults.

Checkinstructionsdiscoverprogrammingerrorsbyexpressingnon-obviousrequirements.Theycanalsoactasdocumentationforfuturereadersofthatcode.You’llusuallyfindcheckinstructionsatthebeginningofafunction,toensurethattheargumentsarelegitimate,andattheend,tocheckthefunction’scalculations.

Checkinstructionstypicallythrowexceptionswhentheyfail.Youcanusuallyusecheckinstructionsinsteadofexplicitlythrowingexceptions.Checkinstructionsareeasiertowriteandthinkabout,andproducemorecomprehensiblecode.Usethemwheneverpossibletotestandilluminateyourprograms.

require()

DesignByContractpreconditionsguaranteeinitializationconstraints.Kotlin’srequire()isnormallyusedtovalidatefunctionarguments,soittypicallyappearsatthebeginningoffunctionbodies.Thesetestscannotbecheckedatcompiletime.Preconditionsarerelativelyeasytoincludeinyourcode,butsometimestheycanbeturnedintounittests.

ConsideranumericalfieldrepresentingamonthontheJuliancalendar.Youknowthisvaluemustalwaysbeintherange1..12.Apreconditionreportsanerrorifthevaluefallsoutsidethatrange:

//CheckInstructions/JulianMonth.kt

packagecheckinstructions

importatomictest.*

dataclassMonth(valmonthNumber:Int){

init{

require(monthNumberin1..12){

"Monthoutofrange:$monthNumber"

}

}

}

funmain(){

Month(1)eq"Month(monthNumber=1)"

capture{Month(13)}eq

"IllegalArgumentException:"+

"Monthoutofrange:13"

}

Weperformtherequire()insidetheconstructor.require()throwsanIllegalArgumentExceptionifitsconditionisn’tsatisfied.Youcanalwaysuserequire()insteadofthrowingIllegalArgumentException.

Thesecondparameterforrequire()isalambdathatproducesaString.IftheStringrequiresconstruction,thatoverheaddoesn’toccurunlessrequire()fails.

WhentheargumentsforQuadratic.ktfromSummary2areinappropriate,itthrowsIllegalArgumentException.Wecansimplifythecodeusingrequire():

//CheckInstructions/QuadraticRequire.kt

packagecheckinstructions

importkotlin.math.sqrt

importatomictest.*

classRoots(

valroot1:Double,

valroot2:Double

)

funquadraticZeroes(

a:Double,

b:Double,

c:Double

):Roots{

require(a!=0.0){"aiszero"}

valunderRadical=b*b-4*a*c

require(underRadical>=0){

"NegativeunderRadical:$underRadical"

}

valsquareRoot=sqrt(underRadical)

valroot1=(-b-squareRoot)/2*a

valroot2=(-b+squareRoot)/2*a

returnRoots(root1,root2)

}

funmain(){

capture{

quadraticZeroes(0.0,4.0,5.0)

}eq"IllegalArgumentException:"+

"aiszero"

capture{

quadraticZeroes(3.0,4.0,5.0)

}eq"IllegalArgumentException:"+

"NegativeunderRadical:-44.0"

valroots=quadraticZeroes(3.0,8.0,5.0)

roots.root1eq-15.0

roots.root2eq-9.0

}

ThiscodeismuchclearerandcleanerthantheoriginalQuadratic.kt.

ThefollowingDataFileclassallowsustoworkwithfilesregardlessofwhethertheexamplesrunintheIDEviatheAtomicKotlincourseorinthestandalonebuildforthebook.AllDataFileobjectsstoretheirfilesinthetargetDirsubdirectory:

//CheckInstructions/DataFile.kt

packagecheckinstructions

importatomictest.eq

importjava.io.File

importjava.nio.file.Paths

valtargetDir=File("DataFiles")

classDataFile(valfileName:String):

File(targetDir,fileName){

init{

if(!targetDir.exists())

targetDir.mkdir()

}

funerase(){if(exists())delete()}

funreset():File{

erase()

createNewFile()

returnthis

}

}

funmain(){

DataFile("Test.txt").reset()eq

Paths.get("DataFiles","Test.txt")

.toString()

}

ADataFilemanipulatestheunderlyingfileintheoperatingsystemtowriteandreadthatfile.ThebaseclassforDataFileisjava.io.File,whichisoneoftheoldestclassesintheJavalibrary;itappearedinthefirstversionofthelanguage,backwhentheythoughtitwasagreatideatousethesameclass(File)torepresentbothfilesanddirectories.KotlincaneffortlesslyinheritFile,despiteitsantiquity.

Duringconstruction,wecreatetargetDirifitdoesn’texist.Theerase()functiondeletesthefile,whilereset()deletesthefileandcreatesanew,emptyfile.

TheJavastandardlibraryPathsclasscontainsonlyanoverloadedget().Theversionofget()wewanttakesanynumberofStringsandbuildsaPathobject,representingadirectorypaththatisindependentoftheoperatingsystem.

Openingafileoftenhasanumberofpreconditions,usuallyinvolvingfilepaths,naming,andcontents.Considerafunctionthatopensandreadsafilewithanamebeginningwithfile_.Usingrequire(),weverifythatthefilenameiscorrectandthatthefileexistsandisnotempty:

//CheckInstructions/GetTrace.kt

packagecheckinstructions

importatomictest.*

fungetTrace(fileName:String):List<String>{

require(fileName.startsWith("file_")){

"$fileNamemuststartwith'file_'"

}

valfile=DataFile(fileName)

require(file.exists()){

"$fileNamedoesn'texist"

}

vallines=file.readLines()

require(lines.isNotEmpty()){

"$fileNameisempty"

}

returnlines

}

funmain(){

DataFile("file_empty.txt").writeText("")

DataFile("file_wubba.txt").writeText(

"wubbalubbadubdub")

capture{

getTrace("wrong_name.txt")

}eq"IllegalArgumentException:"+

"wrong_name.txtmuststartwith'file_'"

capture{

getTrace("file_nonexistent.txt")

}eq"IllegalArgumentException:"+

"file_nonexistent.txtdoesn'texist"

capture{

getTrace("file_empty.txt")

}eq"IllegalArgumentException:"+

"file_empty.txtisempty"

getTrace("file_wubba.txt")eq

"[wubbalubbadubdub]"

}

We’vebeenusingthetwo-parameterversionofrequire(),butthere’salsoasingle-parameterversionthatproducesadefaultmessage:

//CheckInstructions/SingleArgRequire.kt

packagecheckinstructions

importatomictest.*

funsingleArgRequire(arg:Int):Int{

require(arg>5)

returnarg

}

funmain(){

capture{

singleArgRequire(5)

}eq"IllegalArgumentException:"+

"Failedrequirement."

singleArgRequire(6)eq6

}

Thefailuremessageisnotasexplicitasthetwo-parameterversion,butinsomecasesitissufficient.

requireNotNull()

requireNotNull()testsitsfirstargumentandreturnsthatargumentifitisnotnull.Otherwise,itproducesanIllegalArgumentException.

Uponsuccess,requireNotNull()’sargumentisautomaticallysmart-casttoanon-nullabletype.Thus,youusuallydon’tneedrequireNotNull()’sreturnvalue:

//CheckInstructions/RequireNotNull.kt

packagecheckinstructions

importatomictest.*

funnotNull(n:Int?):Int{

requireNotNull(n){//[1]

"notNull()argumentcannotbenull"

}

returnn*9//[2]

}

funmain(){

valn:Int?=null

capture{

notNull(n)

}eq"IllegalArgumentException:"+

"notNull()argumentcannotbenull"

capture{

requireNotNull(n)//[3]

}eq"IllegalArgumentException:"+

"Requiredvaluewasnull."

notNull(11)eq99

}

[2]Noticethatnnolongerrequiresanullcheck,becausethecalltorequireNotNull()hasmadeitnon-nullable.

Aswithrequire(),there’satwo-parameterversionwithamessageyoucancraftyourself([1]),andasingle-parameterversionwithadefaultmessage([3]).BecauserequireNotNull()testsforaspecificissue(nullity),thesingle-parameterversionismoreusefulthanitiswithrequire().

check()

Adesign-by-contractpostconditionteststheresultsofafunction.Postconditionsareimportantforlong,complexfunctionswhereyoumightnottrusttheresults.Wheneveryoucandescribeconstraintsontheresultsofafunction,it’swisetoexpressthemasapostcondition.

check()isidenticaltorequire()exceptthatitthrowsIllegalStateException.Itistypicallyusedattheendofafunction,toverifythattheresults(orthefieldsinthefunction’sobject)arevalid—thatthingshaven’tsomehowgottenintoabadstate.

Supposeacomplexfunctionwritestoafile,andyouareunsurewhetherallexecutionpathswillcreatethatfile.Addingapostconditionattheendofthefunctionhelpsensurecorrectness:

//CheckInstructions/Postconditions.kt

packagecheckinstructions

importatomictest.*

valresultFile=DataFile("Results.txt")

funcreateResultFile(create:Boolean){

if(create)

resultFile.writeText("Results\n#ok")

//...otherexecutionpaths

check(resultFile.exists()){

"${resultFile.name}doesn'texist!"

}

}

funmain(){

resultFile.erase()

capture{

createResultFile(false)

}eq"IllegalStateException:"+

"Results.txtdoesn'texist!"

createResultFile(true)

}

Assumingyourpreconditionsensurevalidarguments,apostconditionfailurealmostalwaysindicatesaprogrammingerror.Forthisreason,you’llseepostconditionslessoftenbecause,oncetheprogrammerisconvincedthecodeiscorrect,thepostconditioncanbecommentedorremovedifitimpactsperformance.Ofcourse,it’salwaysbesttoleavesuchtestsinplacesoproblemscausedbyfuturecodechangesareimmediatelydetected.Onewaytodothisisbymovingpostconditionsintounittests.

assert()

Toavoidcommentinganduncommentingcheck()statements,assert()allowsyoutoenableanddisableassert()checks.

assert()comesfromJava.Assertionsaredisabledbydefault,andareonlyengagedifyouexplicitlyturnthemonusingacommand-lineflag.InKotlin,thisflagis-ea.

Werecommendusingrequire()andcheck(),whicharealwaysavailablewithoutspecialconfiguration.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

TheNothingType

ANothingreturntypeindicatesafunctionthatneverreturns

Thisisusuallyafunctionthatalwaysthrowsanexception.

Here’safunctionthatproducesaninfiniteloop(avoidthese)—becauseitneverreturns,itsreturntypeisNothing:

//NothingType/InfiniteLoop.kt

packagenothingtype

funinfinite():Nothing{

while(true){}

}

Nothingisabuilt-inKotlintypewithnoinstances.

Apracticalexampleisthebuilt-inTODO(),whichhasareturntypeofNothingandthrowsNotImplementedError:

//NothingType/Todo.kt

packagenothingtype

importatomictest.*

funlater(s:String):String=TODO("later()")

funlater2(s:String):Int=TODO()

funmain(){

capture{

later("Hello")

}eq"NotImplementedError:"+

"Anoperationisnotimplemented:later()"

capture{

later2("Hello!")

}eq"NotImplementedError:"+

"Anoperationisnotimplemented."

}

Bothlater()andlater2()returnnon-NothingtypeseventhoughTODO()returnsNothing.Nothingiscompatiblewithanytype.

later()andlater2()compilesuccessfully.Ifyoucalleitherone,anexceptionremindsyoutowriteimplementations.TODO()isausefultoolfor“sketching”a

codeframeworktoverifythateverythingfitstogetherbeforefillinginthedetails.

Inthefollowing,fail()alwaysthrowsanExceptionsoitreturnsNothing.Noticethatacalltofail()ismorereadableandcompactthanexplicitlythrowinganexception:

//NothingType/Fail.kt

packagenothingtype

importatomictest.*

funfail(i:Int):Nothing=

throwException("fail($i)")

funmain(){

capture{

fail(1)

}eq"Exception:fail(1)"

capture{

fail(2)

}eq"Exception:fail(2)"

}

fail()allowsyoutoeasilychangetheerror-handlingstrategy.Forexample,youcanchangetheexceptiontypeorloganadditionalmessagebeforethrowinganexception.

ThisthrowsaBadDataexceptioniftheargumentisnotaString:

//NothingType/CheckObject.kt

packagenothingtype

importatomictest.*

classBadData(m:String):Exception(m)

funcheckObject(obj:Any?):String=

if(objisString)

obj

else

throwBadData("NeedsString,got$obj")

funtest(checkObj:(obj:Any?)->String){

checkObj("abc")eq"abc"

capture{

checkObj(null)

}eq"BadData:NeedsString,gotnull"

capture{

checkObj(123)

}eq"BadData:NeedsString,got123"

}

funmain(){

test(::checkObject)

}

checkObject()’sreturntypeisthereturntypeoftheifexpression.KotlintreatsathrowastypeNothing,andNothingcanbeassignedtoanytype.IncheckObject(),StringtakespriorityoverNothing,sothetypeoftheifexpressionisString.

WecanrewritecheckObject()usingasafecastandanElvisoperator.checkObject2()castsobjtoaStringifitcanbecast,otherwiseitthrowsanexception:

//NothingType/CheckObject2.kt

packagenothingtype

funfailWithBadData(obj:Any?):Nothing=

throwBadData("NeedsString,got$obj")

funcheckObject2(obj:Any?):String=

(objas?String)?:failWithBadData(obj)

funmain(){

test(::checkObject2)

}

Whengivenaplainnullwithnoadditionaltypeinformation,thecompilerinfersanullableNothing:

//NothingType/ListOfNothing.kt

importatomictest.eq

funmain(){

valnone:Nothing?=null

varnullableString:String?=null//[1]

nullableString="abc"

nullableString=none//[2]

nullableStringeqnull

valnullableInt:Int?=none//[3]

nullableInteqnull

vallistNone:List<Nothing?>=listOf(null)

valints:List<Int?>=listOf(null)//[4]

intseqlistNone

}

Youcanassignbothnullandnonetoavarorvalofanullabletype,suchasnullableStringornullableInt.ThisisallowedbecausethetypeofbothnullandnoneisNothing?(nullableNothing).InthesamewaythatanexpressionoftheNothingtype(forexample,fail())canbeinterpretedas“anytype,”anexpressionoftheNothing?type,suchasnull,canbeinterpretedas“anynullabletype.”Assignmentstodifferentnullabletypesareshowninlines[1],[2]and[3].

listNoneisinitializedwithaListcontainingonlythenullvalue.ThecompilerinfersthistobeList<Nothing?>.Forthisreason,youmustexplicitlyspecifytheelementtype([4])thatyouwanttostoreintheListwhenyouinitializeitwithonlynull.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ResourceCleanup

Usingtry-finallyblocksforresourcecleanupistediousanderror-prone.Kotlin’slibraryfunctionsmanagecleanupforyou.

AsyoulearnedinExceptionHandling,thefinallyclausecleansupresourcesregardlessofhowthetryblockexits.Butwhatifanexceptioncanhappenwhileclosingaresource?Youendupwithanothertryinsidethefinallyclause.Ontopofthat,ifoneexceptionisthrowninsideatryandanotherwhileclosingtheresource,thelattershouldn’tconcealtheformer.Ensuringpropercleanupbecomesverymessy.

Toreducethiscomplexity,Kotlin’suse()guaranteespropercleanupofcloseableresources,liberatingyoufromhandwrittencleanupcode.

use()workswithanyobjectthatimplementsJava’sAutoCloseableinterface.Itexecutesthecodewithintheblock,thencallsclose()ontheobject,regardlessofhowyouexittheblock—eithernormally(includingviareturn),orthroughanexception.

use()rethrowsallexceptions,soyoumuststilldealwiththoseexceptions.

Predefinedclassesthatworkwithuse()arefoundintheJavadocumentationforAutoCloseable.Forexample,toreadlinesfromaFileweapplyuse()toaBufferedReader.DataFilefromCheckInstructionsinheritsjava.io.File:

//ResourceCleanup/AutoCloseable.kt

importatomictest.eq

importcheckinstructions.DataFile

funmain(){

DataFile("Results.txt")

.bufferedReader()

.use{it.readLines().first()}eq

"Results"

}

useLines()opensaFileobject,extractsallitslines,andpassesthoselinestoatargetfunction(typicallyalambda):

//ResourceCleanup/UseLines.kt

importatomictest.eq

importcheckinstructions.DataFile

funmain(){

DataFile("Results.txt").useLines{

it.filter{"#"init}.first()//[1]

}eq"#ok"

DataFile("Results.txt").useLines{lines->

lines.filter{line->//[2]

"#"inline

}.first()

}eq"#ok"

}

[1]Theleft-handitreferstothecollectionoflinesinthefile,whiletheright-handitreferstoeachindividualline.Toreduceconfusion,avoidwritingcodewithtwodifferentnearbyits.[2]Namedargumentspreventconfusionfromtoomanyits.

EverythinghappenswithintheuseLines()lambda;outsidethelambdathefilecontentsareunavailableunlessyouexplicitlyreturnthem.Asitclosesthefile,useLines()returnstheresultofthelambda.

forEachLine()makesiteasytoapplyanactiontoeachlineinafile:

//ResourceCleanup/ForEachLine.kt

importcheckinstructions.DataFile

importatomictest.*

funmain(){

DataFile("Results.txt").forEachLine{

if(it.startsWith("#"))

trace("$it")

}

traceeq"#ok"

}

ThelambdainforEachLine()returnsUnit,whichmeansthatanythingyoudowiththelinesmustbeachievedthroughsideeffects.Infunctionalprogramming,wepreferreturningresultsoversideeffects,andthususeLines()isamorefunctionalapproachthanforEachLine().However,forEachLine()isaquicksolutionforsimpleutilities.

Youcancreateyourownclassthatworkswithuse()byimplementingtheAutoCloseableinterface,whichcontainsonlytheclose()function:

//ResourceCleanup/Usable.kt

packageresourcecleanup

importatomictest.*

classUsable():AutoCloseable{

funfunc()=trace("func()")

overridefunclose()=trace("close()")

}

funmain(){

Usable().use{it.func()}

traceeq"func()close()"

}

use()ensuresresourcecleanupatthepointtheresourceiscreated,ratherthanforcingyoutowritecleanupcodewhenyou’refinishedwiththeresource.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

Logging

Loggingcapturesinformationfromarunningprogram.

Forexample,aninstallationprogrammightlog:

Thestepstakenduringsetup.Thedirectoriesforfilestorage.Startupvaluesfortheprogram.

Awebservermightlogtheoriginaddressandstatusofeachrequest.

Loggingisalsohelpfulduringdebugging.Withoutlogging,youmightdecipherthebehaviorofaprogramusingprintln()statements.Thiscanbehelpfulintheabsenceofadebugger(suchastheonebuiltintoIntelliJIDEA).However,onceyoudecidetheprogramisworkingproperly,you’llprobablytaketheprintln()statementsout.Later,ifyourunintomorebugs,youmightputthembackin.Incontrast,loggingcanbedynamicallyenabledwhenyouneedit,andturnedoffotherwise.

Forsomefailuresyoucanonlyreporttheissue.Aprogramthatrecoversfromsometypesoferrors(asshowninExceptionHandling)canlogdetailsaboutthoseerrorsforlateranalysis.Inawebapplication,forexample,youdon’tterminatetheprogramifsomethinggoeswrong.Loggingcapturestheseevents,givingprogrammersandadministratorsawaytodiscovertheproblems.Meanwhile,theapplicationcontinuesrunning.

Weuseanopen-sourceloggingpackagedesignedforKotlincalledKotlin-logging,whichhasthefeelandsimplicityofKotlin.Notethatthereareotherloggingpackagestochoosefrom.

Youmustcreatealoggerbeforeusingit.You’llalmostalwayswanttocreateitatfilescopesoit’savailabletoallcomponentsinthatfile:

//Logging/BasicLogging.kt

packagelogging

importmu.KLogging

privatevallog=KLogging().logger

funmain(){

valmsg="Hello,KotlinLogging!"

log.trace(msg)

log.debug(msg)

log.info(msg)

log.warn(msg)

log.error(msg)

}

main()showsthedifferentlogginglevels:trace(),debug()andinfo()capturebehavioralinformation,whilewarn()anderror()indicateproblems.

Start-upconfigurationdeterminesthelogginglevelsthatareactuallyreported.Thiscanbemodifiedduringexecution.Operatorsoflong-runningapplicationscanchangethelogginglevelwithoutrestartingtheprogram(whichisoftenunacceptable).

Logginglibrarieshavearatheroddhistory.PeopleweredissatisfiedwiththeoriginallogginglibrarydistributedwithJava,sotheycreatedotherlibraries.Inanattempttounifylogging,designersbegandevelopingcommonlogginginterfaces.Acknowledgingthatorganizationsmaybeinvestedinexistinglogginglibraries,thoseinterfaceswerecreatedasfacadesformultipledifferentlogginglibraries.Later,otherprogrammerscreated(presumablyimproved)facadesoverthosefacades.Utilizingaloggingsystemoftenmeanschoosingafacade,thenchoosingoneormoreunderlyingimplementations.

TheKotlin-logginglibraryisafacadeovertheSimpleLoggingFacadeforJava(SLF4J),whichisanabstractionovermultipleloggingframeworks.Youchoosetheframeworkthatmeetsyourneeds—althoughitismorelikelythattheoperationsgroupinyourcompanywillmakethatdecision,astheyaretheonesthatusuallymanageloggingandanalyzetheresultinglogfiles.

Forthisexampleweuseslf4j-simpleasourimplementation.ThiscomesaspartofSLF4Jandthuswearenotrequiredtoinstallorconfigureanadditionallibrary—somelibrarieshaveanannoyingamountofsetupcomplexity.slf4j-simplesendsitsoutputtotheconsoleerrorstream.Whenyouruntheprogram,yousee:

[main]INFOmu.KLogging-Hello,KotlinLogging!

[main]WARNmu.KLogging-Hello,KotlinLogging!

[main]ERRORmu.KLogging-Hello,KotlinLogging!

trace()anddebug()producenooutputbecausethedefaultconfigurationdoesn’treportthoselevels.Togetdifferentreportinglevels,changeyourloggingconfiguration.Loggingconfigurationvariesdependingontheloggingpackageyou’reusing,sowedon’ttalkaboutithere.

Loggingimplementationsthatlogtofilesoftenmanagethoselogfilesbyautomaticallydiscardingtheoldestpartswhenfilesgettoolarge.Thereareadditionaltoolsdesignedtoreadandanalyzelogfiles.Thepracticeofloggingcanrequirefairlyinvolvedresearch.

Forbasicproblems,theworkofinstalling,configuring,andusingaloggingsystemmighttemptyoubacktoprintln()statements.Fortunately,thereareeasierstrategies.

Thequick-and-dirtyapproachistodefineaglobalfunction.Thiscaneasilybedisabledwhenyoudon’tneedit:

//Logging/SimpleLoggingStrategy.kt

packagelogging

importcheckinstructions.DataFile

vallogFile=//Resetensuresanemptyfile:

DataFile("simpleLogFile.txt").reset()

fundebug(msg:String)=

System.err.println("Debug:$msg")

//Todisable:

//fundebug(msg:String)=Unit

funtrace(msg:String)=

logFile.appendText("Trace:$msg\n")

funmain(){

debug("SimpleLoggingStrategy")

trace("Line1")

trace("Line2")

println(logFile.readText())

}

/*SampleOutput:

Debug:SimpleLoggingStrategy

Trace:Line1

Trace:Line2

*/

debug()sendsitsoutputtotheconsoleerrorstream.trace()sendsitsoutputtoalogfile.

Youcanalsocreateyourownsimpleloggingclass:

//Logging/AtomicLog.kt

packageatomiclog

importcheckinstructions.DataFile

classLogger(fileName:String){

vallogFile=DataFile(fileName).reset()

privatefunlog(type:String,msg:String)=

logFile.appendText("$type:$msg\n")

funtrace(msg:String)=log("Trace",msg)

fundebug(msg:String)=log("Debug",msg)

funinfo(msg:String)=log("Info",msg)

funwarn(msg:String)=log("Warn",msg)

funerror(msg:String)=log("Error",msg)

//Forbasictesting:

funreport(msg:String){

trace(msg)

debug(msg)

info(msg)

warn(msg)

error(msg)

}

}

Youcanaddsupportforotherfeatureslikelogginglevelsandtimestamps.

Usingthelibraryisstraightforward:

//Logging/UseAtomicLog.kt

packageuseatomiclog

importatomiclog.Logger

importatomictest.eq

privatevallogger=Logger("AtomicLog.txt")

funmain(){

logger.report("Hello,AtomicLog!")

logger.logFile.readText()eq"""

Trace:Hello,AtomicLog!

Debug:Hello,AtomicLog!

Info:Hello,AtomicLog!

Warn:Hello,AtomicLog!

Error:Hello,AtomicLog!

"""

}

It’stemptingtocreateyetanotherlogginglibrary.Thisisprobablynotagooduseoftime.

-

Loggingisnotassimpleascallinglibraryfunctions—there’sasignificantrun-timecomponent.Loggingistypicallyincludedinthedeliverableproduct,andoperationspeoplemustbeabletoturnloggingonandoff,dynamicallyadjustlogginglevels,andcontrolthelogfiles.Forlong-runningprogramssuchas

servers,thislastissueisparticularlyimportantbecauseitincludesstrategiestopreventlogfilesfromfillingup.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

UnitTesting

Unittestingisthepracticeofcreatingacorrectnesstestforeachaspectofafunction.Unittestsrapidlyrevealbrokencode,acceleratingdevelopmentspeed.

There’sfarmoretotestingthanwecancoverinthisbook,sothisatomisonlyabasicintroduction.

The“Unit”in“Unittesting”describesasmallpieceofcode,usuallyafunction,thatistestedseparatelyandindependently.ThisshouldnotbeconfusedwiththeunrelatedKotlinUnittype.

Unittestsaretypicallywrittenbytheprogrammer,andruneachtimeyoubuildtheproject.Becauseunittestsrunsofrequently,theymustrunquickly.

You’vebeenlearningaboutunittestingwhilereadingthisbook,viatheAtomicTestlibraryweusetovalidatethebook’scode.AtomicTestusestheconciseeqforthemostcommonpatterninunittesting:comparinganexpectedresultwithageneratedresult.

Ofthenumerousunittestframeworks,JUnitisthemostpopularforJava.TherearealsoframeworkscreatedspecificallyforKotlin.TheKotlinstandardlibraryincludeskotlin.test,whichprovidesafacadefordifferenttestlibraries.Thiswayyou’renotlimitedtousingaparticularlibrary.kotlin.testalsocontainswrappersforbasicassertionfunctions.

Tousekotlin.test,youmustmodifythedependenciessectionofyourproject’sbuild.gradlefiletoinclude:

testImplementation"org.jetbrains.kotlin:kotlin-test-common"

Insideaunittest,theprogrammercallsvariousassertionfunctionsthatvalidatetheexpectedbehaviorofthefunctionundertest.AssertionfunctionsincludeassertEquals(),whichcomparestheactualvalueagainstanexpectedvalue,andassertTrue(),whichtestsitsfirstargument,aBooleanexpression.Inthis

example,theunittestsarethefunctionswithnamesbeginningwiththewordtest:

//UnitTesting/NoFramework.kt

packageunittesting

importkotlin.test.assertEquals

importkotlin.test.assertTrue

importatomictest.*

funfortyTwo()=42

funtestFortyTwo(n:Int=42){

assertEquals(

expected=n,

actual=fortyTwo(),

message="Incorrect,")

}

funallGood(b:Boolean=true)=b

funtestAllGood(b:Boolean=true){

assertTrue(allGood(b),"Notgood")

}

funmain(){

testFortyTwo()

testAllGood()

capture{

testFortyTwo(43)

}contains

listOf("expected:","<43>",

"butwas","<42>")

capture{

testAllGood(false)

}containslistOf("Error","Notgood")

}

Inmain(),youcanseethatafailingassertionfunctionproducesanAssertionError—thismeanstheunittesthasfailed,signalingtheproblemtotheprogrammer.

kotlin.testcontainsanassortmentoffunctionsthathavenamesstartingwithassert:

assertEquals(),assertNotEquals()assertTrue(),assertFalse()assertNull(),assertNotNull()assertFails(),assertFailsWith()

Similarfunctionsaretypicallyincludedineveryunittestframework,butthenamesandparameterordercanbedifferent.Forexample,themessageparameter

inassertEquals()mightbefirstorlast.Also,it’seasytomixupexpectedandactual—usingnamedargumentsavoidsthisproblem.

Theexpect()functioninkotlin.testrunsablockofcodeandcomparesthatresultwiththeexpectedvalue:

fun<T>expect(

expected:T,

message:String?,

block:()->T

){

assertEquals(expected,block(),message)

}

Here’stestFortyTwo()rewrittenusingexpect():

//UnitTesting/UsingExpect.kt

packageunittesting

importatomictest.*

importkotlin.test.*

funtestFortyTwo2(n:Int=42){

expect(n,"Incorrect,"){fortyTwo()}

}

funmain(){

testFortyTwo2()

capture{

testFortyTwo2(43)

}contains

listOf("expected:",

"<43>butwas:","<42>")

assertFails{testFortyTwo2(43)}

capture{

assertFails{testFortyTwo2()}

}contains

listOf("Expectedanexception",

"tobethrown",

"butwascompletedsuccessfully.")

assertFailsWith<AssertionError>{

testFortyTwo2(43)

}

capture{

assertFailsWith<AssertionError>{

testFortyTwo2()

}

}contains

listOf("Expectedanexception",

"tobethrown",

"butwascompletedsuccessfully.")

}

It’simportanttoaddtestsforcornercases.Ifafunctionproducesanerrorundercertainconditions,thisshouldbeverifiedwithaunittest(asAtomicTest’scapture()does).assertFails()andassertFailsWith()ensurethattheexceptionisthrown.assertFailsWith()alsochecksthetypeoftheexception.

TestFrameworksAtypicaltestframeworkcontainsacollectionofassertionfunctionsandamechanismtoruntestsanddisplayresults.Mosttestrunnersshowresultswithgreenforsuccessandredforfailure.

ThisatomusesJUnit5astheunderlyinglibraryforkotlin.test.Toincludeitinaproject,thedependenciessectionofyourbuild.gradleshouldlooklikethis:

testImplementation"org.jetbrains.kotlin:kotlin-test"

testImplementation"org.jetbrains.kotlin:kotlin-test-junit"

testImplementation"org.jetbrains.kotlin:kotlin-test-junit5"

testImplementation"org.junit.jupiter:junit-jupiter:$junit_version"

Ifyou’reusingadifferentlibrary,youcanfindsetupdetailsinthatframework’sinstructions.

kotlin.testprovidesfacadesforthemostcommonlyusedfunctions.Assertionsaredelegatedtotheappropriatefunctionsintheunderlyingtestframework.Intheorg.junit.jupiter.api.Assertionsclass,forexample,assertEquals()callsAssertions.assertEquals().

Kotlinsupportsannotationsfordefinitionsandexpressions.Anannotationisthe@signfollowedbytheannotationname,andindicatesspecialtreatmentfortheannotatedelement.The@Testannotationconvertsaregularfunctionintoatestfunction.WecantestfortyTwo()andallGood()usingthe@Testannotation:

//Tests/unittesting/SampleTest.kt

packageunittesting

importkotlin.test.*

classSampleTest{

@Test

funtestFortyTwo(){

expect(42,"Incorrect,"){fortyTwo()}

}

@Test

funtestAllGood(){

assertTrue(allGood(),"Notgood")

}

}

kotlin.testusesatypealiastocreateafacadeforthe@Testannotation:

typealiasTest=org.junit.jupiter.api.Test

Thistellsthecompilertosubstitutethe@org.junit.jupiter.api.Testannotationfor@Test.

Atestclassusuallycontainsmultipleunittests.Ideally,eachunittestonlyverifiesasinglebehavior.Thisquicklyguidesyoutotheproblemifatestfailswhenintroducingnewfunctionality.

@Testfunctionscanberun:

IndependentlyAspartofaclassTogetherwithalltestsdefinedfortheapplication

IntelliJIDEAallowsyoutorerunonlythefailedtests.

Considerasimplestatemachinewiththreestates:On,OffandPaused.Thefunctionsstart(),pause(),resume()andfinish()controlthestatemachine.resume()isvaluablebecauseresumingapausedmachineissignificantlycheaperand/orfasterthanstartingamachine.

//UnitTesting/StateMachine.kt

packageunittesting

importunittesting.State.*

enumclassState{On,Off,Paused}

classStateMachine{

varstate:State=Off

privateset

privatefuntransition(

new:State,current:State=On

){

if(new==Off&&state!=Off)

state=Off

elseif(state==current)

state=new

}

funstart()=transition(On,Off)

funpause()=transition(Paused,On)

funresume()=transition(On,Paused)

funfinish()=transition(Off)

}

Theseoperationsareignored:

resume()orfinish()onamachinethatisOff.pause()orstart()onaPausedmachine.

TotestStateMachine,wecreateapropertysminsidethetestclass.ThetestrunnercreatesafreshStateMachineTestobjectforeachdifferenttest:

//Tests/unittesting/StateMachineTest.kt

packageunittesting

importkotlin.test.*

classStateMachineTest{

valsm=StateMachine()

@Test

funstart(){

sm.start()

assertEquals(State.On,sm.state)

}

@Test

fun`pauseandresume`(){

sm.start()

sm.pause()

assertEquals(State.Paused,sm.state)

sm.resume()

assertEquals(State.On,sm.state)

sm.pause()

assertEquals(State.Paused,sm.state)

}

//...

}

Normally,Kotlinonlyallowslettersanddigitsforfunctionnames.However,ifyouputafunctionnameinsidebackticks,youcanuseanycharacters(includingwhitespaces).Thismeansyoucancreatefunctionnamesthataresentencesdescribingtheirtests,suchaspauseandresume.Thisproducesmoreusefulerrorinformation.

Anessentialgoalofunittestingistosimplifythegradualdevelopmentofcomplicatedsoftware.Afterintroducingeachnewpieceoffunctionality,adevelopernotonlyaddsnewteststocheckitscorrectnessbutalsorunsalltheexistingteststomakesurethatthepriorfunctionalitystillworks.Youfeelsaferwhenintroducingnewchanges,andthesystemismorepredictableandstable.

Intheprocessoffixinganewbug,youcreateadditionalunittestsforthisandsimilarcases,soyoudon’tmakethesamemistakesinthefuture.

Ifyouuseacontinuousintegration(CI)serversuchasTeamcity,allavailabletestsrunautomaticallyandyou’renotifiedifsomethingbreaks.

Consideraclasswithseveralproperties:

//UnitTesting/Learner.kt

packageunittesting

enumclassLanguage{

Kotlin,Java,Go,Python,Rust,Scala

}

dataclassLearner(

valid:Int,

valname:String,

valsurname:String,

vallanguage:Language

)

It’softenhelpfultoaddutilityfunctionsformanufacturingtestdata,especiallywhenyoumustcreatemanyobjectswiththesamedefaultvaluesduringtesting.Here,makeLearner()createsobjectswithdefaultvalues:

//Tests/unittesting/LearnerTest.kt

packageunittesting

importunittesting.Language.*

importkotlin.test.*

funmakeLearner(

id:Int,

language:Language=Kotlin,//[1]

name:String="TestName$id",

surname:String="TestSurname$id"

)=Learner(id,name,surname,language)

classLearnerTest{

@Test

fun`singleLearner`(){

vallearner=makeLearner(10,Java)

assertEquals("TestName10",learner.name)

}

@Test

fun`multipleLearners`(){

vallearners=(1..9).map(::makeLearner)

assertTrue(

learners.all{it.language==Kotlin})

}

}

AddingdefaultargumentstoLearnerthatareonlyfortestingintroducesunnecessarycomplexityandpotentialconfusion.makeLearner()iseasierandcleanerwhenproducingtestinstances,anditeliminatesredundantcode.

TheorderofmakeLearner()’sparameterssimplifiesitsusage.Inthiscase,weexpecttospecifyanon-defaultlangmoreoftenthanchangingdefaulttestvaluesfornameandsurname,sothelangparameterissecond([1]).

MockingandIntegrationTestsAsystemthatdependsonothercomponentscomplicatesthecreationofisolatedtests.Ratherthanintroducingdependenciesonrealcomponents,programmers

oftenuseapracticecalledmocking.

Amockreplacesarealentitywithafakeoneduringtesting.Databasesarecommonlymockedtopreservetheintegrityofthestoreddata.Themockcanimplementthesameinterfaceastherealone,oritcanbecreatedusingmockinglibrariessuchasMockK.

It’svitaltotestseparatepiecesoffunctionalityindependently—that’swhatunittestsdo.It’salsoessentialtoensurethatdifferentpartsofthesystemworkwhencombinedwitheachother—that’swhatintegrationtestsdo.Unittestsare“inward-directed”whileintegrationtestsare“outward-directed”.

TestingInsideIntelliJIDEAIntelliJIDEAandAndroidStudiosupportcreatingandrunningunittests.

Tocreateatest,right-click(control-clickonaMac)theclassorfunctionyouwanttotestandselect“Generate…”fromthepop-upmenu.Fromthe“Generate”menu,choose“Test…”.Asecondapproachistoopenthelistof“intentionactions”,andselect“CreateTest”.

SelectJUnit5asthe“Testinglibrary”.Ifamessageappearssaying“JUnit5librarynotfoundinthemodule,”pushthe“Fix”buttonnexttothemessage.The“Destinationpackage”shouldbeunittesting.Theresultwillendupinanotherdirectory(alwaysseparatetestsfrommaincode).TheGradledefaultisthesrc/test/kotlinfolder,butyoucanchooseadifferentdestination.

Checktheboxesnexttothefunctionsyouwanttested.Youcanautomaticallynavigatefromthesourcecodetothecorrespondingtestclassandback;fordetailsseethedocumentation.

Oncethetestframeworkcodeisgenerated,youcanmodifyittosuityourneeds.Fortheexamplesandexercisesinthisatom,replace:

importorg.junit.Test

importorg.junit.Assert.*

with:

importkotlin.test.*

WhenrunningtestswithinIntelliJIDEA,youmaygetanerrormessagelike“testeventswerenotreceived.”ThisisbecauseIDEA’sdefaultconfigurationassumesyouarerunningyourtestsexternally,usingGradle.TofixitsoyoucanrunyourtestsinsideIDEA,startatthefilemenu:

File|Settings|Build,Execution,Deployment|BuildTools|Gradle

Onthatpageyou’llseeadrop-downtitled“Runtestsusing:”whichissetto“Gradle(Default)”.Changethisto“IntelliJIDEA”andyourtestswillruncorrectly.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

SECTIONVII:POWERTOOLSAnyfoolcanwritecodethatacomputercanunderstand.Goodprogrammerswritecodethathumanscanunderstand.—MartinFowler

ExtensionLambdas

Anextensionlambdaislikeanextensionfunction.Itdefinesalambdainsteadofafunction.

Here,vaandvbyieldthesameresult:

//ExtensionLambdas/Vanbo.kt

packageextensionlambdas

importatomictest.eq

valva:(String,Int)->String={str,n->

str.repeat(n)+str.repeat(n)

}

valvb:String.(Int)->String={

this.repeat(it)+repeat(it)

}

funmain(){

va("Vanbo",2)eq"VanboVanboVanboVanbo"

"Vanbo".vb(2)eq"VanboVanboVanboVanbo"

vb("Vanbo",2)eq"VanboVanboVanboVanbo"

//"Vanbo".va(2)//Doesn'tcompile

}

vaisanordinarylambdaliketheonesyou’veseenthroughoutthisbook.Ittakestwoparameters,aStringandanInt,andreturnsaString.Thelambdabodyalsohastwoparameters,followedbytherequisitearrow:str,n->.

vbmovestheStringparameteroutsidetheparenthesesandusesextensionfunctionsyntax:String.(Int).Justlikeanextensionfunction,theobjectofthetypebeingextended(String,inthiscase),becomesthereceiver,andcanbeaccessedusingthis.

Thefirstcallinvbusestheexplicitformthis.repeat(it).Thesecondcallomitsthethistoproducerepeat(it).Likeanylambda,ifyouhaveonlyoneparameter(Int,inthiscase),itreferstothatparameter.

Inmain(),thecalltova()isjustwhatyou’dexpectfromthelambdatypedeclaration(String,Int)->String—twoargumentsinatraditionalfunctioncall.vb()isanextensionsoitcanbecalledusingtheextensionform

"Vanbo".vb(2).vb()canalsobecalledusingthetraditionalformvb("Vanbo",2).va()cannotbecalledusingtheextensionform.

Whenyoufirstseeanextensionlambda,itcanseemliketheString.(Int)partiswhatyoushouldfocuson.ButStringisnotbeingextendedbytheparameterlist(Int)—itisbeingextendedbytheentirelambda:String.(Int)->String

TheKotlindocumentationusuallyreferstoextensionlambdasasfunctionliteralswithreceiver.Thetermfunctionliteralencompassesbothlambdasandanonymousfunctions.Thetermlambdawithreceiverisoftenusedsynonymouslyforextensionlambda,toemphasizethatit’salambdawiththereceiverasanadditionalimplicitparameter.

Likeanextensionfunction,anextensionlambdacanhavemultipleparameters:

//ExtensionLambdas/Parameters.kt

packageextensionlambdas

importatomictest.eq

valzero:Int.()->Boolean={

this==0

}

valone:Int.(Int)->Boolean={

this%it==0

}

valtwo:Int.(Int,Int)->Boolean={

arg1,arg2->

this%(arg1+arg2)==0

}

valthree:Int.(Int,Int,Int)->Boolean={

arg1,arg2,arg3->

this%(arg1+arg2+arg3)==0

}

funmain(){

0.zero()eqtrue

10.one(10)eqtrue

20.two(10,10)eqtrue

30.three(10,10,10)eqtrue

}

Inone(),itisusedinsteadofnamingtheparameter.Ifthisproducesunclearsyntax,it’sbettertouseexplicitparameternames.

We’vebeendemonstratingextensionlambdasbydefiningvals,buttheymorecommonlyappearasfunctionparameters,asinf2():

//ExtensionLambdas/FunctionParameters.kt

packageextensionlambdas

classA{

funaf()=1

}

classB{

funbf()=2

}

funf1(lambda:(A,B)->Int)=

lambda(A(),B())

funf2(lambda:A.(B)->Int)=

A().lambda(B())

funlambdas(){

f1{aa,bb->aa.af()+bb.bf()}

f2{af()+it.bf()}

}

Inmain(),noticethemoresuccinctsyntaxinthelambdaprovidedtof2().

IfyourextensionlambdareturnsUnit,theresultproducedbythelambdabodyisignored:

//ExtensionLambdas/LambdaUnitReturn.kt

packageextensionlambdas

fununitReturn(lambda:A.()->Unit)=

A().lambda()

funnonUnitReturn(lambda:A.()->String)=

A().lambda()

funlambdaUnitReturn(){

unitReturn{

"Unitignoresthereturnvalue"+

"Soitcanbeanything..."

}

unitReturn{1}//...ofanytype...

unitReturn{}//...ornothing

nonUnitReturn{

"Mustreturnthepropertype"

}

//nonUnitReturn{}//Notanoption

}

Youcanpassanextensionlambdatoafunctionthatexpectsanordinarylambda,aslongastheparameterlistsconformtoeachother:

//ExtensionLambdas/Transform.kt

packageextensionlambdas

importatomictest.eq

funString.transform1(

n:Int,lambda:(String,Int)->String

)=lambda(this,n)

funString.transform2(

n:Int,lambda:String.(Int)->String

)=lambda(this,n)

valduplicate:String.(Int)->String={

repeat(it)

}

valalternate:String.(Int)->String={

toCharArray()

.filterIndexed{i,_->i%it==0}

.joinToString("")

}

funmain(){

"hello".transform1(5,duplicate)

.transform2(3,alternate)eq"hleolhleo"

"hello".transform2(5,duplicate)

.transform1(3,alternate)eq"hleolhleo"

}

transform1()expectsanordinarylambdawhiletransform2()expectsanextensionlambda.Inmain(),theextensionlambdasduplicateandalternatearepassedtobothtransform1()andtransform2().ThethisreceiverinsidetheextensionlambdasduplicateandalternatebecomesthefirstStringargumentwheneitherlambdaispassedtotransform1().

Using::wecanpassafunctionreferencewhenanextensionlambdaisexpected:

//ExtensionLambdas/FuncReferences.kt

packageextensionlambdas

importatomictest.eq

funInt.d1(f:(Int)->Int)=f(this)*10

funInt.d2(f:Int.()->Int)=f()*10

funf1(n:Int)=n+3

funInt.f2()=this+3

funmain(){

74.d1(::f1)eq770

74.d2(::f1)eq770

74.d1(Int::f2)eq770

74.d2(Int::f2)eq770

}

Areferencetoanextensionfunctionhasthesametypeasanextensionlambda:Int::f2hasthetypeInt.()->Int.

Inthecall74.d1(Int::f2)wepassanextensionfunctiontod1()whichdoesnotdeclareanextensionlambdaparameter.

Polymorphismworkswithbothordinaryextensionfunctions(Base.g())andextensionlambdas(theBase.h()parameter):

//ExtensionLambdas/ExtensionPolymorphism.kt

packageextensionlambdas

importatomictest.eq

openclassBase{

openfunf()=1

}

classDerived:Base(){

overridefunf()=99

}

funBase.g()=f()

funBase.h(xl:Base.()->Int)=xl()

funmain(){

valb:Base=Derived()//Upcast

b.g()eq99

b.h{f()}eq99

}

Youwouldn’texpectitnottowork,butit’salwaysworthtestinganassumptionbycreatinganexample.

Youcanuseanonymousfunctionsyntax(describedinLocalFunctions)insteadofextensionlambdas.Hereweuseananonymousextensionfunction:

//ExtensionLambdas/AnonymousFunction.kt

packageextensionlambdas

importatomictest.eq

funexec(

arg1:Int,arg2:Int,

f:Int.(Int)->Boolean

)=arg1.f(arg2)

funmain(){

exec(10,2,funInt.(d:Int):Boolean{

returnthis%d==0

})eqtrue

}

Inmain(),thecalltoexec()showsthattheanonymousextensionfunctionisacceptedasanextensionlambda.

TheKotlinstandardlibrarycontainsanumberoffunctionsthatworkwithextensionlambdas.Forexample,aStringBuilderisamodifiableobjectthatproducesanimmutableStringwhenyoucalltoString().Incontrast,themoremodernbuildString()acceptsanextensionlambda.ItcreatesitsownStringBuilderobject,appliestheextensionlambdatothatobject,thencallstoString()toproducetheresult:

//ExtensionLambdas/StringCreation.kt

packageextensionlambdas

importatomictest.eq

privatefunmessy():String{

valbuilt=StringBuilder()//[1]

built.append("ABCs:")

('a'..'x').forEach{built.append(it)}

returnbuilt.toString()//[2]

}

privatefunclean()=buildString{

append("ABCs:")

('a'..'x').forEach{append(it)}

}

privatefuncleaner()=

('a'..'x').joinToString("","ABCs:")

funmain(){

messy()eq"ABCs:abcdefghijklmnopqrstuvwx"

messy()eqclean()

clean()eqcleaner()

}

Inmessy()werepeatthenamebuiltmultipletimes.WemustalsocreateaStringBuilder([1])andproducetheresult([2]).UsingbuildString()inclean(),youdon’tneedtocreateandmanagethereceiverfortheappend()calls,whichmakeseverythingmuchmoresuccinct.

cleaner()showsthat,ifyoulook,youcansometimesfindamoredirectsolutionthatskipsthebuilderaltogether.

TherearestandardlibraryfunctionssimilartobuildString()thatuseextensionlambdastoproduceinitialized,read-onlyListsandMaps:

//ExtensionLambdas/ListsAndMaps.kt

@file:OptIn(ExperimentalStdlibApi::class)

packageextensionlambdas

importatomictest.eq

valcharacters:List<String>=buildList{

add("Chars:")

('a'..'d').forEach{add("$it")}

}

valcharmap:Map<Char,Int>=buildMap{

('A'..'F').forEachIndexed{n,ch->

put(ch,n)

}

}

funmain(){

characterseq"[Chars:,a,b,c,d]"

//characterseqcharacters2

charmapeq"{A=0,B=1,C=2,D=3,E=4,F=5}"

}

Insidetheextensionlambdas,theListandMaparemutable,buttheresultsofbuildListandbuildMapareread-onlyListsandMaps.

WritingBuildersUsingExtensionLambdasHypothetically,youcancreateconstructorstoproduceallnecessaryobjectconfigurations.Sometimesthenumberofpossibilitiesmakesthismessyandimpractical.TheBuilderpatternhasseveralbenefits:

1. Itcreatesobjectsinamulti-stepprocess.Thiscansometimesbehelpfulwhenobjectconstructioniscomplex.

2. Itproducesdifferentobjectvariationsusingthesamebasicconstructioncode.

3. Itseparatescommonconstructioncodefromspecializedcode,makingiteasiertowriteandreadthecodeforindividualobjectvariations.

Implementingbuildersusingextensionlambdasprovidesanadditionalbenefit,whichisthecreationofaDomain-SpecificLanguage(DSL).ThegoalofaDSLissyntaxthatiscomfortableandsensibletoauserwhoisadomainexpertratherthanaprogrammingexpert.Thisallowsthatusertoproduceworkingsolutionsknowingonlyasmallsubsetofthesurroundinglanguage—whileatthesametimebenefitingfromthestructureandsafetyofthatlanguage.

Forexample,considerasystemthatcapturesactionsandingredientsforpreparingdifferentkindsofsandwiches.WecanuseclassestomodelthepiecesofaRecipe:

//ExtensionLambdas/Sandwich.kt

packagesandwich

importatomictest.eq

openclassRecipe:ArrayList<RecipeUnit>()

openclassRecipeUnit{

overridefuntoString()=

"${this::class.simpleName}"

}

openclassOperation:RecipeUnit()

classToast:Operation()

classGrill:Operation()

classCut:Operation()

openclassIngredient:RecipeUnit()

classBread:Ingredient()

classPeanutButter:Ingredient()

classGrapeJelly:Ingredient()

classHam:Ingredient()

classSwiss:Ingredient()

classMustard:Ingredient()

openclassSandwich:Recipe(){

funaction(op:Operation):Sandwich{

add(op)

returnthis

}

fungrill()=action(Grill())

funtoast()=action(Toast())

funcut()=action(Cut())

}

funsandwich(

fillings:Sandwich.()->Unit

):Sandwich{

valsandwich=Sandwich()

sandwich.add(Bread())

sandwich.toast()

sandwich.fillings()

sandwich.cut()

returnsandwich

}

funmain(){

valpbj=sandwich{

add(PeanutButter())

add(GrapeJelly())

}

valhamAndSwiss=sandwich{

add(Ham())

add(Swiss())

add(Mustard())

grill()

}

pbjeq"[Bread,Toast,PeanutButter,"+

"GrapeJelly,Cut]"

hamAndSwisseq"[Bread,Toast,Ham,"+

"Swiss,Mustard,Grill,Cut]"

}

sandwich()capturesthebasicingredientsandoperationstoproduceanySandwich(here,weassumeallsandwichesaretoasted,butintheexercisesyou’llseehowtomakethatoptional).ThefillingsextensionlambdaallowsthecallertoconfiguretheSandwichinnumerousdifferentways,butwithoutrequiringaconstructorforeachconfiguration.

Thesyntaxseeninmain()showshowthissystemmightbeusedasaDSL—theuseronlyneedstounderstandthesyntaxofcreatingaSandwichbycallingsandwich()andprovidingtheingredientsandoperationsinsidethecurlybraces.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

ScopeFunctions

Scopefunctionscreateatemporaryscopewhereinyoucanaccessanobjectwithoutusingitsname.

Scopefunctionsexistonlytomakeyourcodemoreconciseandreadable.Theydonotprovideadditionalabilities.

Therearefivescopefunctions:let(),run(),with(),apply(),andalso().Theyaredesignedtoworkwithalambdaanddonotrequireanimport.Theydifferinthewayyouaccessthecontextobject,usingeitheritorthis,andinwhattheyreturn.with()usesadifferentcallingsyntaxthantheothers.Hereyoucanseethedifferences:

//ScopeFunctions/Differences.kt

packagescopefunctions

importatomictest.eq

dataclassTag(varn:Int=0){

vars:String=""

funincrement()=++n

}

funmain(){

//let():Accessobjectwith'it'

//Returnslastexpressioninlambda

Tag(1).let{

it.s="let:${it.n}"

it.increment()

}eq2

//let()withnamedlambdaargument:

Tag(2).let{tag->

tag.s="let:${tag.n}"

tag.increment()

}eq3

//run():Accessobjectwith'this'

//Returnslastexpressioninlambda

Tag(3).run{

s="run:$n"//Implicit'this'

increment()//Implicit'this'

}eq4

//with():Accessobjectwith'this'

//Returnslastexpressioninlambda

with(Tag(4)){

s="with:$n"

increment()

}eq5

//apply():Accessobjectwith'this'

//Returnsmodifiedobject

Tag(5).apply{

s="apply:$n"

increment()

}eq"Tag(n=6)"

//also():Accessobjectwith'it'

//Returnsmodifiedobject

Tag(6).also{

it.s="also:${it.n}"

it.increment()

}eq"Tag(n=7)"

//also()withnamedlambdaargument:

Tag(7).also{tag->

tag.s="also:${tag.n}"

tag.increment()

}eq"Tag(n=8)"

}

Therearemultiplescopefunctionsbecausetheysatisfydifferentcombinationsofneeds:

Scopefunctionsthataccessthecontextobjectusingthis(run(),with()andapply())producethecleanestsyntaxwithintheirscopeblock.Scopefunctionsthataccessthecontextobjectusingit(let()andalso())allowyoutoprovideanamedlambdaargument.Scopefunctionsthatproducethelastexpressionintheirlambda(let(),run()andwith())areforcreatingresults.Scopefunctionsthatreturnthemodifiedcontextobject(apply()andalso())areforchainingexpressionstogether.

run()isaregularfunctionandwith()isanextensionfunction;otherwisetheyareidentical.Preferrun()forcallchainsandwhenthereceiverisnullable.

Here’sasummaryofscopefunctioncharacteristics:

thisContext itContextProduceslastexpression with,run let

Producesreceiver apply also

Youcanapplyascopefunctiontoanullablereceiverusingthesafeaccessoperator?.,whichonlycallsthescopefunctionifthereceiverisnotnull:

//ScopeFunctions/AndNullability.kt

packagescopefunctions

importatomictest.eq

importkotlin.random.Random

fungets():String?=

if(Random.nextBoolean())"str!"elsenull

funmain(){

gets()?.let{

it.removeSuffix("!")+it.length

}?.eq("str4")

}

Inmain(),ifgets()producesanon-nullresultthenletisinvoked.Thenon-nullablereceiverofletbecomesthenon-nullableitinsidethelambda.

Applyingthesafeaccessoperatortothecontextobjectnull-checkstheentirescope,asseenin[1]-[4]inthefollowing.Otherwise,eachcallwithinthescopemustbeindividuallynull-checked:

//ScopeFunctions/Gnome.kt

packagescopefunctions

classGnome(valname:String){

funwho()="Gnome:$name"

}

funwhatGnome(gnome:Gnome?){

gnome?.let{it.who()}//[1]

gnome.let{it?.who()}

gnome?.run{who()}//[2]

gnome.run{this?.who()}

gnome?.apply{who()}//[3]

gnome.apply{this?.who()}

gnome?.also{it.who()}//[4]

gnome.also{it?.who()}

//Nohelpfornullability:

with(gnome){this?.who()}

}

Whenyouusethesafeaccessoperatoronlet(),run(),apply()oralso(),theentirescopeisignoredforanullcontextobject:

//ScopeFunctions/NullGnome.kt

packagescopefunctions

importatomictest.*

funwhichGnome(gnome:Gnome?){

trace(gnome?.name)

gnome?.let{trace(it.who())}

gnome?.run{trace(who())}

gnome?.apply{trace(who())}

gnome?.also{trace(it.who())}

}

funmain(){

whichGnome(Gnome("Bob"))

whichGnome(null)

traceeq"""

Bob

Gnome:Bob

Gnome:Bob

Gnome:Bob

Gnome:Bob

null

"""

}

ThetraceshowsthatwhenwhichGnome()receivesanullargument,noscopefunctionsexecute.

AttemptingtoretrieveanobjectfromaMaphasanullableresultbecausethere’snoguaranteeitwillfindanentryforthatkey.HereweshowthedifferentscopefunctionsappliedtotheresultofaMaplookup:

//ScopeFunctions/MapLookup.kt

packagescopefunctions

importatomictest.*

dataclassPlumbus(varid:Int)

fundisplay(map:Map<String,Plumbus>){

trace("displaying$map")

valpb1:Plumbus=map["main"]?.let{

it.id+=10

it

}?:return

trace(pb1)

valpb2:Plumbus?=map["main"]?.run{

id+=9

this

}

trace(pb2)

valpb3:Plumbus?=map["main"]?.apply{

id+=8

}

trace(pb3)

valpb4:Plumbus?=map["main"]?.also{

it.id+=7

}

trace(pb4)

}

funmain(){

display(mapOf("main"toPlumbus(1)))

display(mapOf("none"toPlumbus(2)))

traceeq"""

displaying{main=Plumbus(id=1)}

Plumbus(id=11)

Plumbus(id=20)

Plumbus(id=28)

Plumbus(id=35)

displaying{none=Plumbus(id=2)}

"""

}

Althoughwith()canbeforcedintothisexample,theresultsaretoouglytoconsider.

InthetraceyouseethateachPlumbusobjectiscreatedduringthefirstcalltodisplay()inmain(),butnonearecreatedduringthesecondcall.Lookatthedefinitionofpb1andrecalltheElvisoperator.Iftheexpressiontotheleftof?:isnotnull,itbecomestheresultandisassignedtopb1.Butifthatexpressionisnull,therightsideof?:becomestheresult,whichisreturnsodisplay()returnsbeforecompletingtheinitializationofpb1,andthusnoneofthevaluespb1-pb4arecreated.

Scopefunctionsworkwithnullabletypesinchainedcalls:

//ScopeFunctions/NameTag.kt

packagescopefunctions

importatomictest.trace

valfunctions=listOf(

fun(name:String?){

name

?.takeUnless{it.isBlank()}

?.let{trace("$itinlet")}

},

fun(name:String?){

name

?.takeUnless{it.isBlank()}

?.run{trace("$thisinrun")}

},

fun(name:String?){

name

?.takeUnless{it.isBlank()}

?.apply{trace("$thisinapply")}

},

fun(name:String?){

name

?.takeUnless{it.isBlank()}

?.also{trace("$itinalso")}

},

)

funmain(){

functions.forEach{it(null)}

functions.forEach{it("")}

functions.forEach{it("Yumyulack")}

traceeq"""

Yumyulackinlet

Yumyulackinrun

Yumyulackinapply

Yumyulackinalso

"""

}

functionsisaListoffunctionreferencesthatareappliedbytheforEachcallsinmain(),usingittogetherwithfunction-callsyntax.Eachfunctioninfunctionsusesadifferentscopefunction.TheforEachcallstoit(null)andit("")areeffectivelyignored,soweonlydisplaynon-null,non-blankinput.

Whennestingscopefunctions,multiplethisoritobjectscanbeavailableinagivencontext.Sometimesit’sdifficulttoknowwhichobjectisselected:

//ScopeFunctions/Nesting.kt

packagescopefunctions

importatomictest.eq

funnesting(s:String,i:Int):String=

with(s){

with(i){

toString()

}

}+

s.let{

i.let{

it.toString()

}

}+

s.run{

i.run{

toString()

}

}+

s.apply{

i.apply{

toString()

}

}+

s.also{

i.also{

it.toString()

}

}

funmain(){

nesting("X",7)eq"777XX"

}

Inallcases,thecalltotoString()isappliedtoIntbecausethe“closest”thisoritistheIntimplicitreceiver.apply()andalso()returnthemodifiedobjectsinsteadoftheresultofthecalculation.Asscopefunctionsareintendedtoimprovereadability,nestingscopefunctionsisaquestionablepractice.

Noneofthescopefunctionsprovideresourcecleanupthewaythatuse()does:

//ScopeFunctions/Blob.kt

packagescopefunctions

importatomictest.*

dataclassBlob(valid:Int):AutoCloseable{

overridefuntoString()="Blob($id)"

funshow(){trace("$this")}

overridefunclose()=trace("Close$this")

}

funmain(){

Blob(1).let{it.show()}

Blob(2).run{show()}

with(Blob(3)){show()}

Blob(4).apply{show()}

Blob(5).also{it.show()}

Blob(6).use{it.show()}

Blob(7).use{it.run{show()}}

Blob(8).apply{show()}.also{it.close()}

Blob(9).also{it.show()}.apply{close()}

Blob(10).apply{show()}.use{}

traceeq"""

Blob(1)

Blob(2)

Blob(3)

Blob(4)

Blob(5)

Blob(6)

CloseBlob(6)

Blob(7)

CloseBlob(7)

Blob(8)

CloseBlob(8)

Blob(9)

CloseBlob(9)

Blob(10)

CloseBlob(10)

"""

}

Althoughuse()lookssimilartolet()andalso(),use()doesnotallowanythingtobereturnedfromitslambda.Thispreventsexpressionchainingorproducingresults.

Withoutuse(),close()isnotcalledforanyofthescopefunctions.Touseascopefunctionandguaranteecleanup,placethescopefunctioninsidetheuse()lambdaasinBlob(7).Blob(8)andBlob(9)showhowtoexplicitlycallclose(),andhowtouseapply()andalso()interchangeably.

Blob(10)usesapply()andtheresultispassedintouse(),whichcallsclose()attheendofitslambda.

ScopeFunctionsareInlinedNormally,passingalambdaasanargumentstoresthelambdacodeinanauxiliaryobject,addingasmallbitofruntimeoverheadcomparedtoaregularfunctioncall.Thisoverheadisusuallynotaconcern,consideringthebenefitsof

lambdas(readabilityandcodestructure).Inaddition,theJVMcontainsnumerousoptimizationsthatoftencompensatefortheoverhead.

Anyperformancecost,nomatterhowsmall,producesrecommendationsto“useafeaturewithcare.”Allruntimeoverheadiseliminatedbydefiningthescopefunctionsasinline.Thisway,scopefunctionscanbeusedwithouthesitation.

Whenthecompilerseesaninlinefunctioncall,itsubstitutesthefunctionbodyforthefunctioncall,replacingallparameterswithactualarguments.

Inliningworkswellforsmallfunctions,wherefunction-calloverheadcanbeasignificantportionoftheentirecall.Asfunctionsgetlarger,thecostofthecallshrinksincomparisontothetimerequiredbytheentirecall,diminishingthevalueofinlining.Atthesametime,theresultingbytecodeincreasesbecausetheentirefunctionbodyisinsertedateachcallsite.

Whenaninlinedfunctiontakesalambdaargument,thecompilerinlinesthelambdabodytogetherwiththefunctionbody.Thus,noadditionalclassesorobjectsarecreatedtopassthelambdatothefunction.(Thisonlyworkswhenthelambdaiscalleddirectly,orpassedtoanotherinlinefunction).

Althoughyoucanapplyittoanyfunction,inlineisintendedforeitherinlininglambdabodiesorcreatingreifiedgenerics.Youcanfindmoreinformationaboutinlinefunctionshere.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

CreatingGenerics

Genericcodeworkswithtypesthatare“specifiedlater.”

Ordinaryclassesandfunctionsworkwithspecifictypes.Ifyouwantcodetoworkacrossmoretypes,thisrigiditycanbeoverconstraining.

Polymorphismisanobject-orientedgeneralizationtool.Youwriteafunctionthattakesabase-classobjectasaparameter,thencallthatfunctionwithanobjectofanyclassderivedfromthatbaseclass—includingclassesthathaven’tyetbeencreated.Nowyourfunctionismoregeneral,andusefulinmoreplaces.

Asinglehierarchycanbetoolimitingbecauseyoumustinheritfromthathierarchytoproduceanobjectthatfitsyourfunctionparameter.Ifafunctionparameterisaninterfaceinsteadofaclass,thelimitationsareloosenedtoincludeanythingthatimplementsthatinterface.Thisgivestheclientprogrammertheoptionofimplementinganinterfaceincombinationwithanexistingclass—thatis,toadaptanexistingclasstofitthefunction.Usedthisway,interfacescancutacrossclasshierarchies.

Sometimesevenaninterfaceistoorestrictivebecauseitforcesyoutoworkwithonlythatinterface.Yourcodecanbeevenmoregeneralifitworkswith“someunspecifiedtype,”ratherthanaparticularinterfaceorclass.That“unspecifiedtype”isagenerictypeparameter.

Creatinggenerictypesandfunctionsisafairlycomplextopic,muchofwhichisoutsidethescopeofthisbook.Thisatomattemptstogiveyoujustenoughbackgroundsoyouaren’tsurprisedwhenyoucomeacrossgenericconceptsandkeywords.Ifyouwanttogetseriousaboutwritinggenerictypesandfunctionsyou’llneedtostudymoreadvancedresources.

Any

AnyistherootoftheKotlinclasshierarchy.EveryKotlinclasshasAnyasasuperclass.OnewaytoworkwithunspecifiedtypesisbypassingAny

arguments,andthiscansometimesconfusetheissueofwhentousegenerics.IfAnyworks,it’sthesimplersolution,andsimplerisgenerallybetter.

TherearetwowaystouseAny.Thefirst,andmoststraightforwardapproach,iswhenyouonlyneedtooperateonanAny,andnothingmore.Thisisextremelylimiting—Anyhasonlythreememberfunctions:equals(),hashCode()andtoString().Therearealsoextensionfunctions,butthesecannotperformanydirectoperationsonthetype.Forexample,apply()onlyappliesitsfunctionargumenttotheAny.

IfyouknowthetypeoftheAny,youcancastitandperformtype-specificoperations.Becausethisinvolvesrun-timetypeinformation(asshowninDowncasting),youriskaruntimeerrorifyoupassthewrongtypetoyourfunction(there’salsoaslightperformanceimpact).Sometimesthisisjustifiedtogainthebenefitofeliminatingcodeduplication.

Forexample,supposethreetypeseachhavetheabilitytocommunicate.Theycomefromdifferentlibrariessoyoucan’tjustputtheminthesamehierarchy,andtheyhavedifferentfunctionnamesforcommunicating:

//CreatingGenerics/Speakers.kt

packagecreatinggenerics

importatomictest.eq

classPerson{

funspeak()="Hi!"

}

classDog{

funbark()="Ruff!"

}

classRobot{

funcommunicate()="Beep!"

}

funtalk(speaker:Any)=when(speaker){

isPerson->speaker.speak()

isDog->speaker.bark()

isRobot->speaker.communicate()

else->"Notatalker"//Orexception

}

funmain(){

talk(Person())eq"Hi!"

talk(Dog())eq"Ruff!"

talk(Robot())eq"Beep!"

talk(11)eq"Notatalker"

}

Thewhenexpressiondiscoversthetypeofthespeakerandcallstheappropriatefunction.Ifyoudon’tthinktalk()willeverneedtoworkwithadditionaltypes,thisisatolerablesolution.Otherwise,itrequiresyoutomodifytalk()foreachnewtypeyouadd,andtorelyonruntimeinformationtodiscoverwhenyoumisssomething.

DefiningGenericsDuplicatedcodeisacandidateforconversionintoagenericfunctionortype.Youdothisbyaddinganglebrackets(<>)containingoneormoregenericplaceholders.Here,thegenericplaceholderTrepresentstheunknowntype:

//CreatingGenerics/DefiningGenerics.kt

packagecreatinggenerics

fun<T>gFunction(arg:T):T=arg

classGClass<T>(valx:T){

funf():T=x

}

classGMemberFunction{

fun<T>f(arg:T):T=arg

}

interfaceGInterface<T>{

valx:T

funf():T

}

classGImplementation<T>(

overridevalx:T

):GInterface<T>{

overridefunf():T=x

}

classConcreteImplementation

:GInterface<String>{

overridevalx:String

get()="x"

overridefunf()="f()"

}

funbasicGenerics(){

gFunction("Yellow")

gFunction(1)

gFunction(Dog()).bark()//[1]

gFunction<Dog>(Dog()).bark()

GClass("Cyan").f()

GClass(11).f()

GClass(Dog()).f().bark()//[2]

GClass<Dog>(Dog()).f().bark()

GMemberFunction().f("Amber")

GMemberFunction().f(111)

GMemberFunction().f(Dog()).bark()//[3]

GMemberFunction().f<Dog>(Dog()).bark()

GImplementation("Cyan").f()

GImplementation(11).f()

GImplementation(Dog()).f().bark()

ConcreteImplementation().f()

ConcreteImplementation().x

}

basicGenerics()showsthateachgenerichandlesdifferenttypes:

gFunction()takesaparameteroftypeTandreturnsaTresult.GClassstoresaT.Itsmemberfunctionf()returnsaT.GMemberFunctionparameterizesamemberfunctionwithintheclass,ratherthanparameterizingtheentireclass.YoucanalsodefineaninterfacewithgenericparametersasshowninGInterface.AnimplementationofGInterfacecaneitherredefineatypeparameterasinGImplementation,orprovideaspecifictypeargument,asinConcreteImplementation.

Noticein[1],[2]and[3]thatweareabletocallbark()ontheresult,becausethatresultemergesastypeDog.

Consider[1],[2]and[3],andthelinesimmediatelyfollowingthem.ThetypeTisdeterminedbytypeinferencefor[1],[2]and[3].Sometimesthisisnotpossibleifagenericoritsinvocationistoocomplextobeparsedbythecompiler.Inthiscaseyoumustspecifythetype(s)usingthesyntaxshowninthelinesimmediatelyfollowing[1],[2]and[3].

PreservingTypeInformationAsyouwillseelaterinthisatom,codewithingenericclassesandfunctionscan’tknowthetypeofT—thisiscallederasure.Genericscanbethoughtofasawaytopreservetypeinformationforthereturnvalue.Thisway,youdon’thavetowritecodetoexplicitlycheckandcastareturnvaluetothedesiredtype.

Acommonuseofgenericcodeisforcontainersthatholdotherobjects.ConsideraCarCrateclassthatactsasatrivialcollectionbyholdingandproducingasingleelementoftypeCar:

//CreatingGenerics/CarCrate.kt

packagecreatinggenerics

importatomictest.eq

classCar{

overridefuntoString()="Car"

}

classCarCrate(privatevarc:Car){

funput(car:Car){c=car}

funget():Car=c

}

funmain(){

valcc=CarCrate(Car())

valcar:Car=cc.get()

careq"Car"

}

Whenwecallcc.get(),theresultcomesbackastypeCar.We’dliketomakethistoolavailabletomoreobjectsthanjustCars,sowegenerifythisclassasCrate<T>:

//CreatingGenerics/Crate.kt

packagecreatinggenerics

importatomictest.eq

openclassCrate<T>(privatevarcontents:T){

funput(item:T){contents=item}

funget():T=contents

}

funmain(){

valcc=Crate(Car())

valcar:Car=cc.get()

careq"Car"

}

Crate<T>ensuresthatyoucanonlyput()aTintotheCrate,andwhenyoucallget()onthatCrate,theresultcomesbackastypeT.

Wecanmakeaversionofmap()forCratebydefiningagenericextensionfunction:

//CreatingGenerics/MapCrate.kt

packagecreatinggenerics

importatomictest.eq

fun<T,R>Crate<T>.map(f:(T)->R):List<R>=

listOf(f(get()))

funmain(){

Crate(Car()).map{it.toString()+"x"}eq

"[Carx]"

}

map()returnstheListofresultsproducedbyapplyingf()toeachelementintheinputsequence.BecauseCrateonlycontainsasingleelement,theresultis

alwaysaListofoneelement.Therearetwogenericarguments:TfortheinputvalueandRfortheresult,allowingf()toproducearesulttypethatisdifferentfromtheinputtype.

TypeParameterConstraintsAtypeparameterconstraintsaysthatthegenericargumenttypemustbeinheritedfromtheconstraint.<T:Base>meansthatTmustbeoftypeBaseorsomethingderivedfromBase.Thissectionshowsthatusingconstraintsisdifferentfromanon-generictypethatinheritsBase.

Consideratypehierarchythatmodelsdifferentitemsandwaystodisposeofthem:

//CreatingGenerics/Disposable.kt

packagecreatinggenerics

importatomictest.eq

interfaceDisposable{

valname:String

funaction():String

}

classCompost(overridevalname:String):

Disposable{

overridefunaction()="Addtocomposter"

}

interfaceTransport:Disposable

classDonation(overridevalname:String):

Transport{

overridefunaction()="Callforpickup"

}

classRecyclable(overridevalname:String):

Transport{

overridefunaction()="Putinbin"

}

classLandfill(overridevalname:String):

Transport{

overridefunaction()="Putindumpster"

}

valitems=listOf(

Compost("OrangePeel"),

Compost("AppleCore"),

Donation("Couch"),

Donation("Clothing"),

Recyclable("Plastic"),

Recyclable("Metal"),

Recyclable("Cardboard"),

Landfill("Trash"),

)

valrecyclables=

items.filterIsInstance<Recyclable>()

Usingaconstraint,wecanaccesspropertiesandfunctionsoftheconstrainedtypewithinagenericfunction:

//CreatingGenerics/Constrained.kt

packagecreatinggenerics

importatomictest.eq

fun<T:Disposable>nameOf(disposable:T)=

disposable.name

//Asanextension:

fun<T:Disposable>T.name()=name

funmain(){

recyclables.map{nameOf(it)}eq

"[Plastic,Metal,Cardboard]"

recyclables.map{it.name()}eq

"[Plastic,Metal,Cardboard]"

}

Wecannotaccessnamewithouttheconstraint.

Thisachievesthesameresultwithoutgenerics:

//CreatingGenerics/NonGenericConstraint.kt

packagecreatinggenerics

importatomictest.eq

funnameOf2(disposable:Disposable)=

disposable.name

funDisposable.name2()=name

funmain(){

recyclables.map{nameOf2(it)}eq

"[Plastic,Metal,Cardboard]"

recyclables.map{it.name2()}eq

"[Plastic,Metal,Cardboard]"

}

Whyuseaconstraintinsteadofordinarypolymorphism?Theanswerisinthereturntype.Withgenerics,thereturntypecanbeexact,ratherthanbeingupcasttothebasetype:

//CreatingGenerics/SameReturnType.kt

packagecreatinggenerics

importkotlin.random.Random

privatevalrnd=Random(47)

funList<Disposable>.aRandom():Disposable=

this[rnd.nextInt(size)]

fun<T:Disposable>List<T>.bRandom():T=

this[rnd.nextInt(size)]

fun<T>List<T>.cRandom():T=

this[rnd.nextInt(size)]

funsameReturnType(){

vala:Disposable=recyclables.aRandom()

valb:Recyclable=recyclables.bRandom()

valc:Recyclable=recyclables.cRandom()

}

Withoutgenerics,aRandom()canonlyproduceabase-classDisposable,whilebothbRandom()andcRandom()produceaRecyclable.bRandom()neveraccessesanyelementsofT,thereforeitsconstraintispointlessanditendsupbeingthesameascRandom(),whichdoesn’tuseaconstraint.

Theonlytimeyouneedconstraintsisifyourequirebothofthefollowing:

1. Accessafunctionorproperty.2. Preservethetypewhenreturningit.

//CreatingGenerics/Constraints.kt

packagecreatinggenerics

importkotlin.random.Random

privatevalrnd=Random(47)

//Accessesaction()butcan't

//returntheexacttype:

funList<Disposable>.inexact():Disposable{

vald:Disposable=this[rnd.nextInt(size)]

d.action()

returnd

}

//Can'taccessaction()withoutaconstraint:

fun<T>List<T>.noAccess():T{

vald:T=this[rnd.nextInt(size)]

//d.action()

returnd

}

//Accessaction()andreturntheexacttype:

fun<T:Disposable>List<T>.both():T{

vald:T=this[rnd.nextInt(size)]

d.action()

returnd

}

funconstraints(){

vali:Disposable=recyclables.inexact()

valn:Recyclable=recyclables.noAccess()

valb:Recyclable=recyclables.both()

}

inexact()isanextensiontoList<Disposable>,whichallowsittoaccessaction(),butitisnotgenericsoitcanonlyreturnthebasetypeDisposable.Asageneric,noAccess()isabletoreturntheexacttypeofT,butwithoutaconstraintitcannotaccessaction().OnlywhenyouaddtheconstraintonTinboth()areyouabletoaccessaction()andreturntheexacttypeT.

TypeErasureJavacompatibilityisanessentialpartofKotlin.InJava,genericswerenotpartoftheoriginallanguage—theywereaddedyearslater,afterlargebodiesofcodehadbeenwritten.ForcinggenericsintoJavawithoutbreakingexistingcoderequiredacrucialcompromise:thegenerictypesareonlyavailableduringcompilationbutarenotpreservedatruntime—thetypesareerased.ThiserasureaffectsKotlin.

Let’spretenderasuredoesn’thappen:

//CreatingGenerics/Erasure.kt

packagecreatinggenerics

funmain(){

valstrings=listOf("a","b","c")

valall:List<Any>=listOf(1,2,"x")

useList(strings)

useList(all)

}

funuseList(list:List<Any>){

//if(listisList<String>){}//[1]

}

Uncommentline[1]andyou’llseethefollowingerror:“Cannotcheckforinstanceoferasedtype:List<String>”.Youcan’ttestforthegenerictypeatruntimebecausethetypeinformationhasbeenerased.

Iferasuredidn’thappen,thelistmightlooklikethis,assumingadditionaltypeinformationisplacedattheendofthelist(itdoesnotworkthisway!):

ReifiedGenerics

Becausegenerictypesareerased,typeinformationisnotstoredintheList.Instead,bothstringsandallarejustLists,withnoadditionaltypeinformation:

ErasedGenerics

YoucannotguesstypeinformationfromtheListcontentswithoutanalyzingallelements.Checkingonlythefirstelementfromthesecondlistleadsyoutoincorrectlyassumethatit’saList<Int>.

TheKotlindesignersdecidedtofollowJavaanduseerasure,fortworeasons:

1. Javacompatibility.2. Overhead.Storinggenerictypeinformationsignificantlyincreasesthe

memoryoccupiedbyagenericListorMap.Forexample,astandardMapconsistsofmanyMap.Entryobjects,andMap.Entryisagenericclass.Thus,ifgenericswerereifiedeverywherebydefault,eachkeyandvalueofeveryMap.Entrywouldcontainadditionaltypeinformation.

ReificationofFunctionTypeArgumentsTypeinformationisalsoerasedforgenericfunctioncalls,whichmeansyoucan’tdomuchwithagenericparameterinsideafunction.

Toretaintypeinformationforfunctionarguments,addthereifiedkeyword.Considerafunctiona()thatrequiresclassinformationtoperformitstask:

//CreatingGenerics/ReificationA.kt

packagecreatinggenerics

importkotlin.reflect.KClass

fun<T:Any>a(kClass:KClass<T>){

//UsesKClass<T>

}

Whenwecalla()insideasecondgenericfunctionb(),wewouldliketousetypeinformationforthegenericargument:

//CreatingGenerics/ReificationB.kt

packagecreatinggenerics

//Doesn'tcompilebecauseoferasure:

//fun<T:Any>b()=a(T::class)

ThetypeinformationforTiserasedwhenthiscoderuns,sob()won’tcompile.Youcan’taccesstheclassofthegenerictypeparameterinsidethefunctionbody.

TheJavasolutionistopasstypeinformationintothefunctionbyhand:

//CreatingGenerics/ReificationC.kt

packagecreatinggenerics

importkotlin.reflect.KClass

fun<T:Any>c(kClass:KClass<T>)=a(kClass)

classK

valkc=c(K::class)

PassingexplicittypeinformationshouldberedundantbecausethecompilerknowsthetypeofT,andcouldsilentlypassitforyou.Thisiseffectivelywhatthereifiedkeyworddoes.

Tousereified,thefunctionmustalsobeinline:

//CreatingGenerics/ReificationD.kt

packagecreatinggenerics

inlinefun<reifiedT:Any>d()=a(T::class)

valkd=d<K>()

d()producesthesameeffectasc(),butd()doesn’trequiretheclassreferenceasanargument.

reifiedtellsthecompilertopreservetheinformationaboutthecorrespondingtypeargument.Thetypeinformationisnowavailableatruntimesoyoucanaccessitinsidethefunctionbody.

Reificationallowstheuseofiswithagenericparametertype:

//CreatingGenerics/CheckType.kt

packagecreatinggenerics

importatomictest.eq

inlinefun<reifiedT>check(t:Any)=tisT

//fun<T>check1(t:Any)=tisT//[1]

funmain(){

check<String>("1")eqtrue

check<Int>("1")eqfalse

}

[1]Withoutreified,thetypeinformationiserasedsoyoucan’tcheckwhetheragivenelementisaninstanceofT.

Inthefollowingexample,select()producesthenameofeachDisposableitemofaparticularsubtype.Itusesreifiedcombinedwithaconstraint:

//CreatingGenerics/Select.kt

packagecreatinggenerics

importatomictest.eq

inlinefun<reifiedT:Disposable>select()=

items.filterIsInstance<T>().map{it.name}

funmain(){

select<Compost>()eq

"[OrangePeel,AppleCore]"

select<Donation>()eq"[Couch,Clothing]"

select<Recyclable>()eq

"[Plastic,Metal,Cardboard]"

select<Landfill>()eq"[Trash]"

}

ThelibraryfunctionfilterIsInstance()isitselfdefinedusingthereifiedkeyword.

VarianceCombininggenericsandinheritanceproducestwodimensionsofchange.IfyouhaveaContainer<T>andyouwanttoassignittoaContainer<U>whereTandUhaveaninheritancerelationship,youmustplaceconstraintsuponContainerusingtheinoroutvarianceannotations,dependingonhowyouwanttouseContainer.

HerearethreeversionsofaBoxcontainer:abasicBox<T>,oneusing<inT>andoneusing<outT>:

//CreatingGenerics/InAndOutBoxes.kt

packagevariance

classBox<T>(privatevarcontents:T){

funput(item:T){contents=item}

funget():T=contents

}

classInBox<inT>(privatevarcontents:T){

funput(item:T){contents=item}

}

classOutBox<outT>(privatevarcontents:T){

funget():T=contents

}

inTmeansthatmemberfunctionsoftheclasscanonlyacceptargumentsoftypeT,butcannotreturnvaluesoftypeT.Thatis,TobjectscanbeplacedintoanInBox,butcannotcomeout.

outTmeansthatmemberfunctionscanreturnTobjects,butcannotacceptargumentsoftypeT—youcannotplaceTobjectsintoanOutBox.

Whydoweneedtheseconstraints?Considerthishierarchy:

//CreatingGenerics/Pets.kt

packagevariance

openclassPet

classCat:Pet()

classDog:Pet()

CatandDogarebothsubtypesofPet.IsthereasubtypingrelationbetweenBox<Cat>andBox<Pet>?Itseemslikeweshouldbeabletoassign,forexample,aBoxofCattoaBoxofPetortoaBoxofAny(becauseAnyisasupertypeofeverything):

//CreatingGenerics/BoxAssignment.kt

packagevariance

valcatBox=Box<Cat>(Cat())

//valpetBox:Box<Pet>=catBox

//valanyBox:Box<Any>=catBox

IfKotlinallowedthis,petBoxwouldhaveput(item:Pet).DogisalsoaPet,sothiswouldallowyoutoputaDogintocatBox,violatingthe“cat-ness”ofthatBox.

Worse,anyBoxwouldhaveput(item:Any),soyoucouldputanAnyintocatBox—thecontainerwouldhavenotypesafetyatall.

Ifwepreventtheuseofput(),theassignmentsaresafebecausenoonecanputaDogintoanOutBox<Cat>.ThecompilerallowsustoassignanOutBox<Cat>toanOutBox<Pet>ortoanOutBox<Any>,becausetheoutannotationpreventsthemfromhavingput()functions:

//CreatingGenerics/OutBoxAssignment.kt

packagevariance

valoutCatBox:OutBox<Cat>=OutBox(Cat())

valoutPetBox:OutBox<Pet>=outCatBox

valoutAnyBox:OutBox<Any>=outCatBox

fungetting(){

valcat:Cat=outCatBox.get()

valpet:Pet=outPetBox.get()

valany:Any=outAnyBox.get()

}

Withnoput(),wecannotplaceaDogintoanOutBox<Cat>,soits“cat-ness”ispreserved.

Withoutaget(),anInBox<Any>canbeassignedtoanInBox<Pet>,anInBox<Cat>oranInBox<Dog>:

//CreatingGenerics/InBoxAssignment.kt

packagevariance

valinBoxAny:InBox<Any>=InBox(Any())

valinBoxPet:InBox<Pet>=inBoxAny

valinBoxCat:InBox<Cat>=inBoxAny

valinBoxDog:InBox<Dog>=inBoxAny

funmain(){

inBoxAny.put(Any())

inBoxAny.put(Pet())

inBoxAny.put(Cat())

inBoxAny.put(Dog())

inBoxPet.put(Pet())

inBoxPet.put(Cat())

inBoxPet.put(Dog())

inBoxCat.put(Cat())

inBoxDog.put(Dog())

}

Itissafetoput()anAny,Pet,CatorDogintoanInBox<Any>,whileyoucanonlyput()aPet,CatorDogintoanInBox<Pet>.inBoxCatandinBoxDogwill

onlyacceptCatsandDogs,respectively.Thesearethebehaviorsweexpectforboxesthathavethosetypeparameters,andthecompilerenforcesit.

Here’sasummaryofthesubtypingrelationshipsforBox,OutBoxandInBox:

Variance

Box<T>isinvariant.ThismeansthatneitherBox<Cat>norBox<Pet>isasubtypeoftheother,soneithercanbeassignedtotheother.OutBox<outT>iscovariant.ThismeansthatOutBox<Cat>isasubtypeofOutBox<Pet>.WhenyouupcastanOutBox<Cat>toanOutBox<Pet>,itvariesinthesamewayasupcastingaCattoaPet.InBox<inT>iscontravariant.ThismeansthatInBox<Pet>isasubtypeofInBox<Cat>.WhenyouupcastanInBox<Pet>toanInBox<Cat>,itvariesintheoppositewayasupcastingaCattoaPet.

Aread-onlyListfromtheKotlinstandardlibraryiscovariant.YoucanassignaList<Cat>toaList<Pet>.AMutableListisinvariantbecauseitcontainsanadd():

//CreatingGenerics/CovariantList.kt

packagevariance

funmain(){

valcatList:List<Cat>=listOf(Cat())

valpetList:List<Pet>=catList

varmutablePetList:MutableList<Pet>=

mutableListOf(Cat())

mutablePetList.add(Dog())

//Typemismatch:

//mutablePetList=

//mutableListOf<Cat>(Cat())//[1]

}

[1]Ifthisassignmentworked,wecouldviolatethe“cat-ness”ofthemutableListOf<Cat>byaddingaDog.

Functionscanhavecovariantreturntypes.Thismeansthatanoverridingfunctioncanreturnatypethat’smorespecificthanthefunctionitoverrides:

//CreatingGenerics/CovariantReturnTypes.kt

packagevariance

interfaceParent

interfaceChild:Parent

interfaceX{

funf():Parent

}

interfaceY:X{

overridefunf():Child

}

Noticehowtheoverriddenf()inYreturnsaChild,whilef()inXreturnsaParent.

Thissubsectionhasonlybeenalightintroductiontothetopicofvariance.

-

Repeatedcodeisacandidateforgenerictypesorfunctions.Thisatomonlyprovidesabasicgraspoftheideas—ifyouneeddeeperunderstandingyoumustfinditinamoreadvancedtreatment.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

OperatorOverloading

Inthecontextofcomputerprogramming,overloadingmeans“addingextrameaningtosomethingthatalreadyexists.”

Operatoroverloadingallowsyoutotakeanoperatorlike+andgiveitmeaningforyournewtype,orextrameaningforanexistingtype.

Operatoroverloadinghasatumultuouspast.ItwaspopularizedinC++,butbecauseC++hadnogarbagecollection,writingoverloadedoperatorswasdifficult.Asaresult,theearlyJavadesignersdeemedoperatoroverloading“bad”anddidn’tallowitinJava,eventhoughJava’sgarbagecollectionwouldhavemadeitrelativelyeasy.ThesimplicityofoperatoroverloadingwhensupportedbygarbagecollectionwasdemonstratedinthePythonlanguage,whichconstrainedyoutoalimited(familiar)setofoperators,asdidC++.Scalathenexperimentedwithallowingyoutoinventyourownoperators,causingsomeprogrammerstoabusethisfeatureandcreateincomprehensiblecode.Kotlinlearnedfromtheselanguages,andhassimplifiedtheprocessofoperatoroverloadingbutrestrictsyourchoicestoareasonableandfamiliarsetofoperators.Inaddition,therulesofoperatorprecedencecannotbechanged.

We’llcreateasmallclassNumandaddanoverloaded+asanextensionfunction.Tooverloadanoperatoryouusetheoperatorkeywordbeforefun,followedbythespecialpredefinedfunctionnameforthatoperator.Forexample,thespecialfunctionnameforthe+operatorisplus():

//OperatorOverloading/Num.kt

packageoperatoroverloading

importatomictest.eq

dataclassNum(valn:Int)

operatorfunNum.plus(rval:Num)=

Num(n+rval.n)

funmain(){

Num(4)+Num(5)eqNum(9)

Num(4).plus(Num(5))eqNum(9)

}

Ifyouweredefininganormal(non-operator)functionforusebetweentwooperands,you’dusetheinfixkeyword,butoperatorsarealreadyinfix.Becauseplus()isanordinaryfunction,youcanalsocallitintheconventionalway.

Whenyoudefineanoperatorasamemberfunction,youcanaccessprivateelementsinaclassthatanextensionfunctioncannot:

//OperatorOverloading/MemberOperator.kt

packageoperatoroverloading

importatomictest.eq

dataclassNum2(privatevaln:Int){

operatorfunplus(rval:Num2)=

Num2(n+rval.n)

}

//Cannotaccess'n':itisprivatein'Num2':

//operatorfunNum2.minus(rval:Num2)=

//Num2(n-rval.n)

funmain(){

Num2(4)+Num2(5)eqNum2(9)

}

Insomecontextsit’shelpfultocreatespecialmeaningforanoperator.Here,wemodelaMoleculewitha+thatattachesittoanotherMolecule.TheattachedpropertyisthelinkbetweenMolecules:

//OperatorOverloading/Molecule.kt

packageoperatoroverloading

importatomictest.eq

dataclassMolecule(

valid:Int=idCount++,

varattached:Molecule?=null

){

companionobject{

privatevaridCount=0

}

operatorfunplus(other:Molecule){

attached=other

}

}

funmain(){

valm1=Molecule()

valm2=Molecule()

m1+m2//[1]

m1eq"Molecule(id=0,attached="+

"Molecule(id=1,attached=null))"

}

[1]Readslikeafamiliarmathexpression,buttothepersonusingthemodelitmightbeanespeciallymeaningfulsyntax.

Thisexampleisincomplete;ifyouaddthelinem2+m1,thentrytodisplaym2,you’llgetastackoverflow(canyoufixtheproblem?).

EqualityInvoking==(equality)or!=(inequality)callstheequals()memberfunction.dataclassesautomaticallyredefineequals()tocomparethestoreddata,butifyoudon’tredefineequals()fornon-dataclasses,thedefaultversioncomparesreferencesratherthancontents:

//OperatorOverloading/DefaultEquality.kt

packageoperatoroverloading

importatomictest.eq

classA(vali:Int)

dataclassD(vali:Int)

funmain(){

//Normalclass:

vala=A(1)

valb=A(1)

valc=a

(a==b)eqfalse

(a==c)eqtrue

//Dataclass:

vald=D(1)

vale=D(1)

(d==e)eqtrue

}

aandbrefertodifferentobjectsinmemory,sothereferencesaredifferentanda==bisfalse,eventhoughthetwoobjectsstoreidenticaldata.aandcrefertothesameobjectinmemory,socomparingthemproducestrue.BecausethedataclassDautomaticallygeneratesanequals()thatlooksatthecontentsofD,d==eproducestrue.

equals()istheonlyoperatorthatcannotbeanextensionfunction;itmustbeoverriddenasamemberfunction.Whendefiningyourownequals(),youareoverridingthedefaultequals(other:Any?).NoticethatthetypeofotherisAny?ratherthanthespecifictypeofyourclass.Thisallowsyoutocompareyourtypewithothertypes,whichmeansyoumustchoosethetypesallowedforcomparison:

//OperatorOverloading/DefiningEquality.kt

packageoperatoroverloading

importatomictest.eq

classE(varv:Int){

overridefunequals(other:Any?)=when{

this===other->true//[1]

other!isE->false//[2]

else->v==other.v//[3]

}

overridefunhashCode():Int=v

overridefuntoString()="E($v)"

}

funmain(){

vala=E(1)

valb=E(2)

(a==b)eqfalse//a.equals(b)

(a!=b)eqtrue//!a.equals(b)

//Referenceequality:

(E(1)===E(1))eqfalse

}

[1]Thisisanoptimization:ifotherreferstothesameobjectinmemory,theresultisautomaticallytrue.Thetripleequalitysymbol===testsforreferenceequality.[2]Thisdeterminesthatthetypeofothermustbethesameasthecurrenttype.ForEtobecomparedtoothertypes,addfurthermatchexpressions.[3]Thiscomparesthestoreddata.AtthispointthecompilerknowsthatotherisoftypeE,sowecanaccessother.vwithoutacast.

Whenoverridingequals()youshouldalsooverridehashCode().Thisisacomplextopic,butthebasicruleisthatiftwoobjectsareequal,theymustproducethesamehashCode()value.StandarddatastructureslikeMapandSetwillfailwithoutthisrule.Thingsgetevenmorecomplicatedwithanopenclassbecauseyoumustcompareaninstancewithallpossiblesubclasses.YoucanlearnmoreabouttheconceptofhashinginWikipedia.

Definingaproperequals()andhashCode()isbeyondthescopeofthisbook—whatwedohereillustratestheconceptandworksforoursimpleexamplebutwon’tworkformorecomplicatedcases.Thiscomplexityisthereasonthatdataclassescreatetheirownequals()andhashCode().Ifyoumustdefineyourownequals()andhashCode(),werecommendautomaticallygeneratingthemusingIntelliJIDEAorAndroidStudiowiththeactionGenerate->equalsandhashCode.

Whenyoucomparenullableobjectsusing==,Kotlinenforcesnull-checking.ThiscanbeachievedusingeitherifortheElvisoperator:

//OperatorOverloading/EqualsForNullable.kt

packageoperatoroverloading

importatomictest.eq

funequalsWithIf(a:E?,b:E?)=

if(a===null)

b===null

else

a==b

funequalsWithElvis(a:E?,b:E?)=

a?.equals(b)?:(b===null)

funmain(){

valx:E?=null

valy=E(0)

valz:E?=null

(x==y)eqfalse

(x==z)eqtrue

equalsWithIf(x,y)eqfalse

equalsWithIf(x,z)eqtrue

equalsWithElvis(x,y)eqfalse

equalsWithElvis(x,z)eqtrue

}

equalsWithIf()firstcheckstoseeifthereferenceaisnull,inwhichcasetheonlywaythetwocanbeequalisifthereferencebisalsonull.Ifaisnotanullreference,thememberequals()isusedtocomparethetwo.equalsWithElvis()achievesthesameeffect,butmoresuccinctlyusingboth?.and?:.

ArithmeticoperatorsWecandefinebasicarithmeticoperatorsasextensionstoclassE:

//OperatorOverloading/ArithmeticOperators.kt

packageoperatoroverloading

importatomictest.eq

//Unaryoperators:

operatorfunE.unaryPlus()=E(v)

operatorfunE.unaryMinus()=E(-v)

operatorfunE.not()=this

//Increment/decrement:

operatorfunE.inc()=E(v+1)

operatorfunE.dec()=E(v-1)

fununary(a:E){

+a//unaryPlus()

-a//unaryMinus()

!a//not()

varb=a

b++//inc()(mustbevar)

b--//dec()(mustbevar)

}

//Binaryoperators:

operatorfunE.plus(e:E)=E(v+e.v)

operatorfunE.minus(e:E)=E(v-e.v)

operatorfunE.times(e:E)=E(v*e.v)

operatorfunE.div(e:E)=E(v%e.v)

operatorfunE.rem(e:E)=E(v/e.v)

funbinary(a:E,b:E){

a+b//a.plus(b)

a-b//a.minus(b)

a*b//a.times(b)

a/b//a.div(b)

a%b//a.rem(b)

}

//Augmentedassignment:

operatorfunE.plusAssign(e:E){v+=e.v}

operatorfunE.minusAssign(e:E){v-e.v}

operatorfunE.timesAssign(e:E){v*=e.v}

operatorfunE.divAssign(e:E){v/=e.v}

operatorfunE.remAssign(e:E){v%=e.v}

funassignment(a:E,b:E){

a+=b//a.plusAssign(b)

a-=b//a.minusAssign(b)

a*=b//a.timesAssign(b)

a/=b//a.divAssign(b)

a%=b//a.remAssign(b)

}

funmain(){

vala=E(2)

valb=E(3)

a+beqE(5)

a*beqE(6)

valx=E(1)

x+=b*b

xeqE(10)

}

Whenwritinganextension,rememberthatthepropertiesandfunctionsoftheextendedtypeareimplicitlyavailable.InthedefinitionofunaryPlus(),forexample,thevinE(v)isthevpropertyfromtheEthat’sbeingextended.

Notethatx+=ecanberesolvedtoeitherx=x.plus(e)ifxisavarortox.plusAssign(e)ifxisvalandthecorrespondingplusAssign()memberisavailable.Ifbothoptionswork,thecompileremitsanerrorindicatingthatitcan’tchoose.

Theparametercanbeofadifferenttypethanthetypetheoperatorextends.Here,the+operatorextensionforEtakesanIntparameter:

//OperatorOverloading/DifferentTypes.kt

packageoperatoroverloading

importatomictest.eq

operatorfunE.plus(i:Int)=E(v+i)

funmain(){

E(1)+10eqE(11)

}

Operatorprecedenceisfixed,andisidenticalforbothbuilt-intypesandcustomtypes.Forexample,multiplicationhasahigherprecedencethanaddition,andbothhavehigherprecedencethanequality;thus1+2*3==7istrue.Youcanfindtheoperatorprecedencetableinthedocumentation.

Sometimeswhenyoumixarithmeticandprogrammingoperators,theresultisn’tobvious.Here,wecombine+andtheElvisoperator:

//OperatorOverloading/ConfusingPrecedence.kt

packageoperatoroverloading

importatomictest.eq

funmain(){

valx:Int?=1

valy:Int=2

valsum=x?:0+y

sumeq1

(x?:0)+yeq3//[1]

x?:(0+y)eq1//[2]

}

Insum,+hashigherprecedencethantheElvisoperator?:sotheresultis1?:(0+2)==1.Thismightbenotwhattheprogrammerintended.Whenmixingdifferentoperationswhereprecedenceisnotobvious,werecommendaddingparenthesesasinlines[1]and[2].

ComparisonAllcomparisonoperations<,>,<=,>=areautomaticallyavailablewhenyoudefinecompareTo():

//OperatorOverloading/Comparison.kt

packageoperatoroverloading

importatomictest.eq

operatorfunE.compareTo(e:E):Int=

v.compareTo(e.v)

funmain(){

vala=E(2)

valb=E(3)

(a<b)eqtrue//a.compareTo(b)<0

(a>b)eqfalse//a.compareTo(b)>0

(a<=b)eqtrue//a.compareTo(b)<=0

(a>=b)eqfalse//a.compareTo(b)>=0

}

compareTo()mustreturnanIntindicating:

0iftheelementsareequal.Apositivevalueifthefirstelement(thereceiver)isbiggerthanthesecond(theargument).Anegativevalueifthefirstelementissmallerthanthesecond.

RangesandContainersrangeTo()overloadsthe..operatorforcreatingranges,whilecontains()indicateswhetheravalueiswithinarange:

//OperatorOverloading/Ranges.kt

packageoperatoroverloading

importatomictest.eq

dataclassR(valr:IntRange){//Range

overridefuntoString()="R($r)"

}

operatorfunE.rangeTo(e:E)=R(v..e.v)

operatorfunR.contains(e:E):Boolean=

e.vinr

funmain(){

vala=E(2)

valb=E(3)

valr=a..b//a.rangeTo(b)

(ainr)eqtrue//r.contains(a)

(a!inr)eqfalse//!r.contains(a)

reqR(2..3)

}

ContainerAccessOverloadingcontains()allowsyoutocheckwhetheravalueisinacontainer,whileget()andset()supportreadingandassigningelementsinacontainerusingsquarebrackets:

//OperatorOverloading/ContainerAccess.kt

packageoperatoroverloading

importatomictest.eq

dataclassC(valc:MutableList<Int>){

overridefuntoString()="C($c)"

}

operatorfunC.contains(e:E)=e.vinc

operatorfunC.get(i:Int):E=E(c[i])

operatorfunC.set(i:Int,e:E){

c[i]=e.v

}

funmain(){

valc=C(mutableListOf(2,3))

(E(2)inc)eqtrue//c.contains(E(2))

(E(4)inc)eqfalse//c.contains(E(4))

c[1]eqE(3)//c.get(1)

c[1]=E(4)//c.set(2,E(4))

ceqC(mutableListOf(2,4))

}

InIntelliJIDEAorAndroidStudioyoucannavigatetoadeclarationofafunctionoraclassfromitsusage.Thisalsoworkswithoperators:youcanputthecursoron..thennavigatetoitsdefinitiontoseewhichoperatorfunctioniscalled.

InvokePlacingparenthesesafteranobjectgeneratesacalltoinvoke(),sotheinvoke()operatormakesanobjectlooklikeafunction.Youcandefineinvoke()withanynumberofparameters:

//OperatorOverloading/Invoke.kt

packageoperatoroverloading

importatomictest.eq

classFunc{

operatorfuninvoke()="invoke()"

operatorfuninvoke(i:Int)="invoke($i)"

operatorfuninvoke(i:Int,j:String)=

"invoke($i,$j)"

operatorfuninvoke(

i:Int,j:String,k:Double

)="invoke($i,$j,$k)"

}

funmain(){

valf=Func()

f()eq"invoke()"

f(22)eq"invoke(22)"

f(22,"Hi")eq"invoke(22,Hi)"

f(22,"Three",3.1416)eq

"invoke(22,Three,3.1416)"

}

Youcanalsodefineinvoke()withvarargtoworkwithanynumberofargumentsofthesametype(seeVariableArgumentLists).

invoke()canbedefinedasanextensionfunction.Here,it’sanextensionforString,takingafunctionasaparameterandcallingthatfunctionontheString:

//OperatorOverloading/StringInvoke.kt

packageoperatoroverloading

importatomictest.eq

operatorfunString.invoke(

f:(s:String)->String

)=f(this)

funmain(){

"mumbling"{it.toUpperCase()}eq

"MUMBLING"

}

Becausethelambdaisthefinalinvoke()argument,itcanbecalledwithoutparentheses.

Ifyouhaveafunctionreference,youcanuseittocallthefunctiondirectlyusingparenthesesorviainvoke():

//OperatorOverloading/InvokeFunctionType.kt

packageoperatoroverloading

importatomictest.eq

funmain(){

valfunc:(String)->Int={it.length}

func("abc")eq3

func.invoke("abc")eq3

valnullableFunc:((String)->Int)?=null

if(nullableFunc!=null){

nullableFunc("abc")

}

nullableFunc?.invoke("abc")//[1]

}

[1]Ifafunctionreferenceisnullable,youcancombineinvoke()andsafeaccess.

Themostcommonuseforacustominvoke()iswhencreatingDSLs.

FunctionNamesinBackticksKotlinallowsspaces,certainnonstandardcharacters,andreservedwordsinafunctionnamebyplacingthatfunctionnameinsidebackticks:

//OperatorOverloading/Backticks.kt

packageoperatoroverloading

fun`Alongnamewithspaces`()=Unit

fun`*how*isthisworking?`()=Unit

fun`'when'isakeyword`()=Unit

//fun`Illegalcharacters:<>`()=Unit

funmain(){

`Alongnamewithspaces`()

`*how*isthisworking?`()

`'when'isakeyword`()

}

ThiscanbeparticularlyhelpfulforUnitTestingbecauseyoucancreatereadabletestnamesthatincludedetailsaboutthosetests.ItalsosimplifiesinteractionswithJavacode.

Youcaneasilycreateincomprehensiblecode:

//OperatorOverloading/Swearing.kt

packageoperatoroverloading

importatomictest.eq

infixfunString.`#!%`(s:String)=

"$thisRowzafrazaca$s"

funmain(){

"howdy"`#!%`"Ma'am!"eq

"howdyRowzafrazacaMa'am!"

}

Kotlinacceptsthiscode,butwhatdoesitmeantothereader?Becausecodeisreadmuchmorethanitiswritten,youshouldmakeyourprogramsasunderstandableaspossible.

-

Operatoroverloadingisnotanessentialfeature,butisanexcellentexampleofhowalanguageismorethanjustawaytomanipulatetheunderlyingcomputer.Thechallengeiscraftingthelanguagetoprovidebetterwaystoexpressyourabstractions,sohumanshaveaneasiertimeunderstandingthecodewithoutgettingboggeddowninneedlessdetail.It’spossibletodefineoperatorsinwaysthatobscuremeaning,sotreadcarefully.

Everythingissyntacticsugar.Toiletpaperissyntacticsugar,andIstillwantit.—BarryHawkins

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

UsingOperators

Inpracticeyourarelyoverloadoperators—usuallyonlywhenyoucreateyourownlibrary.

However,youregularlyuseoverloadedoperators,oftenwithoutnoticing.Forexample,theKotlinstandardlibrarydefinesnumerousoperatorsthatimproveyourexperiencewithcollections.Here’ssomefamiliarcodeseenfromanewangle:

//UsingOperators/NewAngle.kt

importatomictest.eq

funmain(){

vallist=MutableList(10){'a'+it}

list[7]eq'h'//operatorget()

list.get(8)eq'i'//Explicitcall

list[9]='x'//operatorset()

list.set(9,'x')//Explicitcall

list[9]eq'x'

('d'inlist)eqtrue//operatorcontains()

list.contains('e')eqtrue//Explicitcall

}

Accessinglistelementsusingsquarebracketscallstheoverloadedoperatorsget()andset(),whileincallscontains().

Calling+=onamutablecollectionmodifiesit,whilecalling+returnsanewcollectioncontainingtheoldelementstogetherwiththenewelement:

//UsingOperators/OperatorPlus.kt

importatomictest.eq

funmain(){

valmutableList=mutableListOf(1,2,3)

mutableList+=4//operatorplusAssign()

mutableList.plusAssign(5)//Explicit

mutableListeq"[1,2,3,4,5]"

mutableList+99eq"[1,2,3,4,5,99]"

mutableListeq"[1,2,3,4,5]"

vallist=listOf(1)//Read-only

valnewList=list+2//operatorplus()

listeq"[1]"

newListeq"[1,2]"

valanother=list.plus(3)//Explicit

anothereq"[1,3]"

}

Calling+=onaread-onlycollectionprobablydoesn’tproducewhatyouexpect:

//UsingOperators/Unexpected.kt

importatomictest.eq

funmain(){

varlist=listOf(1,2)

list+=3//Probablyunexpected

listeq"[1,2,3]"

}

Inamutablecollection,a+=bcallsplusAssign()tomodifya.However,plusAssign()isnotavailableforread-onlycollections,soKotlinrewritesa+=bintoa=a+b.Thiscallsplus(),whichdoesn’tchangethecollection,butrathercreatesanewoneandassignstheresulttothevarlistreference.Theneteffectisthata+=bstillproducestheresultweexpectfora—atleastforsimpletypeslikeInt.

//UsingOperators/ReadOnlyAndPlus.kt

importatomictest.eq

funmain(){

varlist=listOf(1,2)

valinitial=list

list+=3

listeq"[1,2,3]"

list=list.plus(4)

listeq"[1,2,3,4]"

initialeq"[1,2]"

}

Thelastlineshowsthattheinitialcollectionremainsunchanged.Creatinganewcollectionforeveryaddedelementprobablyisn’tyourintent.Theproblemdoesn’tariseifyouusevalforlistinsteadofvarbecausecalling+=won’tcompile.Thisisonemorereasontousevalbydefault—onlyusevarwhennecessary.

compareTo()wasintroducedasastandaloneextensionfunctioninOperatorOverloading.However,yougetgreaterbenefitsifyourclassimplementstheComparableinterfaceandoverridesitscompareTo():

//UsingOperators/CompareTo.kt

packageusingoperators

importatomictest.eq

dataclassContact(

valname:String,

valmobile:String

):Comparable<Contact>{

overridefuncompareTo(

other:Contact

):Int=name.compareTo(other.name)

}

funmain(){

valalice=Contact("Alice","0123456789")

valbob=Contact("Bob","9876543210")

valcarl=Contact("Carl","5678901234")

(alice<bob)eqtrue

(alice<=bob)eqtrue

(alice>bob)eqfalse

(alice>=bob)eqfalse

valcontacts=listOf(bob,carl,alice)

contacts.sorted()eq

listOf(alice,bob,carl)

contacts.sortedDescending()eq

listOf(carl,bob,alice)

}

AnytwoComparablescanbecomparedusing<,<=,>and>=(notethat==and!=arenotincluded).Kotlindoesn’trequiretheoperatormodifierwhenoverridingcompareTo()becauseithasalreadybeendefinedasanoperatorintheComparableinterface.

ImplementingComparablealsoenablesfeatureslikesortability,andcreatingarangeofinstanceswithoutredefiningthe..operator.Youcanthenchecktoseeifavalueisinthatrange:

//UsingOperators/ComparableRange.kt

packageusingoperators

importatomictest.eq

classF(vali:Int):Comparable<F>{

overridefuncompareTo(other:F)=

i.compareTo(other.i)

}

funmain(){

valrange=F(1)..F(7)

(F(3)inrange)eqtrue

(F(9)inrange)eqfalse

}

PreferimplementingComparable.OnlydefinecompareTo()asanextensionfunctionwhenusingaclassyouhavenocontrolover.

DestructuringOperatorsAnothergroupofoperatorsyoudon’ttypicallydefineisthecomponentN()functions(component1(),component2()etc.),usedforDestructuringDeclarations.Inmain(),Kotlinquietlygeneratescallstocomponent1()andcomponent2()forthedestructuringassignment:

//UsingOperators/DestructuringDuo.kt

packageusingoperators

importatomictest.*

classDuo(valx:Int,valy:Int){

operatorfuncomponent1():Int{

trace("component1()")

returnx

}

operatorfuncomponent2():Int{

trace("component2()")

returny

}

}

funmain(){

val(a,b)=Duo(1,2)

aeq1

beq2

traceeq"component1()component2()"

}

ThesameapproachworkswithMaps,whichuseanEntrytypecontainingcomponent1()andcomponent2()memberfunctions:

//UsingOperators/DestructuringMap.kt

importatomictest.eq

funmain(){

valmap=mapOf("a"to1)

for((key,value)inmap){

keyeq"a"

valueeq1

}

//TheDestructuringassignmentbecomes:

for(entryinmap){

valkey=entry.component1()

valvalue=entry.component2()

keyeq"a"

valueeq1

}

}

YoucanusedestructuringdeclarationswithanydataclassbecausecomponentN()functionsareautomaticallygenerated:

//UsingOperators/DestructuringData.kt

packageusingoperators

importatomictest.eq

dataclassPerson(

valname:String,

valage:Int

){

//Compilergenerates:

//funcomponent1()=name

//funcomponent2()=age

}

funmain(){

valperson=Person("Alice",29)

val(name,age)=person

//TheDestructuringassignmentbecomes:

valname_=person.component1()

valage_=person.component2()

nameeq"Alice"

ageeq29

name_eq"Alice"

age_eq29

}

KotlingeneratesacomponentN()functionforeachproperty.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

PropertyDelegation

Apropertycandelegateitsaccessorlogic.

Youconnectapropertytoadelegatewiththebykeyword:

val/varpropertybydelegate

Thedelegate’sclassmustcontainagetValue()functionifthepropertyisaval(readonly)orgetValue()andsetValue()functionsifthepropertyisavar(read/write).Firstconsidertheread-onlycase:

//PropertyDelegation/BasicRead.kt

packagepropertydelegation

importatomictest.eq

importkotlin.reflect.KProperty

classReadable(vali:Int){

valvalue:StringbyBasicRead()

}

classBasicRead{

operatorfungetValue(

r:Readable,

property:KProperty<*>

)="getValue:${r.i}"

}

funmain(){

valx=Readable(11)

valy=Readable(17)

x.valueeq"getValue:11"

y.valueeq"getValue:17"

}

valueinReadableisdelegatedtoaBasicReadobject.getValue()takesaReadableparameterthatallowsittoaccesstheReadable—whenyousaybyitbindstheBasicReadtothewholeReadableobject.NoticethatgetValue()accessesiinReadable.

BecausegetValue()returnsaString,thetypeofvaluemustalsobeString.

ThesecondgetValue()parameterpropertyisofthespecialtypeKProperty,andthisprovidesreflectiveinformationaboutthedelegatedproperty.

Ifthedelegatedpropertyisavar,itmusthandlebothreadingandwriting,sothedelegateclassrequiresbothgetValue()andsetValue():

//PropertyDelegation/BasicReadWrite.kt

packagepropertydelegation

importatomictest.eq

importkotlin.reflect.KProperty

classReadWriteable(vari:Int){

varmsg=""

varvalue:StringbyBasicReadWrite()

}

classBasicReadWrite{

operatorfungetValue(

rw:ReadWriteable,

property:KProperty<*>

)="getValue:${rw.i}"

operatorfunsetValue(

rw:ReadWriteable,

property:KProperty<*>,

s:String

){

rw.i=s.toIntOrNull()?:0

rw.msg="setValueto${rw.i}"

}

}

funmain(){

valx=ReadWriteable(11)

x.valueeq"getValue:11"

x.value="99"

x.msgeq"setValueto99"

x.valueeq"getValue:99"

}

ThefirsttwosetValue()parametersarethesameasgetValue(),andthethirdisthevalueontherightsideofthe=,whichiswhatwewanttoset.BothgetValue()andsetValue()mustagreeonthetypethatisreadandwritten,whichinthiscaseisString(thetypeofvalueinReadWriteable).

NoticethatsetValue()accessesiinReadWriteable,andalsomsg.

BasicRead.ktandBasicReadWrite.ktdonotimplementaninterface.Aclasscanbeusedasadelegateifitsimplyconformstotheconventionofhavingthenecessaryfunction(s)withthenecessarysignature(s).However,youcanalsoimplementtheReadOnlyPropertyinterface,asseenhereinBasicRead2:

//PropertyDelegation/BasicRead2.kt

packagepropertydelegation

importatomictest.eq

importkotlin.properties.ReadOnlyProperty

importkotlin.reflect.KProperty

classReadable2(vali:Int){

valvalue:StringbyBasicRead2()

//SAMconversion:

valvalue2:Stringby

ReadOnlyProperty{_,_->"getValue:$i"}

}

classBasicRead2:

ReadOnlyProperty<Readable2,String>{

overrideoperatorfungetValue(

thisRef:Readable2,

property:KProperty<*>

)="getValue:${thisRef.i}"

}

funmain(){

valx=Readable2(11)

valy=Readable2(17)

x.valueeq"getValue:11"

x.value2eq"getValue:11"

y.valueeq"getValue:17"

y.value2eq"getValue:17"

}

ImplementingReadOnlyPropertycommunicatestothereaderthatBasicRead2canbeusedasadelegateandensuresapropergetValue()definition.

BecauseReadOnlyPropertyhasonlyasinglememberfunction(andithasbeendefinedasafuninterfaceinthestandardlibrary),value2isdefinedmuchmoresuccinctlyusingaSAMconversion.

BasicReadWrite.ktcanbemodifiedtoimplementReadWriteProperty,ensuringpropergetValue()andsetValue()definitions:

//PropertyDelegation/BasicReadWrite2.kt

packagepropertydelegation

importatomictest.eq

importkotlin.properties.ReadWriteProperty

importkotlin.reflect.KProperty

classReadWriteable2(vari:Int){

varmsg=""

varvalue:StringbyBasicReadWrite2()

}

classBasicReadWrite2:

ReadWriteProperty<ReadWriteable2,String>{

overrideoperatorfungetValue(

rw:ReadWriteable2,

property:KProperty<*>

)="getValue:${rw.i}"

overrideoperatorfunsetValue(

rw:ReadWriteable2,

property:KProperty<*>,

s:String

){

rw.i=s.toIntOrNull()?:0

rw.msg="setValueto${rw.i}"

}

}

funmain(){

valx=ReadWriteable2(11)

x.valueeq"getValue:11"

x.value="99"

x.msgeq"setValueto99"

x.valueeq"getValue:99"

}

Thus,adelegateclassmustcontaineitherorbothofthefollowingfunctions,whicharecalledwhenthedelegatedpropertyisaccessed:

1. Forreading:operatorfungetValue(thisRef:T,property:KProperty<*>):V

2. Forwriting:setValue(thisRef:T,property:KProperty<*>,value:V)

Ifthedelegatedpropertyisaval,onlythefirstfunctionisrequiredandReadOnlyPropertycanbeimplementedusingaSAMconversion.

Theparametersare:

thisRef:Tpointstothedelegateobject,whereTisthetypeofthatdelegate.Ifyoudon’twanttousethisRefinthefunction,youcaneffectivelydisableitbyusingAny?forT.property:KProperty<*>providesinformationaboutthepropertyitself.Themostcommonly-usedisname,whichproducesthefieldnameofthedelegatedproperty.valueisthevaluestoredbysetValue()intothedelegatedproperty.Visthetypeofthatproperty.

getValue()andsetValue()caneitherbedefinedbyconvention,orwrittenasimplementationsofReadOnlyPropertyorReadWriteProperty.

Toenableaccesstoprivateelements,nestthedelegateclass:

//PropertyDelegation/Accessibility.kt

packagepropertydelegation

importatomictest.eq

importkotlin.properties.ReadOnlyProperty

importkotlin.reflect.KProperty

classPerson(

privatevalfirst:String,

privatevallast:String

){

valnameby//SAMconversion:

ReadOnlyProperty<Person,String>{_,_->

"$first$last"

}

}

funmain(){

valalien=Person("Floopy","Noopers")

alien.nameeq"FloopyNoopers"

}

Assumingadequateaccesstotheelementsinthedelegatingclass,getValue()andsetValue()canbewrittenasextensionfunctions:

//PropertyDelegation/Add.kt

packagepropertydelegation2

importatomictest.eq

importkotlin.reflect.KProperty

classAdd(vala:Int,valb:Int){

valsumbySum()

}

classSum

operatorfunSum.getValue(

thisRef:Add,

property:KProperty<*>

)=thisRef.a+thisRef.b

funmain(){

valaddition=Add(144,12)

addition.sumeq156

}

Thiswayyoucanuseanexistingclassthatyouareunabletomodifyorinheritandstilldelegateapropertywithit.

Here,whenyousetthevalueoftheproperty,thenumberstoredistheFibonaccinumberforthatvalue,usingthefibonacci()functionfromtheRecursionatom:

//PropertyDelegation/FibonacciProperty.kt

packagepropertydelegation

importkotlin.properties.ReadWriteProperty

importkotlin.reflect.KProperty

importrecursion.fibonacci

importatomictest.eq

classFibonacci:

ReadWriteProperty<Any?,Long>{

privatevarcurrent:Long=0

overrideoperatorfungetValue(

thisRef:Any?,

property:KProperty<*>

)=current

overrideoperatorfunsetValue(

thisRef:Any?,

property:KProperty<*>,

value:Long

){

current=fibonacci(value.toInt())

}

}

funmain(){

varfibbyFibonacci()

fibeq0L

fib=22L

fibeq17711L

fib=90L

fibeq2880067194370816120L

}

fibinmain()isalocaldelegatedproperty—it’sdefinedinsideafunctionratherthanaclass.Adelegatedpropertycanalsobedefinedatfilescope.

ReadWriteProperty’sfirstgenericargumentcanbeAny?becauseweneveruseittoaccessanythinginsideFibonacci,whichwouldrequirespecifictypeinformation.Insteadwemanipulatethecurrentpropertyaswecaninanymemberfunction.

Inmostoftheexampleswe’veseensofar,thefirstparameterofgetValue()andsetValue()areofaspecifictype.Thosedelegatesweretiedtothatspecifictype.Sometimesitispossibletocreateageneral-purposedelegatebyignoringthefirsttypeasAny?.Forexample,supposewe’dliketostoreeachdelegatedStringpropertyinatextfilenamedforthatproperty:

//PropertyDelegation/FileDelegate.kt

packagepropertydelegation

importkotlin.properties.ReadWriteProperty

importkotlin.reflect.KProperty

importcheckinstructions.DataFile

classFileDelegate:

ReadWriteProperty<Any?,String>{

overridefungetValue(

thisRef:Any?,

property:KProperty<*>

):String{

valfile=

DataFile(property.name+".txt")

returnif(file.exists())

file.readText()

else""

}

overridefunsetValue(

thisRef:Any?,

property:KProperty<*>,

value:String

){

DataFile(property.name+".txt")

.writeText(value)

}

}

Thisdelegateonlyneedstointeractwiththefile,anddoesn’tneedanythingthroughthisRef.WeignorethisRefbytypingitasAny?,becauseAny?hasnointerestingoperations.Weareinterestedinproperty.name,whichisthenameofthefield.Nowwecanautomaticallycreateafileassociatedwitheachpropertyandstorethatproperty’sdatainthatfile:

//PropertyDelegation/Configuration.kt

packagepropertydelegation

importcheckinstructions.DataFile

importatomictest.eq

classConfiguration{

varuserbyFileDelegate()

varidbyFileDelegate()

varprojectbyFileDelegate()

}

funmain(){

valconfig=Configuration()

config.user="Luciano"

config.id="Ramalho47"

config.project="MyLittlePython"

DataFile("user.txt").readText()eq"Luciano"

DataFile("id.txt").readText()eq"Ramalho47"

DataFile("project.txt").readText()eq

"MyLittlePython"

}

Becauseitcanignorethesurroundingtype,FileDelegateisreusable.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

PropertyDelegationTools

Thestandardlibrarycontainsspecialpropertydelegationoperations.

MapisoneofthefewtypesintheKotlinlibrarythatispreconfiguredtobeusedasadelegatedproperty.AsingleMapcanbeusedtostoreallthepropertiesinaclass.EachpropertyidentifierbecomesaStringkeyforthemap,andtheproperty’stypeiscapturedintheassociatedvalue:

//DelegationTools/CarService.kt

packagepropertydelegation

importatomictest.eq

classDriver(

map:MutableMap<String,Any?>

){

varname:Stringbymap

varage:Intbymap

varid:Stringbymap

varavailable:Booleanbymap

varcoord:Pair<Double,Double>bymap

}

funmain(){

valinfo=mutableMapOf<String,Any?>(

"name"to"BrunoFiat",

"age"to22,

"id"to"X97C111",

"available"tofalse,

"coord"toPair(111.93,1231.12)

)

valdriver=Driver(info)

driver.availableeqfalse

driver.available=true

infoeq"{name=BrunoFiat,age=22,"+

"id=X97C111,available=true,"+

"coord=(111.93,1231.12)}"

}

NoticethattheoriginalMapinfoismodifiedwhensettingdriver.available=true.ThisworksbecausetheKotlinstandardlibrarycontainsMapextensionfunctionsgetValue()andsetValue()thatenablepropertydelegation.Thesesimplifiedversionsshowhowtheywork:

//DelegationTools/MapAccessors.kt

packagedelegationtools

importkotlin.reflect.KProperty

operatorfunMutableMap<String,Any>.getValue(

thisRef:Any?,property:KProperty<*>

):Any?{

returnthis[property.name]

}

operatorfunMutableMap<String,Any>.setValue(

thisRef:Any?,property:KProperty<*>,

value:Any

){

this[property.name]=value

}

Toseetheactuallibrarydefinitions,putthecursoronthebykeywordinIntelliJIDEAorAndroidStudioandinvoke“GotoDeclaration”.

Delegates.observable()observesmodificationsofamutableproperty.Here,wetraceoldandnewvalues:

//DelegationTools/Team.kt

packagedelegationtools

importkotlin.properties.Delegates.observable

importatomictest.eq

classTeam{

varmsg=""

varcaptain:Stringbyobservable("<0>"){

prop,old,new->

msg+="${prop.name}$oldto$new"

}

}

funmain(){

valteam=Team()

team.captain="Adam"

team.captain="Amanda"

team.msgeq"captain<0>toAdam"+

"captainAdamtoAmanda"

}

observable()takestwoarguments:

1. Theinitialvaluefortheproperty;"<0>"inthiscase.2. Afunctionwhichistheactiontoperformwhenthepropertyismodified.

Here,weusealambda.Thefunctionargumentsarethepropertybeingchanged,thecurrentvalueofthatproperty,andthevalueit’sbeingchangedto.

Delegates.vetoable()allowsyoutopreventachangetoapropertyifthenewpropertyvaluedoesn’tsatisfythegivenpredicate.Here,aName()insiststhattheteamcaptain’snamebeginwiththeletter“A”:

//DelegationTools/TeamWithTraditions.kt

packagedelegationtools

importatomictest.*

importkotlin.properties.Delegates

importkotlin.reflect.KProperty

funaName(

property:KProperty<*>,

old:String,

new:String

)=if(new.startsWith("A")){

trace("$old->$new")

true

}else{

trace("Namemuststartwith'A'")

false

}

interfaceCaptain{

varcaptain:String

}

classTeamWithTraditions:Captain{

overridevarcaptain:String

byDelegates.vetoable("Adam",::aName)

}

classTeamWithTraditions2:Captain{

overridevarcaptain:String

byDelegates.vetoable("Adam"){

_,old,new->

if(new.startsWith("A")){

trace("$old->$new")

true

}else{

trace("Namemuststartwith'A'")

false

}

}

}

funmain(){

listOf(

TeamWithTraditions(),

TeamWithTraditions2()

).forEach{

it.captain="Amanda"

it.captain="Bill"

it.captaineq"Amanda"

}

traceeq"""

Adam->Amanda

Namemuststartwith'A'

Adam->Amanda

Namemuststartwith'A'

"""

}

Delegates.vetoable()takestwoarguments:theinitialvaluefortheproperty,andanonChange()function,whichis::aNameinthisexample.onChange()takesthreearguments:property:KProperty<*>,theoldvaluecurrentlyheld

bytheproperty,andthenewvaluebeingplacedintheproperty.ThefunctionreturnsaBooleanindicatingwhetherthechangeissuccessfulorprevented.

TeamWithTraditions2definesDelegates.vetoable()usingalambdainsteadofthefunctionaName().

Theremainingtoolinproperties.DelegatesisnotNull(),whichproducesapropertythatmustbeinitializedbeforeitcanberead:

//DelegationTools/NeverNull.kt

packagedelegationtools

importatomictest.*

importkotlin.properties.Delegates

classNeverNull{

varnn:IntbyDelegates.notNull()

}

funmain(){

valnon=NeverNull()

capture{

non.nn

}eq"IllegalStateException:Property"+

"nnshouldbeinitializedbeforeget."

non.nn=11

non.nneq11

}

Tryingtoreadnon.nnbeforennhasbeenassignedavalueproducesanexception.Afternnhasbeenassigned,youcansuccessfullyreadit.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

LazyInitialization

Sofar,you’velearnedtwowaystoinitializeproperties.

1. Storetheinitialvalueatthepointofdefinition,orintheconstructor.2. Defineacustomgetterthatcomputesthepropertyforeachaccess.

Thisatomexploresathirdusecase:costlyinitializationthatyoumightnotneedrightaway,orever.Forexample:

Complexandtime-consumingcalculationsNetworkrequestsDatabaseaccess

Thiscanproducetwoproblems:

1. Longapplicationstart-uptime.2. Performingunnecessaryworkforapropertythatisneverused,orthatcan

havedelayedaccess.

ThishappensfrequentlyenoughthatKotlinincludesabuilt-insolution.Alazypropertyisinitializedwhenit’sfirstused,ratherthanwhenit’screated.Ifweneverusealazyproperty,itneverperformsthatexpensiveinitialization.

Theconceptoflazypropertiesisn’tuniquetoKotlin.Lazinesscanbeimplementedwithinotherlanguages,whetherornottheyprovidedirectsupport.Kotlinprovidesaconsistent,recognizableidiomforsuchpropertiesusingpropertydelegation.Withalazyproperty,byisfollowedbyacalltolazy():

vallazyPropertybylazy{initializer}

lazy()takesalambdacontainingtheinitializationlogic.Asusual,thelastexpressioninthelambdabecomestheresult,whichisassignedtotheproperty:

//LazyInitialization/LazySyntax.kt

packagelazyinitialization

importatomictest.*

validle:Stringbylazy{

trace("Initializing'idle'")

"I'mneverused"

}

valhelpful:Stringbylazy{

trace("Initializing'helpful'")

"I'mhelping!"

}

funmain(){

trace(helpful)

traceeq"""

Initializing'helpful'

I'mhelping!

"""

}

Theidlepropertyisn’tinitializedbecauseit’sneveraccessed.

Noticethatbothhelpfulandidlearevals.Withoutlazyinitialization,you’dbeforcedtomakethemvars,producingless-reliablecode.

WecanseealltheworkthatlazyinitializationdoesforyoubyimplementingthebehaviorforanIntpropertywithoutit:

//LazyInitialization/LazyInt.kt

packagelazyinitialization

importatomictest.*

classLazyInt(valinit:()->Int){

privatevarhelper:Int?=null

valvalue:Int

get(){

if(helper==null)

helper=init()

returnhelper!!

}

}

funmain(){

vallater=LazyInt{

trace("Initializing'later'")

5

}

trace("First'value'access:")

trace(later.value)

trace("Second'value'access:")

trace(later.value)

traceeq"""

First'value'access:

Initializing'later'

5

Second'value'access:

5

"""

}

Thevaluepropertydoesn’tstoreavalue,butinsteadhasagetterthatretrievesthevaluefromthehelperproperty.ThisissimilartothecodeKotlingeneratesforlazy.

Nowwecancomparethethreewaystoinitializeaproperty—atthepointofdefinition,usingagetter,andusinglazyinitialization:

//LazyInitialization/PropertyOptions.kt

packagelazyinitialization

importatomictest.trace

funcompute(i:Int):Int{

trace("Compute$i")

returni

}

objectProperties{

valatDefinition=compute(1)

valgetter

get()=compute(2)

vallazyInitbylazy{compute(3)}

valneverbylazy{compute(4)}

}

funmain(){

listOf(

Properties::atDefinition,

Properties::getter,

Properties::lazyInit

).forEach{

trace("${it.name}:")

trace("${it.get()}")

trace("${it.get()}")

}

traceeq"""

Compute1

atDefinition:

1

1

getter:

Compute2

2

Compute2

2

lazyInit:

Compute3

3

3

"""

}

atDefinitionisinitializedwhenyoucreateaninstanceofProperties.“Compute1”appearsbefore“atDefinition:”whichshowsthatinitializationhappensbeforeanyaccesses.getteriscomputedeverytimeyouaccessit.“Compute2”appearstwice,onceforeachaccesstotheproperty.

TheinitializationvalueforlazyInitisonlycalculatedthefirsttimeitisaccessed.Initializationneverhappensifyoudon’taccessthatproperty—noticethat“Compute4”neverappearsinthetrace.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

LateInitialization

Sometimesyouwanttoinitializepropertiesofyourclassafteritiscreated,butinaseparatememberfunctioninsteadofusinglazy.

Forexample,aframeworkorlibrarymightrequireinitializationinaspecialfunction.Ifyouextendthatlibraryclass,youcanprovideyourownimplementationofthatspecialfunction.

ConsideraBaginterfacewithasetUp()thatinitializesinstances:

//LateInitialization/Bag.kt

packagelateinitialization

interfaceBag{

funsetUp()

}

SupposewewanttoreusealibrarythatcreatesandmanipulatesBagsandguaranteesthatsetUp()iscalled.ThislibraryrequiressubclassinitializationinsetUp()insteadofinaconstructor:

//LateInitialization/Suitcase.kt

packagelateinitialization

importatomictest.eq

classSuitcase:Bag{

privatevaritems:String?=null

overridefunsetUp(){

items="socks,jacket,laptop"

}

funcheckSocks():Boolean=

items?.contains("socks")?:false

}

funmain(){

valsuitcase=Suitcase()

suitcase.setUp()

suitcase.checkSocks()eqtrue

}

SuitcaseinitializesitemsbyoverridingsetUp().However,wecan’tjustdefineitemsasaString—ifwedothat,wemustprovideanon-nullinitializerintheconstructor.UsingastubvaluesuchasanemptyStringisabadpractice

becauseyouneverknowwhetherit’sactuallybeeninitialized.nullindicatesthatit’snotinitialized.

DefiningitemsasanullableString?meanswemustcheckfornullinallmemberfunctions,asincheckSocks().However,weknowthatthelibrarywe’rereusinginitializesitemsbycallingsetUp(),sothenullchecksshouldnotbenecessary.

Thelateinitpropertymodifierfixesthisproblem—here,weinitializeitemsaftercreatinganinstanceofBetterSuitcase:

//LateInitialization/BetterSuitcase.kt

packagelateinitialization

importatomictest.eq

classBetterSuitcase:Bag{

lateinitvaritems:String

overridefunsetUp(){

items="socks,jacket,laptop"

}

funcheckSocks()="socks"initems

}

funmain(){

valsuitcase=BetterSuitcase()

suitcase.setUp()

suitcase.checkSocks()eqtrue

}

ComparethisversionofcheckSocks()withtheoneinSuitcase.kt.lateinitmeansitemsissafelydefinedasanon-nullableproperty.

lateinitcanbeusedonapropertyinsidethebodyofaclass,atop-levelproperty,orlocalvar.

Limitations:

lateinitcanonlybeusedonavarproperty,notaval.Thepropertymustbeanon-nullabletype.Thepropertycannotbeaprimitivetype.lateinitisnotallowedforabstractpropertiesinanabstractclassorinterface.lateinitisnotallowedforpropertieswithacustomget()orset().

Whathappensifyouforgettoinitializesuchaproperty?Youwon’tgetcompile-timeerrorsorwarnings,becausetheinitializationlogicmightbecomplexand

dependonotherpropertiesthatKotlincan’tmonitor:

//LateInitialization/FaultySuitcase.kt

packagelateinitialization

importatomictest.*

classFaultySuitcase:Bag{

lateinitvaritems:String

overridefunsetUp(){}

funcheckSocks()="socks"initems

}

funmain(){

valsuitcase=FaultySuitcase()

suitcase.setUp()

capture{

suitcase.checkSocks()

}eq

"UninitializedPropertyAccessException"+

":lateinitpropertyitems"+

"hasnotbeeninitialized"

}

Thisruntimeexceptionhasenoughdetailforyoutoeasilydiscoverandfixtheproblem.Trackingdownanerrorreportedbyanullpointerexceptionisusuallymuchmoredifficult.

.isInitializedwilltellyouwhetheralateinitpropertybeeninitialized.Thepropertymustbeinyourcurrentscope,andisaccessedusingthe::operator:

//LateInitialization/IsInitialized.kt

packagelateinitialization

importatomictest.*

classWithLate{

lateinitvarx:String

funstatus()="${::x.isInitialized}"

}

lateinitvary:String

funmain(){

trace("${::y.isInitialized}")

y="Ready"

trace("${::y.isInitialized}")

valwithlate=WithLate()

trace(withlate.status())

withlate.x="Set"

trace(withlate.status())

traceeq"falsetruefalsetrue"

}

Althoughyoucancreatealocallateinitvar,youcannotcall.isInitializedonitbecausereferencestolocalvarsorvalsarenotsupported.

Exercisesandsolutionscanbefoundatwww.AtomicKotlin.com.

APPENDICES

AppendixA:AtomicTest

Thisminimaltestframeworkisusedtovalidatethebookexamples.Italsohelpsintroduceandpromoteunittestingearlyinthelearningprocess.

Thisframeworkisdescribedinthefollowingatoms:

Testingintroducestheframeworkanddescribestheeqandneqfunctionsandthetraceobject.Exceptionsintroducesthecapture()function.ExceptionHandlingdescribesthecapture()functionimplementation.UnitTestingusesAtomicTesttohelpintroducetheconceptofunittesting.

//AtomicTest/AtomicTest.kt

packageatomictest

importkotlin.math.abs

importkotlin.reflect.KClass

constvalERROR_TAG="[Error]:"

privatefun<L,R>test(

actual:L,

expected:R,

checkEquals:Boolean=true,

predicate:()->Boolean

){

println(actual)

if(!predicate()){

print(ERROR_TAG)

println("$actual"+

(if(checkEquals)"!="else"==")+

"$expected")

}

}

/**

*Comparesthestringrepresentation

*ofthisobjectwiththestring`rval`.

*/

infixfunAny.eq(rval:String){

test(this,rval){

toString().trim()==rval.trimIndent()

}

}

/**

*Verifiesthisobjectisequalto`rval`.

*/

infixfun<T>T.eq(rval:T){

test(this,rval){

this==rval

}

}

/**

*Verifiesthisobjectis!=`rval`.

*/

infixfun<T>T.neq(rval:T){

test(this,rval,checkEquals=false){

this!=rval

}

}

/**

*Verifiesthata`Double`numberisequal

*to`rval`withinapositivedelta.

*/

infixfunDouble.eq(rval:Double){

test(this,rval){

abs(this-rval)<0.0000001

}

}

/**

*Holdscapturedexceptioninformation:

*/

classCapturedException(

privatevalexceptionClass:KClass<*>?,

privatevalactualMessage:String

){

privatevalfullMessage:String

get(){

valclassName=

exceptionClass?.simpleName?:""

returnclassName+actualMessage

}

infixfuneq(message:String){

fullMessageeqmessage

}

infixfuncontains(parts:List<String>){

if(parts.any{it!infullMessage}){

print(ERROR_TAG)

println("Actualmessage:$fullMessage")

println("Expectedparts:$parts")

}

}

overridefuntoString()=fullMessage

}

/**

*Capturesanexceptionandproduces

*informationaboutit.Usage:

*capture{

*//Codethatfails

*}eq"FailureException:message"

*/

funcapture(f:()->Unit):CapturedException=

try{

f()

CapturedException(null,

"$ERROR_TAGExpectedanexception")

}catch(e:Throwable){

CapturedException(e::class,

(e.message?.let{":$it"}?:""))

}

/**

*Accumulatesoutputwhencalledasin:

*trace("info")

*trace(object)

*Latercomparesaccumulatedtoexpected:

*traceeq"expectedoutput"

*/

objecttrace{

privatevaltrc=mutableListOf<String>()

operatorfuninvoke(obj:Any?){

trc+=obj.toString()

}

/**

*Comparestrccontentstoamultiline

*`String`byignoringwhitespace.

*/

infixfuneq(multiline:String){

valtrace=trc.joinToString("\n")

valexpected=multiline.trimIndent()

.replace("\n","")

test(trace,multiline){

trace.replace("\n","")==expected

}

trc.clear()

}

}

AppendixB:JavaInteroperability

ThisappendixdescribesissuesandtechniquesforinterfacingbetweenKotlinandJava.

AnessentialKotlindesigngoalistocreateaseamlessexperienceforJavaprogrammers.IfyouwanttoslowlymigratetoKotlin,youcaneasilystartbysprinklingbitsofKotlinintoyourexistingJavaproject.ThiswayyoucanwritenewKotlincodeatopyourJavabase,benefitingfromKotlinlanguagefeatureswithoutbeingforcedtorewriteJavacodewhenitdoesn’tmakesense.

NotonlyisiteasytocallJavacodefromKotlin,it’salsostraightforwardtocallKotlincodewithinaJavaprogram.

CallingJavafromKotlinTouseaJavaclassfromKotlin,importit,createaninstance,andcallafunction,justasyouwouldinJava.Here,weusejava.util.Random():

//interoperability/Random.kt

importatomictest.eq

importjava.util.Random

funmain(){

valrand=Random(47)

rand.nextInt(100)eq58

}

AswithcreatinganyinstanceinKotlin,youdon’tneedJava’snew.AclassfromaJavalibraryworkslikeanativeKotlinclass.

JavaBean-stylegettersandsettersinaJavaclassbecomepropertiesinKotlin:

//interoperability/Chameleon.java

packageinteroperability;

importjava.io.Serializable;

public

classChameleonimplementsSerializable{

privateintsize;

privateStringcolor;

publicintgetSize(){

returnsize;

}

publicvoidsetSize(intnewSize){

size=newSize;

}

publicStringgetColor(){

returncolor;

}

publicvoidsetColor(StringnewColor){

color=newColor;

}

}

WhenworkingwithJava,thepackagenamemustbeidentical(includingcase)tothedirectoryname.Javapackagenamestypicallycontainonlylowercaseletters.Toconformtothisconvention,thisappendixusesonlylowercaselettersintheinteroperabilityexamplesubdirectoryname.

TheimportedChameleonclassworkslikeaKotlinclasswithproperties:

//interoperability/UseBeanClass.kt

importinteroperability.Chameleon

importatomictest.eq

funmain(){

valchameleon=Chameleon()

chameleon.size=1

chameleon.sizeeq1

chameleon.color="green"

chameleon.coloreq"green"

chameleon.color="turquoise"

chameleon.coloreq"turquoise"

}

ExtensionfunctionsareespeciallyhelpfulwhenyouuseanexistingJavalibrarythatlacksneededmemberfunctions.Forexample,wecanaddanadjustToTemperature()operationtoChameleon:

//interoperability/ExtensionsToJavaClass.kt

packageinterop

importinteroperability.Chameleon

importatomictest.eq

funChameleon.adjustToTemperature(

isHot:Boolean

){

color=if(isHot)"grey"else"black"

}

funmain(){

valchameleon=Chameleon()

chameleon.size=2

chameleon.sizeeq2

chameleon.adjustToTemperature(isHot=true)

chameleon.coloreq"grey"

}

TheKotlinstandardlibrarycontainsmanyextensionsforclassesfromtheJavastandardlibrarysuchasListandString.

CallingKotlinfromJavaKotlinproduceslibrariesthatareusablefromJava.FortheJavaprogrammer,aKotlinlibrarylookslikeaJavalibrary.

BecauseeverythinginJavaisaclass,let’sstartwithaKotlinclasscontainingapropertyandafunction:

//interoperability/KotlinClass.kt

packageinterop

classBasic{

varproperty1=1

funvalue()=property1*10

}

IfyouimportthisclassintoJava,itlookslikeanordinaryJavaclass:

//interoperability/UsingKotlinClass.java

packageinteroperability;

importinterop.Basic;

importstaticatomictest.AtomicTestKt.eq;

publicclassUsingKotlinClass{

publicstaticvoidmain(String[]args){

Basicb=newBasic();

eq(b.getProperty1(),1);

b.setProperty1(12);

eq(b.value(),120);

}

}

property1becomesaprivatefieldcontainingJavaBean-stylegettersandsetters.Thevalue()memberfunctionbecomesaJavamethodwiththesamename.

WehavealsoimportedAtomicTest,whichrequiresadditionalceremonyinJava:wemustimportitusingthestatickeywordandgivethepackagename.eq()canonlybecalledasanordinaryfunctionbecauseJavadoesn’tsupportinfixnotation.

IfaKotlinclassisinthesamepackageasJavacode,youdon’tneedtoimportit:

//interoperability/KotlinDataClass.kt

packageinteroperability

dataclassStaff(

varname:String,

varrole:String

)

dataclassesgenerateextramemberfunctionslikeequals(),hashCode()andtoString(),allofwhichworkseamlesslywithinJava.Attheendofmain(),weverifytheimplementationsofequals()andhashCode()byplacingaDataobjectintoaHashMap,thenretrievingit:

//interoperability/UseDataClass.java

packageinteroperability;

importjava.util.HashMap;

importstaticatomictest.AtomicTestKt.eq;

publicclassUseDataClass{

publicstaticvoidmain(String[]args){

Staffe=newStaff(

"Fluffy","OfficeManager");

eq(e.getRole(),"OfficeManager");

e.setName("Uranus");

e.setRole("Assistant");

eq(e,

"Staff(name=Uranus,role=Assistant)");

//Callcopy()fromthedataclass:

Staffcf=e.copy("Cornfed","Sidekick");

eq(cf,

"Staff(name=Cornfed,role=Sidekick)");

HashMap<Staff,String>hm=

newHashMap<>();

//Employeesworkashashkeys:

hm.put(e,"Cheerful");

eq(hm.get(e),"Cheerful");

}

}

IfyouusethecommandlinetorunJavacodethatincorporatesKotlincode,youmustincludekotlin-runtime.jarasadependency,otherwiseyou’llgetruntimeexceptionscomplainingthatsomeofthelibraryutilityclassesarenotfound.IntelliJIDEAautomaticallyincludeskotlin-runtime.jar.

Kotlintop-levelfunctionsmaptostaticmethodsinaJavaclassthattakesitsnamefromtheKotlinfile:

//interoperability/TopLevelFunction.kt

packageinterop

funhi()="Hello!"

Toimport,specifytheclassnamegeneratedbyKotlin.Thisnamemustalsobeusedwhencallingthestaticmethod:

//interoperability/CallTopLevelFunction.java

packageinteroperability;

importinterop.TopLevelFunctionKt;

importstaticatomictest.AtomicTestKt.eq;

publicclassCallTopLevelFunction{

publicstaticvoidmain(String[]args){

eq(TopLevelFunctionKt.hi(),"Hello!");

}

}

Ifyoudon’twanttoqualifyhi()withthepackagename,useimportstaticaswedowithAtomicTest:

//interoperability/CallTopLevelFunction2.java

packageinteroperability;

importstaticinterop.TopLevelFunctionKt.hi;

importstaticatomictest.AtomicTestKt.eq;

publicclassCallTopLevelFunction2{

publicstaticvoidmain(String[]args){

eq(hi(),"Hello!");

}

}

Ifyoudon’tliketheclassnamegeneratedbyKotlin,youcanchangeitusingthe@JvmNameannotation:

//interoperability/ChangeName.kt

@file:JvmName("Utils")

packageinterop

funsalad()="Lettuce!"

NowinsteadofChangeNameKt,weuseUtils:

//interoperability/MakeSalad.java

packageinteroperability;

importinterop.Utils;

importstaticatomictest.AtomicTestKt.eq;

publicclassMakeSalad{

publicstaticvoidmain(String[]args){

eq(Utils.salad(),"Lettuce!");

}

}

Youcanfindfurtherdetailsinthedocumentation.

AdaptingJavatoKotlinOneofKotlin’sdesigngoalsistotakeanexistingJavatypeandadaptittoyourneeds.Thisabilityisnotrestrictedtolibrarydesigners—thesamelogiccanbe

appliedtoanyexternalcodebase.

InRecursion,wecreatedFibonacci.kttoefficientlyproduceFibonaccinumbers.ThatimplementationislimitedbythesizeoftheLongitreturns.Ifyou’dliketoreturnlargervalues,theJavastandardlibraryincludestheBigIntegerclass.AfewlinesofcodemorphsBigIntegerintosomethingthatfeelslikeanativeKotlinclass:

//interoperability/BigInt.kt

packagebiginteger

importjava.math.BigInteger

funInt.toBigInteger():BigInteger=

BigInteger.valueOf(toLong())

funString.toBigInteger():BigInteger=

BigInteger(this)

operatorfunBigInteger.plus(

other:BigInteger

):BigInteger=add(other)

ThetoBigInteger()extensionfunctionsconvertsanyIntorStringtoaBigIntegerbycallingtheBigIntegerconstructorandpassingthereceiverstringasanargument.

OverloadingtheoperatorBigInteger.plus()allowsyoutowritenumber+other.ThismakesworkingwithBigIntegerenjoyablecomparedtoJava’sclumsynumber.plus(other).

UsingBigInteger,Recursion/Fibonacci.kteasilyconvertstoproducemuchlargerresults:

//interoperability/BigFibonacci.kt

packageinterop

importatomictest.eq

importjava.math.BigInteger

importjava.math.BigInteger.ONE

importjava.math.BigInteger.ZERO

funfibonacci(n:Int):BigInteger{

tailrecfunfibonacci(

n:Int,

current:BigInteger,

next:BigInteger

):BigInteger{

if(n==0)returncurrent

returnfibonacci(

n-1,next,current+next)//[1]

}

returnfibonacci(n,ZERO,ONE)

}

funmain(){

(0..7).map{fibonacci(it)}eq

"[0,1,1,2,3,5,8,13]"

fibonacci(22)eq17711.toBigInteger()

fibonacci(150)eq

"9969216677189303386214405760200"

.toBigInteger()

}

AllLongswerereplacedwithBigInteger.Inmain(),youseebothIntandStringconvertedtoBigIntegerusingdifferenttoBigInteger()extensionproperties.Inline[1]weusetheplusoperatortofindthesumcurrent+next;thisisidenticaltotheoriginalversionusingLong.

fibonacci(150)overflowstheRecursion/Fibonacci.ktversion,butworksfineaftertheconversiontoBigInteger.

JavaCheckedExceptions&KotlinJavawaspredominantlypatternedaftertheC++language,whichallowedyoutospecifytheexceptionsthatafunctionmightthrow.TheJavadesignersdecidedtogoonestepfurtherandforceanyonecallingthatfunctiontocatcheveryspecifiedexception.Thisseemedlikeagoodideaatthetime,andthuswasborncheckedexceptions—anexperimentthat,toourknowledge,hasnotbeenrepeatedinsubsequentprogramminglanguages.

Here’showJavaforcesyoutocatchcheckedexceptionsintheprocessofopening,readingandclosingafile.Weonlyprovidethebasicstoshowthecheckedexceptions;youmustactuallywritemorecomplexcodetocorrectlysolvethisprobleminJava:

//interoperability/JavaChecked.java

packageinteroperability;

importjava.io.*;

importjava.nio.file.*;

importstaticatomictest.AtomicTestKt.eq;

publicclassJavaChecked{

//Buildpathtocurrentsourcefile,based

//ondirectorywhereGradleisinvoked:

staticPaththisFile=Paths.get(

"DataFiles","file_wubba.txt");

publicstaticvoidmain(String[]args){

BufferedReadersource=null;

try{

source=newBufferedReader(

newFileReader(thisFile.toFile()));

}catch(FileNotFoundExceptione){

//Recoverfromfile-openerror

}

try{

Stringfirst=source.readLine();

eq(first,"wubbalubbadubdub");

}catch(IOExceptione){

//Recoverfromread()error

}

try{

source.close();

}catch(IOExceptione){

//Recoverfromclose()error

}

}

}

EachoftheaboveoperationsinvolvescheckedexceptionsandmustbeplacedinsideatryblockorJavaproducescompile-timeerrorsforuncaughtexceptions.

Theonlyreasontocatchanexceptionisifyoucansomehowrecoverfromtheproblem.Ifit’snotsomethingyoucanfix,there’snopointinwritingacatchclauseforthatexception—justletitbecomeanerrorreport.Intheaboveexamples,recoveryfromtheerrorsseemsdubious,butyou’restillforcedtowritethetry-catchblocks.

Let’srewritethisexampleinKotlin:

//interoperability/KotlinChecked.kt

importatomictest.eq

importjava.io.File

funmain(){

File("DataFiles/file_wubba.txt")

.readLines()[0]eq

"wubbalubbadubdub"

}

KotlinallowsustoreducetheoperationtoasinglelineofcodebecauseitaddsextensionfunctionstotheJavaFileclass.Atthesametime,Kotlineliminatesthecheckedexceptions.Ifwewanted,wecouldsurroundintermediateoperationswithtry-catchblocks,butKotlindoesnotenforcecheckedexceptions.Thisprovideserrorreportingwithoutcompellingyoutowritetheadditionalnoisycode.

Javalibrariesoftenusecheckedexceptionsinsituationsthatareoutsidetheprogrammer’scontrolandaretypicallyunrecoverable.Inthesecases,it’sbesttocatchtheexceptionatthetoplevelandrestarttheprocess,ifpossible.Requiringallintermediatelevelstopasstheexceptiononlyaddscognitiveoverheadwhentryingtounderstandthecode.

Ifyou’rewritingKotlincodethatiscalledfromJavaandyoumustspecifyacheckedexception,Kotlinprovidesthe@ThrowsannotationtogivethisinformationtotheJavacaller:

//interoperability/AnnotateThrows.kt

packageinterop

importjava.io.IOException

@Throws(IOException::class)

funhasCheckedException(){

throwIOException()

}

Here’showhasCheckedException()iscalledfromJava:

//interoperability/CatchChecked.java

packageinteroperability;

importinterop.AnnotateThrowsKt;

importjava.io.IOException;

importstaticatomictest.AtomicTestKt.eq;

publicclassCatchChecked{

publicstaticvoidmain(String[]args){

try{

AnnotateThrowsKt.hasCheckedException();

}catch(IOExceptione){

eq(e,"java.io.IOException");

}

}

}

Ifyoudon’thandletheexception,theJavacompilerissuesanerrormessage.

AlthoughKotlinincludeslanguagesupportforexceptionhandling,ittendstoemphasizeerrorreportingandreservesexceptionhandlingforthoseraresituationswhereyoucanactuallyrecoverfromaproblem(almostexclusivelyI/Ooperations).

NullableTypes&JavaKotlinensuresthatpureKotlincodehasnonullerrors,butwhenyoucallintoJava,youhavenosuchguarantees.InthefollowingJavacode,get()sometimesreturnsnull:

//interoperability/JTool.java

packageinteroperability;

publicclassJTool{

publicstaticJToolget(Strings){

if(s==null)returnnull;

returnnewJTool();

}

publicStringmethod(){

return"Success";

}

}

TouseJToolwithinKotlin,youmustknowhowget()behaves.Youhavethreechoices,shownhereinthedefinitionsofa,bandc:

//interoperability/PlatformTypes.kt

packageinterop

importinteroperability.JTool

importatomictest.eq

objectKotlinCode{

vala:JTool?=JTool.get("")//[1]

valb:JTool=JTool.get("")//[2]

valc=JTool.get("")//[3]

}

funmain(){

with(KotlinCode){

a?.method()eq"Success"//[4]

b.method()eq"Success"

c.method()eq"Success"//[5]

::a.returnTypeeq

"interoperability.JTool?"

::b.returnTypeeq

"interoperability.JTool"

::c.returnTypeeq

"interoperability.JTool!"//[6]

}

}

[1]Specifythetypeasnullable.[2]Specifythetypeasnon-nullable.[3]Usetypeinference.

Thewith()inmain()allowsustorefertoa,bandcwithouttheKotlinCodequalification.Becausetheidentifiersareinsideanobject,wecanusememberreferencesyntaxandthereturnTypepropertytodeterminetheirtypes.

Toinitializea,bandc,wepassanon-nullStringtoget(),soa,bandcallendupwithnon-nullreferencesandeachonecansuccessfullycallmethod().

[4]Becauseaisnullable,itmustuse?.duringmemberfunctioncalls.[5]cbehaveslikeanon-nullablereferenceandcanbedereferencedwithoutanyadditionalchecks.[6]Noticethatcreturnsneitheranullabletypenoranon-nullabletype,butsomethingentirelydifferent:JTool!.

Type!isKotlin’splatformtype,andhasnonotation—youcan’twriteitintoyourcode.ItisusedwheneverKotlinmustinferatypeoutsideitsdomain.

IfatypecomesfromJava,accessingitcanproduceanullpointerexception(NPE).Here’swhathappenswhenJTool.get()returnsanullreference:

//interoperability/NPEOnPlatformType.kt

importinteroperability.JTool

importatomictest.*

funmain(){

valxn:JTool?=JTool.get(null)//[1]

xn?.method()eqnull

valyn=JTool.get(null)//[2]

yn?.method()eqnull//[3]

capture{

yn.method()//[4]

}containslistOf("NullPointerException")

capture{

valzn:JTool=JTool.get(null)//[5]

}eq"NullPointerException:"+

"JTool.get(null)mustnotbenull"

}

WhenyoucallaJavamethodlikeJTool.get()insideKotlin,itsreturnvalue(unlessannotatedasexplainedinthenextsection)isaplatformtype,whichinthiscaseisJTool!.

[1]BecausexnisofthenullabletypeJTool?,itcansuccessfullyreceiveanull.Assigningtoanullabletypeissafe,becauseKotlinforcesyoutotestfornullusing?.whencallingmethod().[2]Atthepointofdefinition,ynsuccessfullyreceivesthenullwithoutcomplaintbecauseKotlininfersittobetheplatformtypeJTool!.[3]Youcandereferenceynbyusingasafe-accesscall?.,whichinthiscasereturnsnull.[4]However,using?.isnotrequired.Youcansimplydereferenceyn.InthiscaseyougetaNullPointerExceptionwithoutanyhelpfulmessage.[5]Assigningtoanon-nullabletypecanproduceanNPE.Kotlinchecksfornullityatthepointofassignment.TheinitializationofznfailsbecausethedeclaredtypeJToolpromisesthatznisnotnullable,butitreceivesanullwhichproducesaNullPointerException,thistimewithahelpfulmessage.

Theexceptionmessagecontainsdetailedinformationabouttheexpressionthatproducedthenull:NullPointerException:JTool.get(null)mustnotbe

null.Eventhoughit’saruntimeexception,thecomprehensiveerrormessagemakestheproblemmucheasierthanfixingaregularNPE.

Aplatformtypecontainstheleastamountofinformationavailableforthattype.Inthiscase,itonlytellsyouthatthetypeisJTool.Itmightormightnotbenullable—whenusinganinferredplatformtypeyousimplydon’tknow.

Youcan’texplicitlydeclareaplatformtype(e.g.JTool!).Youcanonlyobserveaplatformtypeinerrormessages,orwhenyoudisplaytheinferredtypeasinPlatformTypes.kt,orbycheckingthetypewithintheIDE.

WhenworkingonamixedKotlinandJavaproject,youmayormaynothavecontrolovertheJavacodebase.WhenusinganexternalJavalibrary,youcan’tmodifythesourcecode,soyoumustworkwithplatformtypes.

PlatformtypesprovideseamlessJavainteroperability,andmaintaintheconsistencyoftypeinference.However,don’trelyonthem.Theproperstrategywhencallingun-annotatedJavacodeistoavoidtypeinference,andinsteadunderstandwhetherornotthecodeyouarecallingcanproducenulls.

NullabilityAnnotationsIfyoucontroltheJavacodebase,youcanaddnullabilityannotationstotheJavacodeandavoidsubtleNPEerrors.@Nullableand@NotNulltellKotlintotreataJavatypeasnullableornon-nullable,respectively.HereweaddKotlinnullabilityannotationstoJTool.java:

//interoperability/AnnotatedJTool.java

packageinteroperability;

importorg.jetbrains.annotations.NotNull;

importorg.jetbrains.annotations.Nullable;

publicclassAnnotatedJTool{

@Nullable

publicstaticJTool

getUnsafe(@NullableStrings){

if(s==null)returnnull;

returngetSafe(s);

}

@NotNull

publicstaticJTool

getSafe(@NotNullStrings){

returnnewJTool();

}

publicStringmethod(){

return"Success";

}

}

ApplyinganannotationtoaJavaparameteraffectsonlythatparameter.ApplyinganannotationinfrontofaJavamethodmodifiesthereturntype.

WhenyoucallgetUnsafe()andgetSafe()inKotlin,KotlintreatstheAnnotatedJToolmemberfunctionsasnativeKotlinnullableornon-nullable:

//interoperability/AnnotatedJava.kt

packageinterop

importinteroperability.AnnotatedJTool

importatomictest.eq

objectKotlinCode2{

vala=AnnotatedJTool.getSafe("")

//Doesn'tcompile:

//valb=AnnotatedJTool.getSafe(null)

valc=AnnotatedJTool.getUnsafe("")

vald=AnnotatedJTool.getUnsafe(null)

}

funmain(){

with(KotlinCode2){

::a.returnTypeeq

"interoperability.JTool"

::c.returnTypeeq

"interoperability.JTool?"

::d.returnTypeeq

"interoperability.JTool?"

}

}

@NotNullJToolistransformedtoKotlin’snon-nullabletypeJTool,andtheannotated@NullableJToolistransformedtoKotlin’sJTool?.Youcanseethisinthetypesshowninmain()fora,c,andd.

Youcan’tpassanullableargumentwhenanon-nullableargumentisexpected,evenifit’saJavatypeannotatedwith@NotNull,soKotlinwon’tcompileAnnotatedJTool.getSafe(null).

Differentkindsofnullabilityannotationsaresupported,usingdifferentnames:

@Nullableand@CheckForNullarespecifiedbytheJSR-305standard.@Nullableand@NonNullareusedinAndroid.@Nullableand@NotNullaresupportedbyJetBrainstools.Thereareothers.YoucanfindthefulllistintheKotlindocumentation.

KotlindetectsdefaultnullabilityannotationsforaJavapackageorclass,asspecifiedintheJSR-305standard.Ifit’s@NotNullbydefault,[email protected]’s@Nullablebydefault,you

shouldexplicitlyspecifyonly@NotNullannotations.Thedocumentationcontainsthetechnicaldetailsforchoosingthedefaultannotation.

IfyoudevelopmixedKotlinandJavaprojects,yourapplicationswillbesaferifyouusenullabilityannotationsinyourJavacode.

Collections&JavaThisbookdoesn’trequireJavaknowledge.However,whenyouwritecodeinKotlinfortheJavaVirtualMachine(JVM),it’shelpfultobefamiliarwiththeJavastandardcollectionslibrary,becauseKotlinusesittocreateitsowncollections.

TheJavacollectionslibraryisasetofclassesandinterfacesthatimplementcollectiondatastructures,suchaslists,setsandmaps.Thesedatastructuresusuallyhaveclearandsimpleinterfaces,butforspeedmayhavecomplicatedimplementations.

Newlanguagestypicallycreatetheirowncollectionslibraryfromscratch.Forexample,theScalalanguagehasitsowncollectionslibrarywhichinmanywayssurpassestheJavacollectionslibrary,butalsomakesitmorechallengingtomixScalaandJava.

Kotlin’scollectionslibraryisintentionallynotrewrittenfromscratch.Instead,itconsistsofimprovementsatoptheJavacollectionslibrary.Forexample,whenyoucreateamutableList,you’reactuallyusingJava’sArrayList:

//interoperability/HiddenArrayList.kt

importatomictest.eq

funmain(){

vallist=mutableListOf(1,2,3)

list.javaClass.nameeq

"java.util.ArrayList"

}

ForseamlessinteroperabilitywithJavacode,KotlinusestheinterfacesfromtheJavastandardlibrary,andoftenthesameimplementations.Thisproducesthreebenefits:

1. KotlincodecaneasilymixwithJavacode.NoadditionalconversionisrequiredwhenpassingKotlincollectionstoJavacode.

2. YearsofperformancetuningintheJavastandardlibraryisautomaticallyavailabletoKotlinprogrammers.

3. ThestandardlibraryincludedwithaKotlinapplicationissmall,becauseitusesJavacollectionsratherthandefiningitsown.TheKotlinstandardlibraryconsistsprimarilyofextensionfunctionsthatimprovetheJavacollections.

Kotlinalsofixesadesignproblem.InJavaallcollectioninterfacesaremutable.Forexample,java.util.Listhasmethodsadd()andremove()thatmodifytheList.Aswe’veshownthroughoutthisbook,mutabilityisthesourceofasignificantnumberofprogrammingproblems.Thus,inKotlin,thedefaultCollectiontypeisread-only:

//interoperability/ReadOnlyByDefault.kt

packageinterop

dataclassAnimal(valname:String)

interfaceZoo{

funviewAnimals():Collection<Animal>

}

funvisitZoo(zoo:Zoo){

valanimals=zoo.viewAnimals()

//Compile-timeerror:

//animals.add(Animal("GrumpyCat"))

}

Read-onlycollectionsaresaferandmorebug-freebecausetheypreventaccidentalmodification.

Javaprovidesapartialsolutionforcollectionimmutability:whenreturningacollectionyoucanplaceitinsideaspecialwrapperthatthrowsanexceptionforanyattempttomodifytheunderlyingcollection.Thisdoesn’tproducestatictypechecking,butcanstillpreventsubtlebugs.However,youmustremembertowrapthecollectiontomakeitread-only,whereasinKotlinyoumustbeexplicitwhenyouwantamutablecollection.

Kotlinhasseparateinterfacesformutableandread-onlycollections:

Collection/MutableCollectionList/MutableListSet/MutableSetMap/MutableMap

TheseduplicatetheinterfacesfromtheJavastandardlibrary:

java.util.Collection

java.util.List

java.util.Set

java.util.Map

InKotlin,asinJava,CollectionisasupertypeforbothListandSet.MutableCollectionextendsCollectionandisasupertypeofMutableListandMutableSet.Here’sthebasicstructure:

//interoperability/CollectionStructure.kt

packagecollectionstructure

interfaceCollection<E>

interfaceList<E>:Collection<E>

interfaceSet<E>:Collection<E>

interfaceMap<K,V>

interfaceMutableCollection<E>

interfaceMutableList<E>:

List<E>,MutableCollection<E>

interfaceMutableSet<E>:

Set<E>,MutableCollection<E>

interfaceMutableMap<K,V>:Map<K,V>

Forsimplicity,weshowonlythenamesandnotthefulldeclarationsfromtheKotlinstandardlibrary.

KotlinmutablecollectionsmatchtheirJavacounterparts.IfyoucompareMutableCollectionfromkotlin.collectionswithjava.util.List,you’llseethattheydeclarethesamememberfunctions(methods,inJavaterminology).Kotlin’sCollection,List,SetandMapalsoduplicateJava’sinterfaces,butwithoutanymutationmethods.

Bothkotlin.collections.Listandkotlin.collections.MutableListarevisiblefromJavaasjava.util.List.Theseinterfacesarespecial:theyexistonlyinKotlin,butatthebytecodeleveltheyarebothreplacedwithJava’sList.

AKotlinListcanbecasttoaJavaList:

//interoperability/JavaList.kt

importatomictest.eq

funmain(){

vallist=listOf(1,2,3)

(listisjava.util.List<*>)eqtrue

}

Thiscodeproducesawarning:

Thisclassshouldn’tbeusedinKotlin.Usekotlin.collections.Listorkotlin.collections.MutableListinstead.

ThisisaremindertouseKotlin’sinterfaces,notJava’s,whenprogramminginKotlin.

Keepinmindthatread-onlyisnotthesameasimmutable.Acollectioncannotbechangedusingaread-onlyreference,butitcanstillchange:

//interoperability/ReadOnlyCollections.kt

importatomictest.eq

funmain(){

valmutable=mutableListOf(1,2,3)

//Read-onlyreferencetoamutablelist:

vallist:List<Int>=mutable

mutable+=4

//listhaschanged:

listeq"[1,2,3,4]"

}

Here,theread-onlylistreferencesaMutableList,whichcanthenbechangedbymanipulatingmutable.BecauseallJavacollectionsaremutable,Javacodecanmodifyaread-onlyKotlincollection,evenifyoupassitviaaread-onlyreference.

Kotlincollectionsdon’tproducefullsafety,butprovideagoodcompromisebetweenhavingabetterlibraryandmaintainingcompatibilitywithJava.

JavaPrimitiveTypesInKotlin,youcallaconstructortocreateanobject,butinJavayoumustusenewtoproduceanobject.newplacestheresultingobjectontheheap.Suchtypesarecalledreferencetypes.

Creatingobjectsontheheapcanbeinefficientforbasictypessuchasnumbers.Forthesetypes,JavafallsbackontheapproachtakenbyCandC++:Insteadofcreatingthevariableusingnew,anon-reference“automatic”variableiscreatedthatholdsthevaluedirectly.Automaticvariablesareplacedonthestack,makingthemmuchmoreefficient.SuchtypesgetspecialtreatmentbytheJVMandarecalledprimitivetypes.

Thereareafixednumberofprimitivetypes:boolean,int,long,char,byte,short,floatanddouble.Primitivetypesalwayscontainanon-nullvalue,andtheycan’tbeusedasgenericarguments.Ifyouneedtostorenullorusesuchtypesasgenericarguments,youcanusethecorrespondingreferencetypedefinedintheJavastandardlibrary,suchasjava.lang.Booleanorjava.lang.Integer.Thesetypesareoftencalledwrappertypesorboxedtypestoemphasizethattheyonlywraptheprimitivevalueandstoreitontheheap.

//interoperability/JavaWrapper.java

packageinteroperability;

importjava.util.*;

publicclassJavaWrapper{

publicstaticvoidmain(String[]args){

//Primitivetype

inti=10;

//Wrappertypes

IntegeriOrNull=null;

List<Integer>list=newArrayList<>();

}

}

Javadistinguishesbetweenreferencetypesandprimitivetypes,butKotlindoesnot.YouusethesametypeIntbothfordefininganintegervar/valorusingitasagenericargument.AttheJVMlevel,Kotlinemploysthesameprimitivetypesupport.Whenpossible,KotlinreplacesIntwithaprimitiveintinthebytecode.AnullableInt?orIntusedasagenericargumentcanonlyberepresentedusingthewrappertype:

//interoperability/KotlinWrapper.kt

packageinterop

funmain(){

//Generatesaprimitiveint:

vali=10

//Generateswrappertypes:

valiOrNull:Int?=null

vallist:List<Int>=listOf(1,2,3)

}

Younormallydon’tneedtothinkmuchaboutwhetherprimitivesorwrappersaregeneratedbytheKotlincompiler,butit’susefultoknowhowit’simplementedontheJVM.

-

ThedocumentationexplainsmoreaboutthenuancesofKotlin/Javainteroperability.