Java Classes e Objetos

Na aula O que é POO você viu a ideia: a classe é o molde e cada objeto é uma coisa feita com ele. Agora é hora de pôr a mão no código: escrever uma classe, criar objetos com new, entender o que a variável realmente guarda (uma referência) e o que acontece quando ela não guarda nada (null).

Você vai aprender
  • Como declarar uma classe e como criar objetos com new, linha por linha.
  • Como criar vários objetos independentes da mesma classe.
  • O que é uma referência: por que Fruta b = a; não cria uma fruta nova, e o que o == compara.
  • O que é null e como nasce o NullPointerException.
  • Como organizar duas classes: em arquivos separados (javac *.java) ou no mesmo arquivo.

Declarar uma classe

A loja da Ana vende frutas. Toda fruta tem um nome e um preço. Essa é a descrição, o molde:

class Fruta {
    String nome;
    double preco;
}
class
Palavra que começa a declaração de uma classe ("classe", em inglês).
Fruta
O nome da classe. Por convenção, começa com letra maiúscula e cada palavra nova também: Fruta, ContaBancaria, AlunoBolsista (o estilo se chama PascalCase). É assim que você distingue, de relance, uma classe (Fruta) de uma variável ou método (fruta, mostrar). E o nome vai no singular: a classe descreve uma fruta.
{ … }
O corpo da classe. Tudo o que pertence a ela fica aqui dentro.
String nome; double preco;
Os atributos: variáveis que cada fruta vai ter. Sem static: cada objeto terá os seus. Eles são o assunto da aula Atributos.

Declarar a classe não cria fruta nenhuma. É como desenhar a ficha em branco: ela descreve como as fichas serão, mas ainda não há ficha preenchida. Criar é o próximo passo.


Criar objetos com new

class Fruta {
    String nome;
    double preco;
}

public class Main {
    public static void main(String[] args) {
        Fruta maca = new Fruta();
        maca.nome = "maçã";
        maca.preco = 7.9;

        Fruta banana = new Fruta();
        banana.nome = "banana";
        banana.preco = 3.5;

        System.out.println(maca.nome + " custa " + maca.preco);
        System.out.println(banana.nome + " custa " + banana.preco);
    }
}
$ javac Main.java && java Main
maçã custa 7.9
banana custa 3.5

A linha mais importante da aula é esta: Fruta maca = new Fruta();. Ela tem quatro partes:

Fruta
O tipo da variável. Toda classe que você cria vira um tipo novo, como int ou String. Esta variável só aceita frutas.
maca
O nome da variável (minúscula, como toda variável).
new
"Novo". Pede ao Java que crie um objeto na memória. Sem new, nenhum objeto nasce.
Fruta()
Diz de qual classe é o objeto. Os parênteses chamam o construtor, um código que prepara o objeto recém-criado. Esta classe não escreveu nenhum; o Java fornece um vazio. Você vai escrever os seus em Construtores.

Depois de criado, o objeto é acessado pelo ponto: maca.nome = "maçã" lê-se "o nome da maçã recebe maçã". Cada new cria um objeto diferente: maca e banana são duas frutas, cada uma com o seu nome e o seu preço. Mudar uma não afeta a outra.

E se eu imprimir o objeto?

$ java Main
maçã custa 7.9
banana custa 3.5
Fruta@251a69d7
Fruta@7344699f

Acrescentando System.out.println(maca); e System.out.println(banana);, o Java mostra o nome da classe, uma @ e um número em hexadecimal que identifica o objeto (o número pode mudar de uma máquina para outra). Repare que os números são diferentes: são dois objetos. Para imprimir algo legível, como maçã (R$ 7.9), a classe precisa de um método especial, o toString(), que você verá em A Classe Object.

Brinque com o código: crie uma terceira fruta (uma uva de 12.0) e mostre-a. Depois imprima a variável direto, com System.out.println(uva);, e veja o Fruta@….

class Fruta {
    String nome;
    double preco;
}

public class Main {
    public static void main(String[] args) {
        Fruta maca = new Fruta();
        maca.nome = "maçã";
        maca.preco = 7.9;

        System.out.println(maca.nome + " custa " + maca.preco);
    }
}

A variável guarda uma referência

