Projeto Agenda de Contatos

Uma agenda que cadastra, lista, busca e remove contatos e, o mais importante, lembra deles: tudo fica gravado num arquivo de texto e volta na próxima vez que o programa abrir. É o primeiro projeto do guia que guarda dados de verdade, e por isso o primeiro em que você vai cuidar de arquivos que podem estar faltando ou estragados.

Você vai praticar

O que vamos construir

A agenda pronta. Ela já tinha quatro contatos salvos de outras vezes; a Ana lista, busca, remove um, adiciona outro e sai. Depois, o cat mostra o arquivo que a agenda gravou:

$ java Agenda.java
=== Agenda da Ana === 4 contato(s) carregado(s)

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 1
1. Bruno Lima | (11) 91234-5678 | bruno@exemplo.com
2. Carla Dias | (21) 99876-5432 | carla@exemplo.com
3. Diego Rocha | (31) 98888-1111 | diego@exemplo.com
4. Elisa Prado | (41) 95555-3333 |
4 contato(s).

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 3
Buscar por: car
- Carla Dias | (21) 99876-5432 | carla@exemplo.com

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 4
Nome para remover: Diego Rocha
Removido: Diego Rocha

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 2
Nome: Fábio Nunes
Telefone: (51) 94444-7777
E-mail: fabio@exemplo.com
Adicionado: Fábio Nunes

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 1
1. Bruno Lima | (11) 91234-5678 | bruno@exemplo.com
2. Carla Dias | (21) 99876-5432 | carla@exemplo.com
3. Elisa Prado | (41) 95555-3333 |
4. Fábio Nunes | (51) 94444-7777 | fabio@exemplo.com
4 contato(s).

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 0
Até logo!
$ cat contatos.txt
Bruno Lima;(11) 91234-5678;bruno@exemplo.com
Carla Dias;(21) 99876-5432;carla@exemplo.com
Elisa Prado;(41) 95555-3333;
Fábio Nunes;(51) 94444-7777;fabio@exemplo.com
  1. O contato (record).
  2. A lista de contatos (ArrayList, listar).
  3. O menu e o cadastro (Scanner.nextLine, switch).
  4. Buscar e remover (contains, remove).
  5. Salvar no arquivo (Files.write, IOException).
  6. Carregar ao abrir (Files.readAllLines, split).

Antes de começar

Você vai usar o que viu nos projetos anteriores (menu com laço, métodos) e precisa conhecer Records e o básico de ArrayList. No seu computador:

$ mkdir -p java/agenda
$ cd java/agenda

Etapa 1 · O contato

Um contato tem nome, telefone e e-mail: três textos que andam sempre juntos. Em vez de três variáveis soltas, criamos um tipo novo, Contato, com um record ("registro"): uma classe feita só para carregar dados, escrita numa linha.

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Contato bruno = new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com");
        Contato carla = new Contato("Carla Dias", "(21) 99876-5432", "carla@exemplo.com");

        System.out.println(bruno);
        System.out.println(carla.nome() + " - " + carla.telefone());
    }
}
record Contato(…)
Declara o tipo Contato. Ele fica dentro da classe Agenda (um record "aninhado"), como o enum do jogo.
String nome, String telefone, String email
Os componentes: os dados que todo contato carrega.
{ }
O corpo, vazio por enquanto. Na etapa 5 vamos colocar um método aqui.

Só com essa linha, o Java cria sozinho tudo isto:

O record ganhaExemploResultado
Um construtor com os três componentesnew Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com")Um contato novo
Um método para ler cada componentecarla.nome()"Carla Dias"
Um toString legívelSystem.out.println(bruno)Contato[nome=…, telefone=…, email=…]
equals e hashCodea.equals(b)true se os três componentes forem iguais
$ java Agenda.java
Contato[nome=Bruno Lima, telefone=(11) 91234-5678, email=bruno@exemplo.com]
Carla Dias - (21) 99876-5432

Repare: para ler o nome, é carla.nome(), com parênteses. Não é getNome() como nas classes comuns (get e set), e não é carla.nome sem parênteses.

Quando dá errado: tentar mudar um contato

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

        Contato bruno = new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com");
        bruno.telefone = "(11) 90000-0000";
$ java Agenda.java
Agenda.java:8: error: cannot assign a value to final variable telefone
        bruno.telefone = "(11) 90000-0000";
             ^
1 error
error: compilation failed

"Não dá para atribuir um valor à variável final telefone." Os componentes de um record são final: um contato, depois de criado, não muda (ele é imutável). Para "trocar o telefone", crie um contato novo com o telefone novo e coloque-o no lugar do antigo. A última linha, error: compilation failed, é do lançador java: no modo arquivo-fonte, ele compila antes de rodar e avisa que a compilação falhou.

