A primeira aula da Fase 2: como criar seus próprios tipos, juntando dados e comportamento num mesmo lugar.
A Fase 1 terminou com um problema: dados que pertencem juntos, como o nome e a nota de um aluno, ou o titular e o saldo de uma conta, ficavam espalhados em variáveis e arrays paralelos, e nada garantia que continuassem juntos. Nesta aula você vai aprender a criar os seus próprios tipos com classes, a fabricar objetos a partir delas e a controlar o nascimento de cada objeto com construtores. No caminho, as últimas palavras misteriosas de public static void main(String[] args) ganham explicação, e o construtor que o javap revelou em "Do Código ao Bytecode" deixa de ser um mistério.
A classe como molde
Uma classe é a descrição de um tipo: quais dados cada valor desse tipo carrega e o que se pode fazer com ele. Um objeto é um valor concreto criado a partir dessa descrição, uma instância da classe. A imagem clássica é a da forma de biscoito e dos biscoitos: a forma define o formato e cada biscoito é uma instância. Uma classe ContaBancaria descreve o que toda conta tem, e cada conta real, a da Ana ou a do Bruno, é um objeto.
A partir desta aula, os programas terão mais de uma classe, e vamos usar a forma clássica, uma classe por arquivo. Crie uma pasta aula-08 com dois arquivos. O primeiro, ContaBancaria.java:
public class ContaBancaria {
String titular;
double saldo;
}
As duas variáveis declaradas diretamente dentro da classe são os campos (também chamados de atributos). Cada objeto da classe terá o seu próprio titular e o seu próprio saldo.
O segundo arquivo, Banco.java, usa a classe:
public class Banco {
public static void main(String[] args) {
ContaBancaria conta = new ContaBancaria();
conta.titular = "Ana";
conta.saldo = 150.0;
ContaBancaria outra = new ContaBancaria();
outra.titular = "Bruno";
IO.println(conta.titular + ": " + conta.saldo);
IO.println(outra.titular + ": " + outra.saldo);
}
}
Execute com java Banco.java. Desde o Java 22, quando o arquivo passado ao java usa uma classe que não está nele, o lançador procura o arquivo correspondente na mesma pasta e o compila também. É por isso que ContaBancaria.java precisa se chamar exatamente assim.
Ana: 150.0
Bruno: 0.0
O operador new cria um objeto novo, reserva memória para os campos dele e devolve uma referência. A variável conta guarda essa referência, exatamente como as variáveis de array da aula anterior. O ponto (conta.titular) acessa um campo do objeto apontado.
Repare no saldo do Bruno, 0.0, embora ninguém o tenha definido. Os campos, como os elementos de um array, recebem valores padrão quando o objeto é criado: zero, false ou null. Só as variáveis locais precisam ser inicializadas antes do uso.
Referências, de novo
Tudo o que você aprendeu sobre referências com arrays vale para qualquer objeto:
ContaBancaria mesma = conta;
mesma.saldo = 0;
IO.println(conta.saldo); // 0.0
IO.println(conta == mesma); // true
mesma e conta são duas referências para o mesmo objeto, e mudar o saldo por uma muda para a outra. O == compara referências. Imprimir o objeto diretamente mostra algo como ContaBancaria@7d68ef40, o nome da classe e um código de identificação, pela mesma razão que imprimir um array mostrava [I@.... Como ensinar um objeto a se descrever de forma legível é assunto de "Iguais ou o Mesmo?".
Os objetos vivem numa área de memória chamada heap. As variáveis locais, que guardam as referências, vivem na pilha de cada chamada de método, a mesma pilha que estourou com a recursão em "Dividir para Resolver". Quando nenhuma referência aponta mais para um objeto, ele se torna inalcançável, e o coletor de lixo pode recuperar a memória dele. Você nunca libera um objeto manualmente.
null: a referência para nada
Uma variável de referência também pode não apontar para objeto nenhum. Esse valor se escreve null, e é o valor padrão de campos de referência, como o titular de uma conta recém-criada. Tentar usar um objeto que não existe interrompe o programa:
ContaBancaria semObjeto = null;
IO.println(semObjeto.titular);
Exception in thread "main" java.lang.NullPointerException: Cannot read field "titular" because "semObjeto" is null
at Banco.main(Banco.java:20)
A NullPointerException é o erro mais comum da história do Java. Desde o Java 14 (e por padrão a partir do 15), a mensagem diz exatamente qual referência estava nula. Um detalhe: o nome da variável só aparece quando o código é compilado com informações de depuração (javac -g). Sem elas, a mensagem mostra algo como "<local4>" no lugar do nome, e é o que você verá ao rodar com java Banco.java. As IDEs compilam com essas informações por padrão. As formas de evitar esse erro vão de disciplina a recursos da linguagem, e têm uma aula própria, "Um Valor que Pode Não Existir".
Métodos de instância: comportamento junto com os dados
Uma classe só com campos é pouco mais que um array com nomes. O ganho real vem de colocar na própria classe as operações sobre os dados. Estes são os métodos de instância:
import java.util.Locale;
public class ContaBancaria {
String titular;
double saldo;
void depositar(double valor) {
saldo += valor;
}
boolean sacar(double valor) {
if (valor > saldo) {
return false;
}
saldo -= valor;
return true;
}
void transferir(double valor, ContaBancaria destino) {
if (sacar(valor)) {
destino.depositar(valor);
}
}
String resumo() {
return String.format(Locale.of("pt", "BR"), "%s: R$ %,.2f", titular, saldo);
}
}
Dentro de um método de instância, os nomes saldo e titular se referem aos campos do objeto sobre o qual o método foi chamado. Em ana.depositar(250.0), o saldo é o da Ana. Em bruno.depositar(250.0), o do Bruno. O mesmo código opera sobre objetos diferentes conforme quem o chama.
Compare com o menu de "Dividir para Resolver". Lá, sacar recebia o saldo como parâmetro e devolvia o saldo novo, porque pela passagem por valor não conseguia alterá-lo. Agora o saldo mora dentro do objeto, e o método o altera diretamente. Os dados e as regras que os protegem ficaram no mesmo lugar.
Repare na primeira linha: import java.util.Locale;. Nos arquivos compactos das aulas anteriores, a biblioteca básica do Java vinha importada automaticamente. Numa classe clássica, as classes que não estão no pacote java.lang (onde vivem String, Math, System e IO) precisam ser importadas. Sem o import, o compilador não sabe o que é Locale:
ContaBancaria.java:33: error: cannot find symbol
return String.format(Locale.of("pt", "BR"), ...
^
symbol: variable Locale
Pacotes e importações têm sua aula, "Do Arquivo Solto ao Projeto". Por ora, basta saber que o import informa ao compilador em que pacote uma classe está.
Construtores: nascer pronto
Do jeito que está, uma conta nasce com titular null e depois recebe os valores, campo por campo. Nada impede esquecer um deles, e uma conta sem titular não faz sentido. Um construtor é um bloco especial, executado quando o objeto é criado com new, que garante que o objeto nasça num estado válido:
public class ContaBancaria {
String titular;
double saldo;
ContaBancaria(String titular, double saldoInicial) {
this.titular = titular;
this.saldo = saldoInicial;
}
ContaBancaria(String titular) {
this(titular, 0.0);
}
// ... depositar, sacar, transferir e resumo, como antes
}
Um construtor tem o mesmo nome da classe e nenhum tipo de retorno, nem void. Os argumentos passados ao new vão para os parâmetros dele:
ContaBancaria ana = new ContaBancaria("Ana", 1000.0);
ContaBancaria bruno = new ContaBancaria("Bruno");
Três detalhes merecem atenção.
O this. Dentro do construtor, o parâmetro titular tem o mesmo nome do campo titular, e o nome mais próximo, o do parâmetro, "esconde" o campo. A palavra this é uma referência ao próprio objeto que está sendo construído, e this.titular significa "o campo titular deste objeto". Sem ela, titular = titular atribuiria o parâmetro a ele mesmo, e o campo continuaria null, um erro que o exercício 4 explora.
A sobrecarga de construtores. Como os métodos em "Dividir para Resolver", construtores podem ser sobrecarregados. O segundo aceita só o titular e, com this(titular, 0.0), delega ao primeiro. Assim, a lógica de inicialização fica num lugar só. A chamada this(...) precisa ser a primeira instrução do construtor.
O construtor padrão. Uma classe sem nenhum construtor ganha automaticamente um construtor sem parâmetros e sem corpo, o construtor padrão. Era ele que aparecia no javap -c Ola de "Do Código ao Bytecode", como public Ola(), e é por isso que new ContaBancaria() funcionava na primeira versão desta aula. Mas assim que você escreve qualquer construtor, o padrão deixa de ser criado:
Erro.java:3: error: no suitable constructor found for ContaBancaria(no arguments)
ContaBancaria c = new ContaBancaria();
^
É exatamente o que queremos: agora é impossível criar uma conta sem titular.
Um Banco.java que usa a classe completa:
public class Banco {
public static void main(String[] args) {
ContaBancaria ana = new ContaBancaria("Ana", 1000.0);
ContaBancaria bruno = new ContaBancaria("Bruno");
ana.depositar(250.0);
ana.transferir(400.0, bruno);
boolean conseguiu = bruno.sacar(1000.0);
IO.println(ana.resumo());
IO.println(bruno.resumo());
IO.println("Saque de 1000 do Bruno: " + (conseguiu ? "ok" : "recusado"));
}
}
Ana: R$ 850,00
Bruno: R$ 400,00
Saque de 1000 do Bruno: recusado
O main agora conversa com objetos que sabem cuidar de si mesmos. Ninguém fora da classe precisa saber como um saque é validado: basta pedir sacar e olhar o resultado.
O que ainda está errado
Acrescente uma linha ao fim do main:
bruno.saldo = -5000;
IO.println(bruno.resumo()); // Bruno: R$ -5.000,00
Todo o cuidado do método sacar com o saldo insuficiente pode ser contornado por uma linha que altera o campo diretamente. A classe tem regras, mas não consegue garantir que elas sejam respeitadas. Resolver isso, escondendo os campos e deixando o acesso só pelos métodos, é o tema da próxima aula, "O que Fica Dentro, o que Fica Fora", e é a primeira das grandes ideias da orientação a objetos.
public static void main, enfim
Com esta aula, dá para ler a assinatura clássica do main palavra por palavra:
publicpermite que o método seja chamado de fora da classe, no caso pela JVM. É um modificador de acesso, tema da próxima aula.staticfaz o método pertencer à classe, e não a cada objeto. Um métodostaticpode ser chamado sem que exista nenhuma instância, e é exatamente o que a JVM precisa ao iniciar o programa, quando ainda não há objeto nenhum.Math.sqrteInteger.parseInttambém sãostatic: você os chama pelo nome da classe, semnew.voiddiz que o método não devolve nada.mainé o nome que a JVM procura.String[] argsé o array de argumentos de linha de comando, da aula anterior.
O static explica um erro que todo iniciante encontra ao sair dos arquivos compactos:
public class Estatico {
public static void main(String[] args) {
IO.println(quadrado(5));
}
int quadrado(int x) {
return x * x;
}
}
Estatico.java:3: error: non-static method quadrado(int) cannot be referenced from a static context
IO.println(quadrado(5));
^
quadrado é um método de instância: precisa de um objeto para ser chamado. O main é static e roda sem objeto nenhum, então não há sobre qual objeto chamar quadrado. Há duas correções: declarar static int quadrado(int x), se o método não depende de nenhum dado de objeto, ou criar um objeto e chamar o método nele.
E os arquivos compactos? Eles funcionavam sem static porque o Java cria uma classe implícita a partir do arquivo, e o lançador cria uma instância dela e chama o main nesse objeto. Os métodos que você escreveu ao lado do main nas aulas anteriores eram métodos de instância dessa classe implícita, por isso podiam ser chamados sem static. Um arquivo compacto também pode declarar outras classes:
void main() {
Ponto p = new Ponto(3, 4);
IO.println(p.distanciaDaOrigem()); // 5.0
}
class Ponto {
double x;
double y;
Ponto(double x, double y) {
this.x = x;
this.y = y;
}
double distanciaDaOrigem() {
return Math.sqrt(x * x + y * y);
}
}
Compilando com javac, surgem dois arquivos: Compacto.class e Compacto$Ponto.class. O $ indica que Ponto virou uma classe declarada dentro da classe implícita, o que o Java chama de classe aninhada. O arquivo compacto nunca foi outro Java: é o mesmo Java com a cerimônia escondida, e a partir de agora você sabe o que ela esconde.
Nas próximas aulas, os exemplos com uma só classe podem continuar usando arquivos compactos, e os exemplos com várias classes usam a forma clássica, um arquivo por classe, que é como os projetos reais se organizam.
Fontes e leituras recomendadas
- The Java Language Specification, Java SE 25, capítulo 8, "Classes": https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html. A definição formal de classes, campos, métodos e construtores, incluindo as regras do construtor padrão (seção 8.8.9).
- Oracle, The Java Tutorials, "Classes and Objects": https://docs.oracle.com/javase/tutorial/java/javaOO/index.html. A trilha clássica do tutorial sobre classes, objetos, construtores e o
this. - JEP 358, "Helpful NullPointerExceptions": https://openjdk.org/jeps/358. A proposta que tornou as mensagens de
NullPointerExceptionprecisas, com os detalhes de quando aparece o nome da variável e quando aparece<localN>. - JEP 458, "Launch Multi-File Source-Code Programs": https://openjdk.org/jeps/458. A proposta que permitiu executar
java Banco.javaencontrandoContaBancaria.javaautomaticamente. - Joshua Bloch, Effective Java, 3ª edição, capítulo 2, "Creating and Destroying Objects": o capítulo sobre construtores e formas alternativas de criar objetos. Algumas técnicas usam recursos das próximas aulas, e vale reler depois da Fase 2.
Exercícios
Exercício 1
No código abaixo, identifique a classe, os campos, o construtor, os objetos e as referências. Quantos objetos ContaBancaria existem ao final?
ContaBancaria a = new ContaBancaria("Ana");
ContaBancaria b = new ContaBancaria("Bia");
ContaBancaria c = a;
a = b;
Ver resposta
✓ Resposta: A classe é ContaBancaria, os campos são titular e saldo, e o construtor usado é o de um parâmetro, ContaBancaria(String titular). Existem dois objetos, porque há dois new: a conta da Ana e a conta da Bia. a, b e c são referências, e não objetos. Ao final, c aponta para a conta da Ana (copiou a referência antes de a mudar), e a e b apontam para a conta da Bia. Se c não tivesse guardado a referência, a conta da Ana ficaria sem nenhuma referência e poderia ser recolhida pelo coletor de lixo. Contar objetos pelos new, e não pelas variáveis, evita muita confusão.
Exercício 2
Sem rodar, diga o que o programa imprime, supondo a classe ContaBancaria completa da aula.
ContaBancaria x = new ContaBancaria("Xavier", 100.0);
ContaBancaria y = x;
y.depositar(50.0);
x.transferir(30.0, y);
IO.println(x.resumo());
Ver resposta
✓ Resposta: Imprime Xavier: R$ 150,00. x e y apontam para o mesmo objeto. O depósito de 50 leva o saldo a 150. A transferência de 30 de x para y saca 30 (saldo 120) e deposita 30 na mesma conta (saldo 150 de novo). O exercício mostra um caso que a classe não previu: transferir de uma conta para ela mesma. Aqui o resultado foi inofensivo, mas num sistema que cobrasse tarifa por transferência, ou registrasse extratos, uma transferência para a própria conta seria um problema. Um if (destino == this) no começo de transferir poderia recusá-la. É um bom uso do ==, porque aqui a pergunta é justamente se as duas referências apontam para o mesmo objeto.
Exercício 3
Crie uma classe Retangulo com os campos largura e altura, um construtor que receba os dois, e os métodos area(), perimetro() e ehQuadrado(). Escreva um main que crie dois retângulos e mostre os resultados.
Ver resposta
✓ Resposta: Retangulo.java:
public class Retangulo {
double largura;
double altura;
Retangulo(double largura, double altura) {
this.largura = largura;
this.altura = altura;
}
double area() {
return largura * altura;
}
double perimetro() {
return 2 * (largura + altura);
}
boolean ehQuadrado() {
return largura == altura;
}
}
Principal.java:
public class Principal {
public static void main(String[] args) {
Retangulo r1 = new Retangulo(3, 4);
Retangulo r2 = new Retangulo(5, 5);
IO.println("r1: área " + r1.area() + ", perímetro " + r1.perimetro()
+ ", quadrado? " + r1.ehQuadrado());
IO.println("r2: área " + r2.area() + ", perímetro " + r2.perimetro()
+ ", quadrado? " + r2.ehQuadrado());
}
}
A saída é r1: área 12.0, perímetro 14.0, quadrado? false e r2: área 25.0, perímetro 20.0, quadrado? true. Os argumentos 3 e 4 são int e são alargados para double. Comparar double com == funciona aqui porque os valores vieram do mesmo literal. Com resultados de contas, a imprecisão de "Cada Valor em sua Caixa" exigiria comparar com uma tolerância. E nada impede um retângulo de largura negativa, o que a próxima aula resolve.
Exercício 4
A classe abaixo imprime null. Explique por quê e corrija.
public class Pessoa {
String nome;
Pessoa(String nome) {
nome = nome;
}
public static void main(String[] args) {
IO.println(new Pessoa("Ana").nome);
}
}
Ver resposta
✓ Resposta: Dentro do construtor, o nome nome se refere ao parâmetro, que esconde o campo de mesmo nome. A linha nome = nome atribui o parâmetro a ele mesmo e não toca no campo, que continua com o valor padrão, null. A correção é this.nome = nome;, em que this.nome é o campo do objeto e nome é o parâmetro. O código compila sem erro, o que torna o problema traiçoeiro, mas IDEs e ferramentas de análise costumam apontar "atribuição de variável a ela mesma". Por isso é convenção escrever sempre this.campo = parametro nos construtores, mesmo quando os nomes são diferentes.
Exercício 5
Explique por que o main da classe Estatico não consegue chamar quadrado, apresente as duas correções possíveis e diga qual é mais adequada nesse caso.
Ver resposta
✓ Resposta: O main é static, pertence à classe e executa sem nenhum objeto. quadrado é um método de instância e precisa de um objeto para ser chamado. Dentro do main não há objeto para usar, e o compilador acusa "non-static method cannot be referenced from a static context". A primeira correção é tornar o método estático, com static int quadrado(int x). A segunda é criar um objeto e chamar o método nele, com new Estatico().quadrado(5). Nesse caso a primeira é a adequada, porque quadrado não usa nenhum campo: o resultado depende só do parâmetro, como Math.sqrt. A regra prática é esta: se o método precisa dos dados de um objeto, é de instância, e se depende só dos parâmetros, pode ser static. Criar um objeto só para chamar um método que não usa o objeto é cerimônia sem propósito. Os arquivos compactos fazem exatamente isso por baixo dos panos, para que o iniciante não precise conhecer o static no primeiro dia.