Operadores, Texto e Conversa com o Usuário — Expressões, Strings e Entrada

Operadores, Texto e Conversa com o Usuário — Expressões, Strings e Entrada

Operadores aritméticos, divisão inteira, precedência, incremento, comparações e curto-circuito; String e seus métodos, imutabilidade, concatenação, a armadilha do == com textos, text blocks, formatação com Locale e a leitura de números digitados pelo usuário.
Java

• • 16 min de leitura

Contas que dão resultados surpreendentes, comparações que enganam, textos que não mudam e números digitados pelo usuário: as ferramentas para transformar valores.

Em "Cada Valor em sua Caixa" você aprendeu a guardar valores. Agora vamos operar sobre eles. Esta aula cobre os operadores do Java, a ordem em que as contas acontecem, o tipo String com seus métodos mais úteis, a formatação de números para exibição e a leitura de dados digitados. Ao final, você vai escrever um programa que pergunta, calcula e responde, e que já é útil de verdade.

Aritmética, e a divisão que surpreende

Os operadores aritméticos são os esperados: +, -, *, / e % (resto da divisão).

void main() {
    IO.println(7 + 2);     // 9
    IO.println(7 - 2);     // 5
    IO.println(7 * 2);     // 14
    IO.println(7 / 2);     // 3
    IO.println(7 % 2);     // 1
    IO.println(7 / 2.0);   // 3.5
}

A quarta linha é a primeira armadilha: a divisão entre dois inteiros produz um inteiro, descartando a parte decimal. 7 dividido por 2 dá 3, e não 3,5. Basta que um dos operandos seja real (2.0) para a conta acontecer em double e dar 3,5.

Essa regra causa um erro clássico:

double media = (7 + 8) / 2;
IO.println(media);          // 7.0

double mediaCerta = (7 + 8) / 2.0;
IO.println(mediaCerta);     // 7.5

É a mesma lição da aula anterior, agora com a divisão: o tipo da variável de destino não muda o tipo da conta. (7 + 8) / 2 é uma conta inteira que dá 7, e só depois o 7 vira 7.0.

O operador % devolve o resto da divisão inteira e é mais útil do que parece. n % 2 == 0 testa se um número é par. n % 10 dá o último dígito de um número. total % 60 dá os segundos que sobram depois de contar os minutos inteiros. Com números negativos há um detalhe: o resultado de % tem o sinal do dividendo, então -7 % 3 dá -1. Quando você precisar de um resto sempre positivo, por exemplo para contas com horas do relógio ou dias da semana, use Math.floorMod(-7, 3), que dá 2.

A ordem das contas

As regras de precedência são as da matemática escolar: *, / e % antes de + e -. Operadores de mesma precedência são avaliados da esquerda para a direita.

IO.println(2 + 3 * 4);      // 14
IO.println((2 + 3) * 4);    // 20
IO.println(10 - 2 * 3 / 4); // 9: 2 * 3 = 6, 6 / 4 = 1 (inteira!), 10 - 1 = 9

Na dúvida, use parênteses. Eles não custam nada na execução e economizam o tempo de quem lê. Um código que exige que o leitor saiba a tabela de precedência de cor é um código que vai ser lido errado algum dia.

Atribuição composta, incremento e decremento

Alterar uma variável a partir do próprio valor é tão comum que existe atalho:

void main() {
    int pontos = 10;
    pontos += 5;    // o mesmo que pontos = pontos + 5
    pontos *= 2;    // o mesmo que pontos = pontos * 2
    IO.println(pontos);   // 30
}

Existem +=, -=, *=, /= e %=. Eles escondem um detalhe: incluem uma conversão automática para o tipo da variável. Por isso nivel += 1 funciona com um byte, enquanto nivel = nivel + 1 não compila. A conta nivel + 1 é feita em int, porque o Java promove byte e short para int antes de qualquer conta, e guardar um int num byte exige cast:

E1.java:3: error: incompatible types: possible lossy conversion from int to byte
    nivel = nivel + 1;
                  ^

Para somar ou subtrair 1, há os operadores ++ e --. Eles podem vir antes ou depois da variável, e a posição importa quando o resultado é usado na mesma expressão:

int a = 5;
int b = a++;   // b recebe 5 (o valor antigo), depois a vira 6
int c = ++a;   // a vira 7 primeiro, depois c recebe 7
IO.println(a + " " + b + " " + c);   // 7 5 7

