Cada Valor em sua Caixa — Tipos Primitivos, Variáveis e Conversões

Cada Valor em sua Caixa — Tipos Primitivos, Variáveis e Conversões

Variáveis, tipagem estática e os oito tipos primitivos do Java: faixas, literais, o overflow silencioso, a imprecisão do double, o char como número, conversões de alargamento e de estreitamento com cast, var e final, e por que String não é um tipo primitivo.
Java

• • 17 min de leitura

Como o Java guarda números, letras e verdades, por que ele exige saber o tipo de cada valor antes de rodar, e o que acontece quando um valor precisa mudar de tipo.

Em "Do Código ao Bytecode", o programa que perguntava o nome da pessoa guardava a resposta numa linha que começava com String nome. Ficou a promessa de explicar essa linha. É o que esta aula faz: vamos ver o que é uma variável, o que é um tipo, quais são os tipos básicos do Java e as regras para passar um valor de um tipo a outro. Parece pouco, mas uma parte grande dos erros de iniciantes, e alguns de profissionais, nasce exatamente aqui.

Variáveis: nomes para valores

Uma variável é um nome que guarda um valor. Imagine uma caixa com uma etiqueta: a etiqueta é o nome, e o conteúdo é o valor. Em Java, toda caixa também tem um formato fixo, que determina o que cabe nela. Esse formato é o tipo.

void main() {
    int idade = 30;
    IO.println("Idade: " + idade);

    idade = 31;             // a caixa é a mesma; o conteúdo mudou
    IO.println("Idade: " + idade);
}

A primeira linha do main faz duas coisas ao mesmo tempo. Declara uma variável chamada idade, do tipo int (número inteiro), e atribui a ela o valor 30. O sinal = em Java não significa "é igual a", como na matemática. Significa "guarde o valor da direita na variável da esquerda". A terceira linha troca o conteúdo da caixa. Não se escreve o tipo de novo, porque a variável já existe.

Os nomes de variáveis seguem regras e convenções. As regras o compilador cobra: o nome começa com letra, _ ou $, não tem espaços e não pode ser uma palavra reservada da linguagem, como int ou class. As convenções a comunidade cobra: variáveis usam camelCase, começando em minúscula e marcando cada palavra nova com maiúscula (idadeDoAluno, precoTotal). Letras acentuadas são permitidas, mas quase todo projeto profissional as evita nos nomes, por causa de ferramentas e teclados de outros países. Neste curso os nomes ficam sem acento, e o texto dentro das aspas pode ter acentos à vontade.

Escolha nomes que revelem a intenção. precoComDesconto explica a si mesmo. p2 obriga quem lê a adivinhar.

Tipagem estática: o tipo é decidido antes de rodar

O Java é uma linguagem de tipagem estática: o tipo de cada variável é conhecido na compilação e nunca muda. Se a caixa é de inteiros, texto não entra nela.

void main() {
    int idade = 30;
    idade = "trinta";
}
E1.java:3: error: incompatible types: String cannot be converted to int
    idade = "trinta";
            ^
1 error

Em linguagens de tipagem dinâmica, como Python e JavaScript, isso seria aceito, e um eventual problema só apareceria durante a execução. O Java prefere acusar o erro antes de o programa existir. A troca é clara: você escreve um pouco mais, e em troca o compilador elimina uma categoria inteira de erros e os editores conseguem ajudar muito mais, completando nomes e apontando problemas enquanto você digita.

O compilador também cobra outra regra: uma variável local precisa receber um valor antes de ser lida.

void main() {
    int x;
    IO.println(x);
}
E2.java:3: error: variable x might not have been initialized
    IO.println(x);
               ^
1 error

Em C, ler uma variável não inicializada é permitido e devolve um valor qualquer que estava na memória, uma fonte clássica de erros difíceis de encontrar. Em Java, simplesmente não compila.

Os oito tipos primitivos

O Java tem oito tipos básicos, chamados primitivos. São os únicos tipos que não são objetos. Guardam o valor diretamente, sem nenhuma estrutura em volta.

