Projeto Sistema de Biblioteca

Este é o projeto de orientação a objetos do guia. Em vez de um programa com um main enorme, você vai construir várias classes pequenas, cada uma responsável por uma coisa: o livro sabe se está disponível, o usuário sabe quantos livros pode levar, a biblioteca guarda tudo e aplica as regras. Quando alguma regra é quebrada, uma exceção com nome próprio avisa o que aconteceu.

Você vai praticar

O que vamos construir

No fim, a pasta do projeto terá dez arquivos, um por classe, e o Main encena um dia na biblioteca: o Bruno (aluno) e a Carla (professora) tentam pegar livros, e as regras entram em ação.

$ ls
Aluno.java                LimiteDeEmprestimosException.java  Professor.java
BibliotecaException.java  LivroIndisponivelException.java    Usuario.java
Biblioteca.java           Livro.java
Emprestimo.java           Main.java
$ java Main.java
Bruno levou L1.
Bruno levou L2.
Não deu: Bruno já está no limite de 2 livro(s).       « regra: aluno leva no máximo 2
Não deu: "Dom Casmurro" já está emprestado.          « regra: um livro, um leitor
Bruno devolveu "Dom Casmurro".
Carla levou L1.
Erro: Livro não encontrado: L9
--- Empréstimos (2) ---
Vidas Secas -> Bruno
Dom Casmurro -> Carla
--- Acervo (4 livros) ---
L1 | Dom Casmurro | Machado de Assis | emprestado
L2 | Vidas Secas | Graciliano Ramos | emprestado
L3 | O Cortiço | Aluísio Azevedo | disponível
L4 | Iracema | José de Alencar | disponível

Com vários arquivos na pasta, java Main.java continua funcionando: desde o Java 22, o lançador compila sozinho as outras classes que o Main usa, se estiverem na mesma pasta. Com javac *.java você compila tudo de uma vez e vê os .class:

$ javac *.java
$ ls
Aluno.class                Emprestimo.java                     Main.class
Aluno.java                 LimiteDeEmprestimosException.class  Main.java
Biblioteca.class           LimiteDeEmprestimosException.java   Professor.class
BibliotecaException.class  Livro.class                         Professor.java
BibliotecaException.java   LivroIndisponivelException.class    Usuario.class
Biblioteca.java            LivroIndisponivelException.java     Usuario.java
Emprestimo.class           Livro.java
  1. O livro (classe, private, getters, toString).
  2. Os usuários (interface Usuario, records Aluno e Professor).
  3. O acervo (classe Biblioteca com HashMap).
  4. Os empréstimos (record Emprestimo, regras de negócio).
  5. Exceções próprias (LivroIndisponivelException e companhia).

Antes de começar

Este projeto pede o Nível 2 quase inteiro: classes, encapsulamento, interfaces, records e exceções, além de HashMap. No seu computador, cada classe pública vai num arquivo com o mesmo nome:

$ mkdir -p java/biblioteca
$ cd java/biblioteca
No editor do guia, tudo num arquivo só

Para as missões caberem numa caixa, as classes estão juntas num único Main.java, uma depois da outra. O Java permite isso, com uma regra: só a classe Main pode ser public; as outras ficam sem o public na frente de class, interface ou record. O comportamento é o mesmo. No seu computador, prefira um arquivo por classe, como nas listagens desta página.


Etapa 1 · O livro

Começamos pela peça mais simples. Um livro tem código, título, autor e uma situação: disponível ou emprestado. A regra de ouro do encapsulamento: ninguém de fora mexe direto nos dados do livro. Os atributos são private, e quem quiser mudar a situação tem que pedir ao livro, pelos métodos emprestar() e devolver().

public class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}
ParteO que é
private final String codigo;Atributo privado (só a própria classe enxerga) e final (definido uma vez, no construtor, e nunca mais muda). Código, título e autor de um livro não mudam.
private boolean disponivel;Privado, mas não final: muda a cada empréstimo, só que apenas pelos métodos da classe.
public Livro(…)O construtor. this.codigo = codigo copia o parâmetro para o atributo (this). Todo livro nasce disponível.
getCodigo(), isDisponivel()Os getters: deixam ler sem deixar mudar. Para boolean, o costume é isAlgo() em vez de getAlgo().
Não há setDisponivel(…)De propósito. Na etapa 5, emprestar() vai recusar um livro já emprestado; com um setter, qualquer um passaria por cima dessa regra.
@Override public String toString()Diz como o livro vira texto no println (sobrescrita). O ? : é o operador ternário.

Um Main para experimentar:

public class Main {
    public static void main(String[] args) {
        Livro livro = new Livro("L1", "Dom Casmurro", "Machado de Assis");
        System.out.println(livro);

        livro.emprestar();
        System.out.println(livro);
        System.out.println("Disponível? " + livro.isDisponivel());

        livro.devolver();
        System.out.println(livro);
    }
}
$ java Main.java
L1 | Dom Casmurro | Machado de Assis | disponível
L1 | Dom Casmurro | Machado de Assis | emprestado
Disponível? false
L1 | Dom Casmurro | Machado de Assis | disponível
Quando dá errado: mexer no atributo privado

Este código tem um erro de propósito.

        Livro livro = new Livro("L1", "Dom Casmurro", "Machado de Assis");
        livro.disponivel = false;
$ java Main.java
Main.java:4: error: disponivel has private access in Livro
        livro.disponivel = false;
             ^
1 error
error: compilation failed

"disponivel tem acesso privado em Livro." É o encapsulamento funcionando: o compilador não deixa o Main pular as regras do livro. Use livro.emprestar().

Missão (etapa 1): a classe Livro está sem os métodos emprestar() e devolver() (rode e veja o cannot find symbol). Escreva os dois: um põe disponivel em false, o outro em true.

public class Main {
    public static void main(String[] args) {
        Livro livro = new Livro("L1", "Dom Casmurro", "Machado de Assis");
        System.out.println(livro);

        livro.emprestar();
        System.out.println(livro);
        System.out.println("Disponível? " + livro.isDisponivel());

        livro.devolver();
        System.out.println(livro);
    }
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    // escreva aqui os métodos emprestar() e devolver()

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}
public class Main {
    public static void main(String[] args) {
        Livro livro = new Livro("L1", "Dom Casmurro", "Machado de Assis");
        System.out.println(livro);

        livro.emprestar();
        System.out.println(livro);
        System.out.println("Disponível? " + livro.isDisponivel());

        livro.devolver();
        System.out.println(livro);
    }
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

Etapa 2 · Os usuários

A biblioteca tem dois tipos de usuário: alunos, que levam até 2 livros, e professores, que levam até 5. Os dois têm nome e um limite, mas o limite é diferente. Isso pede uma interface: um "contrato" que diz o que todo usuário sabe responder, deixando cada tipo decidir como.

public interface Usuario {
    String nome();

    int limiteDeLivros();
}

Cada tipo de usuário é um record que implementa a interface:

public record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}
public record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}
public interface Usuario
O contrato: todo Usuario tem os métodos nome() e limiteDeLivros(). Uma interface só declara; não tem o corpo dos métodos.
record Aluno(String nome)
Um record com um componente, nome. O record já ganha o método nome(), que é exatamente o que a interface pede.
implements Usuario
"Cumpre o contrato Usuario." Se faltar um método da interface, o código não compila.
@Override public int limiteDeLivros()
O método que faltava. @Override avisa ao compilador que este método vem da interface; os métodos de interface são sempre public.
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        List<Usuario> usuarios = List.of(bruno, carla);
        for (Usuario u : usuarios) {
            System.out.println(u.nome() + " pode levar até " + u.limiteDeLivros() + " livro(s). " + u);
        }
    }
}
$ java Main.java
Bruno pode levar até 2 livro(s). Aluno[nome=Bruno]
Carla pode levar até 5 livro(s). Professor[nome=Carla]

Aqui está o polimorfismo ("muitas formas"): as variáveis bruno e carla são do tipo Usuario, e o laço trata os dois do mesmo jeito. Mas u.limiteDeLivros() dá 2 para um e 5 para o outro, porque cada objeto responde do seu jeito. A biblioteca nunca vai precisar de um if (u é aluno). Se um dia surgir o tipo "Funcionário", basta criar um record novo; nada mais muda.

Quando dá errado: criar "um usuário qualquer"

Este código tem um erro de propósito.

$ java Main.java
Main.java:3: error: Usuario is abstract; cannot be instantiated
        Usuario alguem = new Usuario();
                         ^
1 error
error: compilation failed

"Usuario é abstrato; não pode ser instanciado." Uma interface é só o contrato, não dá para criar um objeto dela. Crie um Aluno ou um Professor e guarde numa variável do tipo Usuario.

E a herança com extends?

Daria para fazer com uma classe abstrata Usuario e as subclasses Aluno extends Usuario e Professor extends Usuario (veja Classes Abstratas). Escolhemos interface + records porque aqui os usuários só carregam dados e uma regra: não há código para herdar. A herança aparece na etapa 5, entre as exceções.

Missão (etapa 2): o Main já usa new Professor("Carla"), mas o record Professor não existe. Crie-o, implementando Usuario, com limite de 5 livros.

import java.util.List;

