I Java API Collections Framework finnes det flere viktige samlingstyper som implementerer forskjellige grensesnitt. Eksempler på dette inkluderer ArrayList, LinkedList, PriorityQueue, HashMap og TreeMap. Hver av disse samlingstypene implementerer spesifikke grensesnitt som gjør dem fleksible og kraftige verktøy for utviklere. For eksempel implementerer ArrayList og LinkedList grensesnittet List, PriorityQueue implementerer Queue, HashMap implementerer Map, og TreeMap implementerer SortedMap. Disse grensesnittene inneholder generiske metoder, og samlingene de opererer på, kan være instanser av hvilken som helst klasse. Det er viktig å merke seg at generiske klasser ikke kan lagre samlinger av primitive verdier. De kan imidlertid lagre instanser av primitive wrapper-klasser som Integer, Double og Character.

Deklarasjon av generiske objekter

Normalt sett lagres objektene i en bestemt instans av en samling i API-rammeverket som instanser av samme klasse. Klassen spesifiseres når samlingen erklæres. For eksempel kan en instans av den generiske klassen ArrayList erklæres som følger:

java
ArrayList<WorkOrder> tasks = new ArrayList<>();

Her spesifiserer typeargumentlisten <WorkOrder> hvilken type objekter som skal lagres i samlingen. Dette er viktig for Java-kompilatoren, da det gir typekontroll og sørger for at bare objekter av klassen WorkOrder kan legges til i samlingen. Hvis man prøver å legge til et objekt som ikke er av typen WorkOrder, vil kompilatoren generere en oversettelsesfeil. Det er anbefalt å alltid inkludere typeargumentlisten når samlingen skal lagre objekter av én type.

En annen måte å deklarere en generisk samling på er ved å bruke en to-linjers syntaks:

java
ArrayList<WorkOrder> tasks; tasks = new ArrayList<>();

Begge de nevnte deklarasjonene gir samme resultat, men den første versjonen anses som den beste praksisen. Generelt kan samlinger også deklareres med tomme typeargumenter (dette kalles diamantoperatøren <>), som lar Java inferere typen basert på venstre side av deklarasjonen.

Polymorfisme i generiske klasser

Polymorfisme er et sentralt konsept i Java, der en variabel kan referere til objekter av forskjellige typer gjennom et felles grensesnitt. I tilfelle generiske samlinger kan en variabel som refererer til en samling som implementerer List-grensesnittet, for eksempel ArrayList eller LinkedList, bruke polymorfisme. Hvis vi for eksempel har en variabel deklarert som:

java
List<WorkOrder> aList;

Så kan den referere til en ArrayList, LinkedList eller en annen samling som implementerer List, så lenge samlingen består av objekter av typen WorkOrder. Polymorfisme lar utviklere skrive mer fleksibel og gjenbrukbar kode. For eksempel kan man tildele en ArrayList til en variabel av typen List uten at det skaper problemer:

java
aList = new ArrayList<>();

Imidlertid er det en viktig begrensning ved polymorfisme: Metodene som kalles på en polymorf variabel, må finnes i arvkjeden til det deklarerte grensesnittet. Hvis vi prøver å kalle en metode som ikke er definert i List-grensesnittet, som for eksempel trimToSize() som er spesifikk for ArrayList, vil det føre til en kompilatorfeil. Dette kan løses ved å bruke den ikke-polymorfe deklarasjonen av ArrayList direkte:

java
ArrayList<WorkOrder> aList = new ArrayList<>(); aList.trimToSize();

ArrayList som samlingstype

ArrayList er en av de mest brukte samlingstypene i Java. Den implementerer List-grensesnittet og tilbyr en dynamisk lengde på samlingen, i motsetning til en vanlig array med fast størrelse. Hver verdi i ArrayList er referert til ved hjelp av et indeksnummer, der indeksene starter fra 0. Når ArrayList opprettes, kan vi spesifisere en initial kapasitet som representerer hvor mange elementer samlingen kan inneholde før den må utvides. Hvis kapasiteten ikke spesifiseres, vil den sette en standardverdi.

For eksempel:

java
ArrayList<WorkOrder> aList = new ArrayList<>(200);

Her settes kapasiteten til 200, men listen kan fortsatt vokse etter behov. Det er viktig å merke seg at kapasiteten til en ArrayList alltid er større enn eller lik størrelsen på samlingen. En viktig forskjell fra en array er at en ArrayList kan utvides dynamisk, og nye elementer kan legges til både først, mellom eksisterende elementer, eller på slutten. Når elementer legges til eller fjernes, kan kapasiteten også endres.

Størrelsen på en ArrayList refererer til antall elementer som faktisk er lagret i samlingen, og kan hentes med metoden size(). Denne størrelsen er forskjellig fra kapasiteten, som refererer til hvor mange elementer samlingen kan inneholde før den trenger å utvides.

Viktige hensyn ved bruk av ArrayList

Når man bruker ArrayList, er det noen viktige aspekter man bør vurdere. En av de viktigste fordelene med ArrayList er at det er en veldig fleksibel samlingstype som kan håndtere et dynamisk antall elementer. Imidlertid, for applikasjoner der tidskritiske operasjoner er avgjørende, kan det være nødvendig å vurdere alternative samlingstyper som LinkedList, som gir raskere innsetting og fjerning av elementer i midten av samlingen.