Tipo Tamanho Guarda Faixa de valores
byte 8 bits inteiro −128 a 127
short 16 bits inteiro −32.768 a 32.767
int 32 bits inteiro cerca de −2,1 bilhões a 2,1 bilhões
long 64 bits inteiro cerca de ±9,2 quintilhões
float 32 bits real (ponto flutuante) ~7 dígitos significativos
double 64 bits real (ponto flutuante) ~15 a 16 dígitos significativos
char 16 bits um caractere (unidade UTF-16) 0 a 65.535
boolean — verdade lógica true ou false

Uma diferença importante em relação ao C: em Java, os tamanhos são fixos em qualquer plataforma. Um int tem 32 bits no Windows, no Linux, num celular e num servidor. Em C, o tamanho de um int depende do compilador e da máquina. É mais uma peça do "rode em qualquer lugar".

Na prática, você vai usar quatro tipos quase o tempo todo: int para inteiros, long quando o inteiro pode passar de dois bilhões, double para números com parte decimal e boolean para respostas de sim ou não. byte e short aparecem quando se lida com arquivos binários ou quando a economia de memória importa. float aparece em gráficos e jogos, como você vai ver na trilha de jogos.

void main() {
    int idade = 30;
    long populacaoMundial = 8_100_000_000L;
    double altura = 1.75;
    float temperatura = 36.5f;
    char inicial = 'M';
    boolean estudante = true;
    byte nivel = 127;
    short ano = 2026;

    IO.println("Idade: " + idade);
    IO.println("População mundial: " + populacaoMundial);
    IO.println("Altura: " + altura);
    IO.println("Temperatura: " + temperatura);
    IO.println("Inicial: " + inicial);
    IO.println("Estudante? " + estudante);
    IO.println("Nível: " + nivel + ", ano: " + ano);
}
Idade: 30
População mundial: 8100000000
Altura: 1.75
Temperatura: 36.5
Inicial: M
Estudante? true
Nível: 127, ano: 2026

Repare nos detalhes dos literais, que é o nome dos valores escritos diretamente no código:

  • O _ dentro dos números é só para facilitar a leitura. 8_100_000_000 e 8100000000 são o mesmo valor.
  • O L no fim de um inteiro diz que ele é long. Sem ele, o compilador trata todo número inteiro como int, e um valor acima de 2,1 bilhões nem compila:
E6.java:2: error: integer number too large
    long x = 3000000000;
             ^
  • O f no fim de um real diz que ele é float. Sem ele, todo número com ponto decimal é double.
  • Um char fica entre aspas simples ('M') e guarda exatamente um caractere. Um texto fica entre aspas duplas ("M") e é outra coisa, como você vai ver no fim desta aula.
  • O separador decimal no código é sempre o ponto, independente do idioma do sistema: 1.75, nunca 1,75.

Inteiros também podem ser escritos em hexadecimal (0xFF) e em binário (0b1010). Isso vai ser útil quando o assunto for bits e cores.

Os limites dos números

Uma caixa de tamanho fixo tem limite. O que acontece quando uma conta passa dele?

void main() {
    int maximo = Integer.MAX_VALUE;
    IO.println("Maior int: " + maximo);
    IO.println("Maior int + 1: " + (maximo + 1));
    IO.println("Maior long: " + Long.MAX_VALUE);
}
Maior int: 2147483647
Maior int + 1: -2147483648
Maior long: 9223372036854775807

Somar 1 ao maior int dá o menor int. É o overflow: o valor "dá a volta", como o hodômetro de um carro antigo que passa de 99999 para 00000. O Java não avisa nem interrompe o programa. Simplesmente continua com o número errado. Esse silêncio é perigoso, porque o erro aparece longe de onde nasceu.

Um caso real e comum:

void main() {
    int microPorDia = 24 * 60 * 60 * 1000 * 1000;
    long microCerto = 24L * 60 * 60 * 1000 * 1000;
    IO.println(microPorDia + " " + microCerto);
}
500654080 86400000000

Nas duas linhas, o resultado cabe num long. Mas na primeira todos os números são int, então a conta inteira é feita em int, estoura no meio do caminho, e só depois o resultado errado é guardado. Na segunda, o L do primeiro número faz a conta toda acontecer em long. O tipo da variável que recebe o resultado não muda o tipo da conta. Guarde essa regra, porque ela vai voltar.

Quando um overflow seria um erro de lógica, é melhor que ele pare o programa em vez de passar em silêncio. A biblioteca padrão tem métodos para isso:

IO.println(Math.addExact(2_000_000_000, 2_000_000_000));
Exception in thread "main" java.lang.ArithmeticException: integer overflow

O programa para com um erro explícito, o que é muito melhor do que continuar com um valor errado. Os erros desse tipo, as exceções, têm uma aula própria, "Quando o Contrato se Quebra".

O problema dos números reais

Os tipos float e double guardam números reais em ponto flutuante, um formato binário padronizado (IEEE 754) que representa uma faixa enorme de valores, mas com precisão limitada. Muitas frações decimais simples não têm representação exata em binário, assim como 1/3 não tem representação exata em decimal (0,333...).

void main() {
    IO.println("0.1 + 0.2 = " + (0.1 + 0.2));
    IO.println("1.0 / 0 = " + (1.0 / 0));
    IO.println("0.0 / 0 = " + (0.0 / 0));
}
0.1 + 0.2 = 0.30000000000000004
1.0 / 0 = Infinity
0.0 / 0 = NaN

A primeira linha não é um defeito do Java: qualquer linguagem que use o padrão IEEE 754 dá o mesmo resultado. As outras duas mostram que a divisão por zero com reais não interrompe o programa. Ela produz valores especiais, o infinito e o NaN (not a number, "não é um número").

A consequência prática é importante: não use double para dinheiro. Centavos que somem ou aparecem em milhões de operações viram um problema contábil. Para valores monetários o Java tem uma classe própria, BigDecimal, que é assunto de "Tempo, Dinheiro e Padrões". Até lá, os exemplos com preços usarão double por simplicidade, sabendo dessa limitação.

char: letras que são números

Um char guarda um caractere, mas por baixo ele é um número: o código do caractere na tabela Unicode.

void main() {
    char letra = 'A';
    int codigo = letra;
    IO.println(codigo);

    IO.println(letra + 1);
    char proxima = (char) (letra + 1);
    IO.println(proxima);

    IO.println('\u00E7');
}
65
66
B
ç

A letra A tem o código 65. Somar 1 a um char produz um int (66). Para voltar a ter um caractere, é preciso uma conversão explícita, que é o assunto da próxima seção. O \u00E7 é uma forma de escrever qualquer caractere pelo seu código Unicode em hexadecimal, neste caso o ç.

Uma observação para ser honesto sobre os limites: um char tem 16 bits e não comporta todos os caracteres do Unicode moderno. Alguns emojis, por exemplo, ocupam dois char. Isso raramente atrapalha, e voltaremos ao assunto quando o texto for o tema principal.

boolean: só verdadeiro ou falso

Um boolean guarda true ou false, e nada mais. Diferente do C, em que 0 é falso e qualquer outro número é verdadeiro, em Java números e booleanos não se misturam:

E5.java:3: error: incompatible types: boolean cannot be converted to int
    int n = b;
            ^

Essa rigidez vai se mostrar útil na aula "O Programa que Escolhe e Repete", quando ela impedir um dos erros mais famosos da programação em C.

Conversões: alargar é automático, estreitar exige pedido

Às vezes um valor precisa passar de um tipo para outro. A regra do Java é simples e sensata.

Conversão de alargamento (widening): quando o tipo de destino comporta todos os valores do tipo de origem, a conversão é automática, porque não há risco de perder informação.

byte → short → int → long → float → double
              char ↗

Conversão de estreitamento (narrowing): quando o destino é menor ou menos preciso, o compilador recusa, a menos que você escreva um cast, o tipo de destino entre parênteses. O cast é uma declaração sua: "sei que posso perder informação e aceito".

void main() {
    int inteiro = 42;
    long grande = inteiro;      // alargamento: automático
    double real = grande;       // alargamento: automático
    IO.println(real);

    double preco = 3.99;
    int truncado = (int) preco; // estreitamento: exige cast
    IO.println(truncado);

    int trezentos = 300;
    byte pequeno = (byte) trezentos;
    IO.println(pequeno);
}
42.0
3
44

O resultado mostra dois cuidados. Primeiro, converter um real para inteiro corta a parte decimal, sem arredondar: 3,99 vira 3. Para arredondar existe Math.round(3.99), que dá 4. Segundo, estreitar um inteiro que não cabe descarta os bits mais altos: 300 não cabe num byte (máximo 127), e o que sobra depois do corte é 44. O cast não faz o valor "caber". Ele só obriga a caber, e o resultado costuma ser lixo.