public class Main {
    public static void main(String[] args) {
        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        List<Usuario> usuarios = List.of(bruno, carla);
        for (Usuario u : usuarios) {
            System.out.println(u.nome() + " pode levar até " + u.limiteDeLivros() + " livro(s). " + u);
        }
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        List<Usuario> usuarios = List.of(bruno, carla);
        for (Usuario u : usuarios) {
            System.out.println(u.nome() + " pode levar até " + u.limiteDeLivros() + " livro(s). " + u);
        }
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}

Etapa 3 · O acervo

Agora a classe que junta tudo: Biblioteca. Ela guarda os livros num HashMap, um "dicionário" em que cada chave (o código, "L1") leva a um valor (o livro). Achar um livro pelo código é instantâneo, sem percorrer lista nenhuma.

import java.util.HashMap;
import java.util.Map;

public class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }
}
TrechoO que faz
Map<String, Livro> livrosUm mapa com chave String e valor Livro. É private: só a biblioteca mexe no acervo.
livros.put(livro.getCodigo(), livro)Guarda o livro com o código como chave. Outro livro com o mesmo código substituiria o antigo.
livros.get(codigo)Devolve o livro daquela chave, ou null ("nada") se a chave não existe.
throw new IllegalArgumentException(…)Código inexistente é erro de quem chamou: avisamos na hora, com uma mensagem clara.
livros.values()Todos os livros do mapa, para o for-each.
public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        bib.listarAcervo();
        System.out.println("Busca L2: " + bib.buscar("L2").getTitulo());
        System.out.println("Busca L9: " + bib.buscar("L9").getTitulo());
    }
}
$ java Main.java
--- Acervo (4 livros) ---
L1 | Dom Casmurro | Machado de Assis | disponível
L2 | Vidas Secas | Graciliano Ramos | disponível
L3 | O Cortiço | Aluísio Azevedo | disponível
L4 | Iracema | José de Alencar | disponível
Busca L2: Vidas Secas
Exception in thread "main" java.lang.IllegalArgumentException: Livro não encontrado: L9
	at Biblioteca.buscar(Biblioteca.java:14)
	at Main.main(Main.java:11)

A busca por L9 parou o programa, e isso é bom: a mensagem diz exatamente o que houve. Na etapa 5, o Main vai capturar esse erro.

O HashMap não promete ordem

Os livros apareceram em ordem (L1, L2, L3, L4) por coincidência das chaves curtas. Um HashMap guarda as chaves na ordem que for melhor para ele. Se a ordem importa, use um TreeMap (ordem das chaves) ou um LinkedHashMap (ordem de inserção); veja TreeMap e TreeSet.

Quando dá errado: esquecer de conferir o null

Se o buscar fosse só return livros.get(codigo);, o código L9 devolveria null, e o erro estouraria longe dali, no Main:

$ java Main.java
--- Acervo (4 livros) ---
L1 | Dom Casmurro | Machado de Assis | disponível
L2 | Vidas Secas | Graciliano Ramos | disponível
L3 | O Cortiço | Aluísio Azevedo | disponível
L4 | Iracema | José de Alencar | disponível
Busca L2: Vidas Secas
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Livro.getTitulo()" because the return value of "Biblioteca.buscar(String)" is null
	at Main.main(Main.java:11)

"Não dá para chamar getTitulo() porque o valor devolvido por buscar é null." O Java até ajuda a achar a origem, mas a mensagem fala de null, não de "livro não encontrado". Conferir no lugar certo e lançar uma exceção com mensagem clara economiza horas de depuração.

Missão (etapa 3): rode: o programa termina com NullPointerException. Conserte o método buscar para lançar IllegalArgumentException com a mensagem Livro não encontrado: L9 quando o código não existir.

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        bib.listarAcervo();
        System.out.println("Busca L2: " + bib.buscar("L2").getTitulo());
        System.out.println("Busca L9: " + bib.buscar("L9").getTitulo());
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        // se não achou, lance IllegalArgumentException
        return livro;
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        bib.listarAcervo();
        System.out.println("Busca L2: " + bib.buscar("L2").getTitulo());
        System.out.println("Busca L9: " + bib.buscar("L9").getTitulo());
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}

Etapa 4 · Os empréstimos

Um empréstimo liga um livro a um usuário. Ele não tem comportamento, só dados: é um record.

public record Emprestimo(Livro livro, Usuario usuario) {
}