Aqui está o ponto que mais confunde quem começa. Uma variável de tipo primitivo (int, double) guarda o próprio valor. Uma variável de tipo classe não guarda o objeto: guarda uma referência, o "endereço" de onde o objeto está na memória. Pense no objeto como uma casa e na variável como um papel com o endereço da casa.

Veja o que acontece quando a Ana copia uma variável para outra:

class Fruta {
    String nome;
    double preco;
}

public class Main {
    public static void main(String[] args) {
        Fruta a = new Fruta();
        a.nome = "maçã";

        Fruta b = a;
        b.nome = "pera";

        System.out.println("a = " + a.nome + ", b = " + b.nome);
        System.out.println(a == b);

        Fruta c = new Fruta();
        c.nome = "pera";
        System.out.println(b == c);
        System.out.println(b.nome.equals(c.nome));
    }
}
$ javac Main.java && java Main
a = pera, b = pera       « mudou pelo b, apareceu no a
true                     « a e b apontam para o MESMO objeto
false                    « b e c são objetos diferentes, mesmo com o mesmo nome
true                     « os nomes (textos) são iguais
LinhaO que acontece
Fruta a = new Fruta();Uma fruta é criada (casa nº 1). a guarda o endereço "casa nº 1".
Fruta b = a;Nenhuma fruta nova. b recebe uma cópia do endereço: agora a e b têm o mesmo papel, "casa nº 1".
b.nome = "pera";Vai até a casa nº 1 e troca o nome. Como a aponta para a mesma casa, a.nome também é "pera".
a == bCom objetos, == compara os endereços, não o conteúdo. Mesmo endereço: true.
Fruta c = new Fruta();Um novo new: casa nº 2. Mesmo com o nome igual, b == c é false.

É a mesma regra que você viu com arrays em Parâmetros: passar um objeto para um método copia a referência, então o método mexe no mesmo objeto. E é por isso que textos se comparam com .equals(), não com == (veja Strings): String também é uma classe.

Missão: o programa mostra a = pera, b = pera, mas a Ana queria duas frutas diferentes. Mude uma linha para que b seja uma fruta nova, e a saída seja a = maçã, b = pera.

class Fruta {
    String nome;
    double preco;
}

public class Main {
    public static void main(String[] args) {
        Fruta a = new Fruta();
        a.nome = "maçã";

        Fruta b = a;
        b.nome = "pera";

        System.out.println("a = " + a.nome + ", b = " + b.nome);
    }
}
class Fruta {
    String nome;
    double preco;
}

public class Main {
    public static void main(String[] args) {
        Fruta a = new Fruta();
        a.nome = "maçã";

        Fruta b = new Fruta();
        b.nome = "pera";

        System.out.println("a = " + a.nome + ", b = " + b.nome);
    }
}

null: a variável que não aponta para nada

Uma variável de tipo classe pode guardar um valor especial: null ("nulo"), que quer dizer "nenhum objeto". É o papel em branco, sem endereço. Imprimir null funciona; o problema é tentar usar o objeto que não existe:

class Fruta {
    String nome;
    double preco;
}

public class Main {
    public static void main(String[] args) {
        Fruta escolhida = null;
        System.out.println("Escolhida: " + escolhida);
        escolhida.nome = "uva";
    }
}
$ javac Main.java && java Main
Escolhida: null
Exception in thread "main" java.lang.NullPointerException: Cannot assign field "nome" because "<local1>" is null
	at Main.main(Main.java:10)
Escolhida: null
A linha 9 rodou: imprimir uma referência nula mostra o texto null.
java.lang.NullPointerException
"Exceção de ponteiro nulo", o famoso NPE. Você tentou ir até a casa de um endereço em branco.
Cannot assign field "nome"
"Não é possível atribuir o campo nome…"
because "<local1>" is null
"…porque <local1> é nulo". O <local1> é a variável local número 1 do método (a escolhida; a número 0 é o args). O Java não mostra o nome porque, por padrão, o javac não guarda os nomes das variáveis locais no .class.
at Main.main(Main.java:10)
Onde aconteceu: método main, linha 10. É sempre por aqui que você começa a investigar.

Compilando com a opção -g (de debug, "depuração"), o javac guarda os nomes, e a mensagem fica mais clara:

$ javac -g Main.java && java Main
Escolhida: null
Exception in thread "main" java.lang.NullPointerException: Cannot assign field "nome" because "escolhida" is null
	at Main.main(Main.java:10)

Para ler um atributo, a mensagem muda de Cannot assign field para Cannot read field; para chamar um método, Cannot invoke "…". Um caso muito comum: arrays de objetos começam cheios de null, e o new Fruta[3] cria o array, não as frutas:

class Fruta {
    String nome;
    double preco;
}

public class Main {
    public static void main(String[] args) {
        Fruta[] cesta = new Fruta[3];
        System.out.println(cesta[0]);
        cesta[0] = new Fruta();
        cesta[0].nome = "maçã";
        System.out.println(cesta[0].nome);
        System.out.println(cesta[1].nome);
    }
}
$ javac Main.java && java Main
null
maçã
Exception in thread "main" java.lang.NullPointerException: Cannot read field "nome" because "<local1>[1]" is null
	at Main.main(Main.java:13)

A posição 0 recebeu uma fruta com new; a posição 1 continuou null. O "<local1>[1]" diz exatamente qual posição do array estava vazia.

null × "não inicializada"

São coisas diferentes. Fruta escolhida = null; é uma variável com valor (o valor "nada"); o erro só aparece ao rodar. Já Fruta escolhida; sem valor nenhum, seguido de escolhida.nome = "uva";, nem compila:

$ javac Main.java
Main.java:9: error: variable escolhida might not have been initialized
        escolhida.nome = "uva";
        ^
1 error

Onde colocar a classe: arquivos separados ou o mesmo arquivo

Forma 1: um arquivo por classe (a forma profissional)

Em projetos de verdade, cada classe fica no seu próprio arquivo, com o mesmo nome da classe. A Ana criou uma pasta com dois arquivos:

public class Fruta {
    String nome;
    double preco;
}
public class Main {
    public static void main(String[] args) {
        Fruta maca = new Fruta();
        maca.nome = "maçã";
        maca.preco = 7.9;
        System.out.println(maca.nome + " custa " + maca.preco);
    }
}
$ ls
Fruta.java  Main.java
$ javac *.java
$ ls
Fruta.class  Fruta.java  Main.class  Main.java
$ java Main
maçã custa 7.9
javac *.java
Compila todos os arquivos .java da pasta (o * é um curinga do terminal: "todos os nomes terminados em .java"). Cada classe vira o seu .class.
java Main
Roda a classe que tem o main. A JVM encontra Fruta.class sozinha, na mesma pasta, quando precisa dela.

Na verdade, javac Main.java também funcionaria: ao ver que Main usa Fruta, o javac procura Fruta.java na pasta e compila junto.

$ rm *.class
$ javac Main.java
$ ls
Fruta.class  Fruta.java  Main.class  Main.java      « a Fruta.class foi compilada automaticamente

E, a partir do Java 22, o modo "arquivo-fonte" também encontra as outras classes da pasta, sem compilar antes: Java 22+

$ rm *.class
$ java Main.java
maçã custa 7.9

Forma 2: as duas classes no mesmo arquivo

Foi o que você fez até aqui nesta aula: class Fruta (sem public) e public class Main no mesmo Main.java. A regra é: um arquivo pode ter várias classes, mas só uma public, e ela dá nome ao arquivo.

Qual usar?

FormaVantagensQuando usar
Um arquivo por classe, cada uma publicFácil de achar cada classe; outras partes do programa (outros pacotes) podem usá-la; é o padrão de IDEs, Maven e GradleQualquer programa de verdade
Várias classes num arquivo, só uma publicTudo à vista num lugar só; ótimo para exemplos curtos e para o editor do guiaExemplos, exercícios, classes pequenas de apoio

Quando dá errado

NullPointerException
$ java Main
Exception in thread "main" java.lang.NullPointerException: Cannot assign field "nome" because "<local1>" is null
	at Main.main(Main.java:10)

O que aconteceu: a variável vale null e o programa tentou usar um atributo ou método dela.

Como resolver: vá à linha indicada e descubra qual variável está nula (compile com javac -g para ver o nome). Em geral faltou um new, ou um elemento de array ainda não recebeu objeto. Se null é um valor possível, teste antes: if (escolhida != null) { … }.

"cannot find symbol … symbol: class Fruta"
$ javac Main.java
Main.java:3: error: cannot find symbol
        Fruta maca = new Fruta();
        ^
  symbol:   class Fruta
  location: class Main