Um conselho prático: use ++ e -- sozinhos numa linha (contador++;) ou no for, que você vai ver na próxima aula. Misturá-los com outras operações na mesma expressão é o tipo de esperteza que produz erros sutis e não economiza nada.

Comparações e lógica

Os operadores relacionais comparam valores e produzem um boolean: == (igual), != (diferente), <, <=, > e >=. Repare que a igualdade usa dois sinais de igual. Um só é atribuição.

Os operadores lógicos combinam booleanos: && (e), || (ou) e ! (não).

void main() {
    int idade = 20;
    boolean temIngresso = true;

    boolean podeEntrar = idade >= 18 && temIngresso;
    IO.println(podeEntrar);       // true
    IO.println(idade != 20);      // false
    IO.println(!temIngresso);     // false
}

&& e || são de curto-circuito: se o lado esquerdo já decide o resultado, o lado direito nem é avaliado. Num &&, se a esquerda é falsa, o resultado é falso, e ponto. Isso não é só otimização, é uma ferramenta:

int divisor = 0;
boolean seguro = divisor != 0 && 10 / divisor > 1;
IO.println(seguro);   // false, sem erro

A divisão por zero nunca acontece, porque divisor != 0 já é falso. Invertendo a ordem, 10 / divisor > 1 && divisor != 0, o programa para com ArithmeticException: / by zero. Com inteiros, ao contrário dos reais, a divisão por zero é um erro. A ordem das condições num && importa: primeiro a guarda, depois o que ela protege.

Existe ainda o operador ternário, que escolhe entre dois valores numa só expressão:

String status = idade >= 18 ? "adulto" : "menor";

Lê-se: "se idade >= 18, então "adulto", senão "menor"". É ótimo para escolhas simples entre dois valores. Para decisões com mais ramos ou com várias instruções, existe o if da próxima aula.

A classe Math

Operações além das quatro básicas estão na classe Math, disponível sem importação:

IO.println(Math.sqrt(16));        // 4.0   raiz quadrada
IO.println(Math.pow(2, 10));      // 1024.0 potência
IO.println(Math.abs(-7));         // 7     valor absoluto
IO.println(Math.max(3, 9));       // 9     o maior de dois
IO.println(Math.round(2.6));      // 3     arredonda
IO.println(Math.PI);              // 3.141592653589793

O Java não tem operador de potência: 2 ^ 10 compila, mas ^ é outra operação (o "ou exclusivo") e dá 8. Para potência, use Math.pow, lembrando que ele trabalha com double.

String: texto como objeto

Como adiantado na aula anterior, String não é um tipo primitivo, e sim uma classe. Na prática, isso significa que um texto vem com métodos, operações que você chama com um ponto depois do valor:

void main() {
    String linguagem = "Java";

    IO.println(linguagem.length());          // 4
    IO.println(linguagem.toUpperCase());     // JAVA
    IO.println(linguagem.charAt(0));         // J
    IO.println(linguagem.substring(1, 3));   // av
    IO.println(linguagem.indexOf('v'));      // 2
    IO.println(linguagem.contains("av"));    // true
    IO.println(linguagem.replace('a', 'o')); // Jovo
    IO.println("  espaços  ".strip() + "|"); // espaços|
    IO.println("   ".isBlank());             // true
}

Algumas observações:

  • As posições começam em zero. O J está na posição 0, e o último caractere de um texto de tamanho 4 está na posição 3.
  • substring(1, 3) devolve do índice 1 até o 3, sem incluir o 3. Essa convenção de "início incluído, fim excluído" aparece em toda a biblioteca do Java. Uma vantagem dela é que fim − início dá o tamanho do pedaço.
  • strip() remove espaços do começo e do fim. É o que você quase sempre quer fazer com o que o usuário digitou.

Um detalhe fundamental: Strings são imutáveis. Nenhum método altera o texto original. Todos devolvem um texto novo.

String maiuscula = linguagem.toUpperCase();
IO.println(linguagem + " " + maiuscula);   // Java JAVA

Chamar linguagem.toUpperCase() sozinho numa linha, sem guardar o resultado, não faz nada de útil. É um erro comum de iniciante. Por que a imutabilidade é uma escolha de projeto excelente, e não uma limitação, é assunto de "O que Fica Dentro, o que Fica Fora".