I de fleste tilfeller er det best å bruke ArrayList med standardkapasitet, med mindre det er spesifikke ytelseskrav som tilsier noe annet. ArrayList er effektiv når det gjelder å hente elementer basert på indeks, men innsetting og fjerning av elementer fra midten av listen kan være tregere sammenlignet med LinkedList.

Generelt bør utviklere være bevisste på hvordan kapasiteten til samlingen påvirker ytelsen, spesielt når store mengder data behandles.

Hvordan kan arv redusere kompleksitet og duplisering i objektorientert design?

Når man utvikler et objektorientert system med flere klasser som deler lignende attributter og metoder, oppstår det ofte en overflod av duplisert kode. Dette reduserer vedlikeholdbarheten og øker kompleksiteten. Ved å identifisere felles egenskaper på tvers av klasser og trekke disse opp i en abstrakt superklasse, kan man forenkle både design og implementasjon betydelig. Dette prinsippet demonstreres effektivt gjennom utviklingen av programvaren til Uncle Ed, der en abstrakt klasse Boat blir introdusert som et overordnet nivå for tre spesifikke båttyper.

Tidligere inneholdt hver av de tre båttypene fem identiske datamedlemmer og tilhørende get- og set-metoder. Ved å flytte disse til Boat, elimineres redundansen. Arv gjør det mulig å fjerne duplisert kode uten å miste funksjonalitet: hver barneklasse utvider Boat og arver dermed de felles komponentene. Dette reduserer ikke bare antall linjer med kode, men forenkler også struktur og vedlikehold.

Men fordelene ved arv strekker seg lengre enn bare delte attributter. Alle tre klasser inneholdt metoder som show, calculatePrice og toString, med identiske signaturer og delvis felles funksjonalitet. For eksempel beregnes grunnprisen på båten likt for alle typer, fordi de deler samme skrog. Dette betyr at selve priskalkuleringen, samt det visuelle uttrykket av skroget i show-metoden, kan standardiseres i Boat. Tilsvarende gjelder toString, som returnerer verdiene til de fem felles datamedlemmene i annotert form – enda en mulighet til å sentralisere og gjenbruke kode.

I et godt design må man likevel tillate spesialisering. Hver barneklasse kan overstyre calculatePrice, show og toString for å legge til sine særegne detaljer, men kan samtidig gjenbruke implementasjonen i Boat via superkall. Eksempelvis beregner PowerBoatV2 kostnaden av motoren sin som et tillegg til basisprisen som er definert i Boat. Tilsvarende gjelder for årer i RowBoat og seil i SailBoat.

Ved å anvende denne strukturen viser UML-diagrammene en tydelig reduksjon i kodemengde: datamedlemmene reduseres fra 18 til 8, metodene fra 30 til 22. Denne reduksjonen er ikke bare tallmessig, men også kvalitativ. Når konstruktøren i Boat tar hånd om felles initialisering, blir konstruktørene i underklassene lettere å skrive og vedlikeholde.

Arv handler ikke bare om deling ovenfra og ned – det gir også rom for at foreldermetoder kan kalle på spesialiserte metoder i barneklasser. Dette muliggjør et alternativ til klassisk overstyring. Ved å definere en metode som extras i hver barneklasse, kan calculatePrice i Boat gjøre et kall til extras() for å hente spesifikke tillegg, uten at calculatePrice må overstyres i hver barneklasse. Denne teknikken tillater en mer modulær og lesbar kode, og sikrer at felles logikk ikke utilsiktet dupliseres.

Når slike metoder defineres i forelderen og kalles dynamisk, er det viktig at signaturen er konsistent på tvers av barneklasser. Om extras() ikke eksisterer i Boat, vil koden ikke kompilere. En måte å løse dette på er å implementere en tom extras-metode i Boat, men et mer robust og formelt korrekt valg er å definere den som en abstrakt metode.

Ved å gjøre extras() abstrakt, tvinger man alle konkrete barneklasser til å implementere den. Dette gir en eksplisitt kontrakt mellom Boat og dens etterkommere, noe som styrker designets forutsigbarhet og pålitelighet. Bruken av abstrakte metoder er spesielt gunstig når superklassen aldri skal instansieres direkte, men kun fungerer som en samler for felles data og logikk.

Det som ofte neglisjeres i slike diskusjoner om arv, er viktigheten av å balansere gjenbruk med fleksibilitet. Det er fristende å trekke for mye logikk opp i forelderen, noe som kan gjøre den unødvendig kompleks og avhengig av barneklassers detaljer. Samtidig bør man unngå å overstyre metoder uten reell grunn, noe som bryter med prinsippet om minste overraskelse og øker vedlikeholdsbyrden. Effektiv bruk av arv krever derfor både teknisk presisjon og en forståelse for domeneproblemet. Det handler ikke bare om å flytte kode, men om å modellere virkelighetens kompleksitet på en strukturert og bærekraftig måte.