Translate

quinta-feira, 27 de julho de 2023

Lambda Expressions no Java: O Manual Definitivo para um Programador COBOL Padawan Entender o Operador -> sem Medo (e Descobrir que Ele Não é Magia)

 

Bellacosa Mainframe e a lambda expressions no Java

☕ Um Café no Bellacosa Mainframe

Lambda Expressions no Java: O Manual Definitivo para um Programador COBOL Padawan Entender o Operador -> sem Medo (e Descobrir que Ele Não é Magia)

"Todo programador COBOL já passou por isso: você abre um código Java moderno e encontra algo como (x) -> x * 2. Sua primeira reação é pensar: 'Quem apagou metade do programa?' A boa notícia é que ninguém apagou nada. Você está olhando para uma Lambda Expression."


Introdução

Se você trabalha há anos no universo IBM Z, provavelmente sua realidade é composta por tecnologias como:

  • COBOL

  • JCL

  • CICS

  • IMS

  • Db2

  • MQ

  • VSAM

  • z/OS

  • REXX

  • PL/I

Neste mundo, tudo costuma ser muito explícito.

Você declara.

Você identifica.

Você define.

Você documenta.

Você implementa.

Já no mundo Java moderno acontece exatamente o contrário.

Quanto menos código existir, melhor.

É aí que surgem as Lambda Expressions, introduzidas oficialmente no Java 8 (2014).

Para muitos profissionais Mainframe, elas parecem um idioma alienígena.

Mas a verdade é outra.

Você já conhece o conceito.

Só não conhece a sintaxe.


Antes de tudo…

Existe Arrow Function em Java?

Tecnicamente...

Não.

O nome Arrow Function pertence ao JavaScript.

No Java o nome correto é:

Lambda Expression

O símbolo é exatamente o mesmo conceito visual:

->

Por isso muita gente chama informalmente de Arrow Function.


Por que a IBM adicionou Lambdas ao Java?

A resposta é simples:

Performance + produtividade.

Na década de 2000, Java era conhecido por ser extremamente verboso.

Para executar uma simples ação era comum escrever dezenas de linhas.

Exemplo antigo:

Collections.sort(lista, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

Mais de dez linhas.

Hoje:

lista.sort((a,b) -> a.compareTo(b));

Uma linha.

Muito mais legível.


Mas o que é uma Lambda?

A definição acadêmica diz:

Uma função anônima.

Mas para um COBOL Padawan eu prefiro outra definição.

Pense nela como um PARÁGRAFO temporário.

No COBOL fazemos isso:

PERFORM CALCULAR-TOTAL

E em algum lugar existe:

CALCULAR-TOTAL.

    COMPUTE WS-TOTAL =
        WS-VALOR1 +
        WS-VALOR2.

EXIT.

No Java moderno, às vezes não vale a pena criar um método inteiro.

Você simplesmente entrega a lógica diretamente.

Algo parecido com:

(a,b) -> a+b

É como dizer:

"Aqui está o código. Execute quando precisar."


O significado do operador ->

Observe:

(a,b) -> a+b

Lendo em português:

Receba A

Receba B

Execute

Retorne A+B

Visualmente:

ENTRADA
   │
   ▼
(a,b)
   │
   ▼
   ->
   │
   ▼
a+b
   │
   ▼
SAÍDA

O operador -> separa:

lado esquerdo

parâmetros

lado direito

implementação


Comparando com COBOL

COBOL:

ADD A TO B GIVING C

Java:

(a,b) -> a+b

A intenção é praticamente igual.

A diferença é que no Java essa operação pode ser passada como parâmetro.


Uma mudança de paradigma

Durante décadas escrevemos programas assim:

Programa

↓

Métodos

↓

Chamadas

Com Lambdas passamos a escrever:

Programa

↓

Comportamentos

↓

Funções

↓

Funções recebendo funções

Isso é programação funcional.


Um exemplo simples

Sem Lambda:

Runnable tarefa = new Runnable() {

    @Override
    public void run() {

        System.out.println("Executando");

    }

};

Com Lambda:

Runnable tarefa =
    () -> System.out.println("Executando");

Mesmo comportamento.

Muito menos código.


O que mudou?

Antes era necessário criar uma classe anônima.

Agora basta informar:

  • parâmetros

  • comportamento


Quando usar ()

Sem parâmetros:

() -> ...

Um parâmetro:

x -> ...

Dois parâmetros:

(a,b) -> ...

Três parâmetros:

(a,b,c) -> ...

Quando usar chaves?

Uma linha:

x -> x*2

Mais de uma:

x -> {

    System.out.println(x);

    return x*2;

}

O retorno implícito

Observe:

x -> x+1

Não existe:

return

O Java entende automaticamente.

Já quando usamos bloco:

x -> {

    return x+1;

}

O return passa a ser obrigatório.


Interfaces Funcionais

Aqui existe uma diferença enorme para JavaScript.

No Java você não cria uma Lambda do nada.

Ela precisa implementar uma interface.

Exemplo:

@FunctionalInterface

interface Soma {

    int executar(int a,int b);

}

Uso:

Soma s =
    (a,b) -> a+b;

Pronto.

Sua Lambda implementou automaticamente a interface.


O segredo está na @FunctionalInterface

Ela permite apenas:

um único método abstrato.

Isso faz o compilador saber exatamente qual método será implementado.


Onde as Lambdas aparecem no Java moderno?

Praticamente em toda parte.

Principalmente em:

  • Streams

  • Comparator

  • Runnable

  • CompletableFuture

  • ExecutorService

  • Optional

  • Collections

  • APIs REST

  • Spring Boot

  • Jakarta EE

  • IBM Semeru Runtime

  • Open Liberty

Ou seja...

Se você pretende trabalhar com Java no IBM Z...

Vai encontrá-las diariamente.


Streams

Imagine uma lista.

List<Integer> numeros =
List.of(1,2,3,4,5);

Filtrar pares:

numeros.stream()

.filter(n -> n%2==0)

.forEach(System.out::println);

A Lambda diz apenas:

Receba um número

↓

Se for par

↓

Retorne verdadeiro

Comparando com COBOL

No COBOL seria algo como:

PERFORM VARYING IDX
FROM 1 BY 1

UNTIL IDX > MAX

    IF NUMERO(IDX)
       MOD 2 = ZERO

       DISPLAY NUMERO(IDX)

    END-IF

END-PERFORM

No Java:

.filter(n->n%2==0)

O comportamento é exatamente o mesmo.


Comparator

Ordenar nomes.

Antes:

mais de quinze linhas.

Hoje:

lista.sort(

(a,b)->a.compareTo(b)

);

Method Reference

Existe algo ainda menor.

Em vez de:

x -> System.out.println(x)

Podemos escrever:

System.out::println

Isso se chama:

Method Reference.

É praticamente uma Lambda reduzida.


Predicate

Representa perguntas.

Predicate<Integer> par =

n -> n%2==0;

É equivalente a:

É par?

↓

Sim ou Não

Function

Transforma algo.

Entrada:

Nome

Saída:

Maiúsculo
nome -> nome.toUpperCase()

Consumer

Recebe.

Consome.

Não devolve nada.

x -> System.out.println(x)

Supplier

Não recebe nada.

Mas produz.

() -> UUID.randomUUID()

Curiosidade

O nome Lambda vem da matemática.

Mais especificamente do Lambda Calculus, criado por Alonzo Church, em 1936.

Muito antes da invenção do COBOL.

Muito antes do Java.

Muito antes do C.


Alan Turing conhecia Lambdas?

Sim.

Church e Turing trabalharam praticamente na mesma época.

Os dois provaram matematicamente que era possível criar computação universal.


Então Java virou linguagem funcional?

Não.

Java continua sendo:

  • Orientado a Objetos

Mas ganhou recursos funcionais.


O que acontece na memória?

Muita gente imagina que uma Lambda cria um objeto enorme.

Na verdade não.

O compilador gera bytecode especial usando:

invokedynamic

Introduzido na JVM 7.

Depois entra em ação:

LambdaMetafactory

Ela cria dinamicamente uma implementação da interface funcional.

Ou seja...

Não existe necessariamente uma classe física como antigamente.


O papel do invokedynamic

Antes do Java 8:

Classe anônima

↓

Compilador

↓

Novo .class

Depois:

Lambda

↓

invokedynamic

↓

JVM

↓

Objeto criado dinamicamente

Muito mais eficiente.


Performance

Na maioria dos casos:

Lambda é mais rápida que classe anônima.

Por quê?

Porque:

  • menos bytecode

  • menos classes

  • melhor otimização JIT

  • HotSpot entende melhor o fluxo

No IBM Semeru Runtime isso também acontece.


Existe custo?

Sim.

Toda abstração possui custo.

Mas normalmente ele é insignificante.


Captura de variáveis

Exemplo:

int valor = 10;

Function<Integer,Integer> soma =

n -> n + valor;

Funciona.

Agora:

valor++;

Erro.

Por quê?

Porque a variável precisa ser:

effectively final.


Por que isso existe?

Para evitar problemas de concorrência.

Imagine:

100 Threads.

Todas alterando a mesma variável.

Seria um desastre.


O coletor de lixo

As Lambdas normalmente vivem pouco.

São excelentes candidatas para o Young Generation da JVM.

Resultado:

coleta extremamente rápida.


Java no IBM Z

Se você usa:

  • IBM Semeru Runtime

  • OpenJ9

  • Liberty

  • Spring Boot

  • z/OS Connect

Você está usando Lambdas o tempo inteiro.

Mesmo sem perceber.


Limitações

Nem tudo são flores.

Algumas limitações:

  • não possuem estado próprio

  • não substituem classes

  • não substituem objetos complexos

  • podem reduzir legibilidade quando abusadas

  • debugging pode ficar mais difícil

  • stack traces às vezes ficam estranhos


Pontos fortes

✔ Menos código

✔ Mais legibilidade

✔ Melhor integração com Streams

✔ Melhor paralelismo

✔ Melhor manutenção

✔ Melhor otimização da JVM

✔ Código moderno


Pontos fracos

✘ Curva de aprendizado

✘ Sintaxe inicialmente estranha

✘ Pode esconder lógica demais

✘ Debug mais complexo

✘ Não substitui arquitetura


Dicas para quem vem do COBOL

Não tente decorar a sintaxe.

Entenda a ideia.


Sempre pergunte:

"Esse trecho está apenas descrevendo um comportamento?"

Se sim...

Provavelmente cabe uma Lambda.


Streams + Lambdas são inseparáveis.

Aprenda ambos juntos.


Evite Lambdas gigantes.

Se passou de cinco ou seis linhas...

Crie um método.


Prefira Method Reference

Quando possível.

Em vez de:

x -> System.out.println(x)

Faça:

System.out::println

Easter Eggs

1. Nem toda Lambda gera um novo objeto.

A JVM pode reutilizar instâncias.


2. Uma Lambda pode não existir como classe.

Ao contrário das antigas Anonymous Classes.


3. O compilador gera métodos sintéticos.

Eles aparecem como:

lambda$0

lambda$1

lambda$2

Dentro do bytecode.


4. javap revela a mágica

Compile:

javac Exemplo.java

Depois:

javap -c -v Exemplo.class

Você verá instruções como:

  • invokedynamic

  • BootstrapMethods

  • referências ao LambdaMetafactory

É uma ótima forma de entender o que a JVM realmente executa.


5. Lambdas e paralelismo

Elas combinam naturalmente com:

parallelStream()

Mas atenção: paralelo não significa automaticamente mais rápido. Em cargas pequenas, o custo de dividir o trabalho pode superar o ganho.


Fazendo a ponte para o mundo Mainframe

Se você é um Programador COBOL Padawan, pense assim:

  • Um PERFORM executa um bloco identificado pelo nome.

  • Uma Lambda entrega esse bloco diretamente para quem vai executá-lo.

  • Um IF do COBOL pode virar um Predicate.

  • Uma rotina de transformação de registros pode virar uma Function.

  • Um DISPLAY pode ser representado por um Consumer.

O paradigma muda, mas o raciocínio continua familiar: dados entram, uma lógica é aplicada e um resultado é produzido.


Conclusão

As Lambda Expressions não vieram para substituir a Programação Orientada a Objetos, muito menos os princípios sólidos que sustentam aplicações corporativas. Elas surgiram para reduzir código repetitivo, aumentar a expressividade e permitir que o desenvolvedor descreva comportamentos da mesma forma que descreve dados.

Para quem vem do universo IBM Z, elas representam mais uma evolução do que uma revolução. Você já domina lógica estruturada, modularização, processamento em lote, concorrência, desempenho e confiabilidade. O desafio agora é aprender uma nova forma de expressar essas mesmas ideias.

No fim das contas, a Lambda é apenas uma ferramenta. Ela não torna um sistema melhor por si só. Assim como um bom PERFORM, ela precisa ser usada no lugar certo, com clareza e equilíbrio.

E talvez essa seja a maior lição para todo COBOL Padawan que começa a explorar Java no Mainframe: a tecnologia muda, a boa engenharia de software permanece.


Sem comentários:

Enviar um comentário