Ikke-adgangsmodifikatorer i Java
Introduktion
Modifiers er nøgleord, der lader os finjustere adgangen til vores klasse og dens medlemmer, deres omfang og adfærd i visse situationer. For eksempel kan vi kontrollere, hvilke klasser/objekter der kan få adgang til bestemte medlemmer af vores klasse, om en klasse kan nedarves eller ej, om vi kan tilsidesætte en metode senere, om vi skal tilsidesætte en metode senere osv.
Modifier søgeord skrives før variabel/metode/klasse (retur) type og navn, f.eks. private int myVar
eller public String toString()
.
Modifikatorer i Java falder i en af to grupper - adgang og ikke-adgang :
- Adgang:
public
,private
,protected
. - Ikke-adgang:statisk, endelig, abstrakt, synkroniseret, flygtig, forbigående og
native
.
native
er ikke dækket mere detaljeret nedenfor, da det er et simpelt nøgleord, der markerer en metode, der vil blive implementeret på andre sprog, ikke i Java. Det fungerer sammen med Java Native Interface (JNI). Det bruges, når vi vil skrive ydeevnekritiske sektioner af kode på mere ydeevnevenlige sprog (som C).
Vil du vide mere om adgang modifikatorer, i modsætning til ikke-adgang? Hvis ja, så tjek vores artikel Adgangsmodifikatorer i Java.
Ikke-adgangsmodifikatorer
Disse typer modifikatorer bruges til at kontrollere en række ting, såsom arveevner, om alle objekter i vores klasse deler den samme medlemsværdi eller har deres egne værdier for disse medlemmer, om en metode kan tilsidesættes i en underklasse osv.
En kort oversigt over disse modifikatorer kan findes i følgende tabel:
Ændringsnavn | Oversigt |
---|---|
statisk | Medlemmet tilhører klassen, ikke til objekter i den klasse. |
final | Variabelværdier kan ikke ændres, når de først er tildelt, metoder kan ikke tilsidesættes, klasser kan ikke nedarves. |
abstrakt | Hvis anvendt på en metode - skal implementeres i en underklasse, hvis den anvendes på en klasse - indeholder abstrakte metoder |
synkroniseret | Styrer trådadgang til en blok/metode. |
flygtig | Variabelværdien læses altid fra hovedhukommelsen, ikke fra en specifik tråds hukommelse. |
transient | Medlemmet springes over, når et objekt serialiseres. |
Den statiske modifikator
static
modifier gør et klassemedlem uafhængigt af ethvert objekt i den klasse. Der er et par funktioner, du skal huske på her:
- Variabler erklæret
static
deles mellem alle objekter i en klasse (da variablen i det væsentlige tilhører selve klassen i dette tilfælde), dvs. objekter har ikke deres egne værdier for denne variabel, i stedet deler de alle en enkelt. - Variabler og metoder erklæret
static
kan tilgås via klassenavnet (i stedet for den sædvanlige objektreference, f.eks.MyClass.staticMethod()
ellerMyClass.staticVariable
), og de kan tilgås uden at klassen instansieres . static
metoder kan kun brugestatic
variabler og kalde andrestatic
metoder og kan ikke henvise tilthis
ellersuper
på nogen måde (en objektforekomst eksisterer måske ikke engang, når vi kalder enstatic
metode, såthis
ville ikke give mening).
Bemærk :Det er meget vigtigt at bemærke, at static
variabler og metoder kan ikke adgang ikke-static
(instans) variabler og metoder. På den anden side, ikke-static
variabler og metoder kan få adgang til static
variabler og metoder.
Dette er logisk, da static
medlemmer eksisterer selv uden et objekt fra den klasse, hvorimod instans medlemmer eksisterer kun, efter at en klasse er blevet instansieret.
Statiske variable
For variabler bruger vi static
hvis vi ønsker at variablen skal være fælles/delt for alle objekter.
Lad os tage et kig på hvordan static
variabler opfører sig anderledes end almindelige instansvariabler:
class StaticExample {
public static int staticInt = 0;
public int normalInt = 0;
// We'll use this example to show how we can keep track of how many objects
// of our class were created, by changing the shared staticInt variable
public StaticExample() {
staticInt++;
normalInt++;
}
}
// No instances of StaticExample have been created yet
System.out.println(StaticExample.staticInt); // Prints: 0
// System.out.println(StaticExample.normalInt); // this won't work, obviously
// Let's create two instances of StaticExample
StaticExample object1 = new StaticExample();
// We can refer to static variables via an object reference as well,
// however this is not common practice, we usually access them via class name
// to make it obvious that a variable/method is static
System.out.println(object1.staticInt); // Prints: 1
System.out.println(object1.normalInt); // Prints: 1
StaticExample object2 = new StaticExample();
System.out.println(object2.staticInt); // Prints: 2
System.out.println(object2.normalInt); // Prints: 1
// We can see that increasing object2's staticInt
// increases it for object1 (and all current or future objects of that class)
object1.staticInt = 10;
object1.normalInt = 10;
System.out.println(object2.staticInt); // Prints: 10
System.out.println(object2.normalInt); // Prints: 1 (object2 retained its own value for normalInt as it depends on the class itself)
Statiske metoder
Det mest almindelige eksempel på brug af static
er main()
metode, er den erklæret som static
fordi det skal kaldes før nogen objekter eksisterer. Et andet almindeligt eksempel er Math
klasse, da vi bruger den pågældende klasses metoder uden at lave en forekomst af den først (som Math.abs()
).
En god måde at tænke static
på metoder er "Giver det mening at bruge denne metode uden først at oprette et objekt af denne klasse?" (du behøver f.eks. ikke at instansiere Math
klasse for at beregne den absolutte værdi af et tal).
Statiske metoder kan bruges til at få adgang til og ændre static
medlemmer af en klasse. De bruges dog almindeligvis til at manipulere metodeparametre eller beregne noget og returnere en værdi.
Disse metoder omtales som utility metoder:
static int average(int num1, int num2) {
return (num1+num2)/2;
}
Denne hjælpemetode kan bruges til at beregne gennemsnittet af to tal, for eksempel.
Som nævnt ovenfor er Math
klasse bruges ofte til at kalde static
metoder. Hvis vi ser på kildekoden, kan vi bemærke, at den for det meste tilbyder hjælpemetoder:
public static int abs(int i) {
return (i < 0) ? -i : i;
}
public static int min(int a, int b) {
return (a < b) ? a : b;
}
public static int max(int a, int b) {
return (a > b) ? a : b;
}
Statiske blokke
Der er også en static
blok. En static
blok udføres kun én gang, når klassen første gang instansieres (eller en static
medlem er blevet kaldt, selvom klassen ikke er instansieret), og før resten af koden.
Lad os tilføje en static
blokere til vores StaticExample
klasse:
class StaticExample() {
...
static {
System.out.println("Static block");
}
...
}
StaticExample object1 = new StaticExample(); // "Static block" is printed
StaticExample object2 = new StaticExample(); // Nothing is printed
Uanset deres placering i klassen, static
blokke initialiseres før andre ikke-statiske blokke, inklusive konstruktører:
class StaticExample() {
public StaticExample() {
System.out.println("Hello from the constructor!");
}
static {
System.out.println("Hello from a static block!");
}
}
Instantiering af denne klasse vil udsende:
Hello from a static block!
Hello from the constructor!
Hvis flere static
blokke er til stede, vil de køre i deres respektive rækkefølge:
public class StaticExample {
static {
System.out.println("Hello from the static block! 1");
}
public StaticExample() {
System.out.println("Hello from the constructor!");
}
static {
System.out.println("Hello from the static block! 2");
}
}
Instantiering af denne klasse vil udsende:
Hello from the static block! 1
Hello from the static block! 2
Hello from the constructor!
Statisk import
Som allerede nævnt er det bedre at ringe til static
medlemmer præfikset med klassenavnet i stedet for instansnavnet. I nogle tilfælde instansierer vi aldrig rigtig en klasse med static
metoder, såsom Math
klasse, som tilbyder adskillige hjælpemetoder vedrørende matematik.
Når det er sagt, hvis vi bruger en klasse' static
medlemmer ofte, kan vi importere individuelle medlemmer eller dem alle ved hjælp af en static import
. Dette giver os mulighed for at springe over deres opkald som præfiks med klassenavnet:
package packageOne;
public class ClassOne {
static public int i;
static public int j;
static public void hello() {
System.out.println("Hello World!");
}
}
package packageTwo;
static import packageOne.ClassOne.i;
public class ClassTwo {
public ClassTwo() {
i = 20;
}
}
Eller hvis vi gerne vil importere alle static
medlemmer af ClassOne
, vi kunne gøre det sådan:
package packageTwo;
import static packageOne.ClassOne.*;
public class ClassTwo {
public ClassTwo() {
i = 20;
j = 10;
}
}
Det samme gælder for metoder:
package packageTwo;
import static packageOne.ClassOne.*;
public class ClassTwo {
public ClassTwo() {
hello();
}
}
Hvis du kører dette, udsendes:
Hello World!
Det virker måske ikke så vigtigt, men det hjælper, når vi kalder mange static
medlemmer af en klasse:
public int someFormula(int num1, int num2, int num3) {
return Math.ceil(Math.max(Math.abs(num1), Math.abs(num2))+Math.max(Math.abs(num2), Math.abs(num3)))/(Math.min(Math.abs(num1), Math.abs(num2))+Math.min(Math.abs(num2), Math.abs(num3)));
}
// Versus...
import static java.lang.Math.*;
public int someFormula(int num1, int num2, int num3) {
return ceil(max(abs(num1), abs(num2))+max(abs(num2), abs(num3)))/(min(abs(num1), abs(num2))+min(abs(num2), abs(num3)));
}
Den endelige modifikator
Søgeordet final
kan have en af tre betydninger:
- for at definere navngivne konstanter (variabler, hvis værdier ikke kan ændres efter initialisering)
- for at forhindre en metode i at blive tilsidesat
- for at forhindre en klasse i at blive nedarvet
Navngivne konstanter
Tilføjelse af final
modifikator til en variabelerklæring gør den variabel uændrelig, når den først er initialiseret.
final
modifikator bruges ofte sammen med static
modifikator, hvis vi definerer konstanter. Hvis vi kun anvender static
til en variabel, kan den stadig nemt ændres. Der er også en navnekonvention knyttet til dette:
static final double GRAVITATIONAL_ACCELERATION = 9.81;
Variabler som disse er ofte inkluderet i hjælpeklasser, såsom Math
klasse, ledsaget af adskillige hjælpemetoder.
Selvom de i nogle tilfælde også berettiger deres egne klasser, såsom Constants.java
:
public static final float LEARNING_RATE = 0.3f;
public static final float MOMENTUM = 0.6f;
public static final int ITERATIONS = 10000;
Bemærk :når du bruger final
med objektreferencevariabler skal du være forsigtig med, hvilken type adfærd du forventer. Overvej følgende:
class MyClass {
int a;
int b;
public MyClass() {
a = 2;
b = 3;
}
}
final MyClass object1 = new MyClass();
MyClass object2 = new MyClass();
Referencevariablen object1
er faktisk final
og dens værdi kan ikke ændres, men hvad betyder det alligevel for referencevariabler? Det betyder, at object1
kan ikke ændre, hvilket objekt det peger på længere, men vi kan ændre selve objektet. Dette er noget, der ofte forvirrer folk:
// object1 = object2; // Illegal!
object1.a = 5; // Perfectly fine
Metodeparametre kan også erklæres final
. Dette bruges til at sikre, at vores metode ikke ændrer den parameter, den modtager, når den kaldes.
Lokale variabler kan også erklæres final
. Dette bruges til at sikre, at variablen kun modtager en værdi én gang.
Forhindring af tilsidesættelse
Hvis du angiver final
modifikator, mens en metode defineres, kan enhver fremtidig underklasse ikke tilsidesætte den.
class FinalExample {
final void printSomething() {
System.out.println("Something");
}
}
class ExtendsFinalExample extends FinalExample {
// This would cause a compile-time error
//void printSomething() {
// System.out.println("Some other thing");
//}
// However, you are perfectly free to overload this method
void printSomething(String something) {
System.out.println(something);
}
}
En lille bonus ved at erklære virkelig endelige metoder som final
er et lille præstationsløft, når vi kalder denne metode. Normalt løser Java metodekald dynamisk under kørsel, men med metoder erklæret final
, Java kan løse et kald til den på kompileringstidspunktet, eller hvis en metode er virkelig lille, kan den simpelthen inline kald til den metode, da den "ved", at den ikke vil blive tilsidesat. Dette eliminerer overhead forbundet med et metodekald.
Forebyggelse af arv
Denne brug af final
er ret ligetil, en klasse defineret med final
kan ikke arves. Dette erklærer naturligvis også implicit alle metoder i den pågældende klasses finale (de kan ikke tilsidesættes, hvis klassen ikke kan nedarves i første omgang).
final class FinalExample {...}
Den abstrakte modifikator
abstract
modifier bruges til at definere metoder, der vil blive implementeret i en underklasse senere. Oftest bruges det til at foreslå, at nogle funktioner bør implementeres i en underklasse, eller (af en eller anden grund) kan den ikke implementeres i superklassen. Hvis en klasse indeholder en abstract
metode, skal den også erklæres abstract
.
Bemærk :Du kan ikke oprette et objekt af en abstract
klasse. For at gøre det skal du levere en implementering for alle abstract
metoder.
Et eksempel ville være, hvis vi havde en simpel klasse kaldet Employee
der indkapsler data og metoder for en medarbejder. Lad os sige, at ikke alle medarbejdere bliver aflønnet på samme måde, nogle typer medarbejdere er timelønnede og nogle får en fast løn.
abstract class Employee {
int totalHours; // In a month
int perHour; // Payment per hour
int fixedRate; // Fixed monthly rate
...
abstract int salary();
...
}
class Contractor extends Employee {
...
// Must override salary if we wish to create an object of this class
int salary() {
return totalHours*perHour;
}
...
}
class FullTimeEmployee extends Employee {
...
int salary() {
return fixedRate;
}
...
}
class Intern extends Employee {
...
int salary() {
return 0;
}
...
}
Hvis en underklasse ikke giver en implementering til alle abstract
metoder i superklassen, skal den erklæres som abstract
også, og et objekt af den klasse kan ikke oprettes.
Bemærk :abstract
bruges meget med polymorfi, f.eks. vi vil sige ArrayList<Employee> employees = new ArrayList();
, og tilføj Contractor
, FullTimeEmployee
og Intern
gør indsigelse mod det. Selvom vi ikke kan oprette et objekt af Employee
klasse, kan vi stadig bruge den som en referencevariabeltype.
Den synkroniserede modifikator
Når to eller flere tråde skal bruge den samme ressource, skal vi på en eller anden måde sikre os, at kun én af dem har adgang til den ad gangen, dvs. vi skal synkronisere dem.
Dette kan opnås på flere måder, og en enkel og læsbar måde (omend med noget begrænset brug) er ved at bruge synchronized
søgeord.
Et vigtigt begreb at forstå, før du ser, hvordan du bruger dette søgeord, er begrebet en skærm. Hvert objekt i Java har sin egen implicitte skærm tilknyttet. En skærm er en "gensidigt udelukkende" lås, hvilket betyder, at kun én tråd kan "eje" en skærm ad gangen. Når en tråd kommer ind i monitoren, kan ingen anden tråd komme ind i den, før den første tråd går ud. Dette er hvad synchronized
gør.
Tråde er uden for denne artikels omfang, så jeg vil fokusere på syntaksen for synchronized
kun.
Vi kan synkronisere adgang til metoder og kodeblokke. Synkronisering af kodeblokke fungerer ved at give en objektforekomst, som vi ønsker at synkronisere adgang til, og den kode, som vi ønsker at udføre relateret til det objekt.
class SynchronizedExample {
...
SomeClass object = new SomeClass();
....
synchronized(object) {
// Code that processes objects
// only one thread at a time
}
// A synchronized method
synchronized void doSomething() {
...
}
...
}
Den flygtige modifikator
volatile
modifier fortæller Java, at en variabel kan ændres uventet af en anden del af programmet (som i multithreaded programmering), og så denne variabels værdi altid læses fra hovedhukommelsen (og ikke fra CPU-cachen), og at hver ændring af volatile
variabel gemmes i hovedhukommelsen (og ikke i CPU-cachen). Med dette i tankerne, volatile
bør kun bruges, når det er nødvendigt, da læsning/skrivning til hukommelsen hver gang er dyrere end at gøre det med CPU-cache og kun læsning/skrivning til hukommelsen, når det er nødvendigt.
Forenklet sagt - når en tråd læser en volatile
variabel værdi, er det garanteret, at den vil læse den senest skrevne værdi. Grundlæggende en volatile
variabel gør det samme som synchronized
metoder/blokke gør, vi kan bare ikke erklære en variabel som synchronized
.
Den forbigående modifikator
Når en variabel er erklæret som transient
, det betyder, at dets værdi ikke gemmes, når objektet er gemt i hukommelsen.transient int a;
betyder, at når vi skriver objektet til hukommelsen, vil indholdet af "a" ikke blive inkluderet. For eksempel bruges det til at sikre, at vi ikke gemmer private/fortrolige oplysninger i en fil.
Når vi forsøger at læse et objekt, der indeholder transient
variabler, alle transient
variabelværdier indstilles til null
(eller standardværdier for primitive typer), uanset hvad de var, da vi skrev objektet til filen. Et andet eksempel på brug ville være, når en variabels værdi skal udledes baseret på andre data (såsom en persons nuværende alder), og ikke er en del af den vedvarende objekttilstand.
Bemærk :Der sker noget meget interessant, når vi bruger transient
og final
sammen. Hvis vi har en transient final
variabel, der evalueres som et konstant udtryk (strenge eller primitive typer), vil JVM altid serialisere den og ignorere enhver potentiel transient
modifikator. Når transient final
bruges med referencevariabler, får vi den forventede standardadfærd for transient
.
Konklusion
Modifikatorer er nøgleord, der lader os finjustere adgangen til vores klasse og dens medlemmer, deres omfang og adfærd i visse situationer. De giver grundlæggende træk til vores klasser og deres medlemmer. Enhver udvikler bør være grundigt bekendt med dem for at få den bedst mulige brug af dem.
Som at være opmærksom på, at protected
adgangskontrol kan nemt omgås, eller transient final
modifikator, når det kommer til konstante udtryk.