Sem o cast, o compilador explica o problema:

E3.java:3: error: incompatible types: possible lossy conversion from double to int
    int i = d;
            ^

"Possible lossy conversion" significa "possível conversão com perda". O compilador não sabe se o valor específico caberia. Ele sabe que o tipo em geral não cabe, e pede que você assuma a responsabilidade.

Há uma pegadinha na tabela: de long para float e para double, e de int para float, a conversão é automática, mas pode perder precisão. O float alcança números muito maiores que o long, só que com menos dígitos exatos:

long exato = 123_456_789_123_456_789L;
float aproximado = exato;
IO.println(exato);              // 123456789123456789
IO.println((long) aproximado);  // 123456790519087104

Os primeiros dígitos sobrevivem, e o resto vira aproximação. É raro isso importar, mas é bom saber que "automático" não significa "sem perda nenhuma".

var: deixando o compilador deduzir

Desde o Java 10, variáveis locais podem ser declaradas com var. O compilador descobre o tipo pelo valor inicial:

void main() {
    var contador = 10;      // int
    var media = 7.5;        // double
    var nome = "Java";      // String
    IO.println(contador + " " + media + " " + nome);
}

var não torna o Java dinâmico. O tipo continua sendo decidido na compilação e continua fixo: contador é um int para sempre, e tentar guardar texto nele dá erro. O var só evita escrever um tipo que é óbvio pelo lado direito. Por isso ele exige um valor inicial:

E5.java:4: error: cannot infer type for local variable v
    var v;
        ^
  (cannot use 'var' on variable without initializer)

Use var quando o tipo for evidente pela linha. Quando não for, escreva o tipo, porque quem lê o código agradece. Nesta fase do curso, os exemplos vão preferir o tipo explícito, para que você se acostume com eles.

final: valores que não mudam

Uma variável declarada com final recebe um valor uma vez e nunca mais muda:

final int DIAS_DA_SEMANA = 7;
DIAS_DA_SEMANA = 8;
E4.java:3: error: cannot assign a value to final variable DIAS_DA_SEMANA

A convenção para constantes, valores fixos com significado próprio, é escrever o nome em maiúsculas com _ separando as palavras. Constantes com nome tornam o código legível: precoTotal * TAXA_DE_SERVICO diz muito mais que precoTotal * 0.1. E se a taxa mudar, você altera um único lugar.

E o String?

Você deve ter notado que String não está na tabela dos primitivos. Ele não é um tipo primitivo, e sim uma classe. Um valor String é um objeto, e uma variável String guarda uma referência para esse objeto, não o texto diretamente. É por isso que o nome começa com maiúscula, como toda classe, e é por isso que ele tem métodos, como o .length() que você usou no jshell na aula anterior.

A distinção entre tipos primitivos e tipos de referência é uma das mais importantes do Java e tem consequências que ainda vão aparecer. A primeira delas já na próxima aula, que é dedicada a operar sobre esses valores: as contas com inteiros e reais, as comparações, o texto e seus métodos, e a leitura de números digitados pelo usuário. Você vai ver que comparar dois textos em Java tem uma armadilha que vem justamente de o String ser um objeto.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escolha o tipo mais adequado para guardar cada informação e justifique: a idade de uma pessoa, a população de um país, a altura em metros, se a pessoa aceitou os termos de uso, a letra do conceito de uma prova (A, B, C...) e o número do CPF.

Ver resposta

✓ Resposta: Idade: int. Um byte caberia, mas economizar memória numa variável solta não compensa a inconveniência, e int é o padrão para inteiros. População: long, porque a população da China ou da Índia passa do limite do int. Altura: double. Aceitou os termos: boolean. Conceito da prova: char. O CPF é a pegadinha: parece um número, mas não deve ser guardado como número. Um CPF pode começar com zero, e o zero à esquerda desapareceria. Ninguém faz conta com CPF, e ele costuma ter pontos e traço na exibição. O certo é tratá-lo como texto (String). A regra geral é que só vira tipo numérico o que se usa em cálculo.

Exercício 2

Sem rodar o código, preveja o que cada linha imprime. Depois confira no jshell.

