Programmering

Iterere over samlinger i Java

Når som helst du har en samling ting, trenger du en mekanisme for å systematisk gå gjennom elementene i samlingen. Som et dagligdags eksempel, kan du vurdere fjernsynets fjernkontroll, som lar oss itere gjennom forskjellige TV-kanaler. På samme måte trenger vi i programmeringsverdenen en mekanisme for systematisk å itere gjennom en samling programvareobjekter. Java inkluderer ulike mekanismer for iterasjon, inkludert indeks (for iterering over en matrise), markøren (for å gjenta resultatene av et databasespørsmål), oppregning (i tidlige versjoner av Java), og iterator (i nyere versjoner av Java).

Iterator-mønsteret

An iterator er en mekanisme som gjør det mulig å få tilgang til alle elementene i en samling sekvensielt, med noen operasjoner som utføres på hvert element. I hovedsak gir en iterator et middel til å "løkke" over en innkapslet samling av objekter. Eksempler på bruk av iteratorer inkluderer

  • Gå til hver fil i en katalog (aka mappen) og viser navnet.
  • Besøk hver node i en graf og avgjør om den kan nås fra en gitt node.
  • Besøk hver kunde i kø (for eksempel å simulere en linje i en bank) og finn ut hvor lenge han eller hun har ventet.
  • Besøk hver node i en kompilators abstrakte syntaks-tre (som er produsert av parseren) og utfør semantisk kontroll eller kodegenerering. (Du kan også bruke besøksmønsteret i denne sammenhengen.)

Enkelte prinsipper gjelder for bruk av iteratorer: Generelt sett bør du kunne ha flere gjennomganger samtidig; det vil si at en iterator skal tillate begrepet nestet looping. En iterator bør også være ikke-ødeleggende i den forstand at iterasjonshandlingen i seg selv ikke skal endre samlingen. Selvfølgelig kan operasjonen som utføres på elementene i en samling muligens endre noen av elementene. Det kan også være mulig for en iterator å støtte fjerning av et element fra en samling eller innsetting av et nytt element på et bestemt punkt i samlingen, men slike endringer bør være eksplisitte i programmet og ikke et biprodukt av iterasjonen. I noen tilfeller må du også ha iteratorer med forskjellige traversale metoder; for eksempel forhåndsbestilling og etterbestilling av et tre, eller dybde-første og bredde-første traversering av en graf.

Iterere komplekse datastrukturer

Jeg lærte først å programmere i en tidlig versjon av FORTRAN, der den eneste datastruktureringsfunksjonen var en matrise. Jeg lærte raskt hvordan jeg kunne iterere over en matrise ved hjelp av en indeks og en DO-loop. Derfra var det bare et kort mentalt sprang til ideen om å bruke en felles indeks i flere matriser for å simulere en rekke poster. De fleste programmeringsspråk har funksjoner som ligner på matriser, og de støtter direkte looping over matriser. Men moderne programmeringsspråk støtter også mer komplekse datastrukturer som lister, sett, kart og trær, der mulighetene blir gjort tilgjengelige via offentlige metoder, men de interne detaljene er skjult i private deler av klassen. Programmører må være i stand til å krysse elementene i disse datastrukturene uten å avsløre deres interne struktur, som er formålet med iteratorer.

Iterators and the Gang of Four designmønstre

I følge Gang of Four (se nedenfor), har Iterator designmønster er et atferdsmønster, hvis nøkkelide er "å ta ansvaret for tilgang og kryssing ut av listen [red. tenk samling] objekt og sette det inn i et iteratorobjekt. "Denne artikkelen handler ikke så mye om Iterator-mønsteret som om hvordan iteratorer brukes i praksis. For å dekke mønsteret fullt ut, ville det være nødvendig å diskutere hvordan en iterator ville bli designet, deltakere ( objekter og klasser) i design, mulige alternative design og kompromisser med forskjellige designalternativer. Jeg vil heller fokusere på hvordan iteratorer brukes i praksis, men jeg vil peke på noen få ressurser for å undersøke Iterator-mønsteret og designmønstrene som regel:

  • Designmønstre: Elementer av gjenbrukbar objektorientert programvare (Addison-Wesley Professional, 1994) skrevet av Erich Gamma, Richard Helm, Ralph Johnson og John Vlissides (også kjent som Gang of Four eller ganske enkelt GoF) er den definitive ressursen for å lære om designmønstre. Selv om boken først ble utgitt i 1994, er den fortsatt en klassiker, noe det fremgår av at det har vært mer enn 40 utskrifter.
  • Bob Tarr, en foreleser ved University of Maryland Baltimore County, har et utmerket sett med lysbilder for sitt kurs om designmønstre, inkludert hans introduksjon til Iterator-mønsteret.
  • David Gearys JavaWorld-serie Java Design Patterns introduserer mange av Gang of Four designmønstre, inkludert Singleton, Observer og Composite mønstre. Også på JavaWorld inneholder Jeff Friesens nyere tredelte oversikt over designmønstre en guide til GoF-mønstrene.