Concatenação e sua armadilha

O operador + com um texto de um lado junta os dois lados num texto só. Como o + é avaliado da esquerda para a direita, a posição dos números importa:

IO.println("Soma: " + 2 + 3);    // Soma: 23
IO.println("Soma: " + (2 + 3));  // Soma: 5
IO.println(2 + 3 + " é a soma"); // 5 é a soma

Na primeira linha, "Soma: " + 2 vira o texto "Soma: 2", e em seguida "Soma: 2" + 3 vira "Soma: 23". Na terceira, 2 + 3 acontece antes de qualquer texto aparecer, então é soma de verdade. Em caso de dúvida, os parênteses resolvem.

A comparação que engana

Esta é a armadilha mais importante da aula. Rode o programa e digite java:

void main() {
    String senha = IO.readln("Senha: ");
    IO.println("Com ==: " + (senha == "java"));
    IO.println("Com equals: " + senha.equals("java"));
    IO.println("Ignorando maiúsculas: " + senha.equalsIgnoreCase("JAVA"));
}
Senha: java
Com ==: false
Com equals: true
Ignorando maiúsculas: true

O texto digitado é idêntico, e mesmo assim o == diz que não são iguais. O motivo está na aula anterior: uma variável String guarda uma referência a um objeto. O == compara as referências, isto é, pergunta se as duas variáveis apontam para o mesmo objeto. O texto digitado foi criado durante a execução, num objeto novo, diferente do objeto do literal "java". Os conteúdos são iguais, mas os objetos são dois.

Para comparar o conteúdo de textos, use sempre equals, ou equalsIgnoreCase quando maiúsculas e minúsculas não importarem. A regra vale para qualquer objeto, e a diferença entre ser igual e ser o mesmo tem uma aula inteira, "Iguais ou o Mesmo?". Por enquanto, grave assim: com String, nunca use ==. Às vezes o == "funciona" por coincidência, quando os dois lados são literais escritos no código, e isso só torna o erro mais traiçoeiro.

Text blocks e formatação

Para textos de várias linhas, o Java tem os text blocks, delimitados por três aspas:

void main() {
    String nome = "Marina";
    int aulas = 78;

    String mensagem = """
        Olá, %s!
        O curso tem %d aulas.
          Esta linha tem dois espaços a mais.
        """.formatted(nome, aulas);

    IO.print(mensagem);
}
Olá, Marina!
O curso tem 78 aulas.
  Esta linha tem dois espaços a mais.

A indentação comum a todas as linhas, definida pela posição das aspas de fechamento, é removida. A indentação extra é mantida. IO.print escreve sem pular linha no fim, porque o próprio text block já termina com uma quebra.

O método formatted substitui os marcadores pelos valores, na ordem. Os marcadores mais usados são:

Marcador Formata Exemplo Resultado
%s qualquer valor, como texto "%s".formatted("Java") Java
%d inteiro "%d".formatted(25) 25
%.2f real com 2 casas "%.2f".formatted(3.14159) 3.14
%,.2f real com separador de milhar "%,.2f".formatted(1234.5) 1,234.50
%5d / %-10s largura mínima (à direita / à esquerda) `"%-6s %3d".formatted("Java", 25)` `Java 25`
%05d preenche com zeros "%05d".formatted(42) 00042
%% o próprio símbolo % "10%%".formatted() 10%

A formatação de números depende do idioma do sistema. Num computador configurado em português do Brasil, "%.2f" produz 3,14, com vírgula, e num configurado em inglês produz 3.14. Para não depender da máquina, informe o idioma explicitamente:

double preco = 1234.5;
IO.println(String.format(Locale.of("pt", "BR"), "Preço: R$ %,.2f", preco));
Preço: R$ 1.234,50

String.format faz o mesmo que formatted, com a possibilidade de indicar o idioma (Locale) no primeiro argumento.

Lendo números do usuário

IO.readln devolve sempre um String. Quando o que se espera é um número, é preciso convertê-lo:

int idade = Integer.parseInt(IO.readln("Idade: "));
double peso = Double.parseDouble(IO.readln("Peso: "));

