Java Strings

Quase todo programa trabalha com texto: nomes de alunos, mensagens, endereços, senhas. Em Java, um texto é uma String. Nesta aula você vai entender o que ela é, como criar uma, medir o tamanho e, principalmente, como comparar duas Strings do jeito certo, porque o jeito "óbvio" (==) é a pegadinha mais famosa do Java.

Você vai aprender
  • O que é uma String e como criar uma com aspas duplas.
  • Como descobrir quantos caracteres ela tem com length().
  • Por que uma String nunca muda (imutabilidade) e o que isso significa na prática.
  • A diferença entre == e equals, com a prova real de que == engana.
  • Como comparar ignorando maiúsculas (equalsIgnoreCase) e em ordem alfabética (compareTo).
  • A diferença entre String vazia ("") e null, e a NullPointerException.

Todos os exemplos rodaram no computador fictício da Ana (o notebook), com o JDK 25. Você pode repetir cada um no editor do guia, nas caixas Experimente.


A ideia: um colar de letras

Imagine um colar de contas, em que cada conta tem uma letra. O colar Ana tem três contas: A, n e a. Uma String é exatamente isso: uma sequência de caracteres, um atrás do outro, em ordem. O nome vem do inglês string, "cordão", "fileira" (como em a string of pearls, "um colar de pérolas").

Os caracteres podem ser letras, números, espaços, pontuação, acentos, emojis. Na aula char e boolean você viu o tipo char, que guarda um caractere. A String guarda vários (ou nenhum).

TipoGuardaEscrito comExemplo
charUm caractereAspas simples '…''A'
StringZero ou mais caracteresAspas duplas "…""Ana", "A", ""

Criar uma String

Você cria uma String escrevendo o texto entre aspas duplas. Esse texto escrito direto no código se chama literal (ele é "literalmente" o valor). Depois, guarda numa variável do tipo String:

public class Main {
    public static void main(String[] args) {
        String nome = "Ana";
        String fruta = "maçã";
        String vazia = "";
        System.out.println(nome);
        System.out.println(nome.length());
        System.out.println(fruta.length());
        System.out.println(vazia.length());
        System.out.println("Ana Souza".length());
    }
}
String
O tipo da variável. Repare no S maiúsculo: String não é um tipo primitivo como int; é uma classe que vem pronta com o Java (você vai estudar classes no Nível 2). Por enquanto, basta saber que é o tipo dos textos.
nome
O nome da variável, como em qualquer variável.
"Ana"
O literal: o texto entre aspas duplas. As aspas não fazem parte do texto; elas só marcam onde ele começa e termina.
""
Duas aspas coladas: a String vazia, um texto com zero caracteres. É um texto válido, só que sem nada dentro.
nome.length()
Pergunta à String quantos caracteres ela tem. O ponto quer dizer "da String nome, use o método length" (length = comprimento). Os parênteses vazios são obrigatórios.
$ java Main.java
Ana
3
4
0
9

Linha por linha: Ana é o conteúdo; 3 é o tamanho de "Ana"; 4 é o de "maçã" (o ç e o ã contam como um caractere cada); 0 é o da String vazia; e 9 é o de "Ana Souza", porque o espaço também é um caractere.

Método: uma pergunta ou um pedido que você faz ao valor

Um método é uma ação que a String sabe fazer. Você chama com valor.nome(): o valor, um ponto, o nome do método e parênteses. A String tem dezenas de métodos (deixar em maiúsculas, cortar um pedaço, procurar uma letra); eles têm uma aula só para eles: Métodos de String.

length() com parênteses

Em Strings é length(), um método, com parênteses. Nos arrays, que você vai ver mais adiante, é length sem parênteses. Misturar os dois é um erro muito comum; o javac avisa com cannot find symbol.

Brinque com o código: troque os textos, acrescente espaços ou acentos e veja como o length() muda. Experimente também "".length() e " ".length() (uma String com um espaço).

public class Main {
    public static void main(String[] args) {
        String nome = "Ana";
        String cidade = "São Paulo";
        System.out.println(nome + " tem " + nome.length() + " letras");
        System.out.println(cidade + " tem " + cidade.length() + " caracteres");
    }
}