Missão (etapa 1): crie o contato carla (Carla Dias, (21) 99876-5432, carla@exemplo.com) e mostre Carla Dias - (21) 99876-5432 usando carla.nome() e carla.telefone().

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Contato bruno = new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com");

        System.out.println(bruno);
        // crie a Carla e mostre o nome e o telefone dela
    }
}
public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Contato bruno = new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com");
        Contato carla = new Contato("Carla Dias", "(21) 99876-5432", "carla@exemplo.com");

        System.out.println(bruno);
        System.out.println(carla.nome() + " - " + carla.telefone());
    }
}

Etapa 2 · A lista de contatos

Uma agenda tem muitos contatos, e não sabemos quantos. Um array tem tamanho fixo; uma lista (ArrayList) cresce sozinha a cada add. Criamos também o método listar, que mostra os contatos numerados.

import java.util.ArrayList;
import java.util.List;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        List<Contato> contatos = new ArrayList<>();
        contatos.add(new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com"));
        contatos.add(new Contato("Carla Dias", "(21) 99876-5432", "carla@exemplo.com"));
        contatos.add(new Contato("Diego Rocha", "(31) 98888-1111", "diego@exemplo.com"));

        listar(contatos);
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }
}
TrechoO que faz
List<Contato> contatosUma lista que só aceita Contato. O <Contato> é o "tipo dos itens" (generics). Declaramos como List (a interface) e criamos um ArrayList (a implementação).
new ArrayList<>()Lista vazia. O <> vazio ("diamante") repete o tipo da esquerda.
contatos.add(…)Põe um contato no fim da lista.
contatos.isEmpty()true se a lista não tem nada.
contatos.size() e contatos.get(i)Quantos itens há, e o item da posição i (começando em 0). Por isso mostramos i + 1.
$ java Agenda.java
1. Bruno Lima | (11) 91234-5678 | bruno@exemplo.com
2. Carla Dias | (21) 99876-5432 | carla@exemplo.com
3. Diego Rocha | (31) 98888-1111 | diego@exemplo.com
3 contato(s).

Missão (etapa 2): escreva o corpo do método listar: um for que mostra cada contato no formato 1. Nome | telefone | email e, no fim, 3 contato(s). Se a lista estiver vazia, mostre Nenhum contato.

import java.util.ArrayList;
import java.util.List;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        List<Contato> contatos = new ArrayList<>();
        contatos.add(new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com"));
        contatos.add(new Contato("Carla Dias", "(21) 99876-5432", "carla@exemplo.com"));
        contatos.add(new Contato("Diego Rocha", "(31) 98888-1111", "diego@exemplo.com"));

        listar(contatos);
    }

    static void listar(List<Contato> contatos) {
        // mostre cada contato numerado e, no fim, o total
    }
}
import java.util.ArrayList;
import java.util.List;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        List<Contato> contatos = new ArrayList<>();
        contatos.add(new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com"));
        contatos.add(new Contato("Carla Dias", "(21) 99876-5432", "carla@exemplo.com"));
        contatos.add(new Contato("Diego Rocha", "(31) 98888-1111", "diego@exemplo.com"));

        listar(contatos);
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }
}

Etapa 3 · O menu e o cadastro

Agora a lista começa vazia e quem cadastra é a pessoa, por um menu. O formato é o mesmo da calculadora (while (true) + switch), com uma diferença importante: aqui lemos linhas inteiras com nextLine(), porque nomes têm espaço.

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = new ArrayList<>();
        System.out.println("=== Agenda da Ana ===");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "2" -> adicionar(entrada, contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome: ");
        String nome = entrada.nextLine().trim();
        if (nome.isEmpty()) {
            System.out.println("O nome não pode ficar vazio.");
            return;
        }
        System.out.print("Telefone: ");
        String telefone = entrada.nextLine().trim();
        System.out.print("E-mail: ");
        String email = entrada.nextLine().trim();
        contatos.add(new Contato(nome, telefone, email));
        System.out.println("Adicionado: " + nome);
    }
}
entrada.nextLine()
Lê tudo até o Enter, com espaços: Bruno Lima inteiro. O next() leria só Bruno.
.trim()
Corta espaços do começo e do fim (quem nunca digitou um espaço a mais?).
switch (opcao) { case "1" -> …; }
Aqui o switch é uma instrução (não devolve valor): cada seta só chama um método. A opção é lida como texto, então "abc" cai no default sem exceção nenhuma.
case "0" -> { …; return; }
Duas instruções, então vão entre chaves. O return sai do main e encerra o programa.
if (nome.isEmpty())
Recusa nome vazio: quem aperta Enter sem digitar não cria um contato "fantasma".
$ java Agenda.java
=== Agenda da Ana ===

1) Listar  2) Adicionar  0) Sair
Opção: 1
Nenhum contato.

1) Listar  2) Adicionar  0) Sair
Opção: 2
Nome: Bruno Lima
Telefone: (11) 91234-5678
E-mail: bruno@exemplo.com
Adicionado: Bruno Lima

1) Listar  2) Adicionar  0) Sair
Opção: 2
Nome:                                  « a Ana só apertou Enter
O nome não pode ficar vazio.

1) Listar  2) Adicionar  0) Sair
Opção: 7
Opção inválida: 7

1) Listar  2) Adicionar  0) Sair
Opção: 1
1. Bruno Lima | (11) 91234-5678 | bruno@exemplo.com
1 contato(s).

1) Listar  2) Adicionar  0) Sair
Opção: 0
Até logo!
Por que não usar nextInt() para a opção?

Misturar nextInt() com nextLine() é a armadilha mais famosa do Scanner: o nextInt() lê o número, mas deixa o Enter na entrada, e o nextLine() seguinte lê essa "linha vazia" na hora, pulando a pergunta do nome. Lendo tudo com nextLine(), o problema não existe. Veja a explicação completa em Ler do Teclado (Scanner).

Missão (etapa 3): o menu já mostra a opção 2, mas ela ainda não faz nada. Acrescente o case "2" e escreva o método adicionar: pergunte Nome: , Telefone: e E-mail: com nextLine(), recuse nome vazio, adicione na lista e mostre Adicionado: Bruno Lima.

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = new ArrayList<>();
        System.out.println("=== Agenda da Ana ===");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        // pergunte nome, telefone e e-mail com nextLine(),
        // recuse nome vazio e adicione o contato na lista
    }
}
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = new ArrayList<>();
        System.out.println("=== Agenda da Ana ===");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "2" -> adicionar(entrada, contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome: ");
        String nome = entrada.nextLine().trim();
        if (nome.isEmpty()) {
            System.out.println("O nome não pode ficar vazio.");
            return;
        }
        System.out.print("Telefone: ");
        String telefone = entrada.nextLine().trim();
        System.out.print("E-mail: ");
        String email = entrada.nextLine().trim();
        contatos.add(new Contato(nome, telefone, email));
        System.out.println("Adicionado: " + nome);
    }
}

Etapa 4 · Buscar e remover

Duas opções novas no menu: 3) Buscar e 4) Remover. A busca encontra contatos por parte do nome, sem ligar para maiúsculas; a remoção apaga pelo nome completo.

    static void buscar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Buscar por: ");
        String termo = entrada.nextLine().trim().toLowerCase();
        int achados = 0;
        for (Contato c : contatos) {
            if (c.nome().toLowerCase().contains(termo)) {
                System.out.println("- " + c.nome() + " | " + c.telefone() + " | " + c.email());
                achados++;
            }
        }
        if (achados == 0) {
            System.out.println("Nenhum contato com \"" + termo + "\".");
        }
    }

    static void remover(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome para remover: ");
        String nome = entrada.nextLine().trim();
        for (Contato c : contatos) {
            if (c.nome().equalsIgnoreCase(nome)) {
                contatos.remove(c);
                System.out.println("Removido: " + c.nome());
                return;
            }
        }
        System.out.println("Não encontrei \"" + nome + "\".");
    }
TrechoO que faz
termo.toLowerCase() e c.nome().toLowerCase()Passa os dois lados para minúsculas, para que "carl" ache "Carla". Comparar só um lado não funcionaria.
.contains(termo)true se o termo aparece em qualquer lugar do nome.
for (Contato c : contatos)O for-each: "para cada contato c da lista". Não precisa de índice.
equalsIgnoreCase(nome)Igualdade exata, ignorando maiúsculas: "bruno lima" remove "Bruno Lima".
contatos.remove(c) seguido de returnTira o contato da lista e sai do método na hora. O return é obrigatório aqui; veja o cenário abaixo.
$ java Agenda.java
=== Agenda da Ana ===
…                                       « a Ana cadastrou Bruno, Carla e Carlos
1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 3
Buscar por: carl
- Carla Dias | (21) 99876-5432 | carla@exemplo.com
- Carlos Mendes | (11) 97777-2222 | carlos@exemplo.com

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 3
Buscar por: zé
Nenhum contato com "zé".

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 4
Nome para remover: bruno lima
Removido: Bruno Lima

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 4
Nome para remover: Bruno Lima
Não encontrei "Bruno Lima".               « já tinha sido removido

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 1
1. Carla Dias | (21) 99876-5432 | carla@exemplo.com
2. Carlos Mendes | (11) 97777-2222 | carlos@exemplo.com
2 contato(s).
Quando dá errado: remover dentro do for-each e continuar

A Ana quis remover todos os contatos que começam com "Carl" e tirou o return:

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

        for (Contato c : contatos) {
            if (c.nome().startsWith("Carl")) {
                contatos.remove(c);
            }
        }
$ java Agenda.java
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
	at Agenda.main(Agenda.java:15)

"Modificação concorrente": a lista mudou enquanto o for-each andava por ela, e o Java parou tudo para não pular ou repetir itens. Com o return logo depois do remove, o laço nunca dá o próximo passo, e por isso a etapa 4 funciona. Para remover vários de uma vez, use contatos.removeIf(c -> c.nome().startsWith("Carl")) (com uma lambda) ou um Iterator.

