diff --git a/Solutions/EX10 b/Solutions/EX10 new file mode 100644 index 0000000..4687e58 --- /dev/null +++ b/Solutions/EX10 @@ -0,0 +1,293 @@ +EX10Stargate lahendusjuhend +=========================================================== +(ehk kuidas ehitada planeete ja siis mõttetumad ära visata) +----------------------------------------------------------- +EX10Stargate'i lahendamisel tuleb kindlalt kasuks +PR10-e läbitegu, kuid siin las olla vaid EX10Stargate'i +ülesande juhend. Kui mõni samm tundub liiga lihtne, siis +võib selle julgelt ise ära teha. Siit aga leiab ühe suuremat +sorti rõõmusõnumi ehk evangeeliumi: päris kindlasti leiavad +juhendist kõik väga põhjaliku ülevaate lahendusest ja algajad palju +abi, sest küsija suu pihta ei lööda: kui paned Githubis +issue külge, täiendatakse loodetavasti juhendit vastavalt. +Hõissa ja laulupidu! + +Mooduli ja pakkide ülesseadmine ehk korista enne segaduse teket +--------------------------------------------------------------- +Selleks, et ülesannet alustada, klõpsame parema hiireklahviga +mõne mooduli peale, tekkinud valikmenüüs valime New->Module, +vaatame tekkinud aknas, et me looks Java mooduli, mis kasutab +mõnda juba paigaldatud Java JDK-d. Vajutame Next ja muudame +kausta nimeks EX10Stargate ja vajutame Finish. IntelliJ koostab +ise projekti uue mooduli ja paneb sinna allikkausta src meile +kasutamiseks. Milline rõõm! + +Edasi tuleb luua pakk ee.ttu.iti0202.stargate klõpsates +src kausta peal parema hiireklahviga ja valides valikmenüüs +New->Package. Sisesta ee.ttu.iti0202 paki nimeks ja loo +samamoodi pakile kaks alampakki: planet ja space, seekord +ee.ttu.iti0202.stargate paki peal klõpsates. Siia lähevad +meie ülesande klassid. Nüüd on meil kõik olemas, et hakata +klasse lisama. Juhhei! + +Klass Planet ehk teeme kodus ise oma planeedi, tuleb odavam +----------------------------------------------------------- +Klass Planet hoiab endas järgmist infot: planeedi nime (sõne), +elanikearvu (long ehk ülipikk täisarv), kas planeedil on +tähevärav (tõeväärtus), kas too tähevärav on kasutatav +või lihtsalt vanaraud (ehk kas DHD on olemas - veel üks +tõeväärtus) ja mis tiimid teda külastanud on (järjend sõnedest). +Tegu on ülestuunitud järjendiga: selle asemel, et hoida ühe +planeedi andmeid lihtsas listis, paneme nad meie loodud klassi, +millel on rohkem võimalusi, šarmi ja isikupära. On mugavam +käidelda andmeid, kui IntelliJ pakub muutujanimesid puuvilja- +vaagnal, kui et peab meeles hoidma, mis indeksiga on mingi +kirje järjendis. Kuis metsa hõikad, nõnna vastu kajab! + +Selle jaoks paremklõpsame pakil planet ja valime New->Java class. +Nimeks paneme Planet ja juba saame Planet klassi sisse +kirjutada viis muutujat: + private String name; + private long inhabitants; + private boolean stargateAvailable; + private boolean dhdAvailable; + private List teamsVisited; +Nüüd toimime järgnevalt: paremklõpsame koodiredaktori aknal +kuskil klassi sees ja valime Generate või olles kursoriga klassis +vajutame Alt+Insert. Uues menüüs valime Getter and Setter ja tekkinud +aknas valime kõik muutujad vajutades Shift ja nooleklahve. +Klikkame OK ja tekivad ülesandes vajaminevad getter ja setter +meetodid. Kuid see pole veel kõik! Samamoodi klõpsates ja +valides Generate ja Constructor, saame valida tekkinud aknas +kõik muutujad, vajutada OK ja äkitsi on meil konstruktor, +mis klassis samuti vajalik oli. Nüüd on vaja vaid kirjutada nendele funktsioonide +alla string, valida IntelliJ pakutud public toString() +funktsioon, panna funktsioon planeedi nime tagastama ja +klass on tehtud. Halleluuja! Kristus sündinud meil! + +Klass PlanetBuilder, sest allhanked on kasvava majanduse alus +-------------------------------------------------------------- +Kui ütled PlanetBuilder, siis mõtled StringBuilder aga +planeetidega! Hetkekski ei tohiks olla kõhklust, mida PlanetBuilder +teeb -- ta ehitab planeete! Aga milleks meil siis Planet klassi +konstruktor? Teeb ta ju sama hästi planeedi valmis? Vastus on, +et kuigi IntelliJ ütleb mõnikord ette, mis parameetrid tuleb +klassis algväärtustada, siis ausalt öeldes on see mõnikord tüütu +ja kaka-pähh. Ja mida teha siis, kui tahad andmeid lisada omas +tempos, siis kui *sulle* sobib? Niisiis loome klassi, mis aitab +meil sutsu kergemalt hingata ja selgesti nimetatud funktsioonidega +lisada andmeid ja mis tagastab meile planeedi objekti siis, kui +*meie* seda soovime. Emantsipatsioon võidule! + +Paremklõpsatus koodil (aga mitte näiteks String märksõnal, proovi parem +mõnd tühja rida selleks) ja valik Refactor->Replace Constructor with +Builder toob meie ette akna, mis täidab kõik me unistused. +Tekkinud aknas võtame veerus Setter Name kõikidelt muutujanimedelt +set eest ära ja teeme algustähe väiketäheks, sest ülesanne nõuab nii +ja vajutame Refactor. Tekib uus klass PlanetBuilder, mille objekti +saame luua, anda funktsioonidega sisse erinevaid väärtusi ja luua +Planet objekt funktsiooniga createPlanet(). Nõnnasi saame mängida +jumalat ja luua planeete oma elutoadiivanilt. + +Klass Space ehk ülekosmoseline Rajaleidja sõnarohkete funktsioonidega +------------------------------------------------------------------------------ +Kosmos on suur kant ja tal on palju planeete, algaja võibolla ei teagi, millisele minna. +Selleks on vaja neid sortida ja ülesande kirjelduses on kriteeriumid selgelt kirjas. +Järgnevalt on loetelu neist kõigist, võime nendest silmad üle libistada, sest võtame nad hiljem +niikuinii üksipulgi lahti. + +Selleks, et orienteeruda suures komsoses on meil vaja 7 +funktsiooni: üht, mis tagastab järjendi klassile teada-tuntud planeetidest, +teist, mis saab CSV (Comma separated values) vormis failst kätte plnaaetide andmed, +kolmandat, mis tagastab hulga planeetidest, kus pole elanikke, neljandat,mis annab hulga +planeete, mida saab külastada, aga pole veel külastatud, viiendat, mis tagastab keskmise +täheväravaga planeedi elanike arvu, kuuendat, mis tagastab tiimid, kes on külastanud vähe- +asustatuid planeete, ja seitsmendat, mis tagastab planeetide tüübi koos nende sagedustega. +Kuna tööd on parajalt, siis hakkame aga pihta. Tahad latva ronida, hakka tüvest peale! + +List getPlanets() ehk kui peaksid tahtma kõiki planeete uurida ja külastada +----------------------------------------------------------------------------------- + +On ilmselge, et planeetide tagastamiseks järjendina võiks luua instantsimuutuja, mis +kätkeb endas planeetide järjendit. Niisiis loome ja algväärtustame selle tühjaks järjendiks, +et ei tekiks määramata väärtuse erindit: + private List planets = new ArrayList<>(); +Nüüd pole miskit, kui seada üles getter meetod nii, nagu seda teinud juba oleme. Kes tasa +sõuab, see kaugele jõuab. + +List csvDataToPlanets(String filePath) ehk teiste töö ärakasutamine autorile viitamata +---------------------------------------------------------------------------------------------- +Selleks, et saaksime andmeid töödelda ja esitada, peame tundmatu autori CSV-formaadis andmed vormima ümber +Planet objektideks, esitama neid enda tehtu pähe, salvestama nad ja paluma jumalat, et nood andmed on +usaldusväärsed ja vigadeta. Niisiis peame tegema vähe failist lugemist ja planeediloomet. Lisaks hakkame +esimest korda kogu ülesande jooksul kasutama Stream API-t, niisiis en guarde, noor koodija! + +Kõigepealt aga loeme failist kõik andmed ja paneme need muutujasse: selleks kasutame +Files.readAllLines(Path path) funktsiooni, mis mugavalt tagastab järjendi faili ridadest, millele path +parameeter viitab. Selleks aga, et parameeter filePath järsku Pathiks muutuks, kasutame järgmist koodirida, +mille leidsime Java dokumentatsioonist: + Path path = Paths.get(filePath); +Edasi valime ühe muutujanime, millesse salvestame kõik andmeread, näiteks lines. Nota bene! Faili näidisest järeldame, et esimene +rida sisaldab alati veeru päised, ehk siis metaandmeid, mis meile on silmaga kontrollimisel küll tulusad, +aga mida töödelda on asjatu. Niisiis jätame välja tolle esimese rea: + List lines = Files.readAllLines(path).sublist(1, lines.size()); +IntelliJ Java kompilaator aga pistab röökima selle peale, sest, nagu kõik head programmid, ta usaldab vähe kasutaja hooleks, +veel vähem usaldab kasutajat. Sellel real võib tekkida sisend-väljund erind (InputOutputError), mis võib +tulla sellest, et pole faili või faili ei saa avada (pole õigusi) või fail on kuskil mujal või universum +tahab sulle öelda, et sa võiksid lahendada teised meetodid kõigepealt ära ja minna selle konkreetse reaga +Ago juurde konsulli. Kuna aga Java tahab kindlalt koodiga edasi minna, olgu mis tahes, siis, olles kursoriga +tol real, vajuta Alt-Enter ja vali Surround with try-catch. IntelliJ genereerib koodi, mis Java kompilatori +maha rahustab, ja kõik on jälle korras. Kas pole mitte tore? + +Nüüd on aeg kasutada Java vooge (Stream API-t). Teeme ridade järjendist voo, paneme voole vahemeetodi map(), kus +lisame hiljem andmed omatehtud andmestruktuuridesse ja kogume voo lõpus lisamisel tagastatu järjendisse, salvestades +kogutu intsantsimuutujasse, mille tegime eelmist funktsiooni tehes: + planets = lines.stream() + .map( + // Meetod, mis töötleb ühe rea andmeid, teeb neist PlanetBuilderiga ühe Planeti + ) + .collect(Collectors.toList()); +Meenutame, et .stream() tegi andmestruktuurist tema elementide voo, .map() töötleb igat voo elementi vastavalt +argumendiks antud funktsioonile ja .collect() kasutab argumendiks antud meetodit, mis kogub elemendid kokku uueks +andmestruktuuriks. + +Edasi peame kirjutama igat rida töötleva koodi. Kasutame selleks lambdat ja PlanetBuilderit. Alustame lambdaga: + + line -> { // töötlev kood} + +Seal sees peame rea jaotama sõnemassiiviks split() meetodiga, kasutame poolitajana koma, käivitame +uue PlanetBuilderi klassi, paneme nimeks nullinda elemendi sõnemassiivist (sest esimene +veerg hoiab endas planeedinime), elanike arvuks esimese, tähevärava olemasoluks teise, ja +DHD olemasoluks kolmanda. Neljanda väljaga, milleks on külastanud tiimide nimed, tuleb +rohkem tööd teha, seal peab vaatama, et ta oleks rohkem kui 2 tähemärki pikk (ehk siis +ei ole lihtsalt tühjad nurksulud), jaotama ta sõnemassiiviks split() funktsiooniga, kuid +seejuures ei tohi kaasa võtta esimest tähemärki ja viimast, sest nad on nurksulud ja +järjendi tähiseks, mitte ühegi planeedi nime osa. Viimaks, kui oleme lisanud kõik väljad, +loome planeedi ja tagastame selle lambdas. Seega oleks kogu töötlev kood selline: + line -> { + String[] lineData = line.split(","); + PlanetBuilder planetBuilder = new PlanetBuilder(); + planetBuilder.name(lineData[0]); + planetBuilder.inhabitants(Long.parseLong(lineData[1])); + planetBuilder.stargateAvailable(Boolean.parseBoolean(lineData[2])); + planetBuilder.dhdAvailable(Boolean.parseBoolean(lineData[3])); + + String teamsVisited = lineData[4]; + if (teamsVisited.length() > 2) { + planetBuilder.teamsVisited(Arrays.asList(teamsVisited.substring(1, teamsVisited.length() - 1) + .split("; "))); + } else { + planetBuilder.teamsVisited(new ArrayList<>()); + } + + Planet planet = planetBuilder.createPlanet(); + planets.add(planet); + return planet; + } + +Viimaks tagastame csvDataToPlanets() funktsioonis planeedid, mida kogusime: + +return planets; + +getDeadPlanets(List planets) ehk potentsiaalsed klassiekskursiooniideed +------------------------------------------------------------------------------- + +Siin hakkame kasutama Stream API-d. Kõigepealt loome planeetidest voo: + + planets.stream() + +Filtrime välja need planeedid, kus pole elanikke, aga kuhu saab reisida: + + planets.stream() + .filter(planet -> planet.getTeamsVisited().size() == 0 + && planet.isDhdAvailable() && planet.isStargateAvailable()) + +Ja kogume nad kokku hulgaks ja tagastame ta: + + return planets.stream() + .filter(planet -> planet.getTeamsVisited().size() == 0 + && planet.isDhdAvailable() && planet.isStargateAvailable()) + .collect(Collectors.toSet()); + + +getAvgInhabitantsOnPlanetsWithStargate(List planets) ehk statistiku maiuspala +------------------------------------------------------------------------------------- + +Siin me peame samamoodi sõela peale saama planeedid, kuhu on võimalik reisida, ja võtma +nende elanike arvu ja tagastama kõigi planeetide keskmise elanike arvu. Võtmiseks sobib +mapToLong() funktsioon, millel on lambda-funktsioon sees, mis tagastab planeedi elanike +arvu, ja keskmise leidmiseks sobib .average() funktsioon. Nii tuleb funktsiooni sisuks: + +return planets.stream().filter(planet -> planet.isStargateAvailable()) + .mapToLong(planet -> planet.getInhabitants()).average(); + + +getTeamsWhoHaveVisitedSmallNotDeadPlanets(List planets) ehk keda tunnustada äärealase tegevuse eest +----------------------------------------------------------------------------------------------------------- + +Siin alustame samuti sõelumisega: tahame sõela peale saada planeedid, mille elanike arv +on väiksem kui 2500, aga suurem kui 0. Peale seda peame tiimide järjendite voo muutma +tiimide vooks. Niisiis näeb meie koodirida välja nüüd selline: + + planets.stream().filter(n -> n.getInhabitants() < inhabitantLimit && n.getInhabitants() > 0) + .flatMap(n -> n.getTeamsVisited().stream()) + +Siia lisandub see nõue, et me peame kordustest lahti saama, selleks sobib .distinct(): + + planets.stream().filter(n -> n.getInhabitants() < inhabitantLimit && n.getInhabitants() > 0) + .flatMap(n -> n.getTeamsVisited().stream()) + .distinct() + +Ülesandes on vaja too tiimide voog ära sortida, niisiis peame kasutama .sorted() funktsiooni. +Samas me ei saa puhtalt terve tiimi nime põhjal sortimist teha, peame kasutama numbrilist osa. +Selleks peame tegema võrdleja, ehk siis meetodi, mis tagastab positiivne täisarvu, kui +esimene objekt on teisest suurem, nulli, kui objektid on võrdsed, või negatiivse täisarvu, +kui teine on esimesest suurem. Lisandub kood: + + .sorted((o1, o2) -> { + int first = Integer.parseInt(o1.substring(3, o1.length())); + int second = Integer.parseInt(o2.substring(3, o2.length())); + if (first > second) { + return 1; + } else if (first < second) { + return -1; + } else { + return 0; + } + }) + +Nüüd kogume tulemused kokku ja tagastame need: + + return planets.stream() + ... + .collect(Collectors.toList()); + + +getCodeNameClassifierFrequency(List planets) ehk vaatame planeetide seisu +--------------------------------------------------------------------------------- + +Siin peame planeetide voost sõeluma välja need, mis ei vasta ülesandes kirjeldatud mustrile. +Üks viis seda teha on poolitada planeedi nimi tolle mustri järgi ja vaadata, kas tekkinud +sõnemassiiv on võrdne tühja sõnemassiiviga. Viimaseks sobib staatiline meetod Arrays.equals(), +esimeseks on tarvis sutsu regulaaravaldisi tunda: kui meil on esimene täht P, millele +järgneb kaks numbrit või tähte, millele järgneb sidekriips ja numbririda, siis sel puhul +tuleb järgnev kood kasuks: + + .filter(n -> Arrays.equals( + n.getName().split("P\\p{Alnum}\\p{Alnum}-\\p{Digit}*"), new String[]{})) + +Sealt edasi peame ta koguma Mapiks, niisiis peame kasutama üht erilist kollektorit: +Collector.groupingBy(). Meie juhul anname talle kaks argumenti: esiteks funktsiooni, +mis tagastab võtme, millega ta Mapis toimtama hakkab, ja funktsiooni, mis täpsustab, +mida väärtusega teha. Esimeseks sobib lambda, mis tagastab planeedi identifikaatori, +teiseks sobib Collector.summingLong() funktsioon, mille sees on funktsioon, mis tagastab ühe. + +.collect(Collectors.groupingBy(planet -> planet.getName().substring(0, 3), + Collectors.summingLong(e -> 1))); + + + Sellega on ülesanne lahendatud ja saab minna konsultatsiooni kaitsma! + + + +