Provera jednakosti stringova: razlika i upotreba equals() i == u Javi
"Erge, kako da uporedim dva stringa na jednakost?" upitala je
"To pitanje deluje jednostavno, a na Stack Overflow-u ima preko 3,7 miliona pregleda," rekao sam. "Ovo pitanje se takođe može svesti na: koja je razlika između metode .equals() i operatora '=='."
- Operator "==" služi za proveru da li su adrese dva objekta jednake.
- Metoda
.equals()služi za proveru da li je sadržaj dva objekta jednak.
"Nije mi baš jasno."
Ima jednih blizankinja — starija sestra zove se Alita, a mlađa Lolita. Mi obični ljudi možda uopšte ne možemo da razlikujemo ko je starija sestra, a ko mlađa, ali njihova mama lako ih prepozna.

Metoda .equals() je poput nas običnih ljudi: kad vidimo Alitu pomislimo da je Lolita, a kad vidimo Lolutu pomislimo da je Alita — pošto izgledaju isto, učini nam se da je u pitanju jedna te ista osoba. Operator "==" je poput njihove mame: zahteva stroži i pažljiviji pregled, pa jednim pogledom razlikuje ko je starija sestra, a ko mlađa.
String alita = new String("devojčica");
String luolita = new String("devojčica");
System.out.println(alita.equals(luolita)); // true
System.out.println(alita == luolita); // falseZa gornji kod, metoda .equals() daje rezultat true, dok operator "==" daje rezultat false — prvi zahteva samo jednakost sadržaja, dok drugi zahteva da bude u pitanju jedan te isti objekat.
rekao sam, istovremeno otvarajući izvorni kod klase Object.
public boolean equals(Object obj) {
return (this == obj);
}Vidiš — metoda .equals() klase Object podrazumevano koristi operator == za poređenje. Ako podklasa ne preradi ovu metodu, onda su efekat operatora == i metode .equals() potpuno isti — porede da li su memorijske adrese dva objekta jednake.
Ali u praksi, mnoge klase su preradile metodu .equals(), jer je poređenje memorijskih adresa previše strogo i ne odgovara svim stvarnim scenarijima. Uzmimo klasu String: kada poredimo stringove, zaista želimo samo da proverimo jednakost njihovog sadržaja, a ne da li su u pitanju jedan te isti objekat.
Osim toga, kod stringova postoji koncept bazena stringovnih konstanti i samim tim se preporučuje oblik String s = "string" za kreiranje stringovnog objekta, umesto putem ključne reči new, jer se string može keširati u bazenu konstanti radi lakše sledeće upotrebe, umesto da se pri svakom nailasku na new otvara novi prostor na heap-u.
"Onda hajde da pogledamo izvorni kod metode .equals() klase String," rekao sam.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}Pre svega, ako dva stringovna objekta mogu da zadovolje "==", vraća se true odmah, jer je u tom slučaju sadržaj stringova nužno jednak. U suprotnom se poređenje vrši po karakterima, razlikujući UTF16 i Latin1; razlika nije velika, uzmimo za primer Latin1.
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}Ova verzija JDK-a je Java 17, odnosno najnovija LTS (dugoročna podrška) verzija. U toj verziji klasa String je implementirana pomoću niza bajtova, tako da se pri proveri jednakosti sadržaja dva stringa može prvo uporediti dužina nizova bajtova — ako nisu jednake, odmah se vraća false; inače se prolazi kroz nizove bajtova oba stringa, i čim se naiđe na jedan nejednak bajt vraća se false.
Ovo je izvorni kod metode equals u Java 8:
public boolean equals(Object anObject) {
// Proverava da li je u pitanju isti objekat
if (this == anObject) {
return true;
}
// Proverava da li je objekat tipa String
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
// Proverava da li su dužine stringova jednake
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// Proverava da li je svaki karakter jednak
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}JDK 8 se lakše razume od JDK 17: prvo se proverava da li su dva objekta jedan te isti; ako jesu, vraća se true. Zatim se proverava da li je objekat tipa String; ako nije, vraća se false. Ako je objekat tipa String, poredi se dužina dva stringa; ako dužine nisu jednake, vraća se false. Ako su dužine jednake, redom se poredi svaki karakter; ako su svi jednaki, vraća se true, u suprotnom false.
"Da, Erge, ovaj izvorni kod nije težak za razumevanje," samopouzdano je rekla
"Onda ću ti zadati nekoliko pitanja da te ispitam!" rekao sam.
Prvo pitanje:
new String("devojčica").equals("devojčica")"Šta ispisuje?" upitao sam.
"Metoda .equals() poredi jednakost sadržaja dva stringovna objekta, pa je rezultat true," bez razmišljanja je odgovorila
Drugo pitanje:
new String("devojčica") == "devojčica""Leva strana operatora == je objekat kreiran na heap-u, a desna je objekat u bazenu stringovnih konstanti; iako je sadržaj isti, memorijske adrese se razlikuju, pa se vraća false," odgovorila je
Treće pitanje:
new String("devojčica") == new String("devojčica")"Objekti kreirani preko new zasigurno imaju potpuno različite memorijske adrese, pa se vraća false," odgovorila je
Četvrto pitanje:
"devojčica" == "devojčica""U bazenu stringovnih konstanti postoji samo jedan objekat sa istim sadržajem, pa se vraća true," odgovorila je
Peto pitanje:
"devojčica" == "devo" + "jčica""Pošto su i 'devo' i 'jčica' u bazenu stringovnih konstanti, kompilator pri nailasku na operator '+' automatski ga optimizuje u 'devojčica', pa se vraća true."
PS: Zašto je tome tako, pogledajte članak String, StringBuilder, StringBuffer.
Šesto pitanje:
new String("devojčica").intern() == "devojčica"PS: Metodu intern smo već detaljno proučili.
Pa moram priznati
rekao sam.
"Ako želiš da uporediš sadržaj dva stringovna objekta, pored metode .equals() postoje još dve opcije."
Objects.equals()
Prednost statičke metode Objects.equals() je u tome što ne zahteva proveru na null pre poziva.
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}Ako biste direktno koristili a.equals(b), morali biste pre poziva proveriti da li je a null, inače bi mogao da se baci java.lang.NullPointerException. Sa Objects.equals() te brige nema.
Objects.equals("devojčica", new String("devo" + "jčica")) // --> true
Objects.equals(null, new String("devo" + "jčica")); // --> false
Objects.equals(null, null) // --> true
String a = null;
a.equals(new String("devo" + "jčica")); // baca izuzetak- Metoda
.contentEquals()klase String
Prednost metode .contentEquals() je u tome što može uporediti string sa bilo kojim nizom karaktera (StringBuffer, StringBuilder, String, CharSequence).
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
int n = cs.length();
if (n != length()) {
return false;
}
byte[] val = this.value;
if (isLatin1()) {
for (int i = 0; i < n; i++) {
if ((val[i] & 0xff) != cs.charAt(i)) {
return false;
}
}
} else {
if (!StringUTF16.contentEquals(val, cs, n)) {
return false;
}
}
return true;
}Iz izvornog koda se vidi da ako je cs tipa StringBuffer, metoda još i vrši sinhronizaciju — veoma pametno; ako je tipa String, zapravo se poziva metoda equals(). Naravno, to takođe znači da upotrebom ove metode pri poređenju nastaje više koraka, pa postoji izvestan gubitak u performansama.
Pogledajmo sada i izvorni kod iz JDK 8:
public boolean contentEquals(CharSequence cs) {
// argument can be any CharSequence implementation
if (cs.length() != value.length) {
return false;
}
// Argument is a StringBuffer, StringBuilder or String
if (cs instanceof AbstractStringBuilder) {
char v1[] = value;
char v2[] = ((AbstractStringBuilder)cs).getValue();
int i = 0;
int n = value.length;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
// Argument is a String
if (cs.equals(this))
return true;
// Argument is a non-String, non-AbstractStringBuilder CharSequence
char v1[] = value;
int i = 0;
int n = value.length;
while (n-- != 0) {
if (v1[i] != cs.charAt(i))
return false;
i++;
}
return true;
}I ovde je nešto lakše za razumevanje: prvo se proverava da li je dužina argumenta jednaka; ako nije, vraća se false. Ako je argument instanca AbstractStringBuilder-a, uzima se njegov char niz i prolazi se kroz njega poredeći svaki element oba char niza. Ako je argument instanca String-a, direktno se poziva metoda equals da se uporede dva stringa. Ako je argument neki drugi objekat koji implementira interfejs CharSequence, prolazi se kroz njih poredeći svaki karakter.
