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
Jestá 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 é quefim − iníciodá 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
- The Java Language Specification, Java SE 25, capítulo 15, "Expressions": https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html. A definição oficial de cada operador, da precedência e da ordem de avaliação.
- Documentação da classe
String(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/String.html. A lista completa de métodos. Vale folhear para saber o que já existe antes de reinventar. - Documentação da classe
Formatter: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Formatter.html. A referência de todos os marcadores de formatação, com as opções de largura, precisão e alinhamento. - JEP 378, "Text Blocks": https://openjdk.org/jeps/378. A proposta dos text blocks, com as regras de indentação explicadas em detalhe.
- Documentação da classe
IO(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/IO.html. A classe nova de entrada e saída simples usada nestas primeiras aulas.
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.