Com a versão do JDK 8 prevista para 2013, a Oracle deu uma ideia do que será incluído. Simon Ritter, palestrante do QCon de Londres no começo deste ano, apresentou as novas características que serão parte do JDK 8, que incluem modularidade (projeto Jigsaw, adiado para o JDK 9), convergência da JRockit com o Hotspot, anotações em tipos e projeto Lambda.
Da perspectiva da linguagem, possivelmente a mudança mais importante é introduzida pelo Projeto Lambda, que inclui o suporte para expressões lambda, extensões virtuais de métodos e um suporte melhor para as plataformas multinúcleo na forma de coleções em paralelo.
A maioria dessas características já está disponível em muitas outras linguagens que executam sob a JVM, incluindo o Scala. Aliás, muitos comportamentos usados no Java 8 são surpreendentemente similares aos usados no Scala. Como consequência, brincar com o Scala é uma boa maneira para conhecer como será programar com o Java 8.
Neste artigo serão abordadas as novas funcionalidades do Java 8, usando a sintaxe proposta do Java e do Scala. Tal como, as expressões lambda, funções de ordem superior, coleções em paralelo e extensões virtuais de métodos, também conhecida como traits. Além disso, serão apresentados os novos paradigmas integrados no Java 8, como a programação funcional.
O leitor experimentará como os novos conceitos incorporados no Java 8 - que já estão disponíveis no Scala - não são meras melhorias vistosas, podem introduzir uma verdadeira mudança de paradigma, que oferecerá excelentes possibilidades e pode mudar profundamente a maneira que o software é escrito.
Funções e expressões Lambda
O Java 8 finalmente incluirá as expressões lambda. As expressões lambda foram disponibilizadas na forma de Projeto Lambda desde 2009. Naquela ocasião, as expressões lambda ainda eram referenciadas como Closures do Java. Antes de apresentar alguns exemplos de códigos, será explicado porque as expressões lambda serão uma ferramenta muito bem-vinda no cinto de utilitários dos programadores Java.
Motivação para as expressões lambda
Um uso comum das expressões lambda está no desenvolvimento de interfaces gráficas de usuário (GUI). Em geral, a programação de GUI é feita através da associação entre comportamentos e eventos. Por exemplo, se um usuário pressiona um botão (um evento), seu programa precisa executar algum procedimento. Isso pode ser o armazenamento de informações em um banco de dados. No Swing, por exemplo, isso é feito usando ActionListeners:
class ButtonHandler implements ActionListener { public void actionPerformed(ActionEvent e) { // faz alguma coisa. } } class UIBuilder { public UIBuilder() { button.addActionListener(new ButtonHandler()); } }
Esse exemplo mostra o uso da classe ButtonHandler como uma substituição para uma chamada de retorno. A classe ButtonHandler é apenas para fornecer um único método: actionPerformed, definido na interface ActionListener. Usando classes internas anônimas é possível simplificar um pouco este código:
class UIBuilder { public UIBuilder() { button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { //faz alguma coisa } } } }
Esse código é algo mais limpo que o anterior. Olhando o código em detalhes, foi criada uma instância da classe apenas para a invocação um único método. Esses tipos de problemas são exatamente aqueles resolvidos com a introdução das expressões lambda.
Expressões lambda como funções
Uma expressão lambda é uma literal de função. Ela define uma função com parâmetros de entrada e corpo de função. A sintaxe da expressão lambda no Java 8 ainda está em discussão, mas possivelmente será algo como isto:
(type parameter) -> function_body
Um exemplo concreto é:
(String s1, String s2) -> s1.length() - s2.length();
Essa expressão lambda calcula a diferença de tamanho entre duas strings. Há algumas extensões para essa sintaxe, como evitar a definição do tipo dos argumentos, como será apresentado mais adiante, e o suporte a definições em múltiplas linhas usando { e } para agrupar as declarações.
O método Collections.sort() pode ser um exemplo de uso ideal para essa expressão. Ele permite ordenar uma coleção de Strings com base nos seus tamanhos:
List <String> list = Arrays.asList("looooong", "short", "tiny" ); Collections.sort(list, (String s1, String s2) -> s1.length() - s2.length()); > "tiny", "short", "looooong"
Então, ao invés de montar o método sort com uma implementação de Comparator, como é feito atualmente no Java, o mesmo resultado pode ser obtido passando a expressão lambda acima.
Expressões lambda como closures
As expressões lambda tem algumas propriedades interessantes. Uma é que elas são closures. Um closure permite que a função acesse variáveis que estão fora de seu escopo léxico.
String outer = "Java 8" (String s1) -> s1.length() - outer.length()
O exemplo mostra que a expressão lambda tem acesso a String outer, que está definida fora do seu escopo. Para cenários de usos em uma linha, closures podem ser bem úteis.
Inferência de tipo também para expressões lambda
A inferência de tipos, introduzida no Java 7, também é aplicada às expressões lambda. A inferência de tipo, em poucas palavras, significa que um programador pode omitir a definição de tipo em qualquer lugar onde o compilador possa 'inferir' ou deduzir o tipo por si só.
Se a inferência de tipo for usada na expressão lambda de ordenação, ela pode ser escrita da seguinte maneira:
List<String> list = Arrays.asList(...); Collections.sort(list, (s1, s2) -> s1.length() - s2.length());
Os tipos dos parâmetros s1 e s2 foram omitidos, porque o compilador sabe que a lista contém uma coleção de Strings, ele sabe que a expressão lambda usada como comparadora precisa também ter dois parâmetros do tipos String. Consequentemente, os tipos não precisam ser declarados explicitamente, apesar de se ter a liberdade para declarar os tipos.
A principal vantagem da inferência de tipo é reduzir o código repetitivo. Se o compilador pode inferir o tipo, porque é preciso defini-lo?
Olá expressões lambda, adeus classes internas anônimas
As expressões lambda e a inferência de tipos ajudam a simplificar o exemplo do chamado de retorno discutido anteriormente:
class UIBuilder { public UIBuilder() { button.addActionListener(e -> //process ActionEvent e) } }
Ao invés de criar uma classe para tratar o método de chamada de retorno, agora é passado diretamente uma expressão lambda para o método addActionListener. Além de reduzir muito código repetitivo e melhorar a leitura, a única coisa que realmente interessa é expressa diretamente: o tratamento do evento.
Antes de desvendar mais vantagens das expressões lambda, será apresentado como são as expressões lambda em Scala.
Expressões lambda em Scala
As funções são os blocos de construção básicos de um estilo de programação chamado Programação Funcional. O Scala combina orientação a objetos, conhecido do Java, e a programação funcional. Em Scala, uma expressão lambda é um bloco básico de construção chamado de 'função' ou 'literal de função'. As funções em Scala são cidadãos de primeira classe (first class citizens). Elas podem ser atribuídas a vals ou vars (variáveis final ou não final), podem ser passadas como argumento para outra função, e podem ser combinadas para formar novas funções.
Em Scala uma literal de função é escrito como:
(argumento) => //corpo da função
Por exemplo, a expressão lambda anterior em Java, calculando a diferença de tamanho entre duas Strings, é escrita em Scala como:
(s1: String, s2: String) => s1.length - s2.length
Em Scala, as literais de função são também closures. Elas podem acessar as variáveis definidas fora dos seus escopos léxicos.
val outer = 10 val myFuncLiteral = (y: Int) => y * outer val result = myFuncLiteral(2) > 20
Esse exemplo resultaria em 20. Atribuindo uma literal de função para uma variável chamada myFuncLiteral.
As similaridades sintática e semântica entre as expressões lambda do Java 8 e das funções em Scala são notáveis. Semanticamente são exatamente iguais, enquanto que sintaticamente diferem-se apenas no símbolo da seta (Java8: -> Scala: =>) e a notação abreviada, que ainda não foi apresentada.
Funções de ordem superior como blocos de construção reutilizáveis
A grande vantagem das literais de função é passá-las como qualquer outra literal, como uma String ou um Object arbitrário. Isso oferece uma vasta gama de possibilidades e permite construir códigos altamente compactos e reutilizáveis.
Função superior de primeira ordem
Quando uma literal de função é passada para um método, basicamente é um método que aceita um método via parâmetro. Tais métodos são chamados de funções de ordem superior. O método addActionListener no código de exemplo Swing anterior é exatamente um desses. Também é possível definir as funções de ordem superior, que podem oferecer muitos benefícios. Exemplo:
def measure[T](func: => T):T = { val start = System.nanoTime() val result = func val elapsed = System.nanoTime() - start println("A execução dessa chamada levou: %s ns".format(elapsed)) result }
Neste exemplo, o método measure (medir), que mede o tempo necessário para executar uma literal de função de chamada de retorno nomeada func. A assinatura de func não precisa de parâmetros e retorna um resultado genérico do tipo T. As funções em Scala não precisam necessariamente de parâmetros, apesar deles poderem ter parâmetros (e na maioria das vezes terão).
Desse modo uma literal de função (ou método) pode ser passada para o método measure:
def myCallback = { Thread.sleep(1000) "Eu apenas tirei uma soneca" } val result = measure(myCallback); > A execução dessa chamada levou: 1002449000 ns
Do ponto de vista conceitual, foi separado a preocupação de medir o tamanho da invocação do método da computação de fato. Sendo criadas duas construções de códigos reutilizáveis (a parte de medição e a parte de callback) que estão fracamente acopladas, parecidas com um interceptador.
Reusabilidade através das funções de ordem superior
Outro exemplo hipotético, no qual duas construções reutilizáveis estão ligeiramente mais acopladas:
def doWithContact(fileName:String, handle:Contact => Unit):Unit = { try{ val contactStr = io.Source.fromFile(fileName).mkString val contact = AContactParser.parse(contactStr) handle(contact) } catch { case e: IOException => println("não foi possível carregar o arquivo: " + e) case e: ParseException => println("não foi possível analisar o arquivo de contato: " + e) } }
O método doWithContact lê um contato de um arquivo, como um vCard ou similar, e oferece-o para um analisador que converte-o para um objeto de domínio contact. O objeto de domínio contact é então passado para uma literal de função utilizada como chamada de retorno, que faz com o objeto de domínio contact o que a função determinar. O método doWithContact, assim como a literal de função, retornam o tipo Unit, que é equivalente a um método void no Java.
É possível definir várias chamadas de retorno que podem ser passadas para o método doWithContact:
val storeCallback = (c:Contact) => ContactDao.save(c) val sendCallback = (c:Contact) => { val msgBody = AConverter.convert(c) RestService.send(msgBody) } val combinedCallback = (c:Contact) => { storeCallback(c) sendCallback(c) } doWithContact("custerX.vcf", storeCallback) doWithContact("custerY.vcf", sendCallback) doWithContact("custerZ.vcf", combinedCallback)
A chamada de retorno também pode ser passada na mesma linha:
doWithContact("custerW.vcf", (c:Contact) => ContactDao.save(c))
Funções de ordem superior no Java 8
O equivalente no Java 8 será muito semelhante - usando a proposta de sintaxe atual:
public interface Block<T> { void apply(T t); } public void doWithContact(String fileName, Block<Contact> block) { try{ String contactStr = FileUtils.readFileToString(new File(fileName)); Contact contact = AContactParser.parse(contactStr); block.apply(contact); } catch(IOException e) { System.out.println("não foi possível carregar o arquivo: " + e.getMessage()); } catch(ParseException p) { System.out.println("não foi possível analisar o arquivo de contato: " + p.getMessage()); } } //utilização doWithContact("custerX.vcf", c -> ContactDao.save(c))
O benefício das funções de ordem superior
As funções ajudam a separar claramente o conceito de criar um objeto de domínio da necessidade de processá-lo. Fazendo isso, novas maneiras de tratar os objetos de domínio podem ser facilmente criadas sem precisar acoplar com a lógica que cria os objetos de domínio.
Como resultado, o benefício ganho com as funções de ordem superior é que o código permanece DRY (Don't Repeat Yourself - não se repita), de modo que o programador pode perfeitamente ganhar com a reutilização de código no nível bastante granular.
Coleções e funções de ordem superior
As funções de ordem superior fornecem um maneira muito eficiente de lidar com coleções. Como quase todos os programas as utilizam, o tratamento eficiente dessas podem ser de grande ajuda.
Filtrando coleções: Antes e Depois
Um caso de uso comum envolve as coleções é a aplicação de um cálculo para todos os elementos de uma coleção. Por exemplo, a partir de uma lista de objetos Photo e será filtrado todas as fotos com certo tamanho.
List<Photo> input = Arrays.asList(...); List<Photo> output = new ArrayList(); for (Photo c : input){ if(c.getSizeInKb() < 10) { output.add(c); } }
Este código contém uma boa quantidade de código repetitivo, como a criação da coleção de resultados e a adição dos novos elementos na lista. Uma alternativa é usar a classe Function, para abstrair o comportamento da função:
interface Predicate<T> { boolean apply(T obj); }
O código escrito usando o Guava:
final Collection<Photo> input = Arrays.asList(...); final Collection<Photo> output = Collections2.transform(input, new Predicate<Photo>(){ @Override public boolean apply(final Photo input){ return input.getSizeInKb() > 10; } });
Esse código reduz um pouco os códigos repetitivos, mas ainda é confuso e verboso. Ao trocar este código para Scala ou Java 8, ganha-se o poder e a elegância das expressões lambda.
Scala:
val photos = List(...) val output = photos.filter(p => p.sizeKb < 10)
Java 8:
List<Photo> photos = Arrays.asList(...) List<Photo> output = photos.filter(p -> p.getSizeInKb() < 10)
Ambas implementações são elegantes e bem sucintas. Note que ambas fazem uso da inferência de tipos: o parâmetro p do tipo Photo não é explicitamente definido. No Scala a inferência de tipo é uma característica padrão.
Encadeamento de funções no Scala
Até agora, foram economizadas seis linhas de código e melhorado a legibilidade do código. A diversão começa quando várias funções de ordem superior são encadeadas. O Exemplo a seguir, cria uma classe Photo e adiciona outras propriedades no Scala.
case class Photo(name:String, sizeKb:Int, rates:List[Int])
Sem conhecer muito de Scala, a classe Photo com três variáveis de instância, name, sizeKb e rates. O rates irá conter as classificações do usuário para essa imagem, de 1 a 10. A classe Photo pode ser instanciada da seguinte maneira:
val p1 = Photo("matterhorn.png", 344, List(9,8,8,6,9)) val p2 = ... val photos = List(p1, p2, p3, ...)
Com a lista de Photos, é fácil definir várias consultas encadeando múltiplas funções de ordem superior. Por exemplo: extrair os nomes dos arquivos de todas as imagens que possuam um tamanho maior que 10MB. A primeira questão é como transformar a lista de Photos em uma lista de nome de arquivos. Para isso utilize a função de ordem superior, chamada map:
val names = photos.map(p => p.name)
O método map transforma cada elemento da coleção para um tipo definido na função passado para ela. Neste exemplo a função recebe um objeto Photo e retorna uma String, que é o nome do arquivo da Imagem.
A tarefa então pode ser resolvida com o encadeamento do método map após o método filter:
val fatPhotos = photos.filter(p => p.sizeKb > 10000) .map(p => p.name)
Cada método (filter, map, etc) sempre retorna uma coleção, que pode até ser vazia, mas nunca nula, assim não é preciso se preocupar com os NullPointerExceptions. Então se a coleção de fotos estiver vazia logo de inicio, o resultado do processamento ainda será uma coleção vazia.
O encadeamento das funções também é chamado de 'composição de funções'. Usando a composição de funções é possível percorrer a API de coleções para encontrar blocos de construção que possam resolver o problema.
Outro exemplo mais avançado:
Tarefa: "Retornar os nomes de todas as fotos que cuja média de avaliações seja maior que seis, ordenados pela quantidade total de avaliações dadas:"
val avg = (l:List[Int]) => l.sum / l.size val minAvgRating = 6 val result = photos.filter(p => avg(p.ratings) >= minAvgRating) .sortBy(p => p.ratings.size) .map(p => p.name)
Para realizar esta tarefa, será utilizado o método sortBy, que espera uma função que recebe o tipo do elemento da coleção como entrada (nesse caso Photo) e retorna um objeto do typeOrdered (nesse caso Int). Como a List não possui um método de média, será definido a literal de função avg que calcula a média de uma lista de inteiros em uma função anônima passada para o método filter.
Encadeamento de funções no Java 8
Ainda não está claro quais as funções de ordem superior as classes de coleções do Java 8 oferecerão. Filter e map provavelmente serão suportadas. Consequentemente o primeiro exemplo de encadeamente será muito parecido no Java 8:
List<Photo> photos = Arrays.asList(...) List<String> output = photos.filter(p -> p.getSizeInKb() > 10000) .map(p -> p.name)
Novamente, quase não há diferenças sintáticas quando comparado com a versão em Scala.
As funções de ordem superior em combinação com as coleções são extremamente poderosas. Não são apenas concisas e legíveis, mas também economiza muito código repetitivo, com todos os benefícios relacionados, como menos testes e menos bugs.
Coleções em paralelo
Até agora não foi abordado uma das vantagens mais importantes das funções de ordem superior nas coleções. Além da concisão e legibilidade, as funções de ordem superior adicionam uma camada muito importante de abstração. Nos exemplos anteriores não foi necessário usar nenhum loop, não houve necessidade, sequer uma vez, de iterar sobre elementos da coleção para filtrar, mapear ou ordenar os elementos. As iterações foram abstraídas, ficando escondidas do usuário da coleção.
Esta camada de abstração adicional é a chave para alavancar as plataformas multinúcleo, porque a implementação do loop pode escolher por si mesmo como iterar sobre a coleção. Consequentemente, a iteração pode não ser realizada apenas sequencialmente, mas também em paralelo. Com a vantagem das plataformas multinúcleo, alavancar o processamento em paralelo não é mais apenas algo desejável. As linguagens de programação de hoje precisam estar aptas a lidar com as demandas das plataformas paralelas.
Na teoria, é possível escrever nosso próprio código paralelo. Na prática, isso não é uma boa ideia. Primeiro, escrever código paralelo robusto, especialmente com compartilhamento de estados e resultados intermediários que precisam ser mesclados, é extremamente difícil. Segundo, acabar gerando muitas implementações diferentes não é desejável. Apesar do Java 7 ter o Fork/Join, a problema de decompor e remontar os dados é deixado para o cliente, o que não é o nível de abstração desejável. E terceiro, por que se preocupar se já há a resposta em forma de programação funcional?
Deste modo, deixe as pessoas muito capacitadas escreverem códigos de iteração em paralelo uma vez e abstrair seus usos através de funções de ordem superior.
Coleções paralelas no Scala
O exemplo a seguir em Scala faz uso do processamento paralelo:
def heavyComputation = "abcdefghijk".permutations.size (0 to 10).par.foreach(i => heavyComputation)
Primeiro é definido o método heavyComputation, que executa um processamento pesado. Em um laptop quad core, essa expressão levou cerca de 4 segundos para executar. Então é instanciada uma coleção do tipo range (0 até 10) e invocado o método par. O método par retorna uma implementação paralela, que fornece exatamente a mesma interface que sua contrapartida sequencial. Muitos tipos de coleções em Scala têm suporte ao método par.
Esse exemplo foi executado em um computador com processador de quatro núcleos. A fim de obter as medições, foi utilizado o método measure do exemplo anterior:
//execução única measure(heavyComputation) > A execução desta chamada levou: 4.6 s //execução sequencial measure((1 to 10).foreach(i => heavyComputation)) > A execução desta chamada levou: 46 s //execução paralela measure((1 to 10).par.foreach(i => heavyComputation)) > A execução desta chamada levou: 19 s
O que pode ser surpreendente a primeira vista, é que a execução paralela é apenas 2.5 vezes mais rápida, mesmo usando quatro núcleos. A razão é que o paralelismo vem com o preço da sobrecarga adicional, na forma de threads que precisam ser iniciadas e os resultados intermediários precisam ser mesclados. Portanto, não é uma boa ideia usar as coleções em paralelo por padrão, mas apenas utilizar esta abordagem para os processamentos pesados.
Coleções paralelas no Java 8
No Java 8, a interface proposta para coleções paralelas, mais uma vez, é quase idêntica à do Scala:
Array.asList(1,2,3,4,5,6,7,8,9.0).parallel().foreach(int i -> heavyComputation())
O paradigma é exatamente o mesmo que no Scala, com a única diferença que o nome do método para criar uma coleção paralela é parallel(), ao invés de par.
Tudo em um: um grande exemplo
Para recapitular as funções de ordem superior / expressões lambda em combinação com as coleções em paralelos, o exemplo a seguir em Scala trás vários conceitos introduzidos nas seções anteriores.
Para este exemplo, escolheu se um site aleatório, que fornece uma grande variedade de papéis de parede de natureza. O programa tira todas as urls das imagens dos papéis de parede desta página e baixa as imagens em paralelo. Além das principais bibliotecas do Scala, foram utilizadas outras duas, a biblioteca Dispatch para comunicação htttp e a FileUtils do Apache, para simplificar algumas tarefas. Prepare-se para ver alguns conceitos de Scala que não foram abordados anteriormente neste artigo, mas cuja intenção deve ser razoavelmente compreensível.
import java.io.File import java.net.URL import org.apache.commons.io.FileUtils.copyURLToFile import dispatch._ import dispatch.tagsoup.TagSoupHttp._ import Thread._ object PhotoScraper { def main(args: Array[String]) { val url = "http://www.boschfoto.nl/html/Wallpapers/wallpapers1.html" scrapeWallpapers(url, "/tmp/") } def scrapeWallpapers(fromPage: String, toDir: String) = { val imgURLs = fetchWallpaperImgURLsOfPage(fromPage) imgURLs.par.foreach(url => copyToDir(url, toDir)) } private def fetchWallpaperImgURLsOfPage(pageUrl: String): Seq[URL] = { val xhtml = Http(url(pageUrl) as_tagsouped) val imgHrefs = xhtml \\ "a" \\ "@href" imgHrefs.map(node => node.text) .filter(href => href.endsWith("1025.jpg")) .map(href => new URL(href)) } private def copyToDir(url: URL, dir: String) = { println("%s copy %s to %s" format (currentThread.getName, url, dir)) copyURLToFile(url, new File(toDir, url.getFile.split("/").last)) } }
Explicação do código
O método scrapeWallpapers processa o fluxo do controle, que está obtendo as URLs das imagens do html e baixando cada uma delas.
Por meio do fetchWallpaperImgURLsOfPage, todas as URLs das imagens de papéis de parede são obtidas do html.
O objeto Http é uma classe da biblioteca de HTTP dispatch, que oferece uma DSL para facilitar o uso da biblioteca httpclient da Apache. O método as_tagsouped converte o html em xml, que é um tipo de dado interno do Scala.
val xhtml = Http(url(pageUrl) as_tagsouped)
Do html, no formato de xhtml, é obtido as hrefs relevantes das imagens que serão baixadas:
val imgHrefs = xhtml \\ "a" \\ "@href"
Como o XML é nativo no Scala, pode ser utilizado uma expressão semelhante ao xpath \\ para selecionar os nós que serão utilizados. Depois de obter as hrefs é preciso filtrar as URLs das imagens e convertê-las em objetos URL. A fim de atingir o objetivo, foi encadeada uma sequência de funções de ordem superior da API de Coleções do Scala como map e filter. O resultado é uma List de URLs de imagens.
imgHrefs.map(node => node.text) .filter(href => href.endsWith("1025.jpg")) .map(href => new URL(href))
O próximo passo é baixar cada imagem em paralelo. Para realizar o paralelismo, a lista de nomes das imagens foi modificada para uma coleção paralela. Consequentemente, o método foreach inicia várias threads para percorrer a coleção simultaneamente. Cada thread irá chamar o método copyToDir em algum momento.
imgURLs.par.foreach(url => copyToDir(url, toDir))
O método copyToDir usa o FileUtils do Apache Common. O método estático copyURLToFile da classe FileUtil é importado estaticamente e portanto pode ser invocado diretamente. Para maior clareza foi impresso o nome de cada thread que está executando uma tarefa. Quando executado em paralelo, irá mostrar que muitas threads estão ocupadas processando.
private def copyToDir(url: URL, dir: String) = { println("%s copy %s to %s" format (currentThread.getName, url, dir)) copyURLToFile(url, new File(toDir, url.getFile.split("/").last)) }
Esse método também demonstra que Scala é totalmente interoperável com as bibliotecas existentes do Java.
As características funcionais do Scala e os benefícios resultantes como funções de ordem superior nas coleções e o paralelismo "autônomo", tornam possível realizar a análise de texto, o IO e a conversão de dados em paralelo com apenas algumas linhas de código.
Extensão virtual de métodos / Traits
A extensão virtual dos métodos em Java é similar às traits do Scala. O que exatamente são traits? Uma trait no Scala fornece uma interface, e opcionalmente inclui uma implementação. Essa estrutura fornece grandes possibilidades. Ficará mais claro assim ao compor classes com traits.
Assim como o Java, o Scala não suporta herança múltipla. Tanto no Java quanto no Scala uma subclasse pode apenas estender uma única super classe. No entanto, com as traits, a herança é diferente, pois uma classe pode "misturar" múltiplas traits. Um fato interessante é que a classe ganha ambos os tipos, os métodos e os estados inseridos pela(s) trait(s). Traits também são chamadas de mixins, pois misturam novos comportamentos e estados às classes.
A questão que permanece é: se as traits representam uma forma de herança múltipla, não ocorrerá os notórios "problemas do diamante"? A resposta, claro, é não. O Scala define um conjunto claro de regras de precedência que determinam quando e o que é executado dentro da hierarquia de herança múltipla. Isso é independente do número de traits misturadas. Essas regras fornecem os benefícios da herança múltipla sem qualquer problema associado a ela.
Se ao menos eu tivesse uma trait
O exemplo a seguir mostra um trecho de código familiar para os desenvolvedores Java:
class Photo { final static Logger LOG = LoggerFactory.getLogger(Photo.class); public void save() { if(LOG.isDebugEnabled()) { LOG.debug("Você me salvou." ); } //mais alguns códigos úteis aqui ... } }
O logging é considerado um problema transversal de uma perspectiva de design. No entanto, isso dificilmente é notado na prática diária do desenvolvimento Java. Cada classe repetidamente declara um logger. Também é verificado se o nível do log está habilitado, usando por exemplo isDebugEnable(). Isso é uma clara violação do DRY: não se repita.
No Java, não há uma maneira para validar que um programador declare a verificação do nível de log ou use o logger apropriado associado com a classe. Os desenvolvedores Java têm usado tanto essa prática que é considerada um padrão agora.
As traits oferecem uma excelente alternativa para esse padrão. Se colocar a funcionalidade de log dentro de uma trait, pode se misturar essa trait em qualquer classe que quiser, fornecendo à classe acesso à funcionalidade transversal de 'logging', sem limitar as possibilidades de herdar de outras classes.
Logging com trait, uma solução para o problema de logging
No Scala, a trait Loggable pode ser implementada como o seguinte trecho de código:
trait Loggable { self => val logger = Slf4jLoggerFactory.getLogger(self.getClass()) def debug[T](msg: => T):Unit = { if (logger.isDebugEnabled()) logger.debug(msg.toString) } }
O Scala define uma trait usando a palavra chave 'trait'. O corpo da trait pode conter qualquer coisa que uma classe abstrata possa conter, como atributos e métodos. Outro ponto interessante no exemplo do logging é o uso do self =>. O logger deve registrar a classe que mistura nela a trait Loggable, sem registrar a Loggable em si. A sintaxe self =>, chamada de self-type no Scala, permite que a trait obtenha uma referência para a classe na qual foi inserida nela.
Note o uso da função msg: => T passada como parâmetro de entrada para o método debug (depurar). A razão principal para usar a verificação isDebugEnabled() é para garantir que a String sendo registrada é apenas processada se o nível de debug estiver habilitado. Então, como o método debug aceita apenas uma String como parâmetro, a mensagem de log seria sempre processada, pouco importando se o nível do log como debug está ou não habilitado, o que não é desejável. Ao passar função msg: => T, ao invés da String, a função msg que retorna a String a ser registrada é somente invocada quando houver sucesso na verificação isDebugEnabled. Se a verificação isDebugEnabled falhar, a função msg nunca será chamada e, portanto, não será necessário processar a String.
Para usar a trait Loggable na classe Photo é preciso utilizar o extends para misturar com a trait:
class Photo extends Loggable { def save():Unit = debug("Você me salvou."); }
A palavra chave 'extends' sugere que Photo herda de Loggable, e portanto não pode estender outra classe, o que não é o caso. A sintaxe do Scala exige que a primeira palavra chave para fazer o mixin ou estender uma classe é 'extends'. Se precisar misturar múltiplas traits, deve ser utilizada a palavra chave 'with' para cada 'trait' após a primeira. Adiante será apresentado exemplos do uso do 'with'.
Ao chamar o método save() de uma instância de Photo é apresentado a mensagem do Loggable:
new Photo().save() 18:23:50.967 [main] DEBUG Photo - Você me salvou.
Adicionando mais comportamentos para suas classes
Como discutido no parágrafo anterior, em uma classe é permitido misturar múltiplas traits. Então além de logging, também é possível adicionar outros comportamentos para a classe Photo. Por exemplo: ordenar as fotos com base no tamanho dos arquivos. Felizmente, o Scala oferece uma diversidade de traits prontas. Uma dessas traits é a Ordered[T]. Ordered é similar à interface Comparable do Java. A grande diferença é que a versão Scala também oferece uma implementação:
class Photo(name:String, sizeKb:Int, rates:List[Int]) extends Loggable with Ordered[Photo]{ def compare(other:Photo) = { debug("comparando " + other + " com " + this) this.sizeKb - other.sizeKb } override def toString = "%s: %dkb".format(name, sizeKb) }
Nesse exemplo, duas traits foram misturadas. Além da trait Loggable definida anteriormente, também foi adicionada a trait Ordered[Photo]. A trait Ordered[T] exige a implementação do método compare(type:T). Ainda muito parecido com o Comparable do Java.
Além do método compare, a trait Ordered também oferece vários métodos diferentes que podem ser utilizados para comparar objetos de várias maneiras utilizando a implementação do método compare.
val p1 = new Photo("Matterhorn", 240) val p2 = new Photo("K2", 500) p1 > p2 > false p1 <= p2 > true
Os nomes simbólicos como > e <= etc, não são palavras chave especiais reservadas no Scala como é no Java. O fato de poder comparar objetos usando >, <= etc, deve-se à trait Ordered que implementa os métodos com esses símbolos.
As classes que implementam a trait Ordered podem ser ordenadas por todas as coleções do Scala. Por ter uma coleção preenchida com objetos Ordered, o método 'sorted' (ordenar) pode ser chamada na coleção, que ordena os objetos de acordo com a ordem definida pelo método compare.
val p1 = new Photo("Matterhorn", 240) val p2 = new Photo("K2", 500) val sortedPhotos = List(p1, p2).sorted > List(K2: 500kb, Matterhorn: 240kb)
Os benefícios das traits
O exemplo anterior mostra que é possível isolar as funcionalidades genéricas de maneira modular usando as traits. Podendo ligar a funcionalidade isolada em cada classe se for necessário. Para equipar a classe Photo com a funcionalidade de logging, misturou se com a trait Loggable e para ordenar foi utilizado a trait Ordered. Essas traits podem ser reutilizadas em qualquer classe.
As traits são mecanismos poderosos para criar códigos modulares e DRY (não se repita) usando uma característica da própria linguagem, sem ter que depender de uma complexidade tecnológica extra como a programação orientada a aspectos.
A motivação para a extensão virtual de métodos
A especificação do Java 8 define um esboço da extensão virtual de métodos. A extensão virtual de métodos adicionará a implementação padrão para os novos ou existentes métodos das interfaces existentes. Porque isso?
Para muitas interfaces existentes ele pode beneficiar muito para suportar as expressões lambda na forma de funções de ordem superior. Como um exemplo, considere a interface java.util.Collection. Pode ser desejável que a interface java.util.Collection forneça um método forEach(lambdaExpr). Tal como um método adicionado à interface sem uma implementação padrão, todas as classes de implementação teriam que fornecer. É claro que isso traria muitos problemas de compatibilidade.
Por isso a equipe do JDK incluiu a extensão virtual de métodos. Com esta característica um método forEach, por exemplo, pode ser adicionado à java.util.Collection, incluindo uma implementação padrão. Consequentemente, todas as classes de implementação automaticamente herdarão esse método e sua implementação. Fazendo isso, suas APIs podem evoluir de uma forma completamente não intrusiva, que é exatamente a intenção da extensão virtual dos métodos querem chegar. Se a classe de implementação não estiver satisfeita com a implementação padrão, ela pode simplesmente sobrescreve-la.
Extensão virtual de métodos vs Traits
A motivação básica para a extensão virtual de métodos é evoluir a API. Um efeito bem vindo é que ele oferece uma forma de herança múltipla, que é limitada pelo comportamento. As traits em Scala não apenas fornece o comportamento da herança múltipla, mas também do estado. Além da herança do comportamento e estado, as traits oferecem uma maneira para obter uma referência para a classe de implementação, como mostrado no atributo ' self ' no exemplo do Loggable.
Do ponto de vista da usabilidade, as traits oferecem um rico conjunto de características como a extensão virtual de métodos. No entanto, suas motivações são diferentes: em Scala as traits foram sempre tratadas para construir blocos modulares que ofereçam herança múltipla 'sem os problemas', enquanto que a extensão virtual de métodos tem como base permitir a evolução das APIs e em segundo lugar a 'o comportamento da herança múltipla'.
As traits Loggable e Ordered em Java 8
O exemplo a seguir demonstra o uso da extensão virtual de métodos, quando as traits Ordered e Loggable são implementadas no Java 8.
A trait Ordered pode ser implementada completamente com a extensão virtual de métodos, porque não envolve estado. Como mencionado a contra partida da trait Ordered do Scala é o java.lang.Comparable no Java. A implementação ficaria da seguinte forma:
interface Comparable<T> { public int compare(T that); public boolean gt(T other) default { return compare(other) > 0 } public boolean gte(T other) default { return compare(other) >= 0 } public boolean lt(T other) default { return compare(other) < 0 } public boolean lte(T other) default { return compare(other) <= 0 } }
Foi adicionado novos métodos de comparação à interface Comparable ('maior que', 'maior ou igual que', 'menor que', 'menor ou igual que', idênticos àqueles encontrados na trait Ordered: >, >=, <, <=). A implementação padrão, marcada com a palavra chave default, encaminha todas as chamadas para o método abstrato existente compare. O resultado é que uma interface existente pode ser enriquecida com métodos novos sem a necessidade de classes para implementar esses métodos do Comparable. A trait Ordered no Scala tem uma implementação bem similar a essa.
Se a classe Photo implementasse Comparable, também poderia ser executada as operações de comparação com aqueles métodos novos adicionados:
Photo p1 = new Photo("Matterhorn", 240) Photo p1 = new Photo("K2", 500) p1.gt(p2) > false p1.lte(p2) > true
A trait Loggable não pode ser completamente implementada como uma extensão virtual de método, mas chega perto:
interface Loggable { final static Logger LOG = LoggerFactory.getLogger(Loggable.class); void debug(String msg) default { if(LOG.isDebugEnabled()) LOG.debug(msg) } void info(String msg) default { if(LOG.isInfoEnabled()) LOG.info(msg) } //etc... }
Nesse exemplo foi adicionado os métodos de log, como debug, info etc, para a interface Loggable, que delega por padrão suas chamadas para uma instância estática de Logger. O que falta são os meios para se obter uma referência da implementação. Devido à falta desse mecanismo, a interface Loggable foi utilizada como um logger, para registrar todas as operações feitas pela classe que implementa a interface Loggable. Por causa dessa limitação, métodos de extensão virtual são menos adequados para tais cenários de uso.
Para resumir, ambas as traits e os métodos de extensão virtual fornecem o comportamento da herança múltipla. As traits também oferecem herança múltipla para o estado e um meio para adquirir uma referência da classe de implementação.
Conclusão
O Java 8 vai oferecer uma variedade de novas características, com o potencial para mudar fundamentalmente a maneira as aplicações são escritas. Especialmente a adição de capacidades de programação funcional, tais como as expressões lambda, podem ser consideradas uma troca de paradigma. Essa troca fornece novas possibilidades para um código mais conciso, compacto e fácil de entender.
Complementando, as expressões lambda são fundamentais para habilitar o processamento em paralelo.
Como explicado nesse artigo, todas as características mencionadas já estão disponíveis no Scala. Os desenvolvedores que quiserem experimentá-las, podem explorar as builds iniciais do Java 8 na maioria das plataformas. Alternativamente, recomendamos estudar o Scala como uma maneira de se preparar para essa troca de paradigma que virá.
Apêndice
As informações do Java 8 foram obtidas principalmente dessas apresentações:
Java 7 and 8: WhereWe'veBeen, WhereWe'reGoing
Sobre os autores
Urs Peter é um consultor sênior da Xebia. Com mais de dez anos de experiência com TI, ele já teve muitos papeis, variando de desenvolvedor, arquiteto de software, líder de equipe e Scrum master. Durante sua carreira ele explorou uma ampla gama de linguagens, técnicas de desenvolvimento de software e ferramentas para a plataforma JVM. Ele é um dos primeiros instrutores certificados em Scala na Europa e atualmente presidente da Comunidade de Entusiastas Holandês de Scala (Dutch Scala Enthusiast community - DUSE) |
|
Sander van den Berg tem estado ativo com TI desde 1999, trabalhando como desenvolvedor de software para diversas empresas relacionadas com defesa, principalmente trabalhando com soluções MDA/MDD. Ele juntou-se a Xebia com consultor sênior em 2010, onde ele á responsável pela integração da arquitetura Lean e promoção do Scala. Além da arquitetura, Sander é interessado em linguagem de design. Ele está ativo em muitas comunidades de programação funcional. Sander gosta de soluções elegantes para problemas complexos, preferindo Scala como um meio para expressa-las. Sanders conhece muitas linguagens, entre elas Clojure, Haskell e F#. |