Essenciais do java.lang: Comparable, AutoCloseable, StackWalker, ProcessBuilder
Objective
java.lang reúne um punhado de contratos pequenos e de propósito único que não
pertencem a coleções, strings ou números, mas que sustentam silenciosamente o
código do dia a dia: Comparable dá a um tipo sua única ordenação natural,
AutoCloseable é o contrato que torna o try-with-resources possível,
StackWalker é a forma moderna, baseada em stream, de inspecionar a call
stack atual, e ProcessBuilder é como um programa Java lança e controla
outro processo do sistema operacional. Eles não compartilham uma relação de
herança — cada um está aqui porque é uma ferramenta pequena que aparece
constantemente, não porque formam uma única API.
Use Cases
- Dar a uma classe uma ordem de classificação padrão que
Collections.sort(),TreeSeteTreeMapusam automaticamente quando nenhum comparador é fornecido. - Escrever um wrapper de recurso personalizado (um handle nativo, uma conexão
pooled) que precisa se encaixar no try-with-resources em vez de um
finally { close(); }manual. - Construir uma utilidade de logging, debugging ou framework que precisa saber qual classe chamou um método, sem pagar o preço de um stack trace completo antecipadamente.
- Lançar uma ferramenta externa (um compilador, um script de shell, outra JVM) de dentro de um programa Java, alimentando-a com entrada, capturando sua saída e reagindo quando ela termina.
Deep Dive
Comparable e o contrato do compareTo
Comparable<T> declara exatamente um método:
javapublic interface Comparable<T> {
int compareTo(T other);
}compareTo() retorna um número negativo se o objeto que chama é "menor que"
other, zero se são iguais, e um número positivo se é "maior que" — a
magnitude exata não importa, só o sinal. Essa é a ordenação natural do tipo:
Byte, Character, Double, Integer, Long, String e Enum implementam
ela, e é o que TreeSet, TreeMap e Collections.sort() usam por padrão
quando nenhum Comparator é dado.
javaclass Version implements Comparable<Version> {
final int major;
Version(int major) { this.major = major; }
@Override
public int compareTo(Version other) {
return Integer.compare(this.major, other.major);
}
}Comparable fica embutido no tipo — uma classe ganha exatamente uma ordenação
desse jeito. Comparator<T> vive fora do tipo e pode definir qualquer número
de ordenações para a mesma classe sem tocá-la; veja
Comparators e Algoritmos de Coleção
para Comparator.comparing(), thenComparing() e os algoritmos de
Collections construídos sobre os dois.
AutoCloseable vs. Closeable
AutoCloseable é o que faz o try-with-resources funcionar. Ele declara um
método:
javapublic interface AutoCloseable {
void close() throws Exception;
}Qualquer objeto cuja classe implementa AutoCloseable pode aparecer em um
statement try-with-resources; close() é chamado automaticamente quando o
bloco termina, com sucesso ou falha:
javatry (var resource = acquireResource()) {
resource.use();
} // close() called here, no matter how the block exitsjava.io.Closeable estende AutoCloseable e o restringe de duas formas: seu
close() declara apenas throws IOException em vez do amplo Exception, e é
documentado como idempotente — chamá-lo mais de uma vez não pode ter efeito
adicional. AutoCloseable.close() não carrega essa garantia; o Javadoc diz
explicitamente que chamadas repetidas "podem ter algum efeito colateral
visível", embora ainda incentive os implementadores a torná-lo idempotente de
qualquer forma.
StackWalker: inspecionando a call stack
Adicionado no Java 9, StackWalker substituiu o padrão antigo de chamar
Thread.currentThread().getStackTrace() (ou new Throwable().getStackTrace())
para inspecionar a call stack, e as checagens caller-sensitive baseadas em
SecurityManager que costumavam proteger esse tipo de introspecção. Em vez de
materializar avidamente um array com todo frame, StackWalker transmite
frames de forma preguiçosa, deixando o chamador parar cedo sem pagar pelo
resto da stack:
javaStackWalker walker = StackWalker.getInstance();
List<StackWalker.StackFrame> topThree =
walker.walk(frames -> frames.limit(3).toList());walk() recebe uma Function<Stream<StackFrame>, T> — o stream só é válido
durante aquela chamada e fecha quando walk() retorna. Cada StackFrame
expõe getClassName() e getMethodName() por padrão; frames de reflection e
internos da VM ficam ocultos a menos que Option.SHOW_REFLECT_FRAMES ou
Option.SHOW_HIDDEN_FRAMES sejam solicitados no momento da construção:
javaStackWalker deepWalker = StackWalker.getInstance(
Set.of(StackWalker.Option.RETAIN_CLASS_REFERENCE));
Class<?> caller = deepWalker.getCallerClass();getCallerClass() é uma conveniência para a pergunta comum "quem me chamou",
mas precisa que Option.RETAIN_CLASS_REFERENCE seja fornecida de antemão —
solicitá-la a partir de um getInstance() simples lança
UnsupportedOperationException em vez de retroativamente adicionar a opção.
ProcessBuilder: lançando e controlando processos externos
ProcessBuilder configura e inicia outro processo do sistema operacional:
javaProcessBuilder pb = new ProcessBuilder("grep", "-r", "TODO", ".");
pb.directory(new File("/projects/app"));
pb.redirectErrorStream(true); // merge stderr into stdout
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(new File("grep.log")));
Process process = pb.start();command() e directory() leem/mudam o programa e os argumentos e o
diretório de trabalho antes de start(); environment() retorna um
Map<String, String> mutável, inicializado a partir do ambiente do processo
atual, que só afeta o filho sendo iniciado. redirectInput()/
redirectOutput()/redirectError() (mais o atalho inheritIO(), que conecta
os três ao próprio console do pai) substituem o antigo padrão de drenar
manualmente Process.getInputStream()/getErrorStream() em threads
separadas.
Uma vez iniciado, o Process retornado expõe pid() para o id nativo do
processo e onExit() para um CompletableFuture<Process> que completa quando
o filho termina — permitindo que um programa reaja sem bloquear em
waitFor():
javaprocess.onExit()
.thenApply(p -> p.exitValue() == 0)
.thenAccept(success -> System.out.println("Clean exit: " + success));isAlive(), destroy() (solicita término gracioso — depende da implementação
se consegue, não o quão forçado é) e destroyForcibly() (término forçado)
completam o controle de ciclo de vida; desde o Java 9, toHandle() converte
um Process em um ProcessHandle para informações (como
Info.totalCpuDuration()) além do que o próprio Process expõe.
Trade-offs
- Um tipo ganha exatamente uma ordenação natural — uma segunda ordenação,
situacional, precisa de um
Comparator, não de uma flag enfiada dentro docompareTo()(veja o conceito de Comparators para encadear e construir comparadores sem tocar na classe). return a - bdentro decompareTo()é um bug esperando para acontecer, não um atalho — a subtração pode estourar silenciosamente e inverter o sinal para valores extremos.javapublic int compareTo(Bucket other) { return this.hash - other.hash; // wraps for extreme int values } // fix: return Integer.compare(this.hash, other.hash);AutoCloseable.close()lança o amploException;Closeable.close()sóIOException— código escrito genericamente contraAutoCloseableprecisa capturar ou declararException, perdendo o tipo mais estreito que chamadores específicos de I/O obtêm deCloseable.AutoCloseable.close()não é obrigado a ser idempotente, ao contrário deCloseable.close()— chamá-lo uma segunda vez ainda pode ter um efeito colateral visível a menos que o implementador se proteja especificamente contra isso; o try-with-resources só chamaclose()uma vez por recurso, mas código de limpeza manual que chamaclose()de mais de um lugar não pode assumir que a segunda chamada é um no-op.getCallerClass()precisa deOption.RETAIN_CLASS_REFERENCEfornecida no momento dogetInstance()— não pode ser solicitada depois do fato.javaStackWalker w = StackWalker.getInstance(); w.getCallerClass(); // UnsupportedOperationExceptionProcessBuilder.environment()só edita o ambiente do filho, não o da JVM atual — mutar o map retornado não tem efeito emSystem.getenv()no programa em execução, só em processos iniciados a partir daqueleProcessBuilderdepois.- Um subprocesso não lido pode causar deadlock no pai — se o filho
escreve saída suficiente para encher o buffer do pipe do SO e nada drena
getInputStream()/getErrorStream(), o filho bloqueia na escrita e o pai bloqueia emwaitFor(), com nenhum dos dois lados progredindo;redirectErrorStream(true),inheritIO(), ou consumir os streams em uma thread separada evita isso.