Niezmienny obiekt to obiekt, w którym pola wewnętrzne (lub przynajmniej wszystkie pola wewnętrzne, które wpływają na jego zachowanie zewnętrzne) nie mogą zostać zmienione.
Niezmienne łańcuchy mają wiele zalet:
Wydajność: wykonaj następującą operację:
String substring = fullstring.substring(x,y);
Podstawowy C dla metody substring () jest prawdopodobnie mniej więcej taki:
// Assume string is stored like this:
struct String { char* characters; unsigned int length; };
// Passing pointers because Java is pass-by-reference
struct String* substring(struct String* in, unsigned int begin, unsigned int end)
{
struct String* out = malloc(sizeof(struct String));
out->characters = in->characters + begin;
out->length = end - begin;
return out;
}
Pamiętaj, że nie trzeba kopiować żadnych znaków! Gdyby obiekt String był modyfikowalny (znaki mogłyby się później zmienić), musiałbyś skopiować wszystkie znaki, w przeciwnym razie zmiany znaków w podciągu zostaną odzwierciedlone w innym ciągu później.
Współbieżność: Jeśli wewnętrzna struktura niezmiennego obiektu jest poprawna, zawsze będzie poprawna. Nie ma szans, że różne wątki mogą stworzyć nieprawidłowy stan w tym obiekcie. Dlatego niezmienne obiekty są bezpieczne dla wątków .
Odśmiecanie: Śmieciarka znacznie łatwiej podejmuje logiczne decyzje dotyczące niezmiennych obiektów.
Istnieją jednak również wady niezmienności:
Wydajność: Zaczekaj, myślałem, że powiedziałeś, że wydajność jest zaletą niezmienności! Czasami tak jest, ale nie zawsze. Weź następujący kod:
foo = foo.substring(0,4) + "a" + foo.substring(5); // foo is a String
bar.replace(4,5,"a"); // bar is a StringBuilder
Obie linie zastępują czwarty znak literą „a”. Drugi fragment kodu jest nie tylko bardziej czytelny, ale także szybszy. Zobacz, jak będziesz musiał wykonać podstawowy kod dla foo. Podciągi są łatwe, ale teraz, ponieważ w polu piątym jest już znak i coś innego może odnosić się do foo, nie możesz go po prostu zmienić; musisz skopiować cały ciąg znaków (oczywiście niektóre z tych funkcji są wyodrębnione do funkcji w prawdziwym bazowym C, ale chodzi tutaj o pokazanie kodu, który zostanie wykonany w jednym miejscu).
struct String* concatenate(struct String* first, struct String* second)
{
struct String* new = malloc(sizeof(struct String));
new->length = first->length + second->length;
new->characters = malloc(new->length);
int i;
for(i = 0; i < first->length; i++)
new->characters[i] = first->characters[i];
for(; i - first->length < second->length; i++)
new->characters[i] = second->characters[i - first->length];
return new;
}
// The code that executes
struct String* astring;
char a = 'a';
astring->characters = &a;
astring->length = 1;
foo = concatenate(concatenate(slice(foo,0,4),astring),slice(foo,5,foo->length));
Zauważ, że konkatenat jest wywoływany dwukrotnie, co oznacza, że cały łańcuch musi być zapętlony! Porównaj to z kodem C bar
operacji:
bar->characters[4] = 'a';
Zmienna operacja na łańcuchach jest oczywiście znacznie szybsza.
Podsumowując: W większości przypadków potrzebujesz niezmiennego ciągu. Ale jeśli musisz dużo dodawać i wstawiać do łańcucha, potrzebujesz zmienności prędkości. Jeśli chcesz mieć korzyści z bezpieczeństwa współbieżności i odśmiecania, kluczem jest utrzymanie zmiennych obiektów w pobliżu metody:
// This will have awful performance if you don't use mutable strings
String join(String[] strings, String separator)
{
StringBuilder mutable;
boolean first = true;
for(int i = 0; i < strings.length; i++)
{
if(!first) first = false;
else mutable.append(separator);
mutable.append(strings[i]);
}
return mutable.toString();
}
Ponieważ mutable
obiekt jest lokalnym odwołaniem, nie musisz się martwić o bezpieczeństwo współbieżności (tylko jeden wątek go dotyka). Ponieważ nigdzie indziej nie jest tam przywoływany, jest przydzielany tylko na stosie, więc jest on zwalniany natychmiast po zakończeniu wywołania funkcji (nie musisz się martwić o odśmiecanie). I zyskujesz wszystkie zalety wydajności, zarówno zmienności, jak i niezmienności.