A Biblioteca ganha uma lista de empréstimos e três métodos novos. Nesta etapa, quando uma regra impede o empréstimo, o método mostra o motivo e devolve false:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();
    private final List<Emprestimo> emprestimos = new ArrayList<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public int quantosCom(Usuario usuario) {
        int total = 0;
        for (Emprestimo e : emprestimos) {
            if (e.usuario().equals(usuario)) {
                total++;
            }
        }
        return total;
    }

    public boolean emprestar(String codigo, Usuario usuario) {
        Livro livro = buscar(codigo);
        if (!livro.isDisponivel()) {
            System.out.println("Não deu: \"" + livro.getTitulo() + "\" já está emprestado.");
            return false;
        }
        if (quantosCom(usuario) >= usuario.limiteDeLivros()) {
            System.out.println("Não deu: " + usuario.nome() + " já está no limite de " + usuario.limiteDeLivros() + " livro(s).");
            return false;
        }
        livro.emprestar();
        emprestimos.add(new Emprestimo(livro, usuario));
        System.out.println(usuario.nome() + " levou \"" + livro.getTitulo() + "\".");
        return true;
    }

    public void devolver(String codigo) {
        Livro livro = buscar(codigo);
        for (Emprestimo e : emprestimos) {
            if (e.livro() == livro) {
                emprestimos.remove(e);
                livro.devolver();
                System.out.println(e.usuario().nome() + " devolveu \"" + livro.getTitulo() + "\".");
                return;
            }
        }
        System.out.println("\"" + livro.getTitulo() + "\" não estava emprestado.");
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }

    public void listarEmprestimos() {
        System.out.println("--- Empréstimos (" + emprestimos.size() + ") ---");
        for (Emprestimo e : emprestimos) {
            System.out.println(e.livro().getTitulo() + " -> " + e.usuario().nome());
        }
    }
}
MétodoO que faz
quantosCom(usuario)Conta os empréstimos daquele usuário. e.usuario().equals(usuario) funciona porque records comparam pelo conteúdo: dois Aluno("Bruno") são iguais.
emprestar(codigo, usuario)Confere as duas regras (livro disponível, usuário abaixo do limite). Se passar, marca o livro e registra o empréstimo.
devolver(codigo)Procura o empréstimo daquele livro, remove da lista e devolve o livro. e.livro() == livro compara se é o mesmo objeto. O return logo depois do remove evita a ConcurrentModificationException (veja o projeto da agenda).
listarEmprestimos()Mostra quem está com o quê.
public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        bib.emprestar("L1", bruno);
        bib.emprestar("L2", bruno);
        bib.emprestar("L3", bruno);
        bib.emprestar("L1", carla);
        bib.devolver("L1");
        bib.emprestar("L1", carla);

        bib.listarEmprestimos();
        bib.listarAcervo();
    }
}
$ java Main.java
Bruno levou "Dom Casmurro".
Bruno levou "Vidas Secas".
Não deu: Bruno já está no limite de 2 livro(s).
Não deu: "Dom Casmurro" já está emprestado.
Bruno devolveu "Dom Casmurro".
Carla levou "Dom Casmurro".
--- Empréstimos (2) ---
Vidas Secas -> Bruno
Dom Casmurro -> Carla
--- Acervo (4 livros) ---
L1 | Dom Casmurro | Machado de Assis | emprestado
L2 | Vidas Secas | Graciliano Ramos | emprestado
L3 | O Cortiço | Aluísio Azevedo | disponível
L4 | Iracema | José de Alencar | disponível

Funciona, mas tem um defeito de projeto: a Biblioteca está escrevendo na tela. E se um dia a biblioteca for usada num site, ou num teste automático? Quem chamou emprestar deveria decidir o que fazer com a falha. E o boolean diz que falhou, mas não por quê. É o trabalho das exceções, na próxima etapa.

Missão (etapa 4): o emprestar confere se o livro está disponível, mas esqueceu o limite: o Bruno leva três livros. Acrescente a regra usando quantosCom(usuario) e usuario.limiteDeLivros(), mostrando Não deu: Bruno já está no limite de 2 livro(s).

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        bib.emprestar("L1", bruno);
        bib.emprestar("L2", bruno);
        bib.emprestar("L3", bruno);
        bib.emprestar("L1", carla);
        bib.devolver("L1");
        bib.emprestar("L1", carla);

        bib.listarEmprestimos();
        bib.listarAcervo();
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();
    private final List<Emprestimo> emprestimos = new ArrayList<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public int quantosCom(Usuario usuario) {
        int total = 0;
        for (Emprestimo e : emprestimos) {
            if (e.usuario().equals(usuario)) {
                total++;
            }
        }
        return total;
    }

    public boolean emprestar(String codigo, Usuario usuario) {
        Livro livro = buscar(codigo);
        if (!livro.isDisponivel()) {
            System.out.println("Não deu: \"" + livro.getTitulo() + "\" já está emprestado.");
            return false;
        }
        // confira o limite do usuário (use quantosCom)
        livro.emprestar();
        emprestimos.add(new Emprestimo(livro, usuario));
        System.out.println(usuario.nome() + " levou \"" + livro.getTitulo() + "\".");
        return true;
    }

    public void devolver(String codigo) {
        Livro livro = buscar(codigo);
        for (Emprestimo e : emprestimos) {
            if (e.livro() == livro) {
                emprestimos.remove(e);
                livro.devolver();
                System.out.println(e.usuario().nome() + " devolveu \"" + livro.getTitulo() + "\".");
                return;
            }
        }
        System.out.println("\"" + livro.getTitulo() + "\" não estava emprestado.");
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }

    public void listarEmprestimos() {
        System.out.println("--- Empréstimos (" + emprestimos.size() + ") ---");
        for (Emprestimo e : emprestimos) {
            System.out.println(e.livro().getTitulo() + " -> " + e.usuario().nome());
        }
    }
}