Missão (etapa 4): complete o método buscar. A entrada da missão cadastra Carla e Carlos e busca carl (deve achar os dois) e zé (deve mostrar Nenhum contato com "zé".).

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = new ArrayList<>();
        System.out.println("=== Agenda da Ana ===");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "2" -> adicionar(entrada, contatos);
                case "3" -> buscar(entrada, contatos);
                case "4" -> remover(entrada, contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome: ");
        String nome = entrada.nextLine().trim();
        if (nome.isEmpty()) {
            System.out.println("O nome não pode ficar vazio.");
            return;
        }
        System.out.print("Telefone: ");
        String telefone = entrada.nextLine().trim();
        System.out.print("E-mail: ");
        String email = entrada.nextLine().trim();
        contatos.add(new Contato(nome, telefone, email));
        System.out.println("Adicionado: " + nome);
    }

    static void buscar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Buscar por: ");
        String termo = entrada.nextLine().trim().toLowerCase();
        // mostre os contatos cujo nome contém o termo (sem diferenciar maiúsculas)
        // e avise se nenhum foi encontrado
    }

    static void remover(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome para remover: ");
        String nome = entrada.nextLine().trim();
        for (Contato c : contatos) {
            if (c.nome().equalsIgnoreCase(nome)) {
                contatos.remove(c);
                System.out.println("Removido: " + c.nome());
                return;
            }
        }
        System.out.println("Não encontrei \"" + nome + "\".");
    }
}
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = new ArrayList<>();
        System.out.println("=== Agenda da Ana ===");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "2" -> adicionar(entrada, contatos);
                case "3" -> buscar(entrada, contatos);
                case "4" -> remover(entrada, contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome: ");
        String nome = entrada.nextLine().trim();
        if (nome.isEmpty()) {
            System.out.println("O nome não pode ficar vazio.");
            return;
        }
        System.out.print("Telefone: ");
        String telefone = entrada.nextLine().trim();
        System.out.print("E-mail: ");
        String email = entrada.nextLine().trim();
        contatos.add(new Contato(nome, telefone, email));
        System.out.println("Adicionado: " + nome);
    }

    static void buscar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Buscar por: ");
        String termo = entrada.nextLine().trim().toLowerCase();
        int achados = 0;
        for (Contato c : contatos) {
            if (c.nome().toLowerCase().contains(termo)) {
                System.out.println("- " + c.nome() + " | " + c.telefone() + " | " + c.email());
                achados++;
            }
        }
        if (achados == 0) {
            System.out.println("Nenhum contato com \"" + termo + "\".");
        }
    }

    static void remover(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome para remover: ");
        String nome = entrada.nextLine().trim();
        for (Contato c : contatos) {
            if (c.nome().equalsIgnoreCase(nome)) {
                contatos.remove(c);
                System.out.println("Removido: " + c.nome());
                return;
            }
        }
        System.out.println("Não encontrei \"" + nome + "\".");
    }
}

Etapa 5 · Salvar no arquivo

Até aqui, fechar o programa apaga tudo: a lista mora na memória. Para guardar, vamos gravar cada contato como uma linha de um arquivo de texto, com os campos separados por ;:

Bruno Lima;(11) 91234-5678;bruno@exemplo.com
Carla Dias;(21) 99876-5432;carla@exemplo.com