Se o texto não for um número válido, a conversão falha e o programa para com um erro, NumberFormatException. Vale atenção a um caso que pega todo brasileiro: Double.parseDouble segue o formato do código, e não o do idioma, então espera ponto como separador decimal. Digitar 287,40 produz:

Exception in thread "main" java.lang.NumberFormatException: For input string: "287,40"

Tratar esse erro com elegância, pedindo o valor de novo em vez de encerrar o programa, exige as exceções, que são o tema de "Quando o Contrato se Quebra". Até lá, os programas vão supor que o usuário digita corretamente, e as instruções vão dizer "use ponto".

E o Scanner?

Em quase todo livro e tutorial de Java escrito antes de 2025 você vai encontrar outra forma de ler dados, a classe Scanner:

void main() {
    Scanner entrada = new Scanner(System.in);

    IO.print("Sua idade: ");
    int idade = entrada.nextInt();

    IO.print("Seu nome: ");
    String nome = entrada.nextLine();

    IO.println("[" + nome + "] tem " + idade + " anos");
}

É importante reconhecê-la, porque ela está em milhões de programas. Mas veja o que acontece quando o usuário digita 30, Enter, Ana, Enter:

Sua idade: Seu nome: [] tem 30 anos

O nome ficou vazio, e o programa nem esperou a pessoa digitar. O nextInt lê o número 30 e deixa a quebra de linha do Enter sobrando na entrada. O nextLine seguinte encontra essa quebra de linha, entende que é uma linha vazia e a devolve imediatamente. É a dúvida sobre Java mais repetida em fóruns de iniciantes há duas décadas. A correção é chamar um nextLine() extra depois do nextInt(), ou, melhor, sempre ler linhas inteiras e converter com parseInt. É exatamente o que o IO.readln faz, e é por isso que este curso o usa.

Numa classe clássica, fora de um arquivo compacto, o Scanner precisa de import java.util.Scanner; no topo do arquivo. Nos arquivos compactos, a biblioteca básica do Java já vem importada. As importações são explicadas em "Do Arquivo Solto ao Projeto".

Juntando tudo: dividindo a conta

Um programa que pergunta, calcula e responde, usando tudo o que foi visto até aqui:

void main() {
    IO.println("=== Divisão da conta ===");
    double total = Double.parseDouble(IO.readln("Valor total da conta (use ponto): "));
    int pessoas = Integer.parseInt(IO.readln("Número de pessoas: "));

    final double TAXA_DE_SERVICO = 0.10;
    double gorjeta = total * TAXA_DE_SERVICO;
    double totalComGorjeta = total + gorjeta;
    double porPessoa = totalComGorjeta / pessoas;

    var br = Locale.of("pt", "BR");
    IO.println(String.format(br, "Gorjeta (10%%): R$ %,.2f", gorjeta));
    IO.println(String.format(br, "Total com gorjeta: R$ %,.2f", totalComGorjeta));
    IO.println(String.format(br, "Cada um paga: R$ %,.2f", porPessoa));
}
=== Divisão da conta ===
Valor total da conta (use ponto): 287.40
Número de pessoas: 3
Gorjeta (10%): R$ 28,74
Total com gorjeta: R$ 316,14
Cada um paga: R$ 105,38

A divisão totalComGorjeta / pessoas mistura double com int e dá um double, então não há problema de divisão inteira. O %% na formatação imprime o próprio símbolo de porcentagem.

O programa tem um limite evidente: ele faz sempre a mesma coisa, do começo ao fim. Não consegue recusar um número de pessoas igual a zero, nem oferecer um menu, nem repetir a pergunta. Para isso, um programa precisa decidir e repetir.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Sem rodar, preveja o resultado de cada expressão. Depois confira no jshell: 17 / 5, 17 % 5, 17 / 5.0, 1 + 2 + "3" + 4 + 5 e 10 - 2 * 3 / 4.

Ver resposta

✓ Resposta: 17 / 5 dá 3, porque é divisão inteira. 17 % 5 dá 2, o resto. 17 / 5.0 dá 3.4, porque um operando real torna a conta real. 1 + 2 + "3" + 4 + 5 dá "3345": da esquerda para a direita, 1 + 2 é soma e dá 3, 3 + "3" vira o texto "33", e a partir daí cada + concatena. 10 - 2 * 3 / 4 dá 9: 2 * 3 é 6, 6 / 4 é 1 em divisão inteira, e 10 - 1 é 9.