Strings são imutáveis

Imutável quer dizer "que não pode ser mudado". Depois de criada, uma String nunca muda. Os métodos que parecem "mudar" o texto, como toUpperCase() (deixar em maiúsculas), na verdade criam uma String nova e devolvem essa nova. A original continua igual.

É como uma foto impressa: se você quer a foto em preto e branco, a loja faz outra cópia em preto e branco. A foto colorida que você levou continua colorida. Veja a prova:

public class Main {
    public static void main(String[] args) {
        String nome = "ana";
        nome.toUpperCase();
        System.out.println(nome);

        String grande = nome.toUpperCase();
        System.out.println(grande);
        System.out.println(nome);

        nome = nome.toUpperCase();
        System.out.println(nome);
    }
}
$ java Main.java
ana            « a linha 4 criou "ANA", mas ninguém guardou: o texto se perdeu
ANA            « desta vez a String nova foi guardada em "grande"
ana            « e "nome" continua o mesmo
ANA            « agora "nome" passou a apontar para a String nova
LinhaO que acontece
nome.toUpperCase();Cria a String "ANA" e a devolve, mas o resultado não foi guardado em lugar nenhum. É jogado fora. nome continua "ana".
String grande = nome.toUpperCase();Cria "ANA" e guarda na variável nova grande.
nome = nome.toUpperCase();Cria "ANA" e guarda na própria variável nome. A String "ana" antiga não mudou: a variável é que passou a guardar outra.

Repare que o Java não reclama da linha 4: o código compila e roda, só não faz o que um iniciante espera. Esse é um engano muito comum. A regra: todo método de String que "transforma" o texto devolve uma String nova; guarde o resultado.

A variável pode mudar; a String não

Uma variável String pode receber outro texto a qualquer momento (nome = "Bruno";). O que não muda é o texto em si. Pense na variável como uma etiqueta presa a um colar: dá para tirar a etiqueta e prender em outro colar, mas não dá para trocar as contas do colar.

Por que o Java faz assim? Porque é mais seguro: se você entrega o seu texto para outra parte do programa, tem certeza de que ninguém vai alterá-lo pelas suas costas. E permite uma economia que você vai ver agora, na comparação.


Comparar Strings: == × equals

Esta é a seção mais importante da aula. Em Java existem duas formas de perguntar "estes dois textos são iguais?", e só uma está certa para Strings.

FormaO que perguntaComparação do dia a dia
a == b"a e b são o mesmo objeto na memória?""Este livro é o mesmo exemplar que aquele?"
a.equals(b)"a e b têm o mesmo conteúdo?""Estes dois livros têm o mesmo texto?"

Duas cópias do mesmo livro, compradas em lojas diferentes, têm o mesmo texto (equals dá true), mas são exemplares diferentes (== dá false). Quase sempre, o que você quer saber é se o texto é igual. Veja a prova, com o JDK de verdade:

public class Main {
    public static void main(String[] args) {
        String a = "Ana";
        String b = "Ana";
        String c = new String("Ana");
        String parte = "An";
        String d = parte + "a";

        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a == d);

        System.out.println(a.equals(b));
        System.out.println(a.equals(c));
        System.out.println(a.equals(d));
    }
}
$ java Main.java
true           « a == b: os dois literais são o MESMO objeto
false          « a == c: new String criou um objeto novo
false          « a == d: o texto montado durante a execução é outro objeto
true           « a.equals(b)
true           « a.equals(c): o conteúdo é igual
true           « a.equals(d): o conteúdo é igual

As quatro variáveis guardam o texto Ana. O equals disse true para todas. O == disse true para uma e false para as outras. Por quê?

String a = "Ana"; String b = "Ana";
Quando o mesmo literal aparece no código, o Java guarda uma única cópia num lugar especial da memória, o pool de Strings (pool = "reservatório"). a e b apontam para a mesma cópia. Isso só é seguro porque Strings são imutáveis: ninguém consegue estragar a cópia compartilhada.
new String("Ana")
new (novo) manda o Java criar um objeto novo, fora do pool, com o mesmo texto. Mesmo conteúdo, outro exemplar.
parte + "a"
Um texto montado durante a execução (a partir de uma variável) também é um objeto novo.
A pegadinha: == às vezes funciona

