String é um tipo primitivo?
String não é um tipo primitivo do Java, mas sim um tipo de referência.
Os tipos de dados primitivos disponíveis são eles: byte, short, int, long, float, double, boolean e char, já os tipos de referência são três: classes, interfaces e array. A String é um tipo de referência do subtipo classe, também chamado de class type, apesar disso a linguagem de programação Java dá um suporte especial para sequencias de caracteres através da classe java.lang.String, por conta desse apoio especial dado pela linguagem, faz muitos pensarem que String é um tipo primitivo, mas não é.
Qual a grande diferença entre tipos primitivos e tipos de referência?
É importante entender a diferença de como um tipo de referência (como a String) e um tipo primitivo (como o int) são armazenados na memória.
Um tipo de dado primitivo é armazenado exatamente como ele é atribuido à variavel, por exemplo:
int x = 7;
Se procuramos no endereço de memória que foi armazenado este valor, veremos armazenado o valor "7" assim como o esperado. Agora um tipo de referência armazena a sua referência e não o seu valor em si, por exemplo:
String s1 = "diasnetoj";
Se olharmos para o endereço de memória que a variável s1 está alocada, não veremos o valor "diasnetoj" armazenado, mas apenas uma referencia, isso é, o endereço de memória de onde os detalhes deste objeto estão armazenados.
Como posso criar um objeto String?
Podemos criar um objeto String de duas formas, de forma literal e através do operador new, a primeira e mais simples, é de forma literal utilizando aspas:
"diasnetoj"
A segunda é utilizando o operador new, instanciando a classe String como qualquer outra classe Java, como por exemplo utilizando o construtor que recebe uma String:
new String("diasnetoj");
Quais as diferenças em criar a String com aspas e com o operador new?
As diferenças de criar um objeto String de modo literal utilizando aspas ou instanciando um objeto da classe String através do operador new, é que a primeira maneira, a Java Virtual Machine (JVM) procura se já existe esta String criada no String pool, que é uma área de memória onde as Strings criadas são salvas, para depois poderem ser reaproveitadas. Se já foi criado um objeto com o mesmo valor, o que a JVM faz é apenas referenciar, ou seja, apontar a instancia criada para o mesmo endereço de memória do objeto com o mesmo valor, assim evitando ter que alocar um novo endereço de memória para algo que já existe.
Podemos ver isso na prática, desde que se tenha em mente de que o operador == compara os valores armazenado na memória, ou seja, para os tipos de referência, será comparado se as referências são iguais (mesmos endereços de memória), para os tipos primitivos, serão comparados se os valores primitivos são iguais (mesmos valores primitivos).
Comparando Strings:
public class Example {
public static void main(String[] args){
String s1 = "diasnetoj";
String s2 = "diasnetoj";
System.out.println(s1==s2);
}
}
Este exemplo, irá retornar verdadeiro:
true
Isso por causa do String pool citado anteriormente, mesmo que a String seja um tipo de referência, quando a segunda String é criada, ao invés de alocar um novo endereço para este valor, ele verifica se já não existe um valor igual na memória, e se encontrar, apenas referencia o endereço da variavel s2 para o mesmo endereço que a variável s1 está apontando. Está sendo comparado se os endereços de memória são iguais e não o conteúdo "diasnetoj" em si.
Por outro lado, utilizando o operador new, a JVM cria um objeto String sem que o seu valor seja armazenado no pool de Strings e sem verificar se já existe o valor em memória, ou seja, é criado um novo objeto String com a possibilidade de resultar em dois objetos alocados com o mesmo valor na memória em locais diferentes. Uma redundância, uma vez que poderiam apontar para o mesmo objeto e ocupar menos espaço, reaproveitando o que já tem.
Um exemplo usando o operador new:
public class Example {
public static void main(String[] args){
String s1 = "diasnetoj";
String s2 = new String("diasnetoj");
System.out.println(s1==s2);
}
}
o resultado será falso:
false
Isso porque s1 está apontando para um endereço de memória diferente da variável s2.
Também é interessante conhecer o método intern(), este método verifica se a String já existe no pool de Strings, se existir ele retornará a referência da String no pool, senão, ele armazena a String no pool e a retorna a sua referência recém criada.
Veja um exemplo:
public class Example {
public static void main(String[] args){
String s1 = "diasnetoj";
String s2 = new String("diasnetoj").intern();
System.out.println(s1==s2);
}
}
Neste caso, a única alteração no exemplo, é que foi adicionado o método intern() após instanciar a classe String com o operador new, isso fez com que fosse retornado a referência da String "diasnetoj" atribuida à variavel s1 para ser armazenada à variável s2.
Caso o objetivo seja comparar os valores entre as variáveis String, o método equals é o indicado, para isso, vamos reutilizar o exemplo que era retornado falso, só que agora utilizando o método equals ao invés do operador ==:
public class Example {
public static void main(String[] args){
String s1 = "diasnetoj";
String s2 = new String("diasnetoj");
System.out.println(s1.equals(s2));
}
}
Este exemplo retornará verdadeiro:
true
Já que será comparado se os dois objetos tem a mesma sequências de caracteres, e não se estão apontando para o mesmo endereço na memória.
Recapitulando, variáveis primitivias armazenam valores primitivos, variáveis de referência como mostrado anteriormente armazenam endereços de memória.
public class Example {
public static void main(String[] args){
int primitivo1 = 7;
int primitivo2 = 7;
System.out.println(primitivo1==primitivo2);
}
}
O retorno deste exemplo será verdadeiro:
true
Não porque existe um pool, mas porque o valor armazenado na memória é exatamente este, "7", logo são iguais.
Toda variável tem um valor padrão.
Quando declaramos uma variável, seja do tipo primitivo ou do tipo referência, ela é inicializada com um valor, e nem sempre é necessário atribuir um valor para ela, e quando não é atribuido um valor à variável ela assume o seu valor default.
Para cada tipo de dado, existe um valor default:
| Tipo de dado | Valor default ou Valor padrão |
|---|---|
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
| char | '\u0000' |
| String (ou qualquer outro tipo referência) | null |
| boolean | false |
Perceba que o valor padrão de uma String sem ter recebido nenhuma atribuição é null, assim como acontece com qualquer outro tipo de referência, como até mesmo os arrays.
A classe String, assim como todo outro tipo de referencia no Java, herda da classe Object (com exceção da própria classe Object, porque ele já é o próprio Object), já os tipos de dados primitivos não.
Podemos ver isso na prática, através do operador instanceof. Este operador compara se um objeto é uma instancia de uma classe, no caso, queremos verificar se um objeto String "diasideas" é uma instancia da classe Object.
public class Example {
public static void main(String[] args){
String objetoString = "diasideas";
System.out.println(objetoString instanceof Object);
}
}
true
Agora vamos tentar com um tipo primitivo:
public class Example {
public static void main(String[] args){
int primitivoInt = 1;
System.out.println(primitivoInt instanceof Object);
}
}
Este código nem irá compilar, porque o operador instanceof requer um tipo referencia, e não um tipo primitivo, como neste caso o int.
Podemos utilizar instanceof com um array:
public class Example {
public static void main(String[] args){
int[] array = new int[]{1, 2, 3};
System.out.println(array instanceof Object);
}
}
O resultado será verdadeiro:
true
String são imutáveis.
Um objeto é considerado imutável se o seu valor nunca é alterado uma vez que ele foi construído.
Neste exemplo:
public class Example {
public static void main(String[] args){
String s1 = "diasideas";
System.out.println(s1);
s1 = "diasnetoj";
System.out.println(s1);
}
}
O resultado será:
diasideas
diasnetoj
Mesmo que aparentemente alteramos o valor da String, na verdade o que aconteceu, foi que a variável s1 deixou de apontar para o endereço de memória onde está armazenado a referência para "diasideas" e começou a apontar para o endereço de memória onde está a referência para "diasnetoj", ou seja, foi criada uma nova String e atribuida para a variável s1, e não alterada a String existente, enquanto isso, nenhuma variável aponta para "diasideas", mas essa String ainda está lá, imodificável.
Vamos criar um método que recebe o valor de uma String como parâmetro e dentro do método alteraremos este valor, vamos ver qual será o resultado se pedirmos para mostrar o valor da variável que passamos como parâmetro, se ela será mudada ou não:
public class Example {
public static void main(String[] args){
String s1 = "diasideas";
mudarValor(s1);
System.out.println(s1);
}
public static void mudarValor(String s2){
s2 = "diasnetoj";
}
}
O console apresentará:
"diasideas"
Agora vamos pegar um objeto mutável, como a classe java.util.Date, e fazer o mesmo:
public class Example {
public static void main(String[] args){
Date d1 = new Date(93, Calendar.DECEMBER, 27);
mudarValor(d1);
System.out.println(new SimpleDateFormat("dd/MM/yyyy").format(d1));
}
public static void mudarValor(Date d2){
d2.setYear(92);
d2.setDate(14);
d2.setMonth(Calendar.SEPTEMBER);
}
}
O resultado será:
14/09/1992
Diferente do exemplo com a String, toda a alteração realizada à variável d2 fez com que a alteração se refletisse no valor armazenado no endereço de memória de onde esta variável está alocada. Neste caso, não foi criado um novo Date, mas sim de fato, alterado o seu valor na memória.
Objetos imutáveis traz algumas vantagens, e são muito uteis em aplicações concorrentes, mas isso é assunto para outra publicação.
StringBuffer e StringBuilder, são classes mutáveis para manipulação de Strings.
StringBuffer: é uma mutável sequencia de caracteres, e é thread-safe. Um objeto StringBuffer é como uma String, mas pode ser modificado.
StringBuilder: Também é uma mutável sequência de caracateres assim como StringBuffer, a diferença é que não tem a garantia de sincronização (não thread-safe). O seu uso é recomendado em relação ao StringBuffer uma vez que será mais rápido, mas isso depende muito do cenário.
Referências:
https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
https://docs.oracle.com/javase/tutorial/java/data/numberformat.html
http://pages.cs.wisc.edu/~bahls/cs302/PrimitiveVsReference.html
http://www.programmerinterview.com/index.php/java-questions/difference-between-a-primitive-type-and-a-class-type/
https://www.gitbook.com/book/otaviojava/imergindo-na-jvm
https://docs.oracle.com/javase/tutorial/java/javaOO/variables.html
https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html
https://docs.oracle.com/javase/tutorial/java/data/buffers.html
https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html
http://docs.oracle.com/javase/7/docs/api/java/lang/StringBuffer.html