IO.println(100 * 365 * 24 * 60 * 60);
IO.println(100L * 365 * 24 * 60 * 60);
IO.println((byte) 200);
IO.println((int) -3.99);
Ver resposta

✓ Resposta: A primeira imprime -1141367296: os segundos em cem anos (3.153.600.000) passam do limite do int, a conta é toda feita em int e dá a volta. A segunda imprime 3153600000, porque o L faz a conta ser feita em long. A terceira imprime -56: 200 não cabe num byte, que vai até 127, e o corte dos bits deixa 200 − 256 = −56. A quarta imprime -3, porque o cast para inteiro descarta a parte decimal em direção ao zero, sem arredondar, e isso vale também para negativos.

Exercício 3

O programa abaixo tem três erros de compilação. Encontre cada um, explique o motivo e corrija.

void main() {
    int total = 10.5;
    var desconto;
    final double TAXA = 0.1;
    TAXA = 0.2;
}
Ver resposta

✓ Resposta: Linha 2: 10.5 é um double, e guardá-lo num int é um estreitamento com perda. O compilador recusa com "possible lossy conversion". Correção: declarar double total = 10.5;, ou usar int total = (int) 10.5; se a intenção for de fato descartar a parte decimal. Linha 3: var exige um valor inicial para deduzir o tipo. Correção: var desconto = 0.0; ou double desconto;. Linha 5: TAXA é final e não pode receber outro valor. Correção: remover a linha, ou tirar o final se o valor realmente precisa mudar, o que nesse caso indicaria que não se trata de uma constante.

Exercício 4

Um colega escreveu long microPorDia = 24 * 60 * 60 * 1000 * 1000; e obteve 500654080. Ele diz que o long "não funcionou". Explique o que aconteceu e mostre duas formas de corrigir.

Ver resposta

✓ Resposta: O long funcionou, mas recebeu um valor que já estava errado. Todos os literais da conta são int, então a multiplicação acontece em int e estoura (o resultado correto, 86.400.000.000, passa de 2,1 bilhões). Só depois o int errado é alargado para long. O tipo da variável de destino não influencia o tipo da conta. Correções: marcar o primeiro literal como long (24L * 60 * 60 * 1000 * 1000), o que faz cada multiplicação seguinte acontecer em long, ou converter o primeiro valor explicitamente ((long) 24 * 60 * ...). Colocar o L só no último número até funcionaria aqui, porque 24 * 60 * 60 * 1000 (86.400.000) ainda cabe no int. Mas é sorte: com um fator a mais no meio, a conta estouraria antes de chegar ao L. O seguro é marcar o primeiro.

Exercício 5

Escreva um programa que declare as constantes SEGUNDOS_POR_MINUTO, MINUTOS_POR_HORA, HORAS_POR_DIA e DIAS_POR_ANO (365) e calcule quantos segundos tem uma vida de 80 anos. Use o tipo certo para o resultado.

Ver resposta

✓ Resposta:

void main() {
    final int SEGUNDOS_POR_MINUTO = 60;
    final int MINUTOS_POR_HORA = 60;
    final int HORAS_POR_DIA = 24;
    final int DIAS_POR_ANO = 365;

    int anos = 80;
    long segundos = (long) anos * DIAS_POR_ANO * HORAS_POR_DIA
                    * MINUTOS_POR_HORA * SEGUNDOS_POR_MINUTO;

    IO.println("Oitenta anos têm " + segundos + " segundos.");
}

A saída é Oitenta anos têm 2522880000 segundos. O valor passa de 2,1 bilhões, então o resultado precisa ser long, e, como no exercício anterior, a conta também. O (long) anos converte o primeiro operando e garante que todas as multiplicações aconteçam em long. As constantes com nome deixam a fórmula legível sem nenhum comentário.

Comentários

Mais em Java

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…

Contratos sem Implementação — Interfaces e Classes Abstratas
Contratos sem Implementação — Interfaces e Classes Abstratas

Classes abstratas e interfaces em Java: métodos abstratos, o Template Method…

Uma Linguagem para Torradeiras que Conquistou o Mundo — a História do Java
Uma Linguagem para Torradeiras que Conquistou o Mundo — a História do Java

Como um projeto da Sun para eletrodomésticos fracassou, foi salvo pela web e…