terça-feira, 21 de julho de 2009

Java Generics - Parte I: Definições

Embora tenham sido introduzidos há quase cinco anos, com o lançamento do java 1.5 (codinome Tiger), os generics em java são fonte de confusão para muitas pessoas - mesmo programadores mais experientes. Esta série de posts tem como objetivo explicar como funcionam os generics em java, explicar seus diversos casos de uso e seus principais pontos de confusão. Este primeiro artigo é um pouco teórico mas artigos subsequentes se concentrarão na prática dos conceitos aqui explicados.
O que são generics
Generics é/são um recurso de linguagem que permite às classes especificarem uma parametrização "abstrata" de um tipo. Em outras palavras, os generics permitem que se escreva códigos que dependam de classes que serão definidas pelos clientes da sua classe. O exemplo óbvio são as coleções: quem escreveu a classe List, por exemplo, não poderia escrever uma variante para cada tipo de elemento que seria guardado dentro da List. Para poder definir de maneira uniforme a interface List (e sem que códigos clientes precisem fazer casts ou extender as classes), é possível escrever uma classe que tenha um parâmetro genérico, descrito na declaração da mesma. Em java, o parâmetro genérico é declarado dentro dos angle brackets '<' e '>':
public interface List<E> {

public E get(int index);

//resto da classe

}

Permitindo escrever código cliente deste modo:
ArrayList<String> listaDeStrings = new ArrayList<String>();

listaDeStrings.add("umaString");

String umaStrings = listaDeStrings.get(0);

Este recurso está presente em várias linguagens de programação, tal como C++, C#, Haskel, scala, etc, embora em algumas dessas linguagens seu nome seja diferente. Em teoria de tipos, este recurso também é conhecido como polimorfismo paramétrico. Para as definições a seguir, onde está escrito "A instância de B" deve ser lida como "A é um substituto válido para B" em relação à tipagem.

Covariância, contravariância e invariância
Um pré-requisito para se entender as confusões mais comuns causadas por generics, é preciso entender um conceito conhecido como variância de tipos genéricos:

Covariância
Por definição, diz-se que um tipo T com um parâmetro genérico B é covariante em B se e somente se toda instância de T[A0] é uma instância de T[A1] para toda classe concreta A0 que extende de uma outra classe concreta A1. Por exemplo em java (e em C#) arrays são covariantes, o que nos permite escrever:

Object[] arrayDeObjects = new String[10];
Considerando que o tipo do array é seu "parâmetro genérico", embora não use o generics explicitamente.

Por outro lado, os generics "normais" em java não são covariantes, o que torna o código abaixo inválido:

//erro de compilação
List<Object> listDeObjects = new ArrayList<String>();
Um tipo genérico covariante não permite que se "escreva" nele - em outras palavras, um tipo genérico covariante é read-only, ou seja, o tipo genérico só pode aparecer em posições covariantes (apenas como parte de retorno de métodos). Para verificar porquê, imagine que o compilador permitisse o código abaixo:
ArrayList<String> listDeStrings = new ArrayList<String>();

//a linha abaixo é reportada como erro pelo compilador
ArrayList<Object> listDeObjects = listDeStrings;

//em um array list de objects, podemos inserir um Object
listDeObjects.add(new Object());

A seguinte linha, porém, faria com que fosse lançada uma ClassCastException mesmo que ela não tenha nenhum cast:
String a = listDeStrings.get(0);
Os arrays em java resolvem (ou melhor, contornam) este problema lançando a exceção ArrayStoreException:
// array de strings
String[] arrayDeStrings = new String[10];

//sem exceção, pois arrays são covariantes
Object[] arrayDeObjects = arrayDeStrings;

// a linha abaixo, porém, lança a exceção ArrayStoreException
arrayDeObjects[0] = new Object();



Contravariância
Por outro lado, um tipo genérico T é considerado contravariante em B se e somente se para qualquer classe concreta A0 super classe de A1, toda instância de T[A0] é uma instância de T[A1]. Embora isso pareça estranho a princípio, considere a interface Comparator:
public interface Comparator<E>  {

public int compare(E arg0, E arg1);

}

Sempre que um método precisar receber um Comparator para, por exemplo, String, poderia receber na verdade um Comparator para qualquer coisa que saiba comparar Strings - por exemplo, um Comparator de Objects.

Desse modo, podemos ver que um tipo genérico contravariante é write-only, ou seja, o tipo paramétrico só pode aparecer em posições contravariantes (apenas como parte dos argumentos dos seus métodos).

Em JAVA
Como foi visto anteriormente, arrays em java são covariantes. Os tipos genéricos, porém, são invariantes nos tipos paramétricos nele definidos - ou seja, não são nem covariantes nem contravariantes. Isso permite que tipos genéricos sejam usados tanto em retorno de métodos quanto em seus argumentos. Na declaração de variáveis, porém, é possível especificar a variância de um tipo genérico através das palavras-chave extends (para covariância) e super (para contravariância).

Assim sendo, uma List covariante em um tipo T tem o tipo:

List<? extends T> listDeT = ....;


A mesma sintaxe se extende para parâmetros de métodos, como neste método presente na interface Collection:

public void addAll(Collection<? extends E> collection)


Declarando um parâmetro dessa maneira, a collection é covariante em E, o que significa que no corpo do método, não podemos chamar métodos nessa lista que declarem E como parte do parâmetro.


Por outro lado, um Comparator contravariante em K tem o tipo:

Comparator<? super K> comparatorDeK = ....;


As construções acima funcionam também para declaração de parâmetros de métodos e construtores, bem como o tipo de retorno de um método.

Note a presença do wildcard '?'. O wildcard serve para indicar que o tipo exato da List e do Comparator é desconhecido, mas deve obedecer às restrições de variância impostas (respectivamente, covariante em T e contravariante em K). Como dito anteriormente, uma List covariante é "read-only"[1] (apenas é possível ler coisas da List) e, comparativamente, o Comparator contravariante é write-only. Este tipo de variância é chamado de use-site variance, contrastando com definition-site variance (presentes em, por exemplo, C# 4.0 e scala), onde a variância é indicada na própria declaração do tipo genérico.


Conclusão

Vimos aqui a definição de generics, bem como definições sobre variância e sobre como funciona a variância de arrays e demais tipos genéricos em Java. No próximo post, serão mostrados alguns usos de tipos variantes e os problemas mais comuns que aparecem com o uso de generics.



[1] - não são read-only no sentido de que a List não pode ser modificada, mas sim no sentido que não se pode chamar métodos que usam o tipo genérico como parte do parâmetro de algum método.

sexta-feira, 10 de julho de 2009

Scala parte I - sintaxe

Durante a escrita de um outro artigo (ainda a ser publicado), percebi que preferia escrever os exemplos em uma linguagem de programação chamada Scala ao invés de Java. Por isso, ao invés de explicar todas as linhas do código, resolvi escrever um pouco sobre esta linguagem que, na minha humilde opinião, tem potencial para ser a próxima grande linguagem de programação.

O que é Scala?
Scala é uma linguagem de programação orientada a objetos, com tipagem estática e compilada, que é executada na JVM e na máquina virtual do .Net[1]. Além disso, Scala tem muitas funcionalidades típicas de linguagens de programação funcional, como closures, otimização de chamadas recursivas, etc. Por último, por ser primariamente executada na JVM, apresenta ótima compatibilidade com Java, podendo se aproveitar das muitas bibliotecas existentes. Foi inventada por Martin Odersky e seu nome se refere à escalabilidade da linguagem, na medida em que pode ser usada para escrever pequenos scripts ou grandes sistemas completos, sem que haja uma alteração na maneira de escrever.

Como usar
Todos os exemplos abaixo podem ser testados no console de Scala, invocado por scala ou scala.bat dependendo do seu sistema operacional.

Um pouco sobre a sintaxe
Na sua estrutura geral, Scala se parece muito com Java, embora os detalhes da sintaxe sejam bastante diferentes. Para começar, a declaração de variável é precedida de val ou var e o tipo da variável é pós-escrito e separado do nome da variável por ':', desse modo:

var umaString: String = "aaa";
val umNumero: Int = 1;

uma variável declarada como var pode ser reassignada, enquanto que uma variável declarada com val é imutável, ou seja, para todos os efeitos, equivalente a declarar uma variável 'final' em Java. O compilador de Scala permite, porém, que se omita o tipo de uma variável (que deve ser sempre inicializada durante a declaração):

var outraString = "aaa"
val umDouble = 0.0

E o ';' pode ser omitido se for desnecessário (ou seja, na última expressão de uma linha).
Métodos em Scala são definidos pela palavra chave 'def':

def metodoSemParametros():String = { "a" }

O ':String' significa que o método devolve String. O retorno do método, se omitido, é o resultado da última expressão do método. Um método com parâmetros:

def metodoComUmParametroInteiro(parametro: Int) = parametro + 1

Este método declara um parâmetro do tipo Int (que é o mesmo que 'int'). Neste caso, o compilador também infere e insere o tipo de retorno do método e os delimitadores também são opcionais para métodos compostos por apenas uma expressão. Por falar em expressão, Scala é composto quase que exclusivamente por expressões, o que significa que você pode atribuir quase qualquer coisa a uma variável (embora haja expressões do tipo Unit, o equivalente a void em C/C++/Java).


val maior: String = { if (1 > 2) { "1 maior que 2"} else { "1 menor que 2"} }

Mesmo para expressões desse tipo, o compilador infere o tipo da expressão e o insere se este for omitido. As chaves também podem ser omitidas para expressões únicas:

val maior = if (1 > 2) "1 maior que 2" else "1 menor que 2"

Escrever classes em Scala é bem simples:

class MinhaClasse {
var propriedadeMutavel: String = "a"
val valorImutavel = 10
}


Var e Val dentro do corpo da classe criam propriedades mutáveis e imutáveis respectivamente (o bytecode gerado é o equivalente a um campo e getters e setters).
Isso faz com que classes cliente não saibam se determinado valor vindo de outra classe é um campo ou uma invocação de método, uma propriedade conhecida como princípio de acesso a dados uniforme.

Em Scala, há o conceito de construtor primário que deve ser chamado pelos outros construtores e sua assinatura está junto com a declaracao da classe:

class Ponto(x0: Int, y0: Int) {
val x = x0
val y = y0
}

Construtores auxiliares são declarados por métodos chamados 'this'

class Ponto(x0: Int, y0:Int) {
val x = x0
val y = y0

def this() = this(0, 0) //construtor vazio
}

Em especial, o construtor primário pode já ter a declaração de propriedades de classe:
class Ponto(val x:Int, val y:Int) {
def this() = this(0, 0)
}

Métodos em Scala podem ter praticamente qualquer nome:
class Ponto(val x:Int, val y:Int) {
def this() = this(0, 0)
def +(ponto: Ponto) = new Ponto(x + ponto.x, y + ponto.y)
}

Por último, generics em scala funcionam de maneira um pouco diferente e usam [ ] ao invés de < >, mas os detalhes serão dados em um post futuro. Além disso, posts posteriores vão mostrar construções avançadas, bem como algumas das propriedades que estão tornando scala uma linguagem tão popular

[1] - Embora o suporte ao .Net esteja bem defasado e incompleto.