BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Java 7 - Características que viabilizam o Java 8

Java 7 - Características que viabilizam o Java 8

Favoritos

É uma verdade da indústria de tecnologia que os desenvolvedores não podem ser mais felizes do que quando há cerveja ou uma oportunidade de reclamar sobre algo oferecido.

Então, mesmo após os esforços de Mark Reinhold e a equipe Java para envolver a comunidade no roadmap após a aquisição da Oracle (a decisão Plano A / Plano B), muitos desenvolvedores Java sentiram que o Java 7 "não era bem um release".

Neste artigo, tentamos refutar essa tese, explorando as funcionalidades do Java 7 que preparam o terreno para as novas funcionalidades do Java 8.

Operador Diamante

O Java é muitas vezes criticado por ser excessivamente verboso. Uma das áreas mais comuns desta queixa está na atribuição. No Java 6, somos forçados a escrever declarações de atribuição como esta:


Map<String, String> m = new HashMap<String, String>();

Esta declaração possui muita informação redundante - devemos ser capazes, de alguma forma, de fazer o compilador entender mais sobre isso sozinho, e não exigir que o programador seja tão explicito.

Na verdade, linguagens como Scala fazem uma grande quantidade de inferência de tipos a partir de expressões e, de fato, declarações de atribuição podem ser escritas tão simples quanto isso:

val m = Map("x" -> 24, "y" -> 25, "z" -> 26);

A palavra-chave val indica que a variável não pode ser reatribuída (como a palavra-chave final para variáveis Java). Nenhum tipo de informação é especificado sobre a variável, ao invés disso o compilador Scala examina o lado direito da declaração de atribuição e determina o tipo correto para a variável observando que valor está sendo atribuído.

No Java 7, foram introduzidos alguns recursos de tipo de inferência limitados, e declarações de atribuições podem agora ser escritas da seguinte forma:


Map<String, String> m = new HashMap<>();

As principais diferenças entre essa forma e a do Scala, é que no Scala os valores têm tipos explícitos e esses são os tipos de variáveis inferidos. No Java 7, o tipos das variáveis são explícitos e o tipo de informação sobre esses valores é que é inferido.

Alguns desenvolvedores se queixaram de que eles prefeririam a solução Scala, mas acaba por ser menos conveniente no contexto de uma das principais características do Java 8 - expressões lambda.

No Java 8, podemos escrever uma função que adiciona 2 a um inteiro dessa maneira:


Function<Integer, Integer> fn = x -> x + 2;

A interface Function é nova no Java 8 e está no pacote java.util.function, juntamente com formas especializadas para tipos primitivos. Entretanto, escolhemos essa sintaxe por ser muito similar ao equivalente em Scala e permitir que os desenvolvedores vejam as similaridades mais facilmente.

Ao especificar explicitamente o tipo de fn como uma Function que recebe um argumento Integer e retorna outro Integer, o compilador Java tem condições de inferir o tipo do parâmetro x como Integer. Este é o mesmo padrão que vimos na sintaxe diamante no Java 7 - especificamos os tipos de variáveis e inferimos o tipo de valores.

Vamos olhar a expressão lambda correspondente no Scala:


val fn = (x : Int) => x + 2;

Aqui temos que especificar explicitamente o tipo do parâmetro x. Como não temos o tipo preciso de fn, então não temos nada para inferir. A forma de ler no Scala não é extremamente difícil, mas a forma do Java 8 tem uma certa clareza na sintaxe que lembra a sintaxe diamante do Java 7.

Manipulador de Métodos

Os manipuladores de métodos são simultaneamente o novo recurso mais importante do Java 7 e o que é menos provável de ser usado no dia-a-dia da maioria dos desenvolvedores Java.

Um manipulador de método é uma referência tipada de um método para execução. Podem ser considerados como "ponteiros para função segura" (para desenvolvedores familiarizados com o C/C++) ou como "A base do Reflection reimaginado pelos desenvolvedores modernos Java".

Os manipuladores de métodos desempenham um papel enorme na implementação de expressões lambda. Protótipos anteriores do Java 8 tinham cada expressão lambda convertidas em uma classe interna anônima em tempo de compilação.

Vamos começar lembrando que a expressão (ao menos no Java) lambda é composta por uma assinatura de função (que na API de manipuladores de método será representado pelo objeto MethodType) e um corpo, mas não necessariamente um nome de função.

Isto sugere que poderíamos converter a expressão lambda em um método sintético que tem a assinatura correta e que contém o corpo do lambda. Por exemplo a função:


Function<Integer, Integer> fn = x -> x + 2;

é convertido pelo compilador do Java 8 no método privado com esse bytecode:


private static java.lang.Integer lambda$0(java.lang.Integer);
   descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer;
   flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
   Code:
    stack=2, locals=1, args_size=1
      0: aload_0
      1: invokevirtual #13 // Método java/lang/Integer.intValue:()I
      4: iconst_2
      5: iadd
      6: invokestatic #8 // Método java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      9: areturn

Esse método tem a assinatura e semântica correta (recebe um Integer e retorna um Integer). Para usar essa expressão lambda, precisamos de um manipulador de método referenciando-a, que será usado para construir um objeto com o tipo apropriado, como veremos na próxima funcionalidade que iremos discutir.

invokedynamic

A última funcionalidade do Java 7 que abre as portas para o Java 8 é ainda mais esotérica que os manipuladores de método. Esse é o novo bytecode invokedynamic - o primeiro bytecode a ser adicionado à plataforma desde o Java 1.0. Esta funcionalidade é quase impossível de ser usada por desenvolvedores na versão 7, porque nessa versão o javac não irá, sob nenhuma circunstância, gerar um arquivo class que contenha isso.

Em vez disso, o bytecode foi projetado ser usado por desenvolvedores de outras linguagens além do Java, tal como JRuby, que requer muito mais execução dinâmica que o Java. Para ver como o invokedynamic funciona, discutiremos como as chamadas de método Java são compiladas em bytecode.

Uma chamada padrão de método Java será convertida em um pedaço de bytecode da JVM, que é frequentemente referenciado como uma chamada local. Esta chamada é composta de um código de operação de envio (tal como invokevirtual, para chamadas de método de instância normal) e uma constante (um deslocamento para o Constant Pool da classe) que indica qual é o método a ser chamado.

Os diferentes códigos de invocação têm regras diferentes que definem como a pesquisa de método é feita, mas até Java 7 sempre eram utilizadas constantes para saber diretamente qual o método a ser chamado.

O invokedynamic é diferente. Ao invés de fornecer uma constante que indica diretamente qual método deve ser chamado, o invokedynamic fornece um mecanismo de indireção que permite o código do usuário decidir qual método chamar em tempo de execução.

Quando a primeira chamada com invokedynamic é encontrada, o alvo ainda não é conhecido. Ao invés disso, um manipulador de método (chamado método inicial) é invocado. Esse método inicial retorna um objeto CallSite, que contém outro manipulador de método, que é o alvo atual da chamada do invokedynamic.

1) A chamada do invokedynamic é encontrada no fluxo de execução (inicialmente desvinculado); 2) Chama o método inicial e retorna um objeto CallSite; 3) O objeto CallSite tem um manipulador de método (o alvo); 4) Invoca o método manipulador alvo.

O método inicial é a maneira na qual o código do usuário escolhe qual método precisa ser chamado. Para expressões lambda, a plataforma utiliza um método de inicialização fornecido pela biblioteca, chamado de lambda meta-factory.

Este tem argumentos estáticos que contém um manipulador para o método sintetizado (veja última seção) e a assinatura correta para o lambda.

O meta-factory retorna um CallSite que contém um manipulador de método e que, por sua vez, retorna uma instância do tipo correto que a expressão lambda foi convertida. Logo, uma declaração como:


Function<Integer, Integer> fn = x -> x + 2;

é convertida para uma chamada invokedynamic assim:


Code:
  stack=4, locals=2, args_size=1
     0: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
     5: astore_1

O método de inicialização do invokedynamic é o método estático LambdaMetafactory.metafactory(), que retorna um objeto CallSite ligado ao manipulador de método alvo, que retornará um objeto que implementa a interface Function.

Quando uma instrução invokedynamic termina, um objeto que implementa Function e que tenha uma expressão lambda como conteúdo do método apply() é colocado no topo da fila, e o resto do código pode seguir normalmente.

Conclusão

Obter expressões lambda na plataforma Java sempre foi e vai ser uma tarefa desafiadora, mas garantindo que o terreno adequado estava no lugar, o Java 7 facilitou consideravelmente esse esforço. O plano B não só forneceu aos desenvolvedores o lançamento antecipado do Java 7, mas também permitiu que as principais tecnologias realizassem testes em produção antes de usarem o Java 8 e especialmente expressões lambda.

Sobre o autor

Ben Evans é o CEO da jClarity, uma startup que fornece ferramentas de desempenho para auxiliar equipes de desenvolvimento e ops. Ele é um organizador no LJC (JUG Londres) e membro do comitê executivo do JCP, ajudando a definir padrões para os ecossistemas Java. Ele é um Java Champion; JavaOne Rockstar; coautor do "The Well-Grounded Java Developer" e um palestrante público na plataforma Java, desempenho, concorrência e tópicos relatados.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT