Uma classe Java descreve o que um objeto guarda e o que ele sabe fazer, e um construtor decide como cada objeto começa a vida. Aprenda campos, this, construtores, private, static e final rodando programas pequenos.
Uma classe descreve um tipo de coisa: que dados ela guarda e o que ela sabe fazer. Um objeto é uma dessas coisas, criado a partir da classe com new. Quase todo o Java que você vai ler são classes conversando com objetos, então as regras para criar e proteger objetos importam em todo lugar.
Este post cobre campos e métodos, this, toString, construtores e como eles se encadeiam, a mudança do Java 25 que deixa código rodar antes de super(...), private e os outros níveis de acesso, static e final. Todo programa abaixo rodou no Java 25, e a saída foi colada da execução. Para rodar um você mesmo, salve como Main.java e rode java Main.java.
Uma classe é uma planta, e new cria um objeto
Uma classe declara campos, que guardam os dados de cada objeto, e métodos, que agem sobre esses dados. Aqui está um Player com um nome e uma pontuação:
class Player {
String name;
int score;
void addPoints(int points) {
score = score + points;
}
}
void main() {
var ana = new Player();
ana.name = "Ana";
ana.addPoints(10);
ana.addPoints(5);
var ben = new Player();
ben.name = "Ben";
ben.addPoints(7);
IO.println(ana.name + " has " + ana.score);
IO.println(ben.name + " has " + ben.score);
}
Ele imprime:
Ana has 15
Ben has 7
new Player() cria um objeto novo, e cada objeto ganha a própria cópia de name e score. Somar pontos em ana não mexe em ben. Os campos de um objeto novo começam com um valor padrão: 0 para números, false para boolean e null para referências como String.
Dentro de addPoints, score é a pontuação do objeto em que o método foi chamado. Em ana.addPoints(10), é a pontuação da Ana.
this é o objeto em que o método foi chamado
A palavra-chave this nomeia o objeto atual, e você precisa dela quando um parâmetro tem o mesmo nome de um campo. Esqueça dela e o código ainda compila:
class Player {
String name;
void rename(String name) {
name = name;
}
void renameProperly(String name) {
this.name = name;
}
}
void main() {
var p = new Player();
p.rename("Ana");
IO.println("after rename: " + p.name);
p.renameProperly("Ana");
IO.println("after renameProperly: " + p.name);
}
Ele imprime:
after rename: null
after renameProperly: Ana
Em rename, os dois lados de name = name são o parâmetro. O parâmetro esconde o campo, então o método copia o parâmetro nele mesmo e o campo continua null. Nem javac -Xlint:all avisa. Escrever this.name diz “o campo deste objeto”, e essa é a versão que funciona.
Imprimindo um objeto: sobrescreva toString
Quando você imprime um objeto, o Java chama o método toString() dele, e o padrão não foi feito para pessoas. Imprima um Player sem toString e você recebe algo como Main$Player@ seguido de oito dígitos hexadecimais.
A parte antes do @ é o nome da classe. Aparece Main$Player porque, num arquivo com um void main() solto, suas classes ficam dentro de uma classe escondida chamada Main. Esse detalhe volta na seção sobre private. A parte depois do @ é o identity hash code do objeto em hexadecimal. Não é um endereço de memória e não depende dos campos. A JVM inventa esse número na primeira vez que alguém pede.
Como a JVM inventa, você não pode confiar nele. Quando rodamos o mesmo arquivo cinco vezes com java Main.java, ele imprimiu os mesmos dígitos toda vez. Quando compilamos o mesmo arquivo com javac e rodamos a classe, os dígitos mudaram. Outra JVM, outro modo de execução ou um objeto a mais com hash calculado antes pode mudar o valor. Por isso nenhuma saída desta série mostra um.
Escreva seu próprio toString e imprimir fica útil e previsível:
class Player {
String name;
int score;
Player(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Player[" + name + ", " + score + "]";
}
}
void main() {
var ana = new Player("Ana", 15);
IO.println(ana);
IO.println("winner: " + ana);
}
Ele imprime:
Player[Ana, 15]
winner: Player[Ana, 15]
toString precisa ser public, porque sobrescreve um método público de Object, a classe que toda classe estende. @Override pede ao compilador que confira se você está mesmo sobrescrevendo algo. Erre a grafia para toSting e o build falha, em vez de imprimir o hash sem avisar. A concatenação de strings também chama toString, e é por isso que "winner: " + ana funciona.
Esse programa também usou um construtor, Player(String name, int score). Construtores são o próximo assunto.
Construtores preparam um objeto novo
Um construtor é o código que roda quando você escreve new. Ele tem o nome da classe e nenhum tipo de retorno, e o trabalho dele é colocar o objeto num estado inicial válido.
Sobrecarga e encadeamento com this(...)
Uma classe pode ter vários construtores, desde que as listas de parâmetros sejam diferentes. Isso se chama sobrecarga. Um construtor pode chamar outro com this(...), e assim a preparação de verdade fica num lugar só:
class Pizza {
String size;
int slices;
Pizza(String size, int slices) {
this.size = size;
this.slices = slices;
}
Pizza(String size) {
this(size, size.equals("large") ? 12 : 8);
}
Pizza() {
this("medium");
}
@Override
public String toString() {
return size + " pizza, " + slices + " slices";
}
}
void main() {
IO.println(new Pizza("small", 6));
IO.println(new Pizza("large"));
IO.println(new Pizza());
}
Ele imprime:
small pizza, 6 slices
large pizza, 12 slices
medium pizza, 8 slices
new Pizza() chama this("medium"), que chama this("medium", 8), que define os campos. O compilador escolhe o construtor pelos argumentos que você passa, do mesmo jeito que escolhe entre métodos sobrecarregados.
O construtor grátis some quando você escreve um
Se uma classe não tem nenhum construtor, o Java dá a ela um vazio, sem argumentos. É por isso que new Player() funcionou no primeiro programa. Escreva qualquer construtor e o grátis desaparece:
class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
void main() {
var origin = new Point();
IO.println(origin.x);
}
O build falha com:
Main.java:12: error: constructor Point in class Main.Point cannot be applied to given types;
var origin = new Point();
^
O javac acrescenta que esperava int,int e não encontrou argumentos. Isso é de propósito. Depois que você disse que um Point precisa de dois números, o Java não cria um sem eles. Se quiser os dois jeitos, escreva você mesmo o construtor sem argumentos, de preferência como this(0, 0).
Valide no construtor
O construtor é o único lugar por onde todo objeto passa, então é o lugar certo para recusar entrada ruim. Lance IllegalArgumentException e o objeto nunca é criado:
class Temperature {
double celsius;
Temperature(double celsius) {
if (celsius < -273.15) {
throw new IllegalArgumentException("below absolute zero: " + celsius);
}
this.celsius = celsius;
}
}
void main() {
var room = new Temperature(21.5);
IO.println("room is " + room.celsius);
var impossible = new Temperature(-300);
IO.println("never printed " + impossible.celsius);
}
Ele imprime e para:
room is 21.5
Exception in thread "main" java.lang.IllegalArgumentException: below absolute zero: -300.0
-300 virou -300.0 na mensagem porque o parâmetro é um double. Nada nunca guarda uma Temperature abaixo do zero absoluto, então nenhum outro método precisa conferir.
Código antes de super(...): novidade do Java 25
Um construtor numa subclasse precisa chamar um construtor da classe pai, com super(...). Durante quase toda a história do Java, essa chamada tinha que ser a primeiríssima instrução. O Java 25 tornou definitivos os flexible constructor bodies (corpos de construtor flexíveis), e agora você pode rodar código antes dela.
Isso resolve um incômodo antigo. Muitas vezes você quer conferir um argumento antes de entregá-lo ao pai, e antes era preciso espremer essa verificação num helper estático chamado dentro dos argumentos de super(...). Agora você escreve de forma direta:
class Shape {
String name;
Shape(String name) {
this.name = name;
IO.println("Shape constructor for " + name);
}
}
class Square extends Shape {
int side;
Square(int side) {
if (side <= 0) {
throw new IllegalArgumentException("side must be positive: " + side);
}
String label = "square " + side;
super(label);
this.side = side;
IO.println("Square constructor, area " + side * side);
}
}
void main() {
var sq = new Square(3);
IO.println(sq.name + " is ready");
}
Ele imprime:
Shape constructor for square 3
Square constructor, area 9
square 3 is ready
A verificação e a variável local label rodam primeiro, depois super(label) roda o construtor de Shape, depois o resto do construtor de Square. Um Square(0) lança a exceção antes de Shape entrar em cena. A mesma regra vale para this(...).
Conferimos que é mesmo novidade. A mesma classe, compilada com javac --release 24, falha com flexible constructors is not supported in -source 24. Esse teste usou um public class Main clássico, porque --release 24 também recusa a forma com void main() solto.
Você ainda não pode usar this antes de super(...)
O código antes de super(...) pode usar parâmetros e variáveis locais, mas não o objeto que está sendo construído. A parte que vem do pai ainda não existe:
class Shape {
Shape(String name) {
IO.println("Shape " + name);
}
}
class Square extends Shape {
int side = 1;
Square(int side) {
IO.println("old side was " + this.side);
super("square");
this.side = side;
}
}
void main() {
new Square(3);
}
O build falha com:
Main.java:11: error: cannot reference this before supertype constructor has been called
IO.println("old side was " + this.side);
^
Ler um campo, chamar um método de instância ou passar this para algum lugar: tudo isso é recusado antes de super(...). Há uma exceção: você pode atribuir um campo ali, desde que ele não tenha inicializador. Isso deixa uma subclasse definir os próprios campos antes que o construtor pai possa vê-los.
private mantém as regras num lugar só
O Java tem quatro níveis de acesso, e private é o que você mais vai usar em campos. Um membro privado só pode ser usado dentro da própria classe. Veja por que isso importa. O saldo de uma conta bancária nunca deveria ficar negativo, e se qualquer um pode escrever em balance, qualquer um pode quebrar essa regra:
class BankAccount {
private int balance;
void deposit(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("deposit must be positive");
}
balance += amount;
}
boolean withdraw(int amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}
int balance() {
return balance;
}
}
void main() {
var account = new BankAccount();
account.deposit(100);
IO.println("withdraw 30: " + account.withdraw(30));
IO.println("withdraw 500: " + account.withdraw(500));
IO.println("balance: " + account.balance());
}
Ele imprime:
withdraw 30: true
withdraw 500: false
balance: 70
Toda mudança no saldo passa por deposit ou withdraw, e os dois conferem a regra. O método balance() deixa qualquer um ler o saldo sem deixar escrever. Isso é encapsulamento: a classe é dona dos próprios dados, e o único jeito de entrar é por métodos que os mantêm válidos. Se a regra mudar, digamos para permitir cheque especial, você muda uma classe só.
Num arquivo Java comum, o compilador garante isso. Este programa usa o public class Main clássico, então BankAccount é uma classe top-level de verdade ao lado dela:
class BankAccount {
private int balance = 70;
}
public class Main {
public static void main(String[] args) {
var account = new BankAccount();
account.balance = -1000;
IO.println(account.balance);
}
}
O build falha com:
Main.java:8: error: balance has private access in BankAccount
account.balance = -1000;
^
Uma surpresa nos arquivos-fonte compactos
Tire o public class Main e use um void main() solto, e o mesmo acesso compila. Isso nos surpreendeu, então rodamos:
class BankAccount {
private int balance = 70;
}
void main() {
var account = new BankAccount();
account.balance = -1000;
IO.println(account.balance);
}
Ele imprime:
-1000
O motivo é a classe escondida da seção sobre toString. Num arquivo-fonte compacto, o Java embrulha tudo, inclusive BankAccount, numa classe implícita. Então BankAccount não é top-level. É uma classe aninhada dentro dessa classe implícita, e é por isso que a saída padrão de toString começa com Main$. O Java deixa código em qualquer ponto de uma classe top-level usar os membros privados de todas as classes aninhadas nela.
Então, nos programas de um arquivo só desta série, private não impede main de mexer por dentro. Ele ainda documenta sua intenção, e vira uma parede de verdade no momento em que a classe vai para o próprio arquivo. Continue escrevendo.
Package-private e public
Os outros níveis importam quando um programa tem vários arquivos. Sem modificador, um membro é package-private: qualquer classe do mesmo pacote pode usá-lo. public quer dizer que qualquer código pode usá-lo. protected fica entre os dois e aparece junto com herança. Um bom padrão é campos privados e métodos só tão visíveis quanto precisam ser.
static pertence à classe, não a um objeto
Um campo static tem uma única cópia compartilhada pela classe inteira, em vez de uma por objeto. Um contador de quantos objetos foram criados é o caso clássico:
class Ticket {
static int created = 0;
int number;
Ticket() {
created++;
number = created;
}
static String summary() {
return created + " tickets so far";
}
}
void main() {
IO.println(Ticket.summary());
var a = new Ticket();
var b = new Ticket();
var c = new Ticket();
IO.println("a=" + a.number + " b=" + b.number + " c=" + c.number);
IO.println(Ticket.summary());
}
Ele imprime:
0 tickets so far
a=1 b=2 c=3
3 tickets so far
Cada ticket tem o próprio number, mas os três compartilham um único created. summary() é um método estático, então você o chama na classe, Ticket.summary(), e pode chamá-lo antes de existir qualquer ticket. IO.println e Math.max também são métodos estáticos.
Um método estático não tem this
Um método estático roda sem objeto, então não há this para ele usar nem campos de instância para ler:
class Ticket {
int number;
static void show() {
IO.println(this.number);
}
}
void main() {
Ticket.show();
}
O build falha com:
Main.java:5: error: non-static variable this cannot be referenced from a static context
IO.println(this.number);
^
Escrever só number em vez de this.number falha do mesmo jeito, com non-static variable number. A pergunta a fazer é “o number de qual ticket?”, e um método estático não tem resposta.
Static factory methods
Um método estático que devolve um objeto novo se chama static factory, e o Java moderno usa muito: List.of(...), Path.of(...), Duration.ofSeconds(...). Diferente de um construtor, uma factory ganha um nome que diz o que ela faz.
Num arquivo-fonte compacto, a primeira tentativa óbvia não compila, e essa nos surpreendeu:
class Delay {
static Delay ofSeconds(int seconds) {
return new Delay();
}
}
void main() {
IO.println(Delay.ofSeconds(90) != null);
}
O build falha com:
Main.java:3: error: non-static variable this cannot be referenced from a static context
return new Delay();
^
Não há this em lugar nenhum desse código, então a mensagem parece errada. Não está. Num arquivo-fonte compacto, Delay fica aninhada dentro da classe escondida Main, e uma classe aninhada comum é uma inner class (classe interna): cada objeto Delay fica ligado a um objeto Main. Seu void main() roda num objeto desses, então new Delay() funciona ali. Um método estático não tem objeto Main ao qual ligar o novo Delay, e esse objeto que falta é o this de que o javac fala.
A mesma classe no próprio arquivo, ou ao lado de um public class Main clássico, compila sem problema. Num arquivo compacto, marque a classe como static e ela deixa de precisar de um objeto externo:
static class Delay {
private final int seconds;
private Delay(int seconds) {
this.seconds = seconds;
}
static Delay ofSeconds(int seconds) {
return new Delay(seconds);
}
static Delay ofMinutes(int minutes) {
return new Delay(minutes * 60);
}
@Override
public String toString() {
return seconds + "s";
}
}
void main() {
IO.println(Delay.ofSeconds(90));
IO.println(Delay.ofMinutes(2));
}
Ele imprime:
90s
120s
ofSeconds(90) e ofMinutes(2) recebem um único int, então não poderiam ser dois construtores: as listas de parâmetros seriam idênticas. O construtor é privado, então num projeto normal as factories são o único jeito de criar um. A classe Ticket lá de cima não precisou de static porque o método estático dela nunca criava um Ticket.
Explicado como se você tivesse dez anos
Uma classe é um cortador de biscoito. Cada objeto é um biscoito que você corta com ele. O cortador decide o formato, mas cada biscoito é um biscoito separado. Você pode pôr granulado em um, e os outros continuam sem. Esse granulado são os campos.
O construtor é o momento de apertar o cortador. É quando um biscoito nasce, e é sua chance de se recusar a fazer um biscoito quebrado.
Uma coisa static fica marcada no próprio cortador, não em um biscoito. Imagine que você faz um risquinho no cabo do cortador a cada biscoito que corta. Existe uma contagem só, não importa quantos biscoitos você faça, e dá para ler essa contagem antes de fazer o primeiro biscoito.
A versão precisa
Uma classe é um tipo. new aloca um objeto desse tipo no heap, coloca cada campo no valor padrão, roda os inicializadores de campo e o construtor, e devolve uma referência para o objeto. Campos de instância vivem em cada objeto. Métodos de instância recebem o objeto como um parâmetro escondido, que é o this.
Um campo estático existe uma vez por classe, não por objeto, e um método estático não recebe objeto escondido. Membros estáticos são acessados pelo nome da classe. A classe em si é carregada e inicializada uma vez, na primeira vez que é usada.
Onde a analogia falha: um cortador de biscoito não impede você de fazer um biscoito faltando metade do formato, mas um construtor impede, lançando uma exceção. E biscoitos são pedaços separados de massa, mas uma variável Java não guarda o biscoito. Ela guarda uma referência para ele, então duas variáveis podem apontar para o mesmo objeto e ver o granulado uma da outra.
Campos final são definidos uma vez
Um campo final precisa receber um valor exatamente uma vez, na declaração ou em todo construtor, e não pode mudar depois disso. Tente mudar mais tarde e o build falha:
class Money {
final long cents;
Money(long cents) {
this.cents = cents;
}
void addTip(long tip) {
cents = cents + tip;
}
}
void main() {
var bill = new Money(1250);
bill.addTip(200);
}
O build falha com:
Main.java:9: error: cannot assign a value to final variable cents
cents = cents + tip;
^
Objetos imutáveis
Se todo campo é final e nenhum aponta para algo mutável, o objeto nunca muda depois de construído. Esse é um objeto imutável. Em vez de mudá-lo, os métodos devolvem um objeto novo:
class Money {
private final long cents;
private final String currency;
Money(long cents, String currency) {
if (cents < 0) {
throw new IllegalArgumentException("negative amount: " + cents);
}
this.cents = cents;
this.currency = currency;
}
Money plus(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("currency mismatch");
}
return new Money(cents + other.cents, currency);
}
@Override
public String toString() {
return String.format("%d.%02d %s", cents / 100, cents % 100, currency);
}
}
void main() {
var price = new Money(1250, "EUR");
var tip = new Money(200, "EUR");
var total = price.plus(tip);
IO.println("price: " + price);
IO.println("tip: " + tip);
IO.println("total: " + total);
}
Ele imprime:
price: 12.50 EUR
tip: 2.00 EUR
total: 14.50 EUR
price.plus(tip) deixou price como estava e devolveu um terceiro objeto. É uma escolha de design com ganhos reais. Você pode passar um Money para qualquer método, guardá-lo em qualquer lugar ou compartilhá-lo entre threads, e ninguém consegue mudá-lo pelas suas costas. O construtor confere as regras uma vez, e elas continuam valendo para sempre.
Repare no que final não faz: num campo que guarda uma lista, ele impede você de trocar a lista, não de adicionar itens a ela. final fixa a referência, não o objeto para o qual ela aponta.
Escrever Money custou muitas linhas para dois valores. A parte sobre records mostra como o Java escreve essa classe para você numa linha só.
A ordem em que as coisas rodam
Uma classe também pode ter blocos static, que rodam uma vez quando a classe é usada pela primeira vez, e blocos de inicialização de instância, que rodam para cada objeto novo. A ordem é fácil de conferir:
class Robot {
static int built = 0;
static {
IO.println("1. static block: the class is initialized");
}
String name = "unnamed";
{
IO.println("2. instance block: name is " + name);
}
Robot(String name) {
IO.println("3. constructor: renaming to " + name);
this.name = name;
built++;
}
}
void main() {
IO.println("main starts");
new Robot("R1");
new Robot("R2");
IO.println("built: " + Robot.built);
}
Ele imprime:
main starts
1. static block: the class is initialized
2. instance block: name is unnamed
3. constructor: renaming to R1
2. instance block: name is unnamed
3. constructor: renaming to R2
built: 2
O bloco static rodou uma vez, e só quando Robot foi usada pela primeira vez, depois que main já tinha começado. Para cada objeto, os inicializadores de campo e os blocos de instância rodaram de cima para baixo, e depois o corpo do construtor. Blocos de instância são raros em código real. Um construtor quase sempre é mais claro.
O que lembrar
- Uma classe declara campos e métodos.
newcria um objeto com a própria cópia de cada campo de instância. Usethis.fieldquando um parâmetro tem o mesmo nome. - Sobrescreva
toString. A saída padrãoMain$Player@…é um identity hash que pode mudar entre execuções, modos de execução e JVMs. - Depois que você escreve qualquer construtor, o construtor grátis sem argumentos some. Encadeie construtores com
this(...)e valide os argumentos no construtor. - Desde o Java 25, um construtor pode rodar instruções antes de
super(...)outhis(...), mas não pode usarthisali. - Deixe os campos
privatee mude-os só por métodos que mantêm as regras. Num arquivo-fonte compacto, as classes ficam aninhadas numa classe escondida, entãomainainda alcança membros privados. - Membros
staticpertencem à classe, e um método estático não temthis. Static factories comoof(...)dão um nome aos construtores. - Um campo
finalé atribuído uma vez. Objetos com todos os campos final e imutáveis não mudam, então os métodos devolvem objetos novos.
Um construtor decide como um objeto começa, e
privateefinaldecidem o que pode mudar depois disso.