Aktive iteratorer vs passive iteratorer

Det er to generelle tilnærminger for å implementere en iterator avhengig av hvem som kontrollerer iterasjonen. For en aktiv iterator (også kjent som eksplisitt iterator eller ekstern iterator), klienten styrer iterasjonen i den forstand at klienten oppretter iteratoren, forteller den når den skal gå videre til neste element, tester for å se om hvert element har blitt besøkt, og så videre. Denne tilnærmingen er vanlig i språk som C ++, og det er tilnærmingen som får mest oppmerksomhet i GoF-boka. Selv om iteratorer i Java har tatt forskjellige former, var bruk av en aktiv iterator egentlig det eneste levedyktige alternativet før Java 8.

For en passiv iterator (også kjent som en implisitt iterator, intern iterator, eller tilbakeringing iterator), kontrollerer iteratoren selv iterasjonen. Klienten sier i hovedsak til iteratoren: "Utfør denne operasjonen på elementene i samlingen." Denne tilnærmingen er vanlig i språk som LISP som gir anonyme funksjoner eller nedleggelser. Med utgivelsen av Java 8 er denne tilnærmingen til iterasjon nå et rimelig alternativ for Java-programmerere.

Navngivningsplaner for Java 8

Selv om det ikke er like ille som Windows (NT, 2000, XP, VISTA, 7, 8, ...), inneholder Java versjonshistorikk flere navngivningsplaner. For å starte, skal vi referere til Java-standardutgaven som "JDK", "J2SE" eller "Java SE"? Java's versjonsnummer startet ganske greit - 1.0, 1.1 osv. - men alt endret seg med versjon 1.5, som ble merket Java (eller JDK) 5. Når jeg refererer til tidlige versjoner av Java, bruker jeg setninger som "Java 1.0" eller "Java 1.1, "men etter den femte versjonen av Java bruker jeg setninger som" Java 5 "eller" Java 8. "

For å illustrere de ulike tilnærmingene til iterasjon i Java, trenger jeg et eksempel på en samling og noe som må gjøres med elementene. For den første delen av denne artikkelen vil jeg bruke en samling strenger som representerer navn på ting. For hvert navn i samlingen vil jeg bare skrive ut verdien til standard utdata. Disse grunnleggende ideene utvides lett til samlinger av mer kompliserte objekter (for eksempel ansatte), og der behandlingen for hvert objekt er litt mer involvert (som å gi hver høyt ansatt en 4,5 prosent økning).

Andre former for iterasjon i Java 8

Jeg fokuserer på iterering over samlinger, men det er andre, mer spesialiserte former for iterasjon i Java. For eksempel kan du bruke en JDBC ResultatSett å gjenta over radene som er returnert fra et SELECT-spørsmål til en relasjonsdatabase, eller bruke en Skanner å gjenta over en inngangskilde.

Iterasjon med oppsummeringsklassen

I Java 1.0 og 1.1 var de to primære samlingsklassene Vector og Hashtable, og Iterator-designmønsteret ble implementert i en klasse som heter Oppregning. I ettertid var dette et dårlig navn for klassen. Ikke forvirre klassen Oppregning med begrepet enum typer, som ikke dukket opp før Java 5. I dag begge Vector og Hashtable er generiske klasser, men den gang var ikke generikk en del av Java-språket. Koden for å behandle en vektor av strenger ved hjelp av Oppregning ser ut som Listing 1.

Oppføring 1. Bruk oppregning til å gjenta over en vektor av strenger

 Vector navn = ny Vector (); // ... legg til noen navn i samlingen Enumeration e = names.elements (); mens (e.hasMoreElements ()) {String name = (String) e.nextElement (); System.out.println (navn); } 

Iterasjon med Iterator-klassen

Java 1.2 introduserte samlingsklassene som vi alle kjenner og elsker, og Iterator-designmønsteret ble implementert i en klasse med passende navn Iterator. Fordi vi ennå ikke hadde generikk i Java 1.2, kastet et objekt returnert fra en Iterator var fortsatt nødvendig. For Java-versjoner 1.2 til 1.4 kan gjentagelse over en liste over strenger ligne Listing 2.

Oppføring 2. Bruk en Iterator til å gjenta over en liste over strenger

 Listenavn = ny LinkedList (); // ... legg til noen navn i samlingen Iterator i = names.iterator (); mens (i.hasNext ()) {String name = (String) i.next (); System.out.println (navn); } 

Iterasjon med generiske legemidler og den forbedrede for-loop

