Alguns recursos de linguagem, como o novo recurso de "literais inteiros binários" em Java SE 7, funcionam bem sozinhos. Mas muitos dos principais recursos de linguagem acabam exigindo recursos adicionais para funcionar bem, ou para solucionar interações com recursos existentes. Esse é um problema em potencial, pois incluir recursos importantes de linguagem já é arriscado, e arrastar mais recursos apenas aumenta o risco.
Maior pressão por recursos de conveniência
Um vetor pelo qual um recurso de linguagem convida outro é quando um novo recurso aumenta a pressão para incluir um "recurso de conveniência" não relacionado. Considere autoboxing, incluído em Java SE 5. Java tinha tipos primitivos (por exemplo, int) e classes do wrapper "box" de objeto (por exemplo, Integer) para esses tipos desde o primeiro dia, bem como métodos para converter entre eles. O recurso de autoboxing — uma conversão implícita entre um tipo primitivo e sua classe do wrapper correspondente — poderia ter sido incluído em qualquer momento desde o começo, e houve algumas solicitações para ele antes de Java SE 5. Mas foi a inclusão de genéricos, e a consequente generalização de coleções, que finalmente gerou pressão suficiente para incluir esse recurso de conveniência. A situação na qual é preciso converter entre primitivos e seus wrappers já existia antes, mas as coleções genéricas tornaram essa situação muito mais comum, pois agora era conveniente criar coleções cujas chaves ou valores fossem primitivos encaixotados. O que antes era uma pequena inconveniência tornou-se uma inconveniência importante, e a pressão para incluir autoboxing aumentou.
Um exemplo similar é o relacionamento entre enums e importações estáticas, também incluídas em Java SE 5. Importações estáticas eram outro recurso de conveniência esperando para acontecer; ter que dizer Math.PI em vez de apenas PI sempre foi irritante. No entanto, foi a inclusão do recurso enum em Java SE 5 que gerou a maior pressão para justificar importações estáticas. Enums facilitaram a criação de constantes estruturadas nomeadas — tais como Color.RED, Color.BLUE e assim por diante — e quando se torna uma coisa mais fácil, essa coisa fica mais frequente. Embora antes apenas algumas poucas constantes estáticas definidas pelo sistema estivessem disponíveis, enums abriram as portas para as constantes estáticas criadas pelo usuário — aumentando ainda mais a irritação de precisar qualificar o nome em cada uso (em vez de dizer apenas RED ou BLUE). Por isso, embora a importação estática tenha sido um recurso independente que poderia ter sido incluído muito antes de enums serem incluídos, enums criaram pressão adicional suficiente para justificar a inclusão de importações estáticas.
Isso geralmente não é um problema, mas esses recursos de conveniência podem ter seu lado negro. Por exemplo, autoboxing reage de forma muito precária com o operador condicional ternário, e pode fazer com que NullPointerExceptions sejam lançados em código que parece não lidar com nenhuma referência do objeto.
LinQ: Seis recursos pelo preço de um
Talvez o melhor exemplo de um recurso de linguagem que convidou uma penca de amigos para se mudar junto é o recurso LinQ (Language-Integrated Query), a melhoria central incluída no .NET 3.0. LinQ permite que desenvolvedores integrem consultas de valor de objeto diretamente no código. Essas consultas podem operar não apenas em relação a bancos de dados, mas também em relação a outros provedores de dados, como documentos XML ou coleções na memória. A ideia de integrar uma linguagem de consulta em uma linguagem de programação de propósito geral parece simples, mas, quando se começa a ir mais fundo, descobrimos que muitos recursos adicionais são necessários para conseguir isso.
Este código C# ilustra uma consulta LinQ típica em uma coleção. A consulta toma uma coleção de objetos Person e seleciona e imprime os nomes e sobrenomes de pessoas com menos de 18 anos:
var results =
from p in people
where p.Age < 18
select new {p.FirstName, p.LastName};
foreach (var r in results) {
Console.WriteLine(r.FirstName + " " + r.LastName);
}
|
Muitas coisas precisam contribuir para que isso funcione. Qual é o tipo de resultado dessa consulta? Não é uma coleção de objetos Person, pois a consulta pede apenas as propriedades de nome e sobrenome. Em vez disso, é uma coleção de uma classe com propriedades apenas para o nome e sobrenome; o compilador gera essa classe com base nos campos selecionados na consulta. Para isso, .NET introduziu classes anônimas; de outra forma, o desenvolvedor precisaria criar um novo tipo de classe nomeado para o resultado de praticamente qualquer consulta.
LinQ também exige suporte para expressões lambda (encerramentos), mas isso não é óbvio pelo exemplo de consulta anterior. A estratégia de implementação do LinQ envolve reescrever a consulta em chamadas de API para um provedor. (Essa API de provedor é o motivo pelo qual consultas podem funcionar contra origens de dados diferentes.) O compilador reescreve a consulta como:
var results =
people.Where(p => p.Age < 18)
.Select(p => new {p.FirstName, p.LastName});
|
O método Where() toma um predicado que determina se o elemento dado deve ser selecionado, e produz um fluxo de elementos que passa o filtro. Em seguida, para cada elemento selecionado, o método Select() mapeia o elemento para uma nova instância de uma classe anônima que contém apenas propriedades de nome e sobrenome.
Mas ainda não acabamos. Se o provedor de dados for um banco de dados SQL, a cláusula WHERE deve ser aplicada a cada registro. Uma maneira de fazer isso seria capturar todos os registros do banco de dados para o aplicativo e, em seguida, testar a propriedade Age de cada um. Mas isso deve ser muito ineficiente — seria melhor que a cláusula WHERE fosse avaliada mais próxima aos dados. O mais racional a se fazer é enviar a cláusula WHERE para o banco de dados, mas isso significa que precisaríamos traduzir o predicado p.Age < 18 para SQL e enviar para o banco de dados.
A solução LinQ para esse problema são árvores de expressão, um mecanismo semelhante a reflexo pelo qual é possível refletir não apenas os membros de uma classe, mas também o código de um método. Isso permite que o fornecedor LinQ do SQL analise o encerramento passado para o método Where() e o traduza para SQL.
Traduzir consultas para chamadas para uma API também exige a inclusão de métodos de extensão. O método Where() está sendo chamado em um objeto da coleção, mas ele não é membro da estrutura de coleção. É um método estático definido pelo subsistema LinQ e injetado em IEnumerable (o equivalente em .NET ao Iterable de Java). Do contrário, não seria possível expressar consultas LinQ em relação a coleções tão facilmente.
Por fim, precisamos de variáveis de tipo implícito, para que possamos designar uma consulta a uma variável sem precisar declarar seu tipo explicitamente. Como o resultado da consulta é um IEnumerable de algum tipo anônimo, o compilador conhece o tipo do resultado, mas esse tipo não pode ser denotado em C#. (Por outro lado, para algumas consultas, o tipo de resultado pode ser denotado, mas inconvenientemente detalhado para escrever.) É por isso que C# permite a declaração de variáveis com var, o que permite que o compilador descubra o tipo em vez de exigir que o usuário o diga — não porque deseja incentivar o programador a ser preguiçoso, mas porque às vezes não há maneira de escrever o tipo.
Esse foi um longo caminho! O que começou como um objetivo aparentemente simples — integrar consultas em uma linguagem de propósito geral — acabou exigindo classes anônimas, variáveis de tipo implícito, encerramentos, métodos de extensão e um mecanismo de reflexo para expressões. Cada um desses recursos era essencial para um ou mais dos objetivos principais do LinQ.
Um usuário pode concluir que esse é um bom negócio: seis recursos pelo preço de um. Mas a inclusão de novos recursos em uma linguagem existente sempre tem um custo. Quando se inclui um recurso de linguagem B para apoiar o recurso A, não há nenhuma exigência de que B seja usado apenas com A. B pode não ser tão desejável por si só, ou pode interagir de forma ruim com outros recursos da linguagem. Havia claramente um objetivo específico para incluir A, tal como tornar a linguagem mais segura ou mais expressiva. Mas para avaliar o sucesso, é preciso avaliar o objetivo em relação não a A em si, mas à nova linguagem que surgiu como resultado — incluindo todos os amigos que A convidou. Se não era essa a linguagem desejada desde o começo, pode ser preciso repensar o recurso inicial.
A melhoria central de linguagem para Java SE 8 é expressão lambda ou encerramento. Mas, assim como LinQ em .NET, expressões lambda precisam arrastar alguns recursos adicionais junto — incluindo conversão SAM, inferência de tipo aprimorada, referências de método e métodos de extensão — para dar aos usuários todos os benefícios.
Como expressões lambda — expressões que representam funções — são um novo tipo de valor em Java, precisamos de uma maneira de escrever seu tipo. Propostas iniciais para expressões lambda em Java pediram a inclusão de tipos de função no sistema de tipos, tais como "função de int para int." Os tipos de função são, de fato, a maneira natural de representar o tipo de expressão lambda, mas infelizmente elas não interagem bem com um "recurso" existente da linguagem: encerramento. Como a maneira natural de representar um tipo de função no bytecode subjacente seria usando genéricos, tipos primitivos em assinaturas de função seriam encaixotados, e não seria possível sobrecarregar diversos métodos que tomassem tipos de função, mesmo que seus argumentos fossem completamente diferentes. Tipos de função podem ser a maneira natural de expressar o tipo de uma lambda, mas tipos de função encerrados não são.
Por isso, em vez de tipos de função, expressões lambda em Java SE 8 trarão um outro amigo, conversão SAM. Tipos SAM (Single Abstract Method) são a maneira como nós representamos funções na linguagem Java desde sempre — interfaces com um método, como Runnable, Comparator ou ActionListener. Se desenvolvermos APIs com tipos SAM (muitas das quais já existem em bibliotecas), o compilador pode converter entre uma expressão lambda (que é como um literal de função) e um tipo SAM cujo argumento, tipos, tipo de retorno e tipos de exceção sejam compatíveis com a expressão lambda. Por exemplo, o código a seguir declara Comparator<String> que compara cadeias de caractere em relação ao tamanho e usa uma expressão lambda para definir o Comparator:.
Comparator<String> c
= (String a, String b) -> a.length() — b.length();
|
Como a expressão lambda tem os tipos certos de argumento e retorno, o compilador verifica se ela pode ser convertida em Comparator<String> e gera o código apropriado para isso. Isso é chamado de conversão SAM.
O motivo principal para ter expressões lambda é ter uma maneira de expressar código como dados, de modo que literais de código possam ser passados para bibliotecas que os chamarão no momento conveniente. Outra motivação é reduzir o detalhamento de classes internas, que são atualmente a maneira mais próxima de obter esse efeito. Quando começamos a eliminar construções sintáticas redundantes, geralmente queremos continuar fazendo isso. Por isso as expressões lambda trazem outro amigo — inferência de tipo expandido por meio de tipo de destino. Como a expressão lambda anterior está sendo designada a um Comparator<String>, a declaração explícita de que a e b são do tipo String é redundante — o compilador geralmente pode descobrir isso. Ao usar o tipo do contexto de designação para inferir os tipos de a e b, é possível reduzir esse exemplo para:
Comparator<String> c
= (a, b) -> a.length() — b.length();
|
Se tivéssemos uma coleção de objetos Person e quiséssemos classificar essa lista pelo sobrenome associado a Person, escreveríamos isso hoje como:
Collections.sort(people, new Comparator<Person>() {
Public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
|
Usar uma expressão lambda pode tornar isso mais compacto:
Collections.sort(people, (a, b) ->
a.getLastName().compareTo(b.getLastName());
|
Essa é uma grande etapa para reduzir o detalhamento, mas ainda não é mais abstrato — ainda obriga o usuário a calcular a função de comparação imperativamente. Com algumas pequenas mudanças nas bibliotecas, podemos usar melhor as expressões lambda para separar os aspectos centrais da classificação — seleção de uma chave de classificação. Já que String é Comparable, o método de classificação deve saber como fazer a comparação após a chave de classificação ser extraída:
Collections.sortBy(people, p -> p.getLastName()); |
Isso está definitivamente ficando melhor — o código está começando a se assemelhar à declaração de problema "classificar pessoas por sobrenome". Mas, à medida que retiramos os elementos padronizados, percebemos que o idioma usado para extrair a chave de classificação — aqui, o sobrenome — é em si um pouco confusa. A expressão lambda acima faz nada mais que tomar seus argumentos (neste caso, nenhum) e passá-los para um método existente, getLastName(), tratando o primeiro argumento como o objeto no qual chamar o método. Embora nesse caso não pareça tão ruim — pois não há argumentos extras que precisariam de nomes (e esses nomes seriam repetidos duas vezes) — seria bem melhor simplesmente nomear o método diretamente. Um recurso relacionado, referências de método, permite fazer isso — referir-se a um método por nome e tratá-lo como um datum de valor de função, assim como uma expressão lambda:
Collections.sortBy(people, Person::getLastName); |
Por fim, agora que o texto padronizado foi retirado, está ainda mais óbvio que antes que o método sortBy() não deveria ser um método estático em uma classe do utilitário, mas sim um método de instância na coleção. Mas uma das propriedades infelizes das interfaces é que, após serem especificadas, não é possível incluir novos métodos sem quebrar implementações existentes. O recurso final introduzido com expressões lambda são os métodos de extensão virtual, que permitem incluir novos métodos em interfaces de forma compatível, fornecendo uma implementação padrão (substituível) junto com a declaração de método. Isso permite incluir métodos adequados para lambda (e potencialmente adequados para paralelo) como forEach() a List. Ao incluir um método de extensão para List em sortBy(), nosso exemplo fica assim:
people.sortBy(Person::getLastName); |
Estranhamente, nossa versão final nem sequer usa lambdas! Mas incorpora o objetivo central da inclusão das expressões lambda na linguagem — a capacidade de capturar partes de uma computação de modo que possam ser movimentadas como dados, o que permite funcionalidade de biblioteca com parametrização mais ampla, tais como classificação. Nesse exemplo em particular, uma referência de método é uma expressão mais clara do que queremos que uma expressão lambda, mas a ideia é a mesma.
Seria perfeitamente possível incluir expressões lambda em Java sem conversão SAM, inferência de tipo, referências de método e métodos de extensão. No entanto, é provável que a ausência desses recursos se tornasse motivo de reclamação — talvez sem que nós percebêssemos a origem exata da reclamação.
O motivo para não incluir tipos de função em Java pode ser caracterizado como não querer — ou talvez não poder ter — um recurso de linguagem adicional pegando carona. Embora tipos de função fossem a maneira natural de expressar o tipo de uma expressão lambda, e reduzissem a necessidade para muitos tipos nominais, como Predicate e Mapper, a interação com encerramento é ruim demais. A resposta óbvia é livrar-se do recurso com interação ruim convidando outro amigo — reificação. Há vantagens e desvantagens para incluir genéricos reificados na linguagem Java, mas a realidade é que não era prático incluir algo tão grande e amplo (que afeta a linguagem, compilador e bibliotecas) como reificação ao mesmo tempo em que as expressões lambda eram incluídas. Assim, não podendo sustentar seu amigo, fomos obrigados a dizer adeus — ao menos por ora — aos tipos de função também.
A maioria dos grandes recursos de linguagem não é independente — eles quase sempre precisam convidar seus amigos para obter o benefício completo para o qual foram projetados. Ao considerar a inclusão de um recurso que tem amigos, precisamos pensar cuidadosamente se realmente queremos convidar esses amigos para vir junto — pois seremos obrigados a ficar com eles. Se não pudermos morar com os amigos do recurso, provavelmente devemos concluir que não queremos o recurso também.
Aprender
-
Estado de Lambda: Leia uma estrutura de tópicos do conjunto de recursos para Expressões Lambda em Java SE 8.
-
Entrevista com Trent Gray-Donald, chefe técnico de Java SE 7 da IBM: Descubra como IBM SDK Java Technology Edition Version 7 é diferente dos releases anteriores, incluindo novos recursos de linguagem.
-
Página inicial do projeto Coin: A função do Projeto Coin é determinar quais pequenas mudanças de linguagem devem ser incluídas no JDK 7. Os recursos de Coin foram normatizados pelo JCP como parte do JSR 334.
-
LINQ: Para mais informações sobre LINQ, consulte o artigo Language Integrated Query na Wikipédia e, no Centro de Desenvolvedor .NET Developer Center, a página LINQ.
-
Java Concurrency in Practice (Brian Goetz, Addison-Wesley, 2006): esse livro é um trabalho excelente sobre a programação simultânea em Java.
-
Artigos e tutoriais de Brian Goetz: Confira os demais trabalhos de Brian no developerWorks.
-
Navegue pela livraria de tecnologia para ver livros sobre este e outros tópicos técnicos.
-
Zona tecnologia Java do developerWorks: Encontre centenas de artigos sobre quase todos os aspectos da programação Java.
Obter produtos e tecnologias
-
IBM SDK Java Technology Edition Version 7: IBM SDK for Java V7 está disponível para AIX e Linux.
Discutir
- Participe da comunidade do developerWorks.

Brian Goetz é arquiteto de linguagem Java na Oracle e autor veterano do developerWorks. Os textos de Brian incluem a série de colunas Java theory and practice publicadas aqui de 2002 a 2008, e o trabalho definitivo sobre concorrência em Java, Java Concurrency in Practice (Addison-Wesley, 2006).