Exercício 2

Um colega escreveu um teste de senha com if (senha == "segredo") e diz que "às vezes funciona e às vezes não". Explique por quê e corrija.

Ver resposta

✓ Resposta: O == compara referências, ou seja, se as duas variáveis apontam para o mesmo objeto, e não se os textos são iguais. Quando os dois lados são literais escritos no código, o Java reaproveita o mesmo objeto, e o == dá verdadeiro por coincidência. É por isso que "às vezes funciona". Quando o texto vem de fora, digitado pelo usuário, lido de um arquivo ou montado durante a execução, ele é um objeto novo, e o == dá falso mesmo com conteúdo idêntico. A correção é senha.equals("segredo"). Uma variação comum e um pouco mais segura é "segredo".equals(senha), que funciona mesmo se senha não tiver objeto nenhum, situação que você vai conhecer em "Um Valor que Pode Não Existir".

Exercício 3

Escreva um programa que leia uma quantidade de segundos e a mostre no formato h:mm:ss. Por exemplo, 3725 deve aparecer como 1:02:05.

Ver resposta

✓ Resposta:

void main() {
    int total = Integer.parseInt(IO.readln("Segundos: "));

    int horas = total / 3600;
    int minutos = total % 3600 / 60;
    int segundos = total % 60;

    IO.println("%d:%02d:%02d".formatted(horas, minutos, segundos));
}

Com 3725, a saída é 1:02:05. A divisão inteira por 3600 dá as horas completas. total % 3600 é o que sobra depois de tirar as horas, e dividir isso por 60 dá os minutos. total % 60 dá os segundos que sobram. O %02d garante dois dígitos com zero à esquerda, como num relógio. É um uso típico da divisão inteira e do resto trabalhando juntos: nenhum dos dois é um defeito, cada um é a ferramenta certa para um pedaço da conta.

Exercício 4

Explique por que divisor != 0 && 10 / divisor > 1 é seguro com divisor igual a zero, e o que aconteceria se as duas condições trocassem de lugar. E se o && fosse trocado por &?

Ver resposta

✓ Resposta: O && é de curto-circuito: como divisor != 0 é falso, o resultado da expressão inteira já é falso, e o lado direito, que contém a divisão, não é avaliado. Com as condições invertidas, a divisão é avaliada primeiro, e a divisão inteira por zero interrompe o programa com ArithmeticException: / by zero. O operador & também é um "e" lógico quando aplicado a booleanos, mas sem curto-circuito: avalia sempre os dois lados. Por isso também faria a divisão e daria o mesmo erro. A lição é usar && e || em condições e colocar a verificação de segurança sempre antes da operação que ela protege.

Exercício 5

Um programa usa Scanner para ler primeiro a idade, com nextInt(), e depois o nome, com nextLine(). O nome sempre sai vazio. Explique a causa e apresente duas correções.

Ver resposta

✓ Resposta: O nextInt() lê apenas os dígitos do número e deixa na entrada a quebra de linha produzida pelo Enter. O nextLine() seguinte lê até a próxima quebra de linha, encontra essa que sobrou e devolve imediatamente uma linha vazia, sem esperar o usuário. Primeira correção: chamar entrada.nextLine(); logo depois do nextInt(), só para descartar a quebra de linha pendente. Segunda correção, mais robusta: ler sempre linhas inteiras e converter quando necessário, com int idade = Integer.parseInt(entrada.nextLine().strip());, ou simplesmente usar IO.readln, que faz exatamente isso. A segunda abordagem elimina o problema em vez de contorná-lo, porque todas as leituras passam a se comportar da mesma forma.

Comentários

Mais em Java

Extensão — Java no Android: o que Mudou com o Kotlin e um App que Consome a API
Extensão — Java no Android: o que Mudou com o Kotlin e um App que Consome a API

As Trilhas da Ilha chegam ao celular: por que o Android usa a linguagem Java…

Escreva uma Vez para Todo Tipo — Genéricos
Escreva uma Vez para Todo Tipo — Genéricos

Genéricos em Java: classes, records e métodos genéricos, o operador diamante…

Fazendo Duas Coisas ao Mesmo Tempo — Threads
Fazendo Duas Coisas ao Mesmo Tempo — Threads

O que é uma thread e como criar, esperar e interromper várias delas: start e…