Generiske samlinger i Java gir utviklere muligheten til å arbeide med samlinger som kan lagre objekter av en spesifisert type, og dette kan gjøres på en type-sikker måte. En av de mest brukte samlingstypene er ArrayList, som tilbyr en dynamisk liste med elementer som kan legges til og fjernes i løpet av programmets gang. Når man oppretter en ArrayList-samling, kan man gjøre det ved å bruke konstruktøren uten parametere:

java
ArrayList aList = new ArrayList<>();

Dette skaper en tom liste med en kapasitet som kan utvides dynamisk etter hvert som flere elementer legges til. En god praksis for minnehåndtering er å bruke metoden trimToSize når en betydelig mengde elementer er fjernet fra samlingen. Denne metoden reduserer kapasiteten til listen til dens nåværende størrelse, og dermed frigjør minne som ikke lenger er nødvendig.

I et eksempelprogram, som kan illustreres ved hjelp av applikasjonen LinkedListApp, blir arbeidsposter for en leilighetsbygning lagt til, hentet, modifisert og fjernet fra en ArrayList. Disse arbeidspostene er instanser av klassen WorkOrder, som består av to datamedlemmer: apartmentNumber og description. For å gjøre samlingen type-sikker, kan man erklære en ArrayList som kan inneholde objekter av en bestemt type, som for eksempel WorkOrder-objekter.

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

Denne deklarasjonen sikrer at kun WorkOrder-objekter kan legges til listen. Forsøker man å legge til et annet objekt, vil dette resultere i en oversettelsesfeil. Programmet kan deretter legge til arbeidspostene i samlingen, som vist ved hjelp av add-metoden, og vise resultatene gjennom en for-løkke. Løkken henter hvert objekt ved å bruke get-metoden, som tar en indeks som parameter. Når et objekt i listen skal oppdateres, kan man bruke set-metoden til å erstatte et eksisterende element.

Når man arbeider med samlinger, kan det være nødvendig å fjerne objekter. Dette gjøres ved å bruke remove-metoden. For eksempel kan man fjerne den mest presserende arbeidsposten, som kan være lagret i elementet på posisjon 1. Når objektet er fjernet, kan det legges til en ny samling, som f.eks. completedTasks, ved å bruke add-metoden. Etter at flere elementer er fjernet og lagt til, kan man bruke trimToSize-metoden for å redusere kapasiteten på den opprinnelige listen til dens nåværende størrelse.

En annen viktig egenskap ved generiske samlinger er hvordan man kan sende dem som argumenter til metoder. En generisk samling kan sendes til en metode på samme måte som andre objekter, ved å bruke navnet på variabelen som refererer til samlingen. Dette kan gjøres på to måter. Man kan deklarere metodens parameter som en spesifikk type, som for eksempel:

java
public static void processTasks(ArrayList<WorkOrder> theTasks)

Dette sikrer at kun en samling av WorkOrder-objekter kan sendes til metoden, noe som gir type-sikkerhet. Alternativt kan man bruke en polymorfisk tilnærming, der metoden tar et parameter som implementerer et interface, som List:

java
public static void processTasks(List<WorkOrder> theTasks)

Dette gir fleksibilitet, ettersom metoden kan motta en hvilken som helst samling som implementerer List-interfacet. Metoden kan deretter utføre operasjoner som å legge til nye arbeidsposter i samlingen, hente størrelsen på samlingen og skrive ut innholdet.

I tillegg til å sende samlinger som argumenter, kan en generisk samling også være et datamedlem i en klasse. For eksempel kan man ha en klasse som Apartment, der et datamedlem refererer til en generisk List som lagrer arbeidsposter:

java
public class Apartment {
private List<WorkOrder> tasks; public Apartment(List<WorkOrder> theTasks) { tasks = theTasks; } }

Her brukes et polymorfisk datamedlem som kan referere til hvilken som helst samling som implementerer List. Når en instans av Apartment opprettes, kan konstruktøren tilordne en samling av arbeidsposter til datamedlemmet. Dette gir muligheten til å bruke alle metodene som er definert i List-interfacet på samlingen.

Når man jobber med generiske samlinger i Java, er det viktig å forstå hvordan type-sikkerhet, polymorfisme og fleksibilitet fungerer sammen. Ved å bruke generiske samlinger kan man både sikre at samlingene bare inneholder objekter av en spesifisert type, og samtidig oppnå stor fleksibilitet ved å bruke polymorfe referanser. Dette gjør det mulig å utvikle programmer som er både robuste og vedlikeholdsvennlige.

Endtext

Hvordan utføre dype og grunne sammenligninger, kopiering og kloning av objekter i Java

I objektorientert programmering er sammenligning, kopiering og kloning av objekter essensielle operasjoner som krever en forståelse av dype og grunne sammenligninger, samt hvordan disse operasjonene påvirker minnehåndtering og objektstruktur. En grunnleggende operasjon som utføres på objekter er sammenligning, hvor vi kan skille mellom grunne og dype sammenligninger avhengig av hva vi ønsker å sammenligne.

