Tipos primitivos vs referências em Java e a imutabilidade das Strings

Ana Carolina Cortez - Sep 10 - - Dev Community

Em Java temos dois tipos de dados (ou variáveis): os primitivos e os não-primitivos (também chamados de referências).

Os tipos primitivos têm os seus valores literais guardados na Stack, memória de armazenamento temporário e de curta duração, gerenciada pela Java Virtual Machine (JVM). [leia mais sobre os tipos de memória aqui]

As variáveis primitivas são divididas em quatro grupos:

1. Tipos inteiros: utilizados para armazenar números inteiros (sem parte decimal). São eles: byte, short, int, long. O long possui a letra "L" ou "l" ao final do número, para diferenciação.

2. Tipos de ponto flutuante:: Usados para armazenar números com parte decimal (números reais). São eles: float, double. O float possui a letra "F" ou "f" ao final do número, para diferenciação.

3. Tipo caractere: Utilizado para armazenar caracteres únicos (como letras, dígitos ou símbolos): char. São inicializados com aspas simples '', em vez de duplas "".

4. Tipo boleano: Utilizado para armazenar valores lógicos (true ou false): bool

Veja na tabela abaixo qual o intervalo de valores que cada tipo comporta, além de seus valores "default" (padrão):

Image description
Em formato científico, o E representa um expoente. Por exemplo, 1.23E+10, é igual a 1,23 x 10^10

O que é um valor padrão? É o valor que a variável vai assumir caso ela não tenha sido inicializada. Para assumir esse valor, contudo, ela precisa ser global ou constantes (final).

public final boolean isTrue;
Enter fullscreen mode Exit fullscreen mode

Nessa linha de código, a variável "isTrue" não foi inicializada, mas o compilador não apresentará erro, pois ele mesmo vai considerar o valor padrão "false" para a variável boleana.

Aqui, um alerta importante: se o escopo da variável for local, ou seja, se ela tiver sido declarada dentro de uma função, nós, programadores, seremos obrigados a atribuir um valor para ela. Caso contrário, haverá um erro de compilação.

public void teste(){
        int i = 2;
        int j;

        if (i < 10){
            j = 5;
        }

        System.out.println(j);
    }
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, ainda que saibamos que "2 < 10" retorna "true", o compilador, que jamais executa os códigos que traduz durante seu processo, não sabe que a condição é verdadeira e que a variável primitiva "j" sempre será inicializada. Desta forma, ao tentar rodar o código, aparecerá um erro de compilação: "error: variable j might not have been initialized".

Endereços de memória

O segundo tipo de dado em Java é chamado de referência. Essas variáveis armazenam uma referência, ou seja, o endereço de memória de um objeto, em vez de armazenar o seu valor diretamente, como ocorre com os tipos primitivos. Esse armazenamento ocorre na memória Heap.

Os tipos referências são classes, interfaces, enums e objetos, de maneira geral.

Aqui, um adendo. A String que usamos amplamente nos nossos códigos é uma classe, e não um tipo primitivo. Note que até o nome vem em letra maiúscula, como é convenção de nomenclatura das classes em Java.

A String possui, inclusive, métodos, como o length(), que retorna o tamanho do texto armazenado nela, o charAt(int index), que retorna o índice de um caractere no texto, ou substring(int beginIndex, int endIndex), que retorna um pedaço de uma string.

Mas, se você quiser facilitar a manipulação de dados primitivos, o Java também permite. Para isso, ele possui a classe Wrapper, que já vem com uma série de métodos built-in para trabalhar com os tipos básicos.

Os Wrappers, basicamente, trazem o mesmo nome da variável primitiva, porém, com a primeira letra maiúscula:

  • Byte para byte
  • Short para short
  • Integer para int
  • Long para long
  • Float para float
  • Double para double
  • Character para char
  • Boolean para boolean
public class WrapperExample {
    public static void main(String[] args) {
        String numeroStr = "123";
        Integer num1 = Integer.parseInt(numeroStr);
        Integer num2 = 200;

        int resultadoComparacao = Integer.compare(num1, num2);

        if (resultadoComparacao < 0) {
            System.out.println(num1 + " é menor que " + num2);
        } else if (resultadoComparacao > 0) {
            System.out.println(num1 + " é maior que " + num2);
        } else {
            System.out.println(num1 + " é igual a " + num2);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Nesse código de exemplo, utiliza-se o Wrapper de int para converter uma string em número (Integer.parse) e depois compará-lo com outro número (Integer.compare).

A String, contudo, tem uma particularidade que outras classes não possuem. Ela é imutável.

Vamos refletir por meio deste exemplo básico:

public class Main {
  public static void main(String[] args) {

    String text1 = "Hello";
    String text2 = text1;

    System.out.println(text1); //output: Hello
    System.out.println(text2); //output: Hello

    text1 = "Weird";
    System.out.println(text1); //output: Weird
    System.out.println(text2); //output: Hello

    text2 = "World";
    System.out.println(text1); //output: Weird
    System.out.println(text2); //output: World

    TestClass test1 = new TestClass("propertyValue");
    TestClass test2 = test1;

    System.out.println(test1.getProperty()); //output: propertyValue
    System.out.println(test2.getProperty()); //output: propertyValue

    test2.setProperty("newValue");

    System.out.println(test1.getProperty()); //output: newValue
    System.out.println(test2.getProperty()); //output: newValue   
  }

}
Enter fullscreen mode Exit fullscreen mode

Neste caso, note que, por mais que a String "text2" aponte para "text1", mudanças em "text2" não refletiram mudanças em "text1". Agora, quando o Objeto "test2", que apontava para "test1" teve uma propriedade alterada, essa alteração foi sim refletiva em "test1" também.

Ué, mas as variáveis de referência não armazenam endereços de memória, em vez de valores literais? Sim. Armazenam. O que acontece é que os desenvolvedores da linguagem Java tomaram a decisão de deixar variáveis do tipo String imutáveis. Isso significa que, uma vez definido, o valor de um objeto String não pode ser alterado de maneira indireta por outro objeto.

No exemplo, portanto, não estamos alterando o valor do objeto que text1 referenciava anteriormente (já que String é imutável). Em vez disso, estamos criando um novo objeto String com o valor "Weird" e fazendo text1 apontar para esse novo objeto. Já text2 ainda apontará para o objeto original "Hello" e é por isso que ele mantém o valor "Hello".

Em suma, atribuir um novo valor a uma string não modifica o valor do objeto existente, apenas muda a referência para um novo objeto.

Já os objetos de classes customizadas, como TestClass, são mutáveis. Ambas as referências test1 e test2 apontam para o mesmo objeto, então alterar o estado de uma delas reflete na outra.

. . . .
Terabox Video Player