Entendendo Strings no Java

A String é uma classe fundamental do Java. Ela é basicamente uma sequência de caracteres. Porém, ao contrário de outras linguagens, no Java uma String não é um array de char. Vejamos como criar nossa primeira String:

String nome = "Rômulo";

Também podemos criar através do seu construtor:

String nome = new String("Rômulo");

Ambos os casos te dão uma referência para uma String com o valor “Rômulo”, mas há uma sutil diferença entre as duas abordagens, que será tratada na seção String Pool mais a frente.

Como disse antes, uma String é uma sequência de caracteres, então não seria surpresa dizer que ela implementa a interface CharSequence. Assim como a classe String, outras classes comuns em aplicações do dia a dia como StringBuilder e StringBuffer também implementam essa interface.

Imutabilidade

Uma String em Java é imutável. O que significa que seu conteúdo não pode ser alterado e ela não pode nem diminuir de tamanho. Todas as operações em cima de uma String resultam em uma nova String na memória, devidamente alocada para aquele texto em questão. Portanto, ao invocar um método em uma String, o seu conteúdo não será alterado na referência original.

String nome = "Rômulo";
nome.replace('u', 'o');
System.out.println(nome); // Rômulo

Concatenação

A operação de combinar duas Strings em uma só é chamada de concatenação. A forma mais comum de concatenar duas Strings é através do operador +. Esse operador pode atuar de duas formas em uma mesma linha de código. Eis as regras:

  1. Se ambos os operandos forem números, então o + significa uma soma

  2. Se algum dos operandos for uma String, o + significa concatenação

  3. A expressão é avaliada da esquerda para a direita

Vejamos alguns exemplos:

System.out.println(1 + 2);                 // 3
System.out.println("a" + "b");             // ab
System.out.println("a" + "b" + 3);         // ab3
System.out.println(1 + 2 + "c");           // 3c
System.out.println("c" + 1 + 2);           // c12

int tres = 3;
String quatro = "4";
System.out.println(1 + 2 + tres + quatro); // 64

O primeiro exemplo usa a primeira regra, uma vez que ambos os operandos são números. O segundo usa a regra número 2. Já o terceiro combina as regras 2 e 3. O quarto usa todas as 3 regras, enquanto o quinto nos mostra a importância da terceira regra, uma vez que uma ordem de avaliação diferente mudaria completamente o resultado. Por fim, o sexto é uma pegadinha. Devemos sempre estar atentos aos tipos de cada variável.

Há uma outra forma de concatenar Strings que não é tão usada no dia a dia mas pode pregar peças em quem olha. Veja se você consegue determinar o output do exemplo a seguir:

String s1 = "7";
String s2 = s1.concat("4");
s2.concat("1");
System.out.println(s2);

Se você disse que o resultado é 74, então acertou. Esse foi um teste para ver se você não esqueceu que as Strings são imutáveis.

Métodos importantes

Para todos esses métodos, lembre-se que uma String é uma sequência de caracteres e que seus índices começam em 0. A figura a seguir mostra cada caractere na String “animais” e seus respectivos índices.

length()

O método length() retorna a quantidade de caracteres presentes na String. Sua assinatura é:

  • int length()

      String string = "ANIMAIS";
      System.out.println(string.length()); // 7
    

indexOf()

O método indexOf() retorna o índice dentro da String onde ocorre a primeira ocorrência de um caractere ou substring. Ele também pode começar a busca a partir de uma posição específica, chamada de fromIndex. Caso nenhuma ocorrência seja encontrada, o método retorna -1. Suas assinaturas são as seguintes:

  • int indexOf​(int ch)

      String string = "ANIMAIS";
      System.out.println(string.indexOf('I')); // 2
      System.out.println(string.indexOf('b')); // -1
    
  • int indexOf​(int ch, int fromIndex)

      String string = "ANIMAIS";
      System.out.println(string.indexOf('I', 3)); // 5
      System.out.println(string.indexOf('N', 3)); // -1
    
  • int indexOf​(String str)

      String string = "ANIMAIS";
      System.out.println(string.indexOf("MAI")); // 3
      System.out.println(string.indexOf("nia")); // -1
    
  • int indexOf​(String str, int fromIndex)

      String string = "ANIMAIS";
      System.out.println(string.indexOf("A", 1)); // 4
      System.out.println(string.indexOf("M", 4)); // -1
    

substring()

Esse método nos permite extrair uma parte específica de uma String, com base em índices. É ideal para quando precisamos isolar trechos de um texto. Vejamos suas assinaturas:

  • substring(int beginIndex)

    Retorna a substring que começa com o caractere no índice especificado e se estende até o fim da String. Levanta IndexOutOfBoundsException caso o beginIndex seja negativo ou maior que o comprimento da String.

      String string = "ANIMAIS";
      System.out.println(string.substring(4)); // ais
    
  • substring(int beginIndex, int endIndex)

    Retorna a substring que começa com o caractere no índice especificado e se estende até o caractere do índice endIndex - 1. Portanto, retorna uma substring de tamanho endIndex - beginIndex. Levanta IndexOutOfBoundsException caso o beginIndex seja negativo, maior que o comprimento da String ou maior que endIndex. Também levanta IndexOutOfBoundsException caso endIndex seja maior que o comprimento da String.

      String string = "ANIMAIS";
      System.out.println(string.substring(1, 4)); // nim
    