En grunne sammenligning, som implementert i equals-metoden i Java's Object-klasse, sammenligner bare referanser eller adresseverdier, ikke selve innholdet i objektene. Når vi kaller equals på to objekter, blir resultatet enten true eller false avhengig av om de refererer til samme minneadresse. Eksempelet som vises i koden under illustrerer hvordan dette fungerer:

java
ParentSnowman ps1 = new ParentSnowman();
ParentSnowman ps2 = ps1; // ps2 er satt til samme adresse som ps1
ParentSnowman ps3 = new ParentSnowman();
boolean sameAddresses; sameAddresses = ps1.equals(ps2); // grunne sammenligning, returnerer true System.out.println(sameAddresses); sameAddresses = ps1.equals(ps3); // grunne sammenligning, returnerer false System.out.println(sameAddresses);

I dette tilfellet, selv om innholdet i objektene kan være identisk, vil den grunne sammenligningen kun være true hvis de refererer til nøyaktig samme objekt. Hvis ps1 og ps2 peker til det samme objektet i minnet, vil sammenligningen returnere true, mens en annen instans som ps3 vil gi false, ettersom adressene i minnet er forskjellige.

På den annen side, en dyp sammenligning går videre og sammenligner innholdet i objektene. Dette krever at man bestemmer hvilke dataelementer i objektene som skal sammenlignes. For eksempel, hvis man vurderer at to snømenn er like hvis de er på samme posisjon på et spillbrett, vil man sammenligne deres x og y-koordinater. Denne typen sammenligning sammenligner ikke bare minneadressene, men også de faktiske dataene som er lagret i objektene.

En metode som utfører en dyp sammenligning kan se slik ut:

java
public boolean deepEquals(ParentSnowman ps) {
return (this.x == ps.getX() && this.y == ps.getY() && this.name.equals(ps.getName())); }

I denne metoden sammenlignes x- og y-koordinatene for å sjekke om snømennene befinner seg på samme sted, og name-verdiene sammenlignes ved hjelp av String-klassens equals-metode, som gjør en strengsammenligning av objektinnholdet.

Feil i koden kan oppstå hvis man ikke korrekt håndterer disse sammenligningene. For eksempel:

  1. name == ps.getName(): Dette vil sammenligne minneadressene til to strenger, ikke innholdet.

  2. this.getName() == ps.getName(): Igjen, sammenlignes minneadresser i stedet for innholdet.

  3. name.equals(ps): Her prøver man å sammenligne en streng med et ParentSnowman-objekt, noe som fører til en feil sammenligning.

  4. this.equals(ps): Bruk av equals på objekter som refererer til seg selv kan føre til en stackoverflow-feil på grunn av rekursjon.

Dype sammenligninger kan også brukes til å sammenligne objekters relative rekkefølge. Dette kan gjøres ved hjelp av metoder som heter compareTo, som sammenligner objektene og returnerer en verdi som indikerer deres rekkefølge i forhold til hverandre. En slik metode finnes allerede i String-klassen for å bestemme den leksikografiske rekkefølgen mellom to strenger. Ved å bruke operatorene < og > kan primitive datamedlemmer sammenlignes, mens for objektreferanser, kan man bruke eksisterende metoder som sammenligner objektene i henhold til spesifikasjoner for applikasjonen.

Når det gjelder kopiering og kloning av objekter, kan vi gjøre både grunne og dype kopier. En grunne kopi kopierer bare referansen (minneadressen) til objektet, mens en dyp kopi kopierer innholdet i objektets datafelt til et nytt objekt. En grunne kopi kan føre til at flere variabler peker til det samme objektet, som vist i koden under:

java
ParentSnowman ps1 = new ParentSnowman(250, 250, "A");
ParentSnowman ps2 = new ParentSnowman(10, 20, "X"); ps2 = ps1; // gjør en grunne kopi System.out.println(ps1.show()); System.out.println(ps2.show());

Her ender begge referansene, ps1 og ps2, opp med å peke til det samme objektet, og eventuelle endringer i et objekt vil reflekteres i begge variablene.

En dyp kopi, derimot, lager en fullstendig kopi av objektet, slik at begge objektene eksisterer uavhengig av hverandre. For å gjøre dette må man implementere en metode som bruker klassens set-metoder for å kopiere dataene fra et objekt til et annet. En metode for dyp kopi kan se slik ut:

java
public ParentSnowman deepCopy() {
ParentSnowman copy = new ParentSnowman(); copy.setX(this.x); copy.setY(this.y); copy.setName(this.name); return copy; }

En dyp kopi sørger for at begge objektene er uavhengige, og ingen objekter blir utilsiktet fjernet fra programmet.

Ved kloning av objekter, hvor vi ønsker å lage en nøyaktig kopi av et eksisterende objekt, er det viktig å forstå forskjellen på en grunne kopi og en dyp kopi. En grunne kopi kan være tilstrekkelig i tilfeller hvor objektet ikke inneholder referanser til andre objekter, men i mer komplekse scenarier, som når objektene inneholder andre objekter eller samlinger, må man sørge for at en dyp kopi utføres.