Três mudanças no código. Primeiro, o record aprende a se transformar numa linha, e o caminho do arquivo vira uma constante:

    record Contato(String nome, String telefone, String email) {
        String paraLinha() {
            return nome + ";" + telefone + ";" + email;
        }
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

Segundo, o método que grava:

    static void salvar(List<Contato> contatos) {
        List<String> linhas = new ArrayList<>();
        for (Contato c : contatos) {
            linhas.add(c.paraLinha());
        }
        try {
            Files.write(ARQUIVO, linhas);
        } catch (IOException e) {
            System.out.println("Erro ao salvar: " + e);
        }
    }

Terceiro, salvar(contatos); é chamado logo depois de cada add (em adicionar) e de cada remove (em remover). Assim o arquivo está sempre em dia, mesmo se o programa for fechado de repente. E no topo do arquivo entram três import: java.io.IOException, java.nio.file.Files e java.nio.file.Path.

TrechoO que faz
String paraLinha() dentro do recordUm record pode ter métodos. Dentro dele, nome é o componente do próprio contato.
Path.of("contatos.txt")Um caminho de arquivo. Sem pasta na frente, é relativo à pasta onde você rodou o java.
Files.write(ARQUIVO, linhas)Grava cada String da lista como uma linha. Cria o arquivo se não existir e substitui o conteúdo se existir.
try { … } catch (IOException e) { … }Gravar pode falhar (disco cheio, pasta sem permissão). Se falhar, mostramos o erro em vez de derrubar o programa.
$ java Agenda.java
=== Agenda da Ana ===

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 2
Nome: Bruno Lima
Telefone: (11) 91234-5678
E-mail: bruno@exemplo.com
Adicionado: Bruno Lima

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 2
Nome: Carla Dias
Telefone: (21) 99876-5432
E-mail: carla@exemplo.com
Adicionado: Carla Dias

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 0
Até logo!
$ ls
Agenda.java  contatos.txt                « o arquivo apareceu
$ cat contatos.txt
Bruno Lima;(11) 91234-5678;bruno@exemplo.com
Carla Dias;(21) 99876-5432;carla@exemplo.com
Quando dá errado: esquecer o try/catch

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

        Files.write(ARQUIVO, linhas);
$ java Agenda.java
Agenda.java:104: error: unreported exception IOException; must be caught or declared to be thrown
        Files.write(ARQUIVO, linhas);
                   ^
1 error
error: compilation failed

"Exceção IOException não informada; precisa ser capturada ou declarada." IOException é uma exceção checked: o compilador obriga você a decidir o que fazer com ela, com try/catch ou com throws IOException na assinatura do método. Veja Checked × Unchecked.

Missão (etapa 5): este programa menor salva dois contatos e depois mostra o arquivo com Files.readString. Rode antes: dá NoSuchFileException, porque salvar ainda não grava nada. Escreva o salvar com paraLinha(), Files.write e try/catch.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
        String paraLinha() {
            return nome + ";" + telefone + ";" + email;
        }
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

    public static void main(String[] args) throws IOException {
        List<Contato> contatos = new ArrayList<>();
        contatos.add(new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com"));
        contatos.add(new Contato("Carla Dias", "(21) 99876-5432", "carla@exemplo.com"));

        salvar(contatos);
        System.out.print(Files.readString(ARQUIVO));
    }

    static void salvar(List<Contato> contatos) {
        // transforme cada contato numa linha e grave com Files.write
    }
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
        String paraLinha() {
            return nome + ";" + telefone + ";" + email;
        }
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

    public static void main(String[] args) throws IOException {
        List<Contato> contatos = new ArrayList<>();
        contatos.add(new Contato("Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com"));
        contatos.add(new Contato("Carla Dias", "(21) 99876-5432", "carla@exemplo.com"));

        salvar(contatos);
        System.out.print(Files.readString(ARQUIVO));
    }

    static void salvar(List<Contato> contatos) {
        List<String> linhas = new ArrayList<>();
        for (Contato c : contatos) {
            linhas.add(c.paraLinha());
        }
        try {
            Files.write(ARQUIVO, linhas);
        } catch (IOException e) {
            System.out.println("Erro ao salvar: " + e);
        }
    }
}

Etapa 6 · Carregar ao abrir

Falta o caminho de volta: ao abrir, ler o arquivo e recriar os contatos. Cada linha é cortada nos ; com split, e os pedaços viram um Contato. O main passa a começar com List<Contato> contatos = carregar(); e mostra quantos vieram. A primeira versão, ingênua:

    static List<Contato> carregar() {
        List<Contato> contatos = new ArrayList<>();
        if (!Files.exists(ARQUIVO)) {
            return contatos;
        }
        try {
            for (String linha : Files.readAllLines(ARQUIVO)) {
                String[] partes = linha.split(";");
                contatos.add(new Contato(partes[0], partes[1], partes[2]));
            }
        } catch (IOException e) {
            System.out.println("Erro ao carregar: " + e);
        }
        return contatos;
    }
Files.exists(ARQUIVO)
Na primeira vez, o arquivo ainda não existe. Sem essa conferência, readAllLines lançaria NoSuchFileException.
Files.readAllLines(ARQUIVO)
Lê o arquivo inteiro e devolve uma List<String>, uma linha por item.
linha.split(";")
Corta o texto em cada ; e devolve um array: ["Bruno Lima", "(11) 91234-5678", "bruno@exemplo.com"].
partes[0], partes[1], partes[2]
Nome, telefone e e-mail, nas posições 0, 1 e 2.

Funciona… até a Ana cadastrar alguém sem e-mail:

$ java Agenda.java
=== Agenda da Ana === 3 contato(s) carregado(s)

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 2
Nome: Elisa Prado
Telefone: (41) 95555-3333
E-mail:                                  « a Ana não sabia o e-mail e apertou Enter
Adicionado: Elisa Prado

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 0
Até logo!
$ cat contatos.txt
Bruno Lima;(11) 91234-5678;bruno@exemplo.com
Carla Dias;(21) 99876-5432;carla@exemplo.com
Diego Rocha;(31) 98888-1111;diego@exemplo.com
Elisa Prado;(41) 95555-3333;
$ java Agenda.java
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
	at Agenda.carregar(Agenda.java:119)
	at Agenda.main(Agenda.java:20)

A agenda não abre mais! "Índice 2 fora dos limites para tamanho 2": o array partes tem só 2 itens e o código pediu o terceiro. O culpado é um detalhe do split: ele descarta os pedaços vazios do fim. "Elisa Prado;(41) 95555-3333;" vira só dois pedaços. A versão corrigida:

            for (String linha : Files.readAllLines(ARQUIVO)) {
                String[] partes = linha.split(";", -1);
                if (partes.length == 3) {
                    contatos.add(new Contato(partes[0], partes[1], partes[2]));
                } else if (!linha.isBlank()) {
                    System.out.println("Linha ignorada: " + linha);
                }
            }
CorreçãoO que resolve
split(";", -1)O -1 manda o split manter os pedaços vazios do fim: a Elisa volta com e-mail "".
if (partes.length == 3)Só cria o contato se a linha tiver exatamente três campos.
else if (!linha.isBlank())Linha estragada (e não vazia): avisa e segue em frente, em vez de derrubar a agenda inteira.
$ echo "linha estragada" >> contatos.txt      « a Ana estraga o arquivo de propósito
$ java Agenda.java
Linha ignorada: linha estragada
=== Agenda da Ana === 4 contato(s) carregado(s)

1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair
Opção: 0
Até logo!
Lição de todo programa que lê arquivos

Nunca confie que o arquivo está perfeito. Ele pode não existir, estar vazio, ter sido editado à mão ou gravado por uma versão antiga do programa. Confira antes de usar: Files.exists, o tamanho do array, isBlank.

Missão (etapa 6): este programa grava um arquivo com três linhas (uma sem e-mail e uma estragada) e chama carregar. Rode: dá ArrayIndexOutOfBoundsException. Conserte o carregar com split(";", -1) e a conferência do tamanho, para carregar 2 contatos e mostrar Linha ignorada: linha estragada.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

    public static void main(String[] args) throws IOException {
        Files.writeString(ARQUIVO, """
                Bruno Lima;(11) 91234-5678;bruno@exemplo.com
                Elisa Prado;(41) 95555-3333;
                linha estragada
                """);
        List<Contato> contatos = carregar();
        System.out.println(contatos.size() + " contato(s) carregado(s)");
        System.out.println(contatos);
    }

    static List<Contato> carregar() {
        List<Contato> contatos = new ArrayList<>();
        try {
            for (String linha : Files.readAllLines(ARQUIVO)) {
                String[] partes = linha.split(";");
                contatos.add(new Contato(partes[0], partes[1], partes[2]));
            }
        } catch (IOException e) {
            System.out.println("Erro ao carregar: " + e);
        }
        return contatos;
    }
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

    public static void main(String[] args) throws IOException {
        Files.writeString(ARQUIVO, """
                Bruno Lima;(11) 91234-5678;bruno@exemplo.com
                Elisa Prado;(41) 95555-3333;
                linha estragada
                """);
        List<Contato> contatos = carregar();
        System.out.println(contatos.size() + " contato(s) carregado(s)");
        System.out.println(contatos);
    }

    static List<Contato> carregar() {
        List<Contato> contatos = new ArrayList<>();
        try {
            for (String linha : Files.readAllLines(ARQUIVO)) {
                String[] partes = linha.split(";", -1);
                if (partes.length == 3) {
                    contatos.add(new Contato(partes[0], partes[1], partes[2]));
                } else if (!linha.isBlank()) {
                    System.out.println("Linha ignorada: " + linha);
                }
            }
        } catch (IOException e) {
            System.out.println("Erro ao carregar: " + e);
        }
        return contatos;
    }
}