record Emprestimo(Livro livro, Usuario usuario) {
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        bib.emprestar("L1", bruno);
        bib.emprestar("L2", bruno);
        bib.emprestar("L3", bruno);
        bib.emprestar("L1", carla);
        bib.devolver("L1");
        bib.emprestar("L1", carla);

        bib.listarEmprestimos();
        bib.listarAcervo();
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();
    private final List<Emprestimo> emprestimos = new ArrayList<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public int quantosCom(Usuario usuario) {
        int total = 0;
        for (Emprestimo e : emprestimos) {
            if (e.usuario().equals(usuario)) {
                total++;
            }
        }
        return total;
    }

    public boolean emprestar(String codigo, Usuario usuario) {
        Livro livro = buscar(codigo);
        if (!livro.isDisponivel()) {
            System.out.println("Não deu: \"" + livro.getTitulo() + "\" já está emprestado.");
            return false;
        }
        if (quantosCom(usuario) >= usuario.limiteDeLivros()) {
            System.out.println("Não deu: " + usuario.nome() + " já está no limite de " + usuario.limiteDeLivros() + " livro(s).");
            return false;
        }
        livro.emprestar();
        emprestimos.add(new Emprestimo(livro, usuario));
        System.out.println(usuario.nome() + " levou \"" + livro.getTitulo() + "\".");
        return true;
    }

    public void devolver(String codigo) {
        Livro livro = buscar(codigo);
        for (Emprestimo e : emprestimos) {
            if (e.livro() == livro) {
                emprestimos.remove(e);
                livro.devolver();
                System.out.println(e.usuario().nome() + " devolveu \"" + livro.getTitulo() + "\".");
                return;
            }
        }
        System.out.println("\"" + livro.getTitulo() + "\" não estava emprestado.");
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }

    public void listarEmprestimos() {
        System.out.println("--- Empréstimos (" + emprestimos.size() + ") ---");
        for (Emprestimo e : emprestimos) {
            System.out.println(e.livro().getTitulo() + " -> " + e.usuario().nome());
        }
    }
}

record Emprestimo(Livro livro, Usuario usuario) {
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() {
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}

Etapa 5 · Exceções próprias

Vamos dar nome a cada problema. Uma exceção própria é só uma classe que estende Exception. Criamos uma "mãe", BibliotecaException, e duas "filhas", uma para cada regra:

public class BibliotecaException extends Exception {
    public BibliotecaException(String mensagem) {
        super(mensagem);
    }
}
public class LivroIndisponivelException extends BibliotecaException {
    public LivroIndisponivelException(Livro livro) {
        super("\"" + livro.getTitulo() + "\" já está emprestado.");
    }
}
public class LimiteDeEmprestimosException extends BibliotecaException {
    public LimiteDeEmprestimosException(Usuario usuario) {
        super(usuario.nome() + " já está no limite de " + usuario.limiteDeLivros() + " livro(s).");
    }
}
extends Exception
A BibliotecaException é uma exceção checked: quem chama um método que a lança é obrigado a tratar ou repassar (checked × unchecked).
super(mensagem)
Passa a mensagem para o construtor de Exception; depois, e.getMessage() devolve esse texto (super).
extends BibliotecaException
Herança: as duas filhas são também uma BibliotecaException. Um único catch (BibliotecaException e) pega as duas.
LivroIndisponivelException(Livro livro)
O construtor recebe o livro e monta a mensagem sozinho. Quem lança só escreve throw new LivroIndisponivelException(this).

O Livro passa a defender a própria regra: emprestar um livro já emprestado lança a exceção.

    public void emprestar() throws LivroIndisponivelException {
        if (!disponivel) {
            throw new LivroIndisponivelException(this);
        }
        disponivel = false;
    }

E o emprestar da Biblioteca fica mais curto: nada de println, nada de boolean.

    public void emprestar(String codigo, Usuario usuario) throws BibliotecaException {
        Livro livro = buscar(codigo);
        if (quantosCom(usuario) >= usuario.limiteDeLivros()) {
            throw new LimiteDeEmprestimosException(usuario);
        }
        livro.emprestar();
        emprestimos.add(new Emprestimo(livro, usuario));
    }
PalavraOndeSignificado
throwdentro do método"Lançar": cria e dispara a exceção agora. O método para nessa linha.
throwsna assinatura"Lança": avisa que o método pode lançar essa exceção. throws BibliotecaException cobre as duas filhas.
livro.emprestar(); sem tryna BibliotecaA LivroIndisponivelException do livro "atravessa" o método da biblioteca e chega a quem chamou. Isso é permitido porque ela é uma BibliotecaException, que está no throws.

Quem decide o que fazer com a falha agora é o Main:

public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        tentarEmprestar(bib, "L1", bruno);
        tentarEmprestar(bib, "L2", bruno);
        tentarEmprestar(bib, "L3", bruno);
        tentarEmprestar(bib, "L1", carla);
        bib.devolver("L1");
        tentarEmprestar(bib, "L1", carla);
        tentarEmprestar(bib, "L9", carla);

