Post on 14-Mar-2023
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
MindViewInc@gmail.com
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.
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)
}
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.
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.
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).
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:
this@Seed.nameeq"Seed"
//MustexplicitlyaccessFruit:
this@Fruit.nameeq"Fruit"
//Cannotaccessafurther-innerclass:
//this@DNA.name
}
innerclassDNA{//Implicitlabel@DNA
funchangeColor(color:String){
//changeColor(color)//Recursive
this@Seed.changeColor(color)
this@Fruit.changeColor(color)
}
funplant(){
//Callouter-classfunctions
//Withoutqualification:
germinate()
absorbWater(10)
}
//Extensionfunction:
funInt.grow(){//Implicitlabel@grow
//DefaultistheInt.grow()receiver:
this.nameeq"Int"
//Redundantqualification:
this@grow.nameeq"Int"
//Youcanstillaccesseverything:
this@DNA.nameeq"DNA"
this@Seed.nameeq"Seed"
this@Fruit.nameeq"Fruit"
}
//Extensionfunctionsonouterclasses:
funSeed.plant(){}
funFruit.plant(){}
funwhichThis(){
//Defaultstothecurrentclass:
this.nameeq"DNA"
//Redundantqualification:
this@DNA.nameeq"DNA"
//Theothersmustbeexplicit:
this@Seed.nameeq"Seed"
this@Fruit.nameeq"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.
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,youshouldexplicitlyspecifyonly@Nullableannotations.Ifit’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.