O código completo

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
        String paraLinha() {
            return nome + ";" + telefone + ";" + email;
        }
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = carregar();
        System.out.println("=== Agenda da Ana === " + contatos.size() + " contato(s) carregado(s)");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "2" -> adicionar(entrada, contatos);
                case "3" -> buscar(entrada, contatos);
                case "4" -> remover(entrada, contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome: ");
        String nome = entrada.nextLine().trim();
        if (nome.isEmpty()) {
            System.out.println("O nome não pode ficar vazio.");
            return;
        }
        System.out.print("Telefone: ");
        String telefone = entrada.nextLine().trim();
        System.out.print("E-mail: ");
        String email = entrada.nextLine().trim();
        contatos.add(new Contato(nome, telefone, email));
        salvar(contatos);
        System.out.println("Adicionado: " + nome);
    }

    static void buscar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Buscar por: ");
        String termo = entrada.nextLine().trim().toLowerCase();
        int achados = 0;
        for (Contato c : contatos) {
            if (c.nome().toLowerCase().contains(termo)) {
                System.out.println("- " + c.nome() + " | " + c.telefone() + " | " + c.email());
                achados++;
            }
        }
        if (achados == 0) {
            System.out.println("Nenhum contato com \"" + termo + "\".");
        }
    }

    static void remover(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome para remover: ");
        String nome = entrada.nextLine().trim();
        for (Contato c : contatos) {
            if (c.nome().equalsIgnoreCase(nome)) {
                contatos.remove(c);
                salvar(contatos);
                System.out.println("Removido: " + c.nome());
                return;
            }
        }
        System.out.println("Não encontrei \"" + nome + "\".");
    }

    static void salvar(List<Contato> contatos) {
        List<String> linhas = new ArrayList<>();
        for (Contato c : contatos) {
            linhas.add(c.paraLinha());
        }
        try {
            Files.write(ARQUIVO, linhas);
        } catch (IOException e) {
            System.out.println("Erro ao salvar: " + e);
        }
    }

    static List<Contato> carregar() {
        List<Contato> contatos = new ArrayList<>();
        if (!Files.exists(ARQUIVO)) {
            return contatos;
        }
        try {
            for (String linha : Files.readAllLines(ARQUIVO)) {
                String[] partes = linha.split(";", -1);
                if (partes.length == 3) {
                    contatos.add(new Contato(partes[0], partes[1], partes[2]));
                } else if (!linha.isBlank()) {
                    System.out.println("Linha ignorada: " + linha);
                }
            }
        } catch (IOException e) {
            System.out.println("Erro ao carregar: " + e);
        }
        return contatos;
    }
}
E se eu quiser o record num arquivo separado?

