Programação Orientada a Objetos: Classes e Objetos em Java
Java é uma linguagem puramente orientada a objetos, o que significa que seus conceitos fundamentais, classe e objeto, são a espinha dorsal de qualquer programa. Uma aplicação Java pode ser vista como um conjunto de objetos que interagem entre si para realizar tarefas.
Classes e Objetos
A maneira mais fácil de entender a diferença entre classe e objeto é com uma analogia:
- Uma classe é uma planta de uma casa. Ela define todas as características e funcionalidades que a casa terá: número de quartos, cor das paredes, a capacidade de acender luzes, abrir portas, etc. A planta é apenas o modelo, o projeto.
- Um objeto é a casa construída a partir dessa planta. É uma instância concreta e real que existe na memória. Você pode construir várias casas (vários objetos) a partir da mesma planta (da mesma classe), e cada casa será independente, com suas próprias características (uma pode ser azul, outra vermelha).
Em Java, uma classe é definida com a palavra-chave class
:
// A classe Person é a "planta"
class Person {
// Corpo da classe aqui
}
Todo objeto possui duas características principais:
- Estado: Os dados que ele armazena. Na classe, o estado é representado por campos (ou atributos/variáveis de instância).
- Comportamento: As ações que ele pode executar. Na classe, o comportamento é representado por métodos.
Vamos expandir nossa classe Person
:
class Person {
// --- ESTADO (Campos) ---
String name; // armazena o nome
int age; // armazena a idade
// --- COMPORTAMENTO (Métodos) ---
void displayInfo() {
System.out.printf("Name: %s \tAge: %d\n", name, age);
}
}
Instanciando um Objeto a Partir de uma Classe
Para usar nossa classe, precisamos criar um objeto a partir dela. Este processo é chamado de instanciação.
public class Program {
public static void main(String[] args) {
// 1. Declara uma variável de referência do tipo Person
Person tom;
// 2. Cria um objeto (instância) da classe Person e atribui sua referência a 'tom'
tom = new Person();
}
}
No passo 1, a variável tom
é criada, mas seu valor inicial é null
, ou seja, ela não aponta para nenhum objeto na memória. No passo 2, o operador new
aloca memória para um novo objeto Person
, e a referência a essa memória é atribuída à variável tom
. Agora tom
"sabe" onde encontrar o objeto.
Uma vez que o objeto existe, podemos acessar seus campos e métodos usando o operador .
(ponto):
public static void main(String[] args) {
Person tom = new Person(); // Cria o objeto
// Acessa e modifica os campos do objeto 'tom'
tom.name = "Tom";
tom.age = 34;
// Chama um método do objeto 'tom'
tom.displayInfo(); // Name: Tom Age: 34
}
Nota sobre arquivos: Geralmente, cada classe pública é definida em seu próprio arquivo
.java
com o mesmo nome. Para simplificar, estamos usando múltiplas classes em um só arquivo.
Construtores: A Inicialização do Objeto
Um construtor é um bloco de código especial, semelhante a um método, que é executado automaticamente no momento em que um objeto é criado (new
). Sua principal responsabilidade é inicializar o estado do objeto, garantindo que ele nasça em um estado consistente.
Se você não definir nenhum construtor, o Java fornece um construtor padrão (sem parâmetros) que inicializa os campos com seus valores padrão (0
para números, false
para booleanos, null
para objetos).
Para maior controle, podemos definir nossos próprios construtores. Uma classe pode ter vários construtores, desde que suas assinaturas (a lista de parâmetros) sejam diferentes. Isso é chamado de sobrecarga de construtores.
public class Program {
public static void main(String[] args) {
// Usando o construtor padrão
Person tom = new Person();
tom.displayInfo(); // Name: Undefined Age: 18
// Usando o construtor com nome
Person alice = new Person("Alice");
alice.displayInfo(); // Name: Alice Age: 18
// Usando o construtor com nome e idade
Person bob = new Person("Bob", 25);
bob.displayInfo(); // Name: Bob Age: 25
}
}
class Person {
String name;
int age;
// 1. Construtor padrão (sem parâmetros)
Person() {
name = "Undefined";
age = 18;
}
// 2. Construtor que recebe um nome
Person(String n) {
name = n;
age = 18;
}
// 3. Construtor que recebe nome e idade
Person(String n, int a) {
name = n;
age = a;
}
void displayInfo() {
System.out.printf("Name: %s \tAge: %d\n", name, age);
}
}
A Palavra-chave this
A palavra-chave this
é uma referência para a instância atual do objeto. Ela é usada principalmente para dois propósitos:
- Diferenciar campos de parâmetros: Quando um parâmetro de um construtor ou método tem o mesmo nome de um campo da classe,
this
é usado para desambiguar e se referir ao campo da instância. - Encadeamento de construtores (Constructor Chaining): Um construtor pode chamar outro construtor da mesma classe para evitar a repetição de código.
Vamos refatorar nossa classe Person
usando this
para torná-la mais robusta e menos repetitiva:
public class Program {
public static void main(String[] args) {
// Usando o construtor padrão
Person tom = new Person();
tom.displayInfo(); // Name: Undefined Age: 18
// Usando o construtor com nome
Person alice = new Person("Alice");
alice.displayInfo(); // Name: Alice Age: 18
// Usando o construtor com nome e idade
Person bob = new Person("Bob", 25);
bob.displayInfo(); // Name: Bob Age: 25
}
}
class Person {
String name;
int age;
// Construtor 1: chama o Construtor 3 com valores padrão
Person() {
this("Undefined", 18);
}
// Construtor 2: chama o Construtor 3, fornecendo uma idade padrão
Person(String name) {
this(name, 18);
}
// Construtor 3: o construtor principal, que realiza a inicialização
Person(String name, int age) {
// "this.name" se refere ao campo da classe.
// "name" se refere ao parâmetro do construtor.
this.name = name;
this.age = age;
}
void displayInfo() {
System.out.printf("Name: %s \tAge: %d\n", this.name, this.age);
}
}
Essa abordagem é muito superior, pois centraliza a lógica de atribuição em um único construtor.
Blocos de Inicialização de Instância
Java oferece uma outra forma de inicializar objetos: os blocos de inicialização de instância. É um bloco de código {...}
definido diretamente no corpo da classe.
public class Program {
public static void main(String[] args) {
Person person = new Person();
System.out.println("Nome: " + person.name + ", Idade: " + person.age); // Nome: Undefined, Idade: 18
}
}
class Person {
String name;
int age;
// Bloco de inicialização de instância
{
System.out.println("Bloco de inicialização executado!");
name = "Undefined";
age = 18;
}
Person() {
// Construtor vazio
}
}
Este bloco é executado toda vez que um objeto é instanciado, e sua execução ocorre antes da execução do construtor.
Quando usar? Eles são úteis para compartilhar uma lógica de inicialização complexa entre múltiplos construtores, especialmente se essa lógica não for uma simples chamada a outro construtor.
Boa Prática: Para inicializações simples, como age = 18;
, é mais comum e legível inicializar o campo diretamente em sua declaração: int age = 18;
. Blocos de inicialização são reservados para cenários mais complexos.
📝 Exercícios
Tarefa 1 (Teoria)
Descrição: O que será impresso no console ao executar o método main
abaixo?
public class Main {
public static void main(String[] args) {
Carro meuCarro = null;
System.out.println(meuCarro.modelo);
}
}
class Carro {
String modelo = "Sedan";
}
Alternativas:
Sedan
null
- O código não irá compilar.
- Ocorrerá um
NullPointerException
em tempo de execução. - Nada será impresso.
Resposta
NullPointerException
em tempo de execução.
Carro meuCarro = null;
declara uma variável de referência meuCarro
, mas a instrui explicitamente a não apontar para nenhum objeto (seu valor é null
). Na linha seguinte, o código tenta acessar o campo modelo
através dessa referência nula. Como não há um objeto real na memória para se obter o campo modelo
, o Java lança um NullPointerException
ao tentar "desreferenciar" uma variável nula.
Tarefa 2 (Prática)
Descrição: Complete o construtor da classe Livro
para que ele inicialize os campos titulo
e autor
com os valores recebidos como parâmetros.
Código inicial:
public class Main {
public static void main(String[] args) {
Livro livroFavorito = new Livro("O Senhor dos Anéis", "J.R.R. Tolkien");
livroFavorito.exibirInfo(); // Deve imprimir: Título: O Senhor dos Anéis, Autor: J.R.R. Tolkien
}
}
class Livro {
String titulo;
String autor;
// Complete este construtor
Livro(String titulo, String autor) {
}
void exibirInfo() {
System.out.println("Título: " + this.titulo + ", Autor: " + this.autor);
}
}
Resposta
public class Main {
public static void main(String[] args) {
Livro livroFavorito = new Livro("O Senhor dos Anéis", "J.R.R. Tolkien");
livroFavorito.exibirInfo(); // Deve imprimir: Título: O Senhor dos Anéis, Autor: J.R.R. Tolkien
}
}
class Livro {
String titulo;
String autor;
Livro(String titulo, String autor) {
this.titulo = titulo;
this.autor = autor;
}
void exibirInfo() {
System.out.println("Título: " + this.titulo + ", Autor: " + this.autor);
}
}
titulo
e autor
) são os mesmos que os nomes dos campos da classe. Para diferenciar, usamos a palavra-chave this
. this.titulo
se refere ao campo da instância do objeto, enquanto titulo
(sem o this.
) se refere ao parâmetro que foi passado para o construtor. A atribuição this.titulo = titulo;
armazena o valor do parâmetro no campo correspondente do objeto que está sendo criado.
Tarefa 3 (Prática)
Descrição: Crie um método chamado acelerar()
na classe Moto
que aumente a velocidade (velocidadeAtual
) em 10 a cada vez que for chamado.
Código inicial:
public class Main {
public static void main(String[] args) {
Moto minhaMoto = new Moto();
minhaMoto.exibirVelocidade(); // Deve imprimir: Velocidade: 0 km/h
minhaMoto.acelerar();
minhaMoto.exibirVelocidade(); // Deve imprimir: Velocidade: 10 km/h
minhaMoto.acelerar();
minhaMoto.acelerar();
minhaMoto.exibirVelocidade(); // Deve imprimir: Velocidade: 30 km/h
}
}
class Moto {
int velocidadeAtual = 0;
// Crie o método acelerar() aqui
void exibirVelocidade() {
System.out.println("Velocidade: " + this.velocidadeAtual + " km/h");
}
}
Resposta
// Método acelerar()
void acelerar() {
this.velocidadeAtual += 10;
}
acelerar()
modifica o estado do objeto. Ele acessa o campo velocidadeAtual
da própria instância (this.velocidadeAtual
) e incrementa seu valor em 10. Cada chamada a este método altera o valor armazenado nesse campo específico para aquele objeto, demonstrando como os métodos operam sobre os dados da instância.