charAt()

Retorna o caractere presente no index especificado. Levanta IndexOutOfBoundsException se o índice especificado for negativo ou maior que o comprimento da String. Eis sua assinatura e exemplo:

  • char charAt​(int index)

      String string = "ANIMAIS";
      System.out.println(string.charAt(0)); // A
      System.out.println(string.charAt(3)); // M
      System.out.println(string.charAt(8)); // StringIndexOutOfBoundsException: String index out of range: 8
    

toLowerCase()

Esse método converte todos os caracteres dentro da String para minúsculo. Eis sua assinatura e exemplo:

  • String toLowerCase()

      String string = "ANIMAIS";
      System.out.println(string.toLowerCase()); // animais
    

toUpperCase()

Método irmão do toLowerCase() mas nesse caso converte todos os caracteres para maiúsculo. Assinatura e exemplo a seguir:

  • String toUpperCase()

      String string = "aNimais";
      System.out.println(string.toUpperCase()); // ANIMAIS
    

startsWith()

O startsWith() permite que verifiquemos se a String que está invocando o método começa com o prefixo informado.

  • boolean startsWith​(String prefix)

    Verifica se a String começa com o prefixo especificado.

      String string = "ANIMAIS";
      System.out.println(string.startsWith("AN")); // true
      System.out.println(string.startsWith("NI")); // false
    
  • boolean startsWith​(String prefix, int toffset)

    Verifica se a String começa com o prefixo especificado a partir de um determinado índice (offset). No exemplo abaixo queremos ver se a quinta posição da String começa com “ai”.

      String string = "ANIMAIS";
      System.out.println(string.startsWith("AI", 4)); // true
      System.out.println(string.startsWith("MAI", 4)); // false
    

endsWith()

Método irmão do startsWith(). Nesse caso é verificado se a String termina com o sufixo informado. Para realizar essa verificação, o Java usa internamente o método startsWith(String prefix, int tooffset) apresentado acima.

  • boolean endsWith​(String suffix)

      String string = "ANIMAIS";
      System.out.println(string.endsWith("IS")); // true
      System.out.println(string.endsWith("aIS")); // false
    

equals()

O equals() compara a String com o objeto passado por parâmetro. Retorna true se e somente se o objeto passado não for null e representar a mesma sequência de caracteres da String que invocou o método.

  • boolean equals​(Object anObject)

      String string = "ANIMAIS";
      System.out.println(string.equals("ANIMAIS")); // true
      System.out.println(string.equals("Animais")); // false
    

equalsIgnoreCase()

Método irmão do equals(). Nesse caso, porém, o case é ignorado. Ou seja, um caractere minúsculo é considerado igual ao seu equivalente maiúsculo, e vice-versa.

  • boolean equalsIgnoreCase​(String anotherString)

      String string = "ANIMAIS";
      System.out.println(string.equals("ANIMAIS")); // true
      System.out.println(string.equals("Animais")); // true
      System.out.println(string.equals("AniMaiS")); // true
    

replace()

O replace() permite substituir caracteres ou substrings dentro de uma String. Caso o caractere ou a substring não exista dentro da String, então a String original é retornada. Importante notar que esse método substitui todas as ocorrências do caractere ou substring informada.

  • String replace​(char oldChar, char newChar)

    Substitui todas as ocorrências de um caractere por outro.

      String string = "ANIMAIS";
      System.out.println(string.replace('A', 'B')); // BNIMBIS
      System.out.println(string.replace('a', 'b')); // ANIMAIS
    
  • String replace​(CharSequence target, CharSequence replacement)

    Substitui todas as ocorrências da sequência de caracteres passada por outra.

      String string = "ANIMAIS";
      StringBuilder sb = new StringBuilder("mais");
      System.out.println(string.replace("ani", "ina")); // ANIMAIS
      System.out.println(string.replace("be", "in")); // ANIMAIS
      System.out.println(string.replace(sb, "MENOS")); // ANIMENOS
    

contains()

O método contains() verifica se uma sequência de caracteres está presente na String que o invocou. Ele retorna true se e somente se a substring estiver presente.

  • boolean contains​(CharSequence s)

      String string = "ANIMAIS";
      StringBuilder sb = new StringBuilder("NIM");
      System.out.println(string.contains("An")); // false
      System.out.println(string.contains("ANI")); // true
      System.out.println(string.contains(sb)); // true
    

trim()

Esse método retorna uma String com todos os espaços em branco removidos do começo e do fim da String. Porém, ele não lida bem com todos os whitespace Unicode. Um whitespace consiste de espaços, tabs (\t), nova linha (\n), carriage return (\r), entre outros. A seguir sua assinatura e exemplos:

  • String trim()

      var textoComEspacos = "  ANIMAIS  ";
      var textoComEspacosUnicode = "  ANIMAIS\u2000";
      System.out.println(textoComEspacos.trim()); // "ANIMAIS"
      System.out.println(textoComEspacosUnicode.trim()); // "ANIMAIS "
    