        bib.listarEmprestimos();
        bib.listarAcervo();
    }

    static void tentarEmprestar(Biblioteca bib, String codigo, Usuario usuario) {
        try {
            bib.emprestar(codigo, usuario);
            System.out.println(usuario.nome() + " levou " + codigo + ".");
        } catch (BibliotecaException e) {
            System.out.println("Não deu: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.out.println("Erro: " + e.getMessage());
        }
    }
}

O tentarEmprestar tem dois catch: o primeiro pega as regras da biblioteca (as duas filhas), o segundo pega o código inexistente (IllegalArgumentException, da etapa 3). A saída é a do começo da página:

$ java Main.java
Bruno levou L1.
Bruno levou L2.
Não deu: Bruno já está no limite de 2 livro(s).
Não deu: "Dom Casmurro" já está emprestado.
Bruno devolveu "Dom Casmurro".
Carla levou L1.
Erro: Livro não encontrado: L9
--- Empréstimos (2) ---
Vidas Secas -> Bruno
Dom Casmurro -> Carla
--- Acervo (4 livros) ---
L1 | Dom Casmurro | Machado de Assis | emprestado
L2 | Vidas Secas | Graciliano Ramos | emprestado
L3 | O Cortiço | Aluísio Azevedo | disponível
L4 | Iracema | José de Alencar | disponível
Quando dá errado: esquecer o throws

Se o emprestar da Biblioteca não tiver throws BibliotecaException:

Este código tem um erro de propósito.

$ javac *.java
Biblioteca.java:35: error: unreported exception LimiteDeEmprestimosException; must be caught or declared to be thrown
            throw new LimiteDeEmprestimosException(usuario);
            ^
Biblioteca.java:37: error: unreported exception LivroIndisponivelException; must be caught or declared to be thrown
        livro.emprestar();
                       ^
2 errors

O compilador encontrou duas exceções checked "sem dono": uma lançada ali mesmo (linha 35) e outra que pode vir do livro.emprestar() (linha 37). Ou o método trata com try/catch, ou declara com throws. Aqui, a biblioteca não sabe o que fazer com a falha, então declara e deixa o Main decidir.

Missão (etapa 5): está faltando a classe LimiteDeEmprestimosException (rode e veja o cannot find symbol). Crie-a estendendo BibliotecaException, com um construtor que recebe o Usuario e monta a mensagem Bruno já está no limite de 2 livro(s).

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        tentarEmprestar(bib, "L1", bruno);
        tentarEmprestar(bib, "L2", bruno);
        tentarEmprestar(bib, "L3", bruno);
        tentarEmprestar(bib, "L1", carla);
        bib.devolver("L1");
        tentarEmprestar(bib, "L1", carla);
        tentarEmprestar(bib, "L9", carla);

        bib.listarEmprestimos();
        bib.listarAcervo();
    }

    static void tentarEmprestar(Biblioteca bib, String codigo, Usuario usuario) {
        try {
            bib.emprestar(codigo, usuario);
            System.out.println(usuario.nome() + " levou " + codigo + ".");
        } catch (BibliotecaException e) {
            System.out.println("Não deu: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.out.println("Erro: " + e.getMessage());
        }
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();
    private final List<Emprestimo> emprestimos = new ArrayList<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public int quantosCom(Usuario usuario) {
        int total = 0;
        for (Emprestimo e : emprestimos) {
            if (e.usuario().equals(usuario)) {
                total++;
            }
        }
        return total;
    }

    public void emprestar(String codigo, Usuario usuario) throws BibliotecaException {
        Livro livro = buscar(codigo);
        if (quantosCom(usuario) >= usuario.limiteDeLivros()) {
            throw new LimiteDeEmprestimosException(usuario);
        }
        livro.emprestar();
        emprestimos.add(new Emprestimo(livro, usuario));
    }

    public void devolver(String codigo) {
        Livro livro = buscar(codigo);
        for (Emprestimo e : emprestimos) {
            if (e.livro() == livro) {
                emprestimos.remove(e);
                livro.devolver();
                System.out.println(e.usuario().nome() + " devolveu \"" + livro.getTitulo() + "\".");
                return;
            }
        }
        System.out.println("\"" + livro.getTitulo() + "\" não estava emprestado.");
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }

    public void listarEmprestimos() {
        System.out.println("--- Empréstimos (" + emprestimos.size() + ") ---");
        for (Emprestimo e : emprestimos) {
            System.out.println(e.livro().getTitulo() + " -> " + e.usuario().nome());
        }
    }
}

class BibliotecaException extends Exception {
    public BibliotecaException(String mensagem) {
        super(mensagem);
    }
}

record Emprestimo(Livro livro, Usuario usuario) {
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() throws LivroIndisponivelException {
        if (!disponivel) {
            throw new LivroIndisponivelException(this);
        }
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

class LivroIndisponivelException extends BibliotecaException {
    public LivroIndisponivelException(Livro livro) {
        super("\"" + livro.getTitulo() + "\" já está emprestado.");
    }
}

record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca();
        bib.cadastrar(new Livro("L1", "Dom Casmurro", "Machado de Assis"));
        bib.cadastrar(new Livro("L2", "Vidas Secas", "Graciliano Ramos"));
        bib.cadastrar(new Livro("L3", "O Cortiço", "Aluísio Azevedo"));
        bib.cadastrar(new Livro("L4", "Iracema", "José de Alencar"));

        Usuario bruno = new Aluno("Bruno");
        Usuario carla = new Professor("Carla");

        tentarEmprestar(bib, "L1", bruno);
        tentarEmprestar(bib, "L2", bruno);
        tentarEmprestar(bib, "L3", bruno);
        tentarEmprestar(bib, "L1", carla);
        bib.devolver("L1");
        tentarEmprestar(bib, "L1", carla);
        tentarEmprestar(bib, "L9", carla);

        bib.listarEmprestimos();
        bib.listarAcervo();
    }

    static void tentarEmprestar(Biblioteca bib, String codigo, Usuario usuario) {
        try {
            bib.emprestar(codigo, usuario);
            System.out.println(usuario.nome() + " levou " + codigo + ".");
        } catch (BibliotecaException e) {
            System.out.println("Não deu: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.out.println("Erro: " + e.getMessage());
        }
    }
}

record Aluno(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 2;
    }
}

class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();
    private final List<Emprestimo> emprestimos = new ArrayList<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public int quantosCom(Usuario usuario) {
        int total = 0;
        for (Emprestimo e : emprestimos) {
            if (e.usuario().equals(usuario)) {
                total++;
            }
        }
        return total;
    }

    public void emprestar(String codigo, Usuario usuario) throws BibliotecaException {
        Livro livro = buscar(codigo);
        if (quantosCom(usuario) >= usuario.limiteDeLivros()) {
            throw new LimiteDeEmprestimosException(usuario);
        }
        livro.emprestar();
        emprestimos.add(new Emprestimo(livro, usuario));
    }

    public void devolver(String codigo) {
        Livro livro = buscar(codigo);
        for (Emprestimo e : emprestimos) {
            if (e.livro() == livro) {
                emprestimos.remove(e);
                livro.devolver();
                System.out.println(e.usuario().nome() + " devolveu \"" + livro.getTitulo() + "\".");
                return;
            }
        }
        System.out.println("\"" + livro.getTitulo() + "\" não estava emprestado.");
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }

    public void listarEmprestimos() {
        System.out.println("--- Empréstimos (" + emprestimos.size() + ") ---");
        for (Emprestimo e : emprestimos) {
            System.out.println(e.livro().getTitulo() + " -> " + e.usuario().nome());
        }
    }
}

class BibliotecaException extends Exception {
    public BibliotecaException(String mensagem) {
        super(mensagem);
    }
}

record Emprestimo(Livro livro, Usuario usuario) {
}

class LimiteDeEmprestimosException extends BibliotecaException {
    public LimiteDeEmprestimosException(Usuario usuario) {
        super(usuario.nome() + " já está no limite de " + usuario.limiteDeLivros() + " livro(s).");
    }
}

class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() throws LivroIndisponivelException {
        if (!disponivel) {
            throw new LivroIndisponivelException(this);
        }
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}

class LivroIndisponivelException extends BibliotecaException {
    public LivroIndisponivelException(Livro livro) {
        super("\"" + livro.getTitulo() + "\" já está emprestado.");
    }
}

record Professor(String nome) implements Usuario {
    @Override
    public int limiteDeLivros() {
        return 5;
    }
}

interface Usuario {
    String nome();

    int limiteDeLivros();
}

O código completo

Os arquivos que ainda não apareceram inteiros nesta página, na versão final:

public class Livro {
    private final String codigo;
    private final String titulo;
    private final String autor;
    private boolean disponivel;

    public Livro(String codigo, String titulo, String autor) {
        this.codigo = codigo;
        this.titulo = titulo;
        this.autor = autor;
        this.disponivel = true;
    }

    public String getCodigo() {
        return codigo;
    }

    public String getTitulo() {
        return titulo;
    }

    public String getAutor() {
        return autor;
    }

    public boolean isDisponivel() {
        return disponivel;
    }

    public void emprestar() throws LivroIndisponivelException {
        if (!disponivel) {
            throw new LivroIndisponivelException(this);
        }
        disponivel = false;
    }

    public void devolver() {
        disponivel = true;
    }

    @Override
    public String toString() {
        String situacao = disponivel ? "disponível" : "emprestado";
        return codigo + " | " + titulo + " | " + autor + " | " + situacao;
    }
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Biblioteca {
    private final Map<String, Livro> livros = new HashMap<>();
    private final List<Emprestimo> emprestimos = new ArrayList<>();

    public void cadastrar(Livro livro) {
        livros.put(livro.getCodigo(), livro);
    }

    public Livro buscar(String codigo) {
        Livro livro = livros.get(codigo);
        if (livro == null) {
            throw new IllegalArgumentException("Livro não encontrado: " + codigo);
        }
        return livro;
    }

    public int quantosCom(Usuario usuario) {
        int total = 0;
        for (Emprestimo e : emprestimos) {
            if (e.usuario().equals(usuario)) {
                total++;
            }
        }
        return total;
    }

    public void emprestar(String codigo, Usuario usuario) throws BibliotecaException {
        Livro livro = buscar(codigo);
        if (quantosCom(usuario) >= usuario.limiteDeLivros()) {
            throw new LimiteDeEmprestimosException(usuario);
        }
        livro.emprestar();
        emprestimos.add(new Emprestimo(livro, usuario));
    }

    public void devolver(String codigo) {
        Livro livro = buscar(codigo);
        for (Emprestimo e : emprestimos) {
            if (e.livro() == livro) {
                emprestimos.remove(e);
                livro.devolver();
                System.out.println(e.usuario().nome() + " devolveu \"" + livro.getTitulo() + "\".");
                return;
            }
        }
        System.out.println("\"" + livro.getTitulo() + "\" não estava emprestado.");
    }

    public void listarAcervo() {
        System.out.println("--- Acervo (" + livros.size() + " livros) ---");
        for (Livro livro : livros.values()) {
            System.out.println(livro);
        }
    }

    public void listarEmprestimos() {
        System.out.println("--- Empréstimos (" + emprestimos.size() + ") ---");
        for (Emprestimo e : emprestimos) {
            System.out.println(e.livro().getTitulo() + " -> " + e.usuario().nome());
        }
    }
}

Os outros oito estão completos nas etapas: Usuario, Aluno e Professor (etapa 2), Emprestimo (etapa 4), as três exceções e o Main (etapa 5).

Desafios extras

  1. Devolver o que não foi emprestado. Troque o println do devolver por uma exceção nova, LivroNaoEmprestadoException.
  2. Livros de um autor. Um método livrosDe(String autor) que devolve uma List<Livro>.
  3. Acervo em ordem. Troque o HashMap por um TreeMap e veja a listagem sair sempre ordenada pelo código.
  4. Novo tipo de usuário. Crie Visitante, que só pode levar 1 livro. Repare que nenhuma outra classe precisa mudar.
  5. Prazo de devolução. Guarde a data no Emprestimo (LocalDate, veja Datas e Horas) e um prazo por tipo de usuário.
Dica para o desafio 1

São três passos: a classe LivroNaoEmprestadoException extends BibliotecaException com a mensagem "Iracema" não está emprestado.; no devolver, throw new LivroNaoEmprestadoException(livro) no lugar do println e throws LivroNaoEmprestadoException na assinatura; no Main, um try/catch em volta do bib.devolver(…). Teste com bib.devolver("L4"): a saída deve ter Não deu: "Iracema" não está emprestado.


Exercícios

1. Por que Livro não tem um método setDisponivel(boolean)?

2. O que livros.get("L9") devolve se não existe a chave "L9" no HashMap?

3. Por que um único catch (BibliotecaException e) pega as duas exceções da biblioteca?

4. Qual a diferença entre throw e throws?

Complete a declaração do record que implementa a interface:

public record Aluno(String nome) Usuario {

Resumo

  • Cada classe tem uma responsabilidade: o Livro cuida da própria situação, a Biblioteca aplica as regras, o Main conversa com o mundo.
  • Atributos private + métodos com regras = encapsulamento; o compilador barra quem tenta pular.
  • Uma interface define o contrato; records podem implementá-la. Variáveis do tipo da interface dão polimorfismo.
  • HashMap acha pela chave; get devolve null quando não acha, então confira.
  • Exceções próprias dão nome aos problemas; herança entre elas permite tratar um grupo com um catch.
  • java Main.java compila sozinho as outras classes da pasta (Java 22+); javac *.java compila tudo.

Próximo projeto: Conta Bancária com Testes, em que o computador confere o seu código por você.