Main.java:3: error: cannot find symbol
        Fruta maca = new Fruta();
                         ^
  symbol:   class Fruta
  location: class Main
2 errors

O que aconteceu: o compilador não encontrou a classe Fruta: não há Fruta.java na pasta, nem uma class Fruta no mesmo arquivo. O erro aparece duas vezes porque Fruta é usada duas vezes na linha (no tipo e no new).

Como resolver: confira se o arquivo existe, se está na mesma pasta e se o nome bate exatamente, com maiúsculas (Fruta.java, não fruta.java).

Rodar a classe errada
$ java Fruta
Erro: o método main não é Fruta na classe javafx.application.Application; defina o método main como:
   public static void main(String[] args)

O que aconteceu: a Ana mandou rodar Fruta, que não tem main. A frase em português sai embaralhada (é um defeito da tradução do próprio JDK). Em inglês, com LANG=C java Fruta, a mensagem é clara: Error: Main method not found in class Fruta, please define the main method as: public static void main(String[] args).

Como resolver: rode a classe que tem o main: java Main.

"class … is public, should be declared in a file named …"

O que aconteceu: uma classe public está num arquivo com outro nome, ou há duas classes public no mesmo arquivo.

Como resolver: deixe só uma public por arquivo, com o mesmo nome do arquivo; tire o public das outras ou mova-as para arquivos próprios. Veja O que é POO.


Referência

EscritaSignificado
class Nome { … }Declara uma classe (nome em PascalCase, no singular)
Nome var = new Nome();Cria um objeto e guarda a referência dele em var
var.campoAcessa um atributo do objeto
Nome b = a;Copia a referência: a e b apontam para o mesmo objeto
a == bCom objetos: true só se for o mesmo objeto
Nome var = null;Variável sem objeto; usar var.campo dá NullPointerException
new Nome[3]Cria um array com 3 posições null (não cria os objetos)
javac *.javaCompila todas as classes da pasta
javac -g …Guarda os nomes das variáveis (mensagens de NPE mais claras)
java Main.java com várias classes na pastaRoda sem compilar antes Java 22+

Missão final: a classe Livro, em outro arquivo

Missão: esta caixa tem dois arquivos (veja as abas). Em Livro.java, complete a classe com os atributos String titulo e String autor. Em Main.java, crie dois livros (Dom Casmurro, de Machado de Assis, e A Hora da Estrela, de Clarice Lispector) e imprima cada um no formato Dom Casmurro, de Machado de Assis.

public class Main {
    public static void main(String[] args) {
        // crie os dois livros aqui

    }
}
public class Livro {
    // atributos aqui
}
public class Main {
    public static void main(String[] args) {
        Livro l1 = new Livro();
        l1.titulo = "Dom Casmurro";
        l1.autor = "Machado de Assis";

        Livro l2 = new Livro();
        l2.titulo = "A Hora da Estrela";
        l2.autor = "Clarice Lispector";

        System.out.println(l1.titulo + ", de " + l1.autor);
        System.out.println(l2.titulo + ", de " + l2.autor);
    }
}
public class Livro {
    String titulo;
    String autor;
}

Exercícios

1. A palavra que cria

Complete para criar um objeto da classe Aluno:

Aluno a = Aluno();

2. Nome do arquivo

A classe public class ContaBancaria precisa ficar em qual arquivo?

3. Nenhum objeto

Qual valor indica que uma variável de tipo classe não aponta para objeto nenhum?

4. Depois de Fruta x = new Fruta(); Fruta y = x; y.preco = 5;, quanto vale x.preco?

5. Quantos objetos Fruta existem depois de Fruta[] f = new Fruta[10];?

6. new Fruta() == new Fruta() vale:


Resumo

  • Uma classe se declara com class Nome { … }; o nome é PascalCase e singular. Declarar não cria objetos.
  • new Nome() cria um objeto; cada new é um objeto diferente, com os seus próprios atributos.
  • A variável guarda uma referência (o endereço). b = a faz as duas apontarem para o mesmo objeto; == compara referências.
  • null = nenhum objeto. Usar um atributo ou método de null causa NullPointerException; a mensagem diz qual variável era nula.
  • Cada classe public fica no arquivo de mesmo nome. Compile tudo com javac *.java e rode a classe do main.
  • Num mesmo arquivo, várias classes, mas só uma public.