Java 5 ga oss generiske grensesnitt Iterabel, og den forbedrede for-loop. Den forbedrede for-loop er et av mine favoritt små tillegg til Java. Opprettelsen av iteratoren og kaller til sin hasNext () og neste () metoder kommer ikke eksplisitt til uttrykk i koden, men de foregår fortsatt bak kulissene. Så selv om koden er mer kompakt, bruker vi fortsatt en aktiv iterator. Ved å bruke Java 5 vil eksemplet vårt se ut som det du ser i Listing 3.

Oppføring 3. Bruke generiske og forbedrede for-loop for å gjenta over en liste over strenger

 Listenavn = ny LinkedList (); // ... legg til noen navn i samlingen for (String name: names) System.out.println (name); 

Java 7 ga oss diamantoperatøren, noe som reduserer den generelle generøsiteten. Borte var dagene med å måtte gjenta typen som ble brukt til å starte den generiske klassen etter å ha påkalt ny operatør! I Java 7 kan vi forenkle den første linjen i liste 3 ovenfor til følgende:

 Listenavn = ny LinkedList (); 

En mild rant mot generiske stoffer

Utformingen av et programmeringsspråk innebærer avveininger mellom fordelene med språkfunksjoner versus kompleksiteten de pålegger syntaksen og semantikken til språket. For generiske stoffer er jeg ikke overbevist om at fordelene oppveier kompleksiteten. Generics løste et problem som jeg ikke hadde med Java. Jeg er generelt enig i Ken Arnolds oppfatning når han sier: "Generics er en feil. Dette er ikke et problem basert på tekniske uenigheter. Det er et grunnleggende språkdesignproblem [...] Kompleksiteten til Java har blitt turboladet til det som virker for meg relativt liten fordel. "

Heldigvis, mens utforming og implementering av generiske klasser noen ganger kan være for kompliserte, har jeg funnet ut at bruk av generiske klasser i praksis vanligvis er grei.

Iterasjon med forEach () -metoden

Før vi går inn i Java 8 iterasjonsfunksjoner, la oss reflektere over hva som er galt med koden som er vist i de forrige oppføringene - som egentlig ikke er noe. Det er millioner av linjer med Java-kode i for tiden distribuerte applikasjoner som bruker aktive iteratorer som ligner på de som er vist i oppføringene mine. Java 8 gir ganske enkelt ekstra muligheter og nye måter å utføre iterasjon på. For noen scenarier kan de nye måtene bli bedre.

De viktigste nye funksjonene i Java 8 sentrerer på lambda-uttrykk, sammen med relaterte funksjoner som strømmer, metodereferanser og funksjonelle grensesnitt. Disse nye funksjonene i Java 8 lar oss seriøst vurdere å bruke passive iteratorer i stedet for de mer konvensjonelle aktive iteratorene. Spesielt den Iterabel grensesnitt gir en passiv iterator i form av en standardmetode kalt for hver().

EN standardmetoden, en annen ny funksjon i Java 8, er en metode i et grensesnitt med standardimplementering. I dette tilfellet for hver() metoden er faktisk implementert ved hjelp av en aktiv iterator på en måte som ligner på det du så i Listing 3.

Samlingskurs som implementeres Iterabel (for eksempel alle liste- og angitte klasser) har nå en for hver() metode. Denne metoden tar en enkelt parameter som er et funksjonelt grensesnitt. Derfor ble den faktiske parameteren sendt til for hver() metoden er en kandidat for et lambdauttrykk. Ved å bruke funksjonene i Java 8, vil vårt løpeeksempel utvikle seg til skjemaet som vises i Listing 4.

Oppføring 4. Iterasjon i Java 8 ved hjelp av metoden forEach ()

 Listenavn = ny LinkedList (); // ... legg til noen navn i samlingsnavnene. forEach (navn -> System.out.println (navn)); 

Legg merke til forskjellen mellom den passive iteratoren i Listing 4 og den aktive iteratoren i de tre foregående oppføringene. I de tre første oppføringene kontrollerer sløyfestrukturen iterasjonen, og under hver passering gjennom sløyfen blir et objekt hentet fra listen og deretter skrevet ut. I oppføring 4 er det ingen eksplisitt sløyfe. Vi forteller ganske enkelt for hver() metode hva du skal gjøre med objektene i listen - i dette tilfellet skriver vi ganske enkelt ut objektet. Kontroll av iterasjonen ligger innenfor for hver() metode.

Iterasjon med Java-strømmer

La oss nå vurdere å gjøre noe litt mer involvert enn å bare skrive ut navnene i listen vår. Anta for eksempel at vi vil telle antall navn som begynner med bokstaven EN. Vi kan implementere den mer kompliserte logikken som en del av lambda-uttrykket, eller vi kan bruke Java 8s nye Stream API. La oss ta den sistnevnte tilnærmingen.

$config[zx-auto] not found$config[zx-overlay] not found