O perigo do == é que ele dá true quando os dois textos são literais escritos no código. Você testa, funciona, e acha que está certo. Mas, quando o texto vem do teclado (Scanner), de um arquivo ou de uma conta, ele é um objeto novo, e o == passa a dar false com o mesmo texto. Para comparar o conteúdo de Strings, use sempre equals.

Um caso de verdade: a senha correta é java25, e a senha "digitada" foi montada juntando "java" com o número 25:

public class Main {
    public static void main(String[] args) {
        String senha = "java25";
        int versao = 25;
        String digitada = "java" + versao;
        System.out.println(digitada);
        if (digitada == senha) {
            System.out.println("Acesso liberado");
        } else {
            System.out.println("Senha errada");
        }
        if (digitada.equals(senha)) {
            System.out.println("Acesso liberado");
        }
    }
}
$ java Main.java
java25           « o texto é exatamente "java25"
Senha errada     « mas o == recusou: são objetos diferentes
Acesso liberado  « o equals compara o conteúdo e aceita

O if ("se") executa um bloco só quando a condição é verdadeira; você vai estudá-lo em if, else if, else. Aqui ele mostra bem o estrago: com ==, a usuária digitou a senha certa e foi barrada.

Missão: rode o programa e veja a mensagem Senha errada, mesmo com a senha certa. Depois corrija a comparação para usar equals e fazer aparecer Acesso liberado.