Em programas maiores, cada tipo vai no seu arquivo: Contato.java com public record Contato(…) { … }, e Agenda.java com o resto. Desde o Java 22, java Agenda.java encontra sozinho o Contato.java na mesma pasta. O próximo projeto é organizado assim.

Desafios extras

  1. Sem nomes repetidos. Em adicionar, recuse um nome que já existe (ignorando maiúsculas).
  2. Lista em ordem alfabética. Antes de listar, ordene: contatos.sort(Comparator.comparing(Contato::nome)) (veja Comparable e Comparator).
  3. Editar o telefone. Uma opção 5 que troca o telefone. Como o record é imutável, use contatos.set(i, new Contato(…)) na posição i.
  4. O ; dentro do nome. O que acontece se alguém cadastrar Ana; a melhor? Recuse nomes com ; ou troque o separador.
  5. Buscar também pelo telefone. Faça a busca olhar nome ou telefone.

Desafio 1 no editor: em adicionar, logo depois de conferir o nome vazio, percorra a lista e, se já houver um contato com o mesmo nome (ignorando maiúsculas), mostre Já existe um contato chamado Bruno Lima. e não adicione.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
        String paraLinha() {
            return nome + ";" + telefone + ";" + email;
        }
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = carregar();
        System.out.println("=== Agenda da Ana === " + contatos.size() + " contato(s) carregado(s)");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "2" -> adicionar(entrada, contatos);
                case "3" -> buscar(entrada, contatos);
                case "4" -> remover(entrada, contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome: ");
        String nome = entrada.nextLine().trim();
        if (nome.isEmpty()) {
            System.out.println("O nome não pode ficar vazio.");
            return;
        }
        System.out.print("Telefone: ");
        String telefone = entrada.nextLine().trim();
        System.out.print("E-mail: ");
        String email = entrada.nextLine().trim();
        contatos.add(new Contato(nome, telefone, email));
        salvar(contatos);
        System.out.println("Adicionado: " + nome);
    }

    static void buscar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Buscar por: ");
        String termo = entrada.nextLine().trim().toLowerCase();
        int achados = 0;
        for (Contato c : contatos) {
            if (c.nome().toLowerCase().contains(termo)) {
                System.out.println("- " + c.nome() + " | " + c.telefone() + " | " + c.email());
                achados++;
            }
        }
        if (achados == 0) {
            System.out.println("Nenhum contato com \"" + termo + "\".");
        }
    }

    static void remover(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome para remover: ");
        String nome = entrada.nextLine().trim();
        for (Contato c : contatos) {
            if (c.nome().equalsIgnoreCase(nome)) {
                contatos.remove(c);
                salvar(contatos);
                System.out.println("Removido: " + c.nome());
                return;
            }
        }
        System.out.println("Não encontrei \"" + nome + "\".");
    }

    static void salvar(List<Contato> contatos) {
        List<String> linhas = new ArrayList<>();
        for (Contato c : contatos) {
            linhas.add(c.paraLinha());
        }
        try {
            Files.write(ARQUIVO, linhas);
        } catch (IOException e) {
            System.out.println("Erro ao salvar: " + e);
        }
    }

    static List<Contato> carregar() {
        List<Contato> contatos = new ArrayList<>();
        if (!Files.exists(ARQUIVO)) {
            return contatos;
        }
        try {
            for (String linha : Files.readAllLines(ARQUIVO)) {
                String[] partes = linha.split(";", -1);
                if (partes.length == 3) {
                    contatos.add(new Contato(partes[0], partes[1], partes[2]));
                } else if (!linha.isBlank()) {
                    System.out.println("Linha ignorada: " + linha);
                }
            }
        } catch (IOException e) {
            System.out.println("Erro ao carregar: " + e);
        }
        return contatos;
    }
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Agenda {

    record Contato(String nome, String telefone, String email) {
        String paraLinha() {
            return nome + ";" + telefone + ";" + email;
        }
    }

    static final Path ARQUIVO = Path.of("contatos.txt");

    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        List<Contato> contatos = carregar();
        System.out.println("=== Agenda da Ana === " + contatos.size() + " contato(s) carregado(s)");

        while (true) {
            System.out.println();
            System.out.println("1) Listar  2) Adicionar  3) Buscar  4) Remover  0) Sair");
            System.out.print("Opção: ");
            String opcao = entrada.nextLine().trim();
            switch (opcao) {
                case "1" -> listar(contatos);
                case "2" -> adicionar(entrada, contatos);
                case "3" -> buscar(entrada, contatos);
                case "4" -> remover(entrada, contatos);
                case "0" -> {
                    System.out.println("Até logo!");
                    return;
                }
                default -> System.out.println("Opção inválida: " + opcao);
            }
        }
    }

    static void listar(List<Contato> contatos) {
        if (contatos.isEmpty()) {
            System.out.println("Nenhum contato.");
            return;
        }
        for (int i = 0; i < contatos.size(); i++) {
            Contato c = contatos.get(i);
            System.out.println((i + 1) + ". " + c.nome() + " | " + c.telefone() + " | " + c.email());
        }
        System.out.println(contatos.size() + " contato(s).");
    }

    static void adicionar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome: ");
        String nome = entrada.nextLine().trim();
        if (nome.isEmpty()) {
            System.out.println("O nome não pode ficar vazio.");
            return;
        }
        for (Contato c : contatos) {
            if (c.nome().equalsIgnoreCase(nome)) {
                System.out.println("Já existe um contato chamado " + c.nome() + ".");
                return;
            }
        }
        System.out.print("Telefone: ");
        String telefone = entrada.nextLine().trim();
        System.out.print("E-mail: ");
        String email = entrada.nextLine().trim();
        contatos.add(new Contato(nome, telefone, email));
        salvar(contatos);
        System.out.println("Adicionado: " + nome);
    }

    static void buscar(Scanner entrada, List<Contato> contatos) {
        System.out.print("Buscar por: ");
        String termo = entrada.nextLine().trim().toLowerCase();
        int achados = 0;
        for (Contato c : contatos) {
            if (c.nome().toLowerCase().contains(termo)) {
                System.out.println("- " + c.nome() + " | " + c.telefone() + " | " + c.email());
                achados++;
            }
        }
        if (achados == 0) {
            System.out.println("Nenhum contato com \"" + termo + "\".");
        }
    }

    static void remover(Scanner entrada, List<Contato> contatos) {
        System.out.print("Nome para remover: ");
        String nome = entrada.nextLine().trim();
        for (Contato c : contatos) {
            if (c.nome().equalsIgnoreCase(nome)) {
                contatos.remove(c);
                salvar(contatos);
                System.out.println("Removido: " + c.nome());
                return;
            }
        }
        System.out.println("Não encontrei \"" + nome + "\".");
    }

    static void salvar(List<Contato> contatos) {
        List<String> linhas = new ArrayList<>();
        for (Contato c : contatos) {
            linhas.add(c.paraLinha());
        }
        try {
            Files.write(ARQUIVO, linhas);
        } catch (IOException e) {
            System.out.println("Erro ao salvar: " + e);
        }
    }

    static List<Contato> carregar() {
        List<Contato> contatos = new ArrayList<>();
        if (!Files.exists(ARQUIVO)) {
            return contatos;
        }
        try {
            for (String linha : Files.readAllLines(ARQUIVO)) {
                String[] partes = linha.split(";", -1);
                if (partes.length == 3) {
                    contatos.add(new Contato(partes[0], partes[1], partes[2]));
                } else if (!linha.isBlank()) {
                    System.out.println("Linha ignorada: " + linha);
                }
            }
        } catch (IOException e) {
            System.out.println("Erro ao carregar: " + e);
        }
        return contatos;
    }
}

Exercícios

1. Como se lê o telefone de um record Contato c?

2. O que "Elisa;(41) 95555-3333;".split(";") devolve?

3. Por que o remover tem um return logo depois de contatos.remove(c)?

4. Por que a agenda lê tudo com nextLine()?

Complete para ler todas as linhas do arquivo:

List<String> linhas = Files.(ARQUIVO);

Resumo

  • Um record junta dados relacionados e ganha construtor, nome(), toString e equals de graça; os componentes não mudam.
  • ArrayList cresce sozinho; add, get, size, isEmpty, remove.
  • Com nextLine() + trim() em tudo, nomes com espaço funcionam e o menu não "pula" perguntas.
  • Remover dentro de um for-each só é seguro se você sair do laço logo depois.
  • Files.write grava uma lista de linhas; Files.readAllLines lê de volta. IOException precisa de try/catch (ou throws).
  • split(";", -1) mantém campos vazios; confira o tamanho antes de usar as partes.

Próximo projeto: Sistema de Biblioteca, com várias classes conversando entre si.