strip()

O strip() faz tudo que o trim() faz, mas suporta Unicode. Sua assinatura é:

  • String strip()

      var textoComEspacos = "  ANIMAIS  ";
      var textoComEspacosUnicode = "  ANIMAIS\u2000";
      System.out.println(textoComEspacos.strip()); // "ANIMAIS"
      System.out.println(textoComEspacosUnicode.strip()); // "ANIMAIS"
    

stripLeading()

Remove os espaços apenas do início da String. Vejamos sua assinatura e exemplos:

  • String stripLeading()

      var textoComEspacos = "\u2000  ANIMAIS  ";
      System.out.println(textoComEspacos.strip()); // "ANIMAIS  "
    

stripTrailing()

Remove os espaços apenas do fim da String.

  • String stripTrailing()

      var textoComEspacos = "  ANIMAIS\u2000  ";
      System.out.println(textoComEspacos.strip()); // "  ANIMAIS"
    

intern()

Quando esse método é invocado, ele verifica se na String Pool já existe uma String cujo conteúdo seja igual ao da String que o invocou. Caso exista, a referência dessa String é retornada. Caso contrário, a String é adicionada à String Pool e sua referência é retornada. Veremos sobre String Pool logo em seguida.

  • String intern()

      System.out.println(string == new String("ANIMAIS")); // false
      System.out.println(string == new String("animais").intern()); // true
    

Encadeamento de métodos

É muito comum utilizarmos os métodos acima combinados, para chegar no resultado que queremos, como mostrado aqui:

String string = " animais  ";
System.out.println(string.trim().toUpperCase().replace('A', 'a')); // aNIMaIS

String Pool

Como as Strings estão em todo lugar no Java, elas usam muita memória das aplicações. Sabendo que várias Strings podem repetir em uma aplicação e que isso pode acarretar em um desperdício de recursos, o Java resolve esse problema reutilizando as repetidas. A String Pool ou Intern Pool é onde a JVM armazena todas essas Strings.

Na String Pool ficam os valores literais e constantes que aparecem em nossa aplicação. Por exemplo, “Método” é um valor literal e portanto vai pra String Pool. Já meuObjeto.toString() é uma String mas não literal, e portanto não vai automaticamente pra String Pool. Vejamos alguns cenários interessantes:

String x = "cavalos";
String y = "cavalos";
System.out.println(x == y); // true

Lembre-se Strings são imutáveis e os literais são postos na String Pool. Nesse caso a JVM só precisou criar um único literal na memória. Ambas as variáveis x e y apontam para o mesmo endereço de memória e portanto a verificação de igualdade é verdadeira. Agora, considere o seguinte cenário:

String x = "zebras";
String z = " zebras".trim();
System.out.println(x == z); // false

Nesse caso nós não temos o mesmo literal. Apesar de x e z serem o mesmo texto, z só é computado em tempo de execução. Já que z não é igual a x em tempo de compilação, uma nova String é criada na memória.

String stringUnica = "gatos adultos";
String concat = "gatos ";
concat += "adultos";
System.out.println(stringUnica == concat); // false

Concatenação de Strings é a mesma coisa que invocar um método e resulta em uma nova String em memória.

String x = "Elefantes";
String y = new String("Elefantes");
System.out.println(x == y); // false

A declaração da variável x usa a String Pool normalmente. Já a declaração de y pede explicitamente para JVM não utilizar a String Pool e sim alocar uma nova String em memória.

No entanto, nós podemos dizer ao Java explicitamente que queremos usar a String Pool. Para isso usamos o método intern() que vimos anteriormente.

String nome = "Rômulo";
String nome2 = new String("Rômulo").intern();
System.out.println(nome == nome2); // true

Para a variável nome estamos usando a String Pool igual comentamos anteriormente. Já para nome2 nós forçamos o uso da String Pool ao chamar o método intern(). Quando fazemos isso o Java verifica que a String “Rômulo” já está na String Pool e atribui a nome2 a referência para essa String já existente. Vejamos mais alguns casos interessantes.

String um = "mosca" + 1;
String dois = "m" + "o" + "s" + "c" + "a" + "1";
String tres = "m" + "o" + "s" + "c" + "a" + new String("1");
System.out.println(um == dois); // true
System.out.println(um == dois.intern()); // true
System.out.println(um == tres); // false
System.out.println(um == tres.intern()); // true

As variáveis um e dois são constantes em tempo de compilação e portanto usam a String Pool. Já a variável tres não é constante em tempo de execução, porque usa o construtor da String, o que leva a uma alocação em tempo de execução. Portanto, ao compararmos um e tres, temos que eles não apontam para a mesma referência. Esse cenário muda quando chamamos o método intern() na variável tres.

Referências

Java: The Complete Reference

Certificação de Java:

Java 11

Java 17

Java 21

0
Subscribe to my newsletter

Read articles from Rômulo Borges de Almeida directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rômulo Borges de Almeida
Rômulo Borges de Almeida