public class Main {
    public static void main(String[] args) {
        String senha = "java25";
        int versao = 25;
        String digitada = "java" + versao;
        if (digitada == senha) {
            System.out.println("Acesso liberado");
        } else {
            System.out.println("Senha errada");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        String senha = "java25";
        int versao = 25;
        String digitada = "java" + versao;
        if (digitada.equals(senha)) {
            System.out.println("Acesso liberado");
        } else {
            System.out.println("Senha errada");
        }
    }
}

Uma curiosidade: o javac junta literais antes

Se você trocar "java" + versao por "java" + 25 (os dois pedaços escritos direto no código), o == dá true. O javac percebe que "java" + 25 é sempre "java25", faz a conta na hora de compilar e usa a cópia do pool. O mesmo vale para "An" + "a". Isso só piora a pegadinha: o resultado do == depende de detalhes que você não controla. Não tente decorar quando ele funciona; use equals.

No JShell dá para ver o mesmo comportamento, uma linha por vez:

$ jshell
|  Welcome to JShell -- Version 25.0.4.1
|  For an introduction type: /help intro

jshell> String nome = "Ana"
nome ==> "Ana"

jshell> nome.length()
$2 ==> 3

jshell> nome == new String("Ana")
$3 ==> false

jshell> /exit
|  Goodbye

Brinque no JShell: crie String a = "Ana" e compare com a == "Ana", a == new String("Ana"), a.equals(new String("Ana")). Qual delas é confiável?


Ignorar maiúsculas: equalsIgnoreCase

O equals compara caractere por caractere, e S maiúsculo é um caractere diferente de s minúsculo. Quando a diferença entre maiúsculas e minúsculas não importa (uma resposta "sim", o nome de uma cidade), use equalsIgnoreCase (ignore case = "ignorar a caixa", ou seja, maiúsculas/minúsculas):

public class Main {
    public static void main(String[] args) {
        String resposta = "SIM";
        System.out.println(resposta.equals("sim"));
        System.out.println(resposta.equalsIgnoreCase("sim"));
        System.out.println("Maçã".equalsIgnoreCase("MAÇÃ"));
    }
}
$ java Main.java
false     « equals: "SIM" e "sim" têm caracteres diferentes
true      « equalsIgnoreCase: só a caixa muda, então é igual
true      « funciona com acentos também

Ele não ignora outras diferenças: espaços sobrando e acentos diferentes ("maca" × "maçã") continuam contando. Para tirar espaços das pontas antes de comparar, existe o strip(), visto em Métodos de String.


Ordem alfabética: compareTo

O equals responde "igual ou não". Para saber qual vem antes no dicionário (para ordenar uma lista de nomes, por exemplo), use compareTo (compare to = "comparar com"). Ele devolve um número:

a.compareTo(b) devolveQuer dizer
Um número negativoa vem antes de b
0São iguais
Um número positivoa vem depois de b
public class Main {
    public static void main(String[] args) {
        System.out.println("Ana".compareTo("Bruno"));
        System.out.println("Bruno".compareTo("Ana"));
        System.out.println("Ana".compareTo("Ana"));
        System.out.println("Ana".compareTo("Ana Souza"));
        System.out.println("ana".compareTo("Ana"));
        System.out.println("Zeca".compareTo("ana"));
        System.out.println("Zeca".compareToIgnoreCase("ana"));
    }
}
$ java Main.java
-1     « "Ana" vem antes de "Bruno"
1      « "Bruno" vem depois de "Ana"
0      « iguais
-6     « "Ana" é o começo de "Ana Souza": o mais curto vem antes
32     « "ana" vem DEPOIS de "Ana": minúsculas vêm depois das maiúsculas
-7     « "Zeca" vem ANTES de "ana"!
25     « ignorando a caixa, "Zeca" volta a vir depois de "ana"

De onde vêm esses números? O compareTo anda pelas duas Strings até achar a primeira letra diferente e devolve a diferença entre os códigos dessas letras (lembre que, em Java, cada char é um número; veja char e boolean). 'A' é 65 e 'B' é 66: 65 − 66 = -1. 'a' é 97: 97 − 65 = 32. Se uma String é o começo da outra, devolve a diferença de tamanho: 3 − 9 = -6.

Não use o valor exato, só o sinal (negativo, zero, positivo). O que importa é "antes, igual ou depois".

Maiúsculas vêm antes de todas as minúsculas

Como a ordem segue os códigos dos caracteres, todas as maiúsculas (65 a 90) vêm antes de todas as minúsculas (97 a 122). Por isso "Zeca" fica antes de "ana". Para uma ordem "de dicionário", use compareToIgnoreCase. Acentos também ficam fora do lugar ("Ágata" vem depois de "Zeca"); a ordenação correta para português usa o Collator, assunto de Comparable e Comparator.


String vazia × null

Duas coisas parecem "nada", mas são bem diferentes:

String vazia ""null
O que éUma String de verdade, com zero caracteresNenhuma String: a variável não aponta para objeto nenhum
ComparaçãoUm copo vazioNenhum copo
length()0Erro: NullPointerException
Ao imprimirNada apareceAparece a palavra null

Você pode perguntar o tamanho de um copo vazio (zero). Mas não dá para perguntar o tamanho de um copo que não existe. null (em inglês, "nulo") é uma palavra do Java que quer dizer "esta variável não aponta para nada". Veja:

public class Main {
    public static void main(String[] args) {
        String vazia = "";
        String nada = null;
        System.out.println("[" + vazia + "]");
        System.out.println(vazia.length());
        System.out.println(vazia.isEmpty());
        System.out.println("[" + nada + "]");
        System.out.println(nada.length());
    }
}
$ javac Main.java
$ java Main
[]
0
true
[null]
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local2>" is null
	at Main.main(Main.java:9)
$ echo $?
1

Os colchetes mostram onde o texto começa e termina. A String vazia aparece como [], tem tamanho 0 e o isEmpty() ("está vazia?") responde true. Já o null, juntado com texto, vira a palavra null. E, na linha 9, pedir o length() de nada derrubou o programa.

Lendo a NullPointerException

Uma exceção é o jeito do Java avisar que algo deu errado durante a execução (o código compilou, mas falhou ao rodar). Você vai estudá-las em O que são Exceções; por ora, leia a mensagem por partes:

Exception in thread "main"
"Exceção na linha de execução principal": o seu programa.
java.lang.NullPointerException
O tipo do erro: "exceção de ponteiro nulo". Você tentou usar algo que é null.
Cannot invoke "String.length()"
"Não foi possível chamar o método length() de String"…
because "<local2>" is null
…"porque a variável local número 2 é nula". Contando a partir de zero, a variável 0 é o args, a 1 é vazia e a 2 é nada.
at Main.main(Main.java:9)
Onde aconteceu: no método main da classe Main, arquivo Main.java, linha 9.

Essa mensagem detalhada ("because … is null") existe desde o Java 14 (as versões antigas diziam só java.lang.NullPointerException, sem explicação). Por que <local2> e não nada? Porque o javac, por padrão, não guarda os nomes das variáveis locais dentro do .class. Compilando com a opção -g (de debug, "depuração"), os nomes ficam guardados e a mensagem fica ainda melhor:

$ javac -g Main.java
$ java Main
[]
0
true
[null]
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "nada" is null
	at Main.main(Main.java:9)

As IDEs (veja Editores e IDEs) já compilam com -g, por isso nelas você vê o nome da variável.

Comparar com segurança quando pode haver null

Chamar equals numa variável null também quebra. O truque é colocar o literal na frente: o literal nunca é null.

public class Main {
    public static void main(String[] args) {
        String nome = null;
        System.out.println("Ana".equals(nome));
        System.out.println(nome.equals("Ana"));
    }
}
$ java Main.java
false
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>" is null
	at Main.main(Main.java:5)

"Ana".equals(nome) responde false com calma ("Ana" não é igual a nada). nome.equals("Ana") tenta chamar um método em null e derruba o programa. Para saber se uma variável é null, aí sim se usa ==: nome == null, porque a pergunta é "aponta para algum objeto?", e não sobre o conteúdo.

Missão: o programa quebra com NullPointerException. Corrija a linha 4 para comparar de forma segura (o literal na frente) e acrescente uma linha que mostre se apelido é null (apelido == null). A saída deve ser false e depois true.

public class Main {
    public static void main(String[] args) {
        String apelido = null;
        System.out.println(apelido.equals("Ana"));
    }
}
public class Main {
    public static void main(String[] args) {
        String apelido = null;
        System.out.println("Ana".equals(apelido));
        System.out.println(apelido == null);
    }
}

Qual comparação usar?

Você quer saber…UseExemplo
Se dois textos são iguaisequalssenha.equals(digitada)
Se são iguais, sem ligar para maiúsculasequalsIgnoreCaseresposta.equalsIgnoreCase("sim")
Qual vem antes em ordem alfabéticacompareTo / compareToIgnoreCasea.compareTo(b) < 0
Se a variável pode ser null e vai comparar com um texto fixoLiteral na frente"Ana".equals(nome)
Se a variável é null==nome == null
Se a String está vaziaisEmpty()nome.isEmpty()
Se são o mesmo objeto (raro)==quase nunca é o que você quer

Quando dá errado

error: cannot find symbol … symbol: class string

Este código tem um erro de propósito.

public class Main {
    public static void main(String[] args) {
        string nome = "Ana";
        System.out.println(nome);
    }
}
$ javac Main.java
Main.java:3: error: cannot find symbol
        string nome = "Ana";
        ^
  symbol:   class string
  location: class Main
1 error

O que aconteceu: Java diferencia maiúsculas de minúsculas. string, com s minúsculo, não existe; o javac procurou uma classe com esse nome (symbol: class string, "símbolo: classe string") e não achou.

Como resolver: escreva String, com S maiúsculo.

error: unclosed character literal

Este código tem um erro de propósito.

public class Main {
    public static void main(String[] args) {
        String nome = 'Ana';
        System.out.println(nome);
    }
}
$ javac Main.java
Main.java:3: error: unclosed character literal
        String nome = 'Ana';
                      ^
Main.java:3: error: unclosed character literal
        String nome = 'Ana';
                          ^
Main.java:3: error: not a statement
        String nome = 'Ana';
                        ^
3 errors

O que aconteceu: aspas simples são para um char. O javac leu 'A, esperava a aspa de fechamento logo depois de uma letra e não achou: "literal de caractere não fechado". Um único engano gerou três erros em cascata; conserte sempre o primeiro.

Como resolver: texto vai entre aspas duplas: "Ana".

Duas Strings iguais e o if diz que são diferentes

O que aconteceu: a comparação foi feita com ==, que compara objetos e não o conteúdo. Não aparece mensagem de erro nenhuma: o programa só toma a decisão errada (veja o exemplo da senha, acima).

Como resolver: troque a == b por a.equals(b) e a != b por !a.equals(b).

NullPointerException: Cannot invoke "String…" because … is null

O que aconteceu: você chamou um método (length(), equals, toUpperCase()…) numa variável que vale null. A linha está no final da mensagem (Main.java:9).

Como resolver: descubra por que a variável ficou null e dê um valor a ela; se null for possível, teste antes com if (nome != null) ou ponha o literal na frente: "Ana".equals(nome).

O texto não mudou depois do toUpperCase()

O que aconteceu: Strings são imutáveis. nome.toUpperCase(); sozinho cria uma String nova e a joga fora. Não há erro nem aviso.

Como resolver: guarde o resultado: nome = nome.toUpperCase();.


Referência

EscritaO que fazExemplo → resultado
String s = "texto";Cria uma String com um literalString nome = "Ana";
""String vazia"".length() → 0
s.length()Quantidade de caracteres"Ana Souza".length() → 9
s.isEmpty()Está vazia?"".isEmpty() → true
a.equals(b)Mesmo conteúdo?"Ana".equals("ana") → false
a.equalsIgnoreCase(b)Mesmo conteúdo, ignorando a caixa?"Ana".equalsIgnoreCase("ANA") → true
a.compareTo(b)Ordem: negativo, 0 ou positivo"Ana".compareTo("Bruno") → -1
a.compareToIgnoreCase(b)Ordem, ignorando a caixa"Zeca".compareToIgnoreCase("ana") → 25
a == bMesmo objeto? (não use para conteúdo)"Ana" == new String("Ana") → false
s == nullA variável aponta para nada?null == null → true

A lista completa de métodos está em Referência: Métodos de String.


Exercícios

Missão final: num jogo de perguntas, a cidade certa é Recife e a jogadora respondeu RECIFE. Complete o programa para mostrar Acertou! quando a resposta for igual à cidade ignorando maiúsculas, e depois a linha A palavra tem 6 letras usando length() (não escreva o 6 à mão).

public class Main {
    public static void main(String[] args) {
        String cidade = "Recife";
        String resposta = "RECIFE";
        // escreva aqui
    }
}
public class Main {
    public static void main(String[] args) {
        String cidade = "Recife";
        String resposta = "RECIFE";
        if (resposta.equalsIgnoreCase(cidade)) {
            System.out.println("Acertou!");
        }
        System.out.println("A palavra tem " + cidade.length() + " letras");
    }
}

1. Tamanho

Complete para mostrar quantos caracteres tem a variável nome:

System.out.println(nome.);

2. Comparar o conteúdo

Complete para comparar o conteúdo de a e b:

boolean iguais = a.;

3. Sem quebrar com null

A variável cor pode ser null. Complete para comparar com "azul" sem risco de NullPointerException:

boolean azul = ;

4. O que System.out.println("Ana" == new String("Ana")); mostra?

5. Depois de String s = "oi"; s.toUpperCase(); System.out.println(s);, o que aparece?

6. "banana".compareTo("abacaxi") devolve um número…

7. Qual é a diferença entre "" e null?


Resumo

  • Uma String é uma sequência de caracteres, escrita entre aspas duplas. String tem S maiúsculo.
  • length() devolve a quantidade de caracteres (espaços contam); "" tem tamanho 0.
  • Strings são imutáveis: métodos como toUpperCase() devolvem uma String nova. Guarde o resultado.
  • == compara se é o mesmo objeto; equals compara o conteúdo. Para texto, use equals, mesmo quando o == "parece funcionar".
  • equalsIgnoreCase ignora maiúsculas; compareTo diz a ordem (negativo, 0, positivo).
  • null é "nenhuma String". Chamar método em null dá NullPointerException; compare com "literal".equals(variavel) e teste com variavel == null.