Projeto Tarefas com Maven
O último projeto é um programa "de verdade", do tipo que se usa no terminal todo dia: um gerenciador de tarefas. Nada de menu nem de perguntas: você diz o que quer na própria linha de comando (tarefas add "Comprar pão", tarefas list, tarefas done 1), ele faz e termina. As tarefas ficam num arquivo CSV, e o programa inteiro vira um único JAR executável.
- Ler argumentos da linha de comando e montar subcomandos com switch.
- Avisar erros como um programa de terminal:
System.erre código de saída comSystem.exit. - Um record que vira linha de CSV e volta, com Files (sem bibliotecas externas).
- Streams para filtrar e contar.
- Organizar o projeto com Maven e gerar um JAR executável.
O que vamos construir
O programa pronto. Depois de criar um alias (um apelido do Bash), a Ana usa o tarefas de qualquer pasta, como se fosse um comando do sistema:
$ java -jar target/tarefas.jar list [x] 1. Comprar pão [ ] 2. Estudar streams; capítulo 3 [ ] 3. Pagar a conta de luz 3 tarefa(s), 2 pendente(s). $ alias tarefas='java -jar ~/java/tarefas/target/tarefas.jar' $ cd $ tarefas done 2 Concluída: [x] 2. Estudar streams; capítulo 3 $ tarefas list --pendentes [ ] 3. Pagar a conta de luz 3 tarefa(s), 1 pendente(s). $ tarefas Uso: tarefas add "descrição" | list [--pendentes|--feitas] | done NÚMERO
- O projeto e os argumentos (
args). - Os subcomandos (
switch,System.err,System.exit). - Guardar em CSV (record
Tarefa, classeArquivo). - Concluir e filtrar (
done, streams). - O JAR executável (
maven-jar-plugin,java -jar).
Antes de começar
Você vai usar Maven (como no projeto do banco), args, Streams e, para o CSV, o que viu no projeto da agenda. A estrutura:
$ mkdir -p java/tarefas/src/main/java/br/com/ana/tarefas $ cd java/tarefas
O editor do guia não roda Maven. As caixas Experimente usam programas de um arquivo só, e os argumentos da linha de comando vêm prontos na missão (como se você tivesse digitado java Main.java list). As saídas de mvn e java -jar desta página são reais, da máquina da Ana.
Etapa 1 · O projeto e os argumentos
Primeiro, o pom.xml mais simples possível (sem dependências: este projeto não usa bibliotecas externas):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>br.com.ana</groupId>
<artifactId>tarefas</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
E uma classe que só mostra o que recebeu. Todo main recebe String[] args: um array com as palavras digitadas depois do nome da classe.
package br.com.ana.tarefas;
public class App {
public static void main(String[] args) {
System.out.println("Recebi " + args.length + " argumento(s):");
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] = " + args[i]);
}
}
}
$ mvn -q compile $ java -cp target/classes br.com.ana.tarefas.App add "Comprar pão" Recebi 2 argumento(s): args[0] = add args[1] = Comprar pão $ java -cp target/classes br.com.ana.tarefas.App add Comprar pão Recebi 3 argumento(s): args[0] = add args[1] = Comprar args[2] = pão « sem aspas, cada palavra é um argumento $ java -cp target/classes br.com.ana.tarefas.App Recebi 0 argumento(s):
Quem corta a linha em argumentos é o Bash, não o Java: ele separa nos espaços, e as aspas juntam o que está dentro num argumento só (veja Argumentos da Linha de Comando). Por isso o programa vai pedir a descrição entre aspas. O args[0] será sempre o subcomando (add, list, done), do mesmo jeito que git commit ou apt install.
args[0] sem argumentos
Se o programa fizer String comando = args[0]; direto e a pessoa não digitar nada:
$ java -cp target/classes br.com.ana.tarefas.App Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0 at br.com.ana.tarefas.App.main(App.java:5)
"Índice 0 fora dos limites para tamanho 0": o array args existe, mas está vazio. Confira args.length antes de ler qualquer posição.
Missão (etapa 1): a missão roda o programa como java Main.java list. Se não houver argumentos, mostre Sem comando; se houver, mostre Comando: seguido de args[0].
public class Main {
public static void main(String[] args) {
System.out.println("Recebi " + args.length + " argumento(s).");
// se não houver argumentos, mostre "Sem comando";
// se houver, mostre "Comando: " seguido do primeiro argumento
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Recebi " + args.length + " argumento(s).");
if (args.length == 0) {
System.out.println("Sem comando");
} else {
System.out.println("Comando: " + args[0]);
}
}
}
Etapa 2 · Os subcomandos
Agora o args[0] escolhe o que fazer. Programas de terminal seguem duas convenções que vamos respeitar:
- Mensagens de erro vão para a saída de erro,
System.err, e não para a saída normal,System.out. Assim, quem redireciona a saída para um arquivo (> lista.txt) não mistura erros com dados. - O código de saída diz se deu certo: 0 é sucesso, qualquer outro número é erro. Scripts e o próprio Bash (
&&,if) olham esse número.System.exit(1)termina o programa na hora, com o código 1.
package br.com.ana.tarefas;
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
if (args.length == 0) {
uso();
System.exit(1);
}
List<String> tarefas = new ArrayList<>();
switch (args[0]) {
case "add" -> {
tarefas.add(args[1]);
System.out.println("Adicionada: " + args[1]);
}
case "list" -> {
for (String t : tarefas) {
System.out.println(t);
}
System.out.println(tarefas.size() + " tarefa(s).");
}
default -> {
System.err.println("Comando desconhecido: " + args[0]);
uso();
System.exit(1);
}
}
}
static void uso() {
System.err.println("Uso: tarefas add \"descrição\" | list");
}
}
$ java -cp target/classes br.com.ana.tarefas.App add "Comprar pão" Adicionada: Comprar pão $ java -cp target/classes br.com.ana.tarefas.App list 0 tarefa(s). « cadê o pão? $ java -cp target/classes br.com.ana.tarefas.App Uso: tarefas add "descrição" | list $ echo $? 1 « o código de saída do último comando $ java -cp target/classes br.com.ana.tarefas.App apagar Comando desconhecido: apagar Uso: tarefas add "descrição" | list $ echo $? 1
O echo $? é do Bash: mostra o código de saída do comando anterior. E o pão sumiu: cada java … é um programa novo, que começa com a lista vazia e a perde ao terminar. Para lembrar entre uma execução e outra, só gravando num arquivo, na próxima etapa.
Missão (etapa 2): a missão roda java Main.java apagar, e o programa termina calado, com código 0, como se tivesse dado certo. Acrescente um default que escreve Comando desconhecido: apagar em System.err e sai com System.exit(1).
public class Main {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Uso: tarefas add \"descrição\" | list");
System.exit(1);
}
switch (args[0]) {
case "add" -> System.out.println("Adicionar: " + args[1]);
case "list" -> System.out.println("Listar");
}
}
}
public class Main {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Uso: tarefas add \"descrição\" | list");
System.exit(1);
}
switch (args[0]) {
case "add" -> System.out.println("Adicionar: " + args[1]);
case "list" -> System.out.println("Listar");
default -> {
System.err.println("Comando desconhecido: " + args[0]);
System.exit(1);
}
}
}
}
Etapa 3 · Guardar em CSV
CSV (comma-separated values, "valores separados por vírgula") é o formato de planilha mais simples: uma linha por registro, campos separados por um caractere. Como em português a vírgula aparece nos textos, usamos ;. Cada tarefa vira uma linha id;feita;descrição:
1;false;Comprar pão 2;false;Estudar streams; capítulo 3
A tarefa é um record que sabe virar linha e voltar:
package br.com.ana.tarefas;
public record Tarefa(int id, String descricao, boolean feita) {
public String paraLinha() {
return id + ";" + feita + ";" + descricao;
}
public static Tarefa deLinha(String linha) {
String[] partes = linha.split(";", 3);
return new Tarefa(Integer.parseInt(partes[0]), partes[2], Boolean.parseBoolean(partes[1]));
}
@Override
public String toString() {
return (feita ? "[x] " : "[ ] ") + id + ". " + descricao;
}
}
- paraLinha()
- Junta os campos com
;. A descrição fica por último de propósito (vejasplitabaixo). - static Tarefa deLinha(String linha)
- Um método
staticque fabrica uma tarefa a partir de uma linha. É chamado pelo nome do tipo:Tarefa.deLinha(…). - linha.split(";", 3)
- Corta em no máximo 3 pedaços: id, feita e "todo o resto". Se a descrição tiver
;(comoEstudar streams; capítulo 3), ela fica inteira no terceiro pedaço. - Integer.parseInt / Boolean.parseBoolean
- Convertem o texto
"1"noint1 e"true"nobooleantrue (wrappers). - toString()
- O record já tem um
toString, mas aqui escrevemos o nosso:[ ] 1. Comprar pão, ou[x]se estiver feita.
A leitura e gravação do arquivo ficam numa classe separada, para o App não se preocupar com isso:
package br.com.ana.tarefas;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class Arquivo {
private final Path caminho;
public Arquivo(Path caminho) {
this.caminho = caminho;
}
public List<Tarefa> carregar() throws IOException {
List<Tarefa> tarefas = new ArrayList<>();
if (Files.exists(caminho)) {
for (String linha : Files.readAllLines(caminho)) {
if (!linha.isBlank()) {
tarefas.add(Tarefa.deLinha(linha));
}
}
}
return tarefas;
}
public void salvar(List<Tarefa> tarefas) throws IOException {
List<String> linhas = new ArrayList<>();
for (Tarefa t : tarefas) {
linhas.add(t.paraLinha());
}
Files.write(caminho, linhas);
}
}
Os dois métodos declaram throws IOException: em vez de tratar o erro aqui, deixam o App decidir. E o App decide deixar a exceção subir (main(…) throws IOException): se o disco falhar, o programa termina com o erro na tela, o que é razoável para uma ferramenta de terminal.
package br.com.ana.tarefas;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
public class App {
static final Path CAMINHO = Path.of(System.getProperty("user.home"), "tarefas.csv");
public static void main(String[] args) throws IOException {
if (args.length == 0) {
uso();
System.exit(1);
}
Arquivo arquivo = new Arquivo(CAMINHO);
List<Tarefa> tarefas = arquivo.carregar();
switch (args[0]) {
case "add" -> adicionar(tarefas, args, arquivo);
case "list" -> listar(tarefas);
default -> {
System.err.println("Comando desconhecido: " + args[0]);
uso();
System.exit(1);
}
}
}
static void adicionar(List<Tarefa> tarefas, String[] args, Arquivo arquivo) throws IOException {
if (args.length < 2) {
System.err.println("Uso: tarefas add \"descrição\"");
System.exit(1);
}
int id = tarefas.size() + 1;
Tarefa nova = new Tarefa(id, args[1], false);
tarefas.add(nova);
arquivo.salvar(tarefas);
System.out.println("Adicionada: " + nova);
}
static void listar(List<Tarefa> tarefas) {
for (Tarefa t : tarefas) {
System.out.println(t);
}
System.out.println(tarefas.size() + " tarefa(s).");
}
static void uso() {
System.err.println("Uso: tarefas add \"descrição\" | list");
}
}
| Trecho | O que faz |
|---|---|
System.getProperty("user.home") | A pasta pessoal de quem roda o programa (/home/ana). O arquivo fica sempre em ~/tarefas.csv, não importa de que pasta o comando foi chamado. |
Path.of(pasta, "tarefas.csv") | Junta os pedaços do caminho com a barra certa para o sistema. |
case "add" -> adicionar(tarefas, args, arquivo); | Cada subcomando vira um método. O main só carrega, escolhe e chama. |
int id = tarefas.size() + 1; | Numera as tarefas 1, 2, 3… Funciona porque não existe remoção (veja os desafios). |
$ java -cp target/classes br.com.ana.tarefas.App add "Comprar pão" Adicionada: [ ] 1. Comprar pão $ java -cp target/classes br.com.ana.tarefas.App add "Estudar streams; capítulo 3" Adicionada: [ ] 2. Estudar streams; capítulo 3 $ java -cp target/classes br.com.ana.tarefas.App list [ ] 1. Comprar pão [ ] 2. Estudar streams; capítulo 3 2 tarefa(s). « agora o pão ficou $ cat ~/tarefas.csv 1;false;Comprar pão 2;false;Estudar streams; capítulo 3 $ java -cp target/classes br.com.ana.tarefas.App add Uso: tarefas add "descrição"
Missão (etapa 3): este programa lê duas linhas de CSV, mas a segunda tarefa aparece cortada: [ ] 2. Estudar streams. Conserte o split para a descrição chegar inteira, com o ; capítulo 3.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> linhas = List.of(
"1;true;Comprar pão",
"2;false;Estudar streams; capítulo 3");
for (String linha : linhas) {
System.out.println(deLinha(linha));
}
}
static Tarefa deLinha(String linha) {
String[] partes = linha.split(";");
return new Tarefa(Integer.parseInt(partes[0]), partes[2], Boolean.parseBoolean(partes[1]));
}
}
record Tarefa(int id, String descricao, boolean feita) {
@Override
public String toString() {
return (feita ? "[x] " : "[ ] ") + id + ". " + descricao;
}
}
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> linhas = List.of(
"1;true;Comprar pão",
"2;false;Estudar streams; capítulo 3");
for (String linha : linhas) {
System.out.println(deLinha(linha));
}
}
static Tarefa deLinha(String linha) {
String[] partes = linha.split(";", 3);
return new Tarefa(Integer.parseInt(partes[0]), partes[2], Boolean.parseBoolean(partes[1]));
}
}
record Tarefa(int id, String descricao, boolean feita) {
@Override
public String toString() {
return (feita ? "[x] " : "[ ] ") + id + ". " + descricao;
}
}
Etapa 4 · Concluir e filtrar
Duas novidades: o subcomando done e os filtros do list (--pendentes e --feitas), feitos com streams. Primeiro, o record ganha um método que devolve uma cópia "concluída" (records são imutáveis, então não dá para mudar o feita da tarefa existente):
public Tarefa concluida() {
return new Tarefa(id, descricao, true);
}
O listar com filtros:
static void listar(List<Tarefa> tarefas, String[] args) {
String filtro = args.length > 1 ? args[1] : "--todas";
List<Tarefa> escolhidas = switch (filtro) {
case "--pendentes" -> tarefas.stream().filter(t -> !t.feita()).toList();
case "--feitas" -> tarefas.stream().filter(Tarefa::feita).toList();
default -> tarefas;
};
escolhidas.forEach(System.out::println);
long pendentes = tarefas.stream().filter(t -> !t.feita()).count();
System.out.println(tarefas.size() + " tarefa(s), " + pendentes + " pendente(s).");
}
| Trecho | O que faz |
|---|---|
tarefas.stream() | Abre um "fluxo" com os itens da lista, um atrás do outro, para passar por etapas. |
.filter(t -> !t.feita()) | Deixa passar só os itens em que a condição é verdadeira. t -> !t.feita() é uma lambda: "dada uma tarefa t, ela não está feita?". |
.filter(Tarefa::feita) | O mesmo, ao contrário, com uma referência de método: "chame feita() em cada tarefa". |
.toList() | Junta o que sobrou numa lista nova (que não pode ser alterada). A lista original não muda. |
.count() | Conta quantos itens passaram. Devolve long. |
escolhidas.forEach(System.out::println) | Para cada tarefa, chama System.out.println. |
E o concluir, que acha a tarefa pelo número e põe a cópia concluída no lugar com set:
static void concluir(List<Tarefa> tarefas, String[] args, Arquivo arquivo) throws IOException {
if (args.length < 2) {
System.err.println("Uso: tarefas done NÚMERO");
System.exit(1);
}
int id;
try {
id = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
System.err.println("O número da tarefa deve ser um número: " + args[1]);
System.exit(1);
return;
}
for (int i = 0; i < tarefas.size(); i++) {
if (tarefas.get(i).id() == id) {
Tarefa feita = tarefas.get(i).concluida();
tarefas.set(i, feita);
arquivo.salvar(tarefas);
System.out.println("Concluída: " + feita);
return;
}
}
System.err.println("Não existe a tarefa " + id + ".");
System.exit(1);
}
Repare no return; logo depois de System.exit(1) dentro do catch. Ele nunca roda (o exit encerra o programa antes), mas o compilador não sabe disso: sem ele, reclamaria que id pode não ter valor na linha do for (variable id might not have been initialized). E no main, o switch ganha case "done" -> concluir(tarefas, args, arquivo); e o listar passa a receber args.
$ java -cp target/classes br.com.ana.tarefas.App add "Pagar a conta de luz" Adicionada: [ ] 3. Pagar a conta de luz $ java -cp target/classes br.com.ana.tarefas.App done 1 Concluída: [x] 1. Comprar pão $ java -cp target/classes br.com.ana.tarefas.App list [x] 1. Comprar pão [ ] 2. Estudar streams; capítulo 3 [ ] 3. Pagar a conta de luz 3 tarefa(s), 2 pendente(s). $ java -cp target/classes br.com.ana.tarefas.App list --pendentes [ ] 2. Estudar streams; capítulo 3 [ ] 3. Pagar a conta de luz 3 tarefa(s), 2 pendente(s). $ java -cp target/classes br.com.ana.tarefas.App list --feitas [x] 1. Comprar pão 3 tarefa(s), 2 pendente(s). $ cat ~/tarefas.csv 1;true;Comprar pão 2;false;Estudar streams; capítulo 3 3;false;Pagar a conta de luz $ java -cp target/classes br.com.ana.tarefas.App done 9 Não existe a tarefa 9. $ java -cp target/classes br.com.ana.tarefas.App done abc O número da tarefa deve ser um número: abc
done abc sem o try/catch
Na primeira versão do concluir, a Ana escreveu só int id = Integer.parseInt(args[1]);:
$ java -cp target/classes br.com.ana.tarefas.App done abc Exception in thread "main" java.lang.NumberFormatException: For input string: "abc" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:565) at java.base/java.lang.Integer.parseInt(Integer.java:662) at br.com.ana.tarefas.App.concluir(App.java:59) at br.com.ana.tarefas.App.main(App.java:21)
"Erro de formato de número para a entrada "abc"." Para quem usa a ferramenta, um stack trace assusta e não ajuda. O try/catch troca isso por uma frase clara e o código de saída 1.
Missão (etapa 4): mostre só as tarefas pendentes, com stream().filter(…), e depois 2 pendente(s)., com count(). A tarefa feita ([x]) não pode aparecer.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Tarefa> tarefas = List.of(
new Tarefa(1, "Comprar pão", true),
new Tarefa(2, "Estudar streams", false),
new Tarefa(3, "Pagar a conta de luz", false));
// mostre só as tarefas pendentes (feita == false) usando stream().filter(...)
// e depois a linha "2 pendente(s)." usando count()
}
}
record Tarefa(int id, String descricao, boolean feita) {
@Override
public String toString() {
return (feita ? "[x] " : "[ ] ") + id + ". " + descricao;
}
}
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Tarefa> tarefas = List.of(
new Tarefa(1, "Comprar pão", true),
new Tarefa(2, "Estudar streams", false),
new Tarefa(3, "Pagar a conta de luz", false));
List<Tarefa> pendentes = tarefas.stream().filter(t -> !t.feita()).toList();
pendentes.forEach(System.out::println);
long quantas = tarefas.stream().filter(t -> !t.feita()).count();
System.out.println(quantas + " pendente(s).");
}
}
record Tarefa(int id, String descricao, boolean feita) {
@Override
public String toString() {
return (feita ? "[x] " : "[ ] ") + id + ". " + descricao;
}
}
Etapa 5 · O JAR executável
Digitar java -cp target/classes br.com.ana.tarefas.App toda vez é cansativo. Um JAR junta as classes num arquivo só, e um JAR executável já sabe qual é a classe principal. O mvn package gera um JAR, mas sem a classe principal:
$ mvn -q clean package $ ls target classes generated-sources maven-archiver maven-status tarefas-1.0.jar $ java -jar target/tarefas-1.0.jar list nenhum atributo de manifesto principal em target/tarefas-1.0.jar
"Nenhum atributo de manifesto principal": falta o Main-Class no arquivo de manifesto do JAR (a explicação completa está em Empacotar em JAR). Quem escreve o manifesto é o plugin maven-jar-plugin; basta configurá-lo no pom.xml, que fica assim:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>br.com.ana</groupId>
<artifactId>tarefas</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<finalName>tarefas</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<archive>
<manifest>
<mainClass>br.com.ana.tarefas.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
- <finalName>tarefas</finalName>
- O nome do arquivo gerado:
tarefas.jar, em vez detarefas-1.0.jar. - maven-jar-plugin 3.5.1
- O plugin que monta o JAR (o Maven já o usava; agora nós o configuramos).
- <mainClass>br.com.ana.tarefas.App</mainClass>
- Vira a linha
Main-Class: br.com.ana.tarefas.Appdo manifesto.
$ mvn clean package [INFO] Scanning for projects... [INFO] [INFO] -------------------------< br.com.ana:tarefas >------------------------- [INFO] Building tarefas 1.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] Loaded 24119 auto-discovered prefixes for remote repository central (prefixes-central.txt) [INFO] [INFO] --- clean:3.5.0:clean (default-clean) @ tarefas --- [INFO] Deleting /home/ana/java/tarefas/target [INFO] [INFO] --- resources:3.5.0:resources (default-resources) @ tarefas --- [INFO] skip non existing resourceDirectory /home/ana/java/tarefas/src/main/resources [INFO] [INFO] --- compiler:3.16.0:compile (default-compile) @ tarefas --- [INFO] Loaded 74 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt) [INFO] Recompiling the module because of changed source code. [INFO] Compiling 3 source files with javac [debug release 25] to target/classes [INFO] [INFO] --- resources:3.5.0:testResources (default-testResources) @ tarefas --- [INFO] skip non existing resourceDirectory /home/ana/java/tarefas/src/test/resources [INFO] [INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ tarefas --- [INFO] No sources to compile [INFO] [INFO] --- surefire:3.6.0:test (default-test) @ tarefas --- [INFO] No tests to run. [INFO] [INFO] --- jar:3.5.1:jar (default-jar) @ tarefas --- [INFO] Building jar: /home/ana/java/tarefas/target/tarefas.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.003 s [INFO] Finished at: 2026-10-16T19:02:43-03:00 [INFO] ------------------------------------------------------------------------ $ ls target classes generated-sources maven-archiver maven-status tarefas.jar $ java -jar target/tarefas.jar list [x] 1. Comprar pão [ ] 2. Estudar streams; capítulo 3 [ ] 3. Pagar a conta de luz 3 tarefa(s), 2 pendente(s).
O package passou por todas as fases (clean, compile, test, que aqui não tem testes, e jar) e gerou target/tarefas.jar. Esse arquivo é o programa inteiro: copie para qualquer computador com Java 25 e rode com java -jar tarefas.jar.
O último toque é o alias do Bash, que transforma o comando comprido em tarefas (a sessão completa está no começo da página). Para o alias valer sempre, coloque a linha alias tarefas='java -jar ~/java/tarefas/target/tarefas.jar' no seu ~/.bashrc.
O código completo
O pom.xml está na etapa 5 e o Arquivo.java na etapa 3. A versão final dos outros dois:
package br.com.ana.tarefas;
public record Tarefa(int id, String descricao, boolean feita) {
public Tarefa concluida() {
return new Tarefa(id, descricao, true);
}
public String paraLinha() {
return id + ";" + feita + ";" + descricao;
}
public static Tarefa deLinha(String linha) {
String[] partes = linha.split(";", 3);
return new Tarefa(Integer.parseInt(partes[0]), partes[2], Boolean.parseBoolean(partes[1]));
}
@Override
public String toString() {
return (feita ? "[x] " : "[ ] ") + id + ". " + descricao;
}
}
package br.com.ana.tarefas;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
public class App {
static final Path CAMINHO = Path.of(System.getProperty("user.home"), "tarefas.csv");
public static void main(String[] args) throws IOException {
if (args.length == 0) {
uso();
System.exit(1);
}
Arquivo arquivo = new Arquivo(CAMINHO);
List<Tarefa> tarefas = arquivo.carregar();
switch (args[0]) {
case "add" -> adicionar(tarefas, args, arquivo);
case "list" -> listar(tarefas, args);
case "done" -> concluir(tarefas, args, arquivo);
default -> {
System.err.println("Comando desconhecido: " + args[0]);
uso();
System.exit(1);
}
}
}
static void adicionar(List<Tarefa> tarefas, String[] args, Arquivo arquivo) throws IOException {
if (args.length < 2) {
System.err.println("Uso: tarefas add \"descrição\"");
System.exit(1);
}
int id = tarefas.size() + 1;
Tarefa nova = new Tarefa(id, args[1], false);
tarefas.add(nova);
arquivo.salvar(tarefas);
System.out.println("Adicionada: " + nova);
}
static void listar(List<Tarefa> tarefas, String[] args) {
String filtro = args.length > 1 ? args[1] : "--todas";
List<Tarefa> escolhidas = switch (filtro) {
case "--pendentes" -> tarefas.stream().filter(t -> !t.feita()).toList();
case "--feitas" -> tarefas.stream().filter(Tarefa::feita).toList();
default -> tarefas;
};
escolhidas.forEach(System.out::println);
long pendentes = tarefas.stream().filter(t -> !t.feita()).count();
System.out.println(tarefas.size() + " tarefa(s), " + pendentes + " pendente(s).");
}
static void concluir(List<Tarefa> tarefas, String[] args, Arquivo arquivo) throws IOException {
if (args.length < 2) {
System.err.println("Uso: tarefas done NÚMERO");
System.exit(1);
}
int id;
try {
id = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
System.err.println("O número da tarefa deve ser um número: " + args[1]);
System.exit(1);
return;
}
for (int i = 0; i < tarefas.size(); i++) {
if (tarefas.get(i).id() == id) {
Tarefa feita = tarefas.get(i).concluida();
tarefas.set(i, feita);
arquivo.salvar(tarefas);
System.out.println("Concluída: " + feita);
return;
}
}
System.err.println("Não existe a tarefa " + id + ".");
System.exit(1);
}
static void uso() {
System.err.println("Uso: tarefas add \"descrição\" | list [--pendentes|--feitas] | done NÚMERO");
}
}
Desafios extras
- Remover. Um subcomando
rm NÚMERO. Cuidado: com remoção,tarefas.size() + 1pode repetir um número. Troque portarefas.stream().mapToInt(Tarefa::id).max().orElse(0) + 1("o maior id + 1"). - Buscar.
tarefas find pãomostra as tarefas cuja descrição contém o texto (umfiltercomcontains). - Prioridade. Um campo
prioridade(um enumALTA, MEDIA, BAIXA) no record e no CSV, e olistordenado por ela comsorted(Comparator.comparing(Tarefa::prioridade)). - Testes. Acrescente o JUnit (como no projeto do banco) e teste
paraLinhaedeLinha: uma tarefa que vira linha e volta deve ser igual à original. - Arquivo antigo. O que acontece se o CSV tiver uma linha estragada? Trate como no projeto da agenda.
Missão final: a missão roda java Main.java done abc e o programa estoura com NumberFormatException. Trate com try/catch: mostre O número da tarefa deve ser um número: abc em System.err e saia com o código 1.
public class Main {
public static void main(String[] args) {
if (args.length < 2 || !args[0].equals("done")) {
System.err.println("Uso: tarefas done NÚMERO");
System.exit(1);
}
int id = Integer.parseInt(args[1]);
System.out.println("Concluir a tarefa " + id);
}
}
public class Main {
public static void main(String[] args) {
if (args.length < 2 || !args[0].equals("done")) {
System.err.println("Uso: tarefas done NÚMERO");
System.exit(1);
}
int id;
try {
id = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
System.err.println("O número da tarefa deve ser um número: " + args[1]);
System.exit(1);
return;
}
System.out.println("Concluir a tarefa " + id);
}
}
Exercícios
1. Quantos itens tem args em java -jar tarefas.jar add Comprar pão (sem aspas)?
2. Por que as mensagens de erro usam System.err?
3. O que "2;false;Ler; e anotar".split(";", 3) devolve?
4. Sem o maven-jar-plugin configurado, java -jar target/tarefas-1.0.jar diz…
Complete o filtro que deixa passar só as tarefas pendentes:
Resumo
argstraz as palavras da linha de comando; o Bash corta nos espaços e as aspas juntam. Confiraargs.lengthantes de ler.- Subcomandos:
switch (args[0]), um método por comando, erros emSystem.erreSystem.exit(1). - CSV simples:
paraLinha/deLinha, com o campo de texto livre por último esplit(";", 3). - Streams:
stream().filter(…).toList()para escolher,count()para contar. maven-jar-plugincommainClass+mvn package= JAR executável;java -jar tarefas.jar list.
Você terminou os projetos do guia. Para consultar algo rápido, use a Referência; para se testar, faça o Quiz.