Página 9 de 16

Caminhos relativos, absolutos e outras rotinas de arquivos em Java

A classe File do Java encapsula de forma simplificada um arquivo ou diretório do sistema de arquivos local. O seu construtor pode receber caminhos absolutos ou relativos ao diretório atual do programa, por exemplo:

new File(".") //--> diretório atual do programa

O problema de usar caminhos relativos é que pode haver confusão em algumas situações, já que o diretório atual do programa pode ser modificado. Além disso, se o usuário pode digitar o caminho ou parte dele em algum campo, em geral deve-se evitar que ele use caminhos relativos, com exceção no caso de ser uma configuração do próprio programa.

Verificando se um caminho é relativo

O método File.isAbsolute() nos ajuda nessas tarefas e diz se o caminho é absoluto.

Veja um exemplo:

File f1 = new File("..");
System.out.println(f1.isAbsolute()); //imprime false

File f2 = new File("c:\\temp");
System.out.println(f2.isAbsolute()); //imprime true

Recuperando o caminho absoluto

Outro método útil é getAbsolutePath(). Ele retorna o caminho completo de uma instância da classe File.

Veja mais um exemplo:

File arquivo1 = new File("\\pasta\\arquivo.xml");
System.out.println(arquivo1.getAbsolutePath()); //imprime C:\pasta\arquivo.xml

File arquivo2 = new File("c:\\pasta\\arquivo.xml");
System.out.println(arquivo2.getAbsolutePath()); //imprime c:\pasta\arquivo.xml

Outras funcionalidades interessantes de File

A classe File possui vários métodos interessantes para situações específicas, por exemplo

  • getParentFile: retorna um File apontando para o diretório que contém o arquivo ou diretório atual.
  • getAbsoluteFile: retorna outra instância de File com o caminho absoluto.
  • toURI: retorna uma URI (Universal Resource Identifier) que começa com file:. É interessante para uso na rede.
  • isFile e idDirectory: informa se File aponta para um arquivo ou diretório, respectivamente.
  • exists: informa se o arquivo existe.
  • canRead e canWrite: informa se o arquivo pode ser lido ou gravado, respectivamente.
  • createNewFile: cria um novo arquivo em branco.
  • delete: apaga o arquivo ou diretório (se estiver vazio).
  • length: retorna o tamanho do arquivo em bytes.
  • list e listFiles: lista arquivos e diretórios, caso File seja um diretório.
  • mkdir e mkdirs: cria um diretório, caso File seja um diretório. O último também cria os diretórios “pais”, caso não existam.
  • getFreeSpace: retorna o espaço disponível na unidade para onde File está apontando.
  • createTempFile: método estático que retorna um arquivo temporário único para ser usado pelo programa. O método deleteOnExit faz com que esse arquivo seja apagado quando o programa Java terminar de executar.

Além dos métodos, a classe File possui algumas constantes (atributos estáticos) importantes para leitura e gravação de arquivos em diferentes plataformas:

  • File.separator: separador de nomes de diretórios. No Unix e Linux é /, enquanto no Windows é \.
  • File.pathSeparator: separador de vários caminhos de diretórios, para permitir criar uma lista de vários diretórios, como a variável PATH do sistema. No Unix e Linux é :, enquanto no Windows é ;.

Este artigo foi baseado na minha resposta no StackOverflow em Português!

Strings em Java: há mais detalhes do que você imagina

Quem estudou um pouco sobre Java sabe que Strings possuem algumas peculiaridades. Provavelmente o leitor já sabe que elas são imutáveis, já ouviu falar do pool de Strings e que deve-se usar o método equals() ao invés do operador == para comparar o conteúdo de variáveis.

Neste artigo quero ir um pouco mais além, entendendo como isso funciona internamente.

Brincando com o == e com o pool de Strings

O Java utiliza um mecanismo chamado String interning, colocando as Strings num pool para tentar armazenar apenas uma cópia de cada sequência de caracteres em memória. Em tese, o programa usaria mesmo memória e seria mais eficiente em decorrência dessa otimização.

Quando o Java encontra literais String no código, ele retorna sempre uma mesma instância de String, que aponta para uma entrada no pool interno da JVM. Sendo assim, é bem possível usar o operador == para comparar duas variáveis que recebem literais String:

String literal = "str";
String outraLiteral = "str";

System.out.println(literal == outraLiteral); //exibe true

Inclusive, como o Java trata literais String como instâncias é possível comparar um literal diretamente, assim:

System.out.println(literal == "str"); //também exibe true

Por outro lado, não podemos confiar no operador de comparação quando não sabemos como a String foi criada, já que é possível criar outras instâncias de várias formas. Exemplo:

String novaInstancia = new String("str");
System.out.println("str" == novaInstancia); //exibe false

O código acima cria uma nova instância de String, que não é a mesma retornada pela JVM para o literal "str".

Mas, contudo, entretanto, isso não quer dizer que temos duas entradas de "str" no pool do Java. Como podemos verificar isso? Usando o método String.intern(), que retorna uma referência para a String que está no pool. Exemplo:

String novaInstancia = new String("str");
System.out.println("str" == novaInstancia.intern()); //exibe true

Outro exemplo:

String str1 = "teste";
String str2 = "outro teste".substring(6);
System.out.println(str1 == str2.intern()); //exibe true

Tudo muito interessante. Mas, e se criássemos uma String de uma forma mirabolante?

StringBuilder sb = new StringBuilder();
sb.append('s');
sb.append('t');
sb.append('r');
System.out.println("str" == sb.toString().intern()); //continua sendo true

Até aqui aprendemos que uma instância da classe String não representa diretamente o seu conteúdo, isto é, o conjunto de caracteres. Várias instâncias de String podem coexistir com o mesmo texto. A questão é que todas apontam para a mesma entrada no pool.

Continue lendo, pois ainda não esgotamos este assunto!

Mas então pare que serve o equals()?

Com as informações do tópico anterior poderíamos chegar precipitadamente à conclusão de que é sempre melhor comparar duas Strings usando o operador == e o método intern().

O método equals() da classe String compara todos os caracteres de duas Strings para verificar a igualdade, enquanto o == apenas verifica se as duas Strings apontam para a mesma entrada do pool, uma comparação numérica infinitamente mais eficiente do ponto de vista computacional.

Já que a comparação com == é muito mais rápida do que com o método equals(), devemos abandonar o equals() e usar o intern() em todo lugar? A resposta é não.

A verdade é que nem todas as Strings são internalizadas no pool imediatamente. Quando chamamos o método intern(), se ela não estiver lá, então o Java irá acrescentá-la.

O problema é que, uma vez no pool, a String vai para a memória permanente e não será mais coletada pelo garbage collector.

Quando se quer velocidade e o conjunto de valores é relativamente pequeno, usar o método intern() pode ser vantajoso. Mas se usarmos este recurso, por exemplo, para processamento de arquivos texto, XML, bancos de dados, logo esbarraremos num OutOfMemoryError.

Além disso, adicionar uma Strings no pool também pode ser uma operação “cara”. Além de ser necessário verificar se a String já existe lá (envolve o método hashCode() e modificação de um mapa), o Java provavelmente terá que tratar acessos concorrentes (mais de uma thread pode inserir elementos no pool).

Finalmente, uma grande desvantagem é o código ficar mais propenso a bugs (error prone), já que é preciso que o desenvolvedor sempre coloque o intern() quando necessário.

Concluindo, o conhecimento sobre o pool ajuda em casos específicos para otimização “fina” do código, mas o uso deve ser moderado.

Outras formas de comparação

Indo um pouco além da comparação exata de Strings, temos outras formas interessantes de comparação:

Case insensitive (sem considerar maiúsculas e minúsculas)

System.out.println("STR".equalsIgnoreCase("str")); //retorna true

Uma string contida em outra

System.out.println("###STR###".contains("STR")); //retorna true

Qual string é “maior” que a outra?

System.out.println("str1".compareTo("str2")); //retorna -1, pois "str1" é menor que "str2"

Ou:

System.out.println("str1".compareToIgnoreCase("STR2")); //retorna -1, ignorando a capitalização

O método compareTo retorna:

  • 1 se a primeira String for maior que a segunda
  • 0 se forem iguais
  • -1 se a primeira String for menor que a segunda

Começa com…

System.out.println("str1".startsWith("str")); //returna true, pois "str1" começa com "str"

Termina com…

System.out.println("str1".endsWith("r1")); //return true, pois "str1" termina com "r1"

Expressão regular

System.out.println("str2".matches("\\w{3}\\d")); //return true, pois corresponde à expressão regular

Está vazia?

String str1 = "";
System.out.println(str1.isEmpty());
System.out.println(str1.length() == 0);
System.out.println(str1.equals(""));

Particularmente eu prefiro o primeiro método para Java >= 6 e o segundo para as versões anteriores.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Uma introdução ao Ant

554px-Apache-Ant-logo.svg

Ambientes de desenvolvimento Java, tanto em Linux quanto em Windows, precisam de algum tipo de automação para diminuir o tempo despendido pelo desenvolvedor, por exemplo, para gerar uma release (versão do sistema para distribuição ou homologação).

Uma das formas de automatizar tarefas uniformemente em todos os ambientes é usar uma ferramenta como o Apache Ant.

Ant é uma ferramenta poderosa e versátil que permite a criação de builds para compilação de código, montagem de pacotes, tratamento e conversão de arquivos e muito mais.

Não estamos falando de uma linguagem de programação. Ant é uma forma de declaração de atividades (tasks) necessárias em um determinado processo. Isso é feito através de um ou mais arquivos XML.

Instalando o Ant

Baixe o pacote binário na página de download oficial, descompacte-o numa pasta e adicione o caminho ao PATH do seu sistema operacional.

No Windows podemos fazer isso para uma sessão do prompt de comando da seguinte forma:

set path=%path%;c:\caminho\apache-ant-1.9.3\bin

Esta técnica é adequada se for necessário usar mais de uma versão do Ant. Mas o melhor é alterar o PATH diretamente nas configurações de sistema para sempre tê-lo disponível em linha de comando.

Escrevendo um build no Ant

O seguinte projeto Ant faz uma substituição usando expressões regulares em diversas linhas de um arquivo:

<project name="MeuProjeto" default="substituicao" basedir=".">
    <target name="substituicao">
        <replaceregexp
                file="${file}"
                byline="true"
                match="meu nome é (\w+)"
                replace="me chamo \1"
                flags="gs" />
    </target>
</project>

A tag <project> declara o projeto atual e seus atributos básicos. Ela deve ser a raiz do arquivo. O atributo default indica qual target será executado se nenhum outro for informado via linha de comando. O atributo basedir define o diretório base onde as tarefas serão executadas.

A tag <target>, por sua vez, declara um conjunto das atividades. Nesse caso, temos apenas a task <replaceregexp>.

Note o valor do atributo file da nossa task: ${file}. A cifra com as chaves de abertura e fechamento é algo similar à expression language do JSP, só que bem simplificada, tratando-se de uma interpolação de propriedades simples. O Ant substitui essa expressão por um valor definido anteriormente, de forma análoga a uma variável. Porém, não declaramos isso em lugar algum, então o valor terá que ser informado via linha de comando.

Executando o Projeto

Ao ser executado, o Ant procura automaticamente por um arquivo chamado build.xml no diretório atual. Então, se file.txt é um arquivo a ser processado pelo nosso build, o comando a seguir irá realizar a substituição:

ant -Dfile=file.txt

Caso o projeto Ant tenha outro nome, pode-se usar o parâmetro -f:

ant -f /caminho/meu-build.xml -Dfile=file.txt

O que o Ant pode fazer

O Ant possui muitas tasks prontas, dentre as quais posso destacar:

  • Javac: compila classes Java.
  • Sshexec: executa comandos remotos via SSH.
  • Copy: copia um ou mais arquivos, possibilitando filtrar o conteúdo e substituir trechos do mesmo.
  • Jar, War, Ear: empacota arquivos em uma dessas estruturas.

Além disso, o Ant possui alguns pontos de extensão. Por exemplo, você pode criar tasks personalizadas ou até seu próprio interpolador de variáveis.

Note que o Ant não é uma linguagem procedural e não tem comandos de controle. Entretanto, existe um projeto chamado Ant Contrib que disponibiliza tasks adicionais como If, For e TryCatch. Isso vai um pouco contra a filosofia do Ant, mas pode ajudar seus build a serem mais poderosos.

Outro projeto que estende o Ant chama-se Flaka. Ele acrescenta uma expression language muito mais poderosa que a original, estruturas condicionais, tratamento de exceções e muitas tasks.

Aprendendo mais sobre o Ant

Minha dica é: apenas leia o manual todo, começando pela seção Writing a simple Buildfile. Ele não é muito extenso e explica bem os conceitos.

Entenda bem os conceitos gerais antes de usar as extensões mencionadas no tópico anterior a fim de evitar surpresas.

Ainda vale a pena usar o Ant se há outras opções para build?

Sim e não.

Hoje temos o Maven, por exemplo, que gerencia o ciclo de vida de um projeto, da compilação à publicação, de forma padronizada. Porém, a arquitetura dos builds Maven também limita a execução de atividades arbitrárias que são necessárias em alguns projetos. Por isso, quem conhece Ant pode usar o Maven Antrun Plugin para executar tarefas personalizadas em qualquer fase do processo de build. É muito mais simples que criar e manter, por exemplo, um plugin próprio para o Maven.

Outra ideia é criar tasks independentes de projetos. Por exemplo, para automatizar um processo batch executado no servidor ou mesmo uma tarefa repetitiva no ambiente de desenvolvimento, como compilação de relatórios JasperReports.

Por outro lado, não fique preso a uma única ferramenta. Procure ter um conhecimento geral sobre ferramentas de build como Ant, Maven, Graddle, Ivy e outros semelhantes. Em geral, é melhor ter um canivete suíço do que um facão grande para fazer tudo. 😉


Este artigo foi baseado na minha resposta no StackOverflow em Português!

JavaScript: substituição em Strings

Substituir texto em Strings é algo muito comum em qualquer linguagem. Neste artigo, quero analisar qual seria a forma mais eficiente de fazer isso em JavaScript.

Dada a seguinte frase numa String:

var frase = "O céu está azul hoje!";

Qual é a melhor forma de trocarmos a cor do céu na frase acima para “verde”?

“O céu está verde hoje!”

Métodos de substituição

Após algumas pesquisas cheguei a quatro variações das técnicas para substituição de Strings. Elas fazem substituição de todas as ocorrências da String a ser substituída, caso haja mais de uma.

Criando uma expressão regular com RegExp

var regex = new RegExp("azul", "g");
var resultado = frase.replace(regex, "verde");

O RegExp é um tipo de objeto que recebe como parâmetro em seu construtor uma expressão regular e um modificador. A expressão usada foi a mais simples possível (“azul”). O modificador "g" significa global, isto é, a expressão irá afetar todas as ocorrências na frase, caso contrário, somente a primeira seria localizada.

Após criar a expressão regular, usei-a no método replace() da String. O primeiro argumento é a expressão usada para localizar partes do texto, as quais serão substituídas pelo conteúdo do segundo argumento (“verde”).

Criando uma expressão regular “nativa”

var regex  = /azul/g;
var resultado = frase.replace(regex, "verde");

Este código faz o mesmo que o anterior, mas escrevendo a expressão regular diretamente no código.

Usando as funções split e join

var resultado = frase.split('azul').join('verde');

Explicando o código acima, a função split() da String divide nossa frase em duas partes: uma com o conteúdo anterior da palavra “azul” e outra com o conteúdo posterior. O resultado é o vetor ["O céu está ", " hoje!"]. Depois disso, a função join() do array une os itens do vetor separando-os pela palavra “verde”.

Usando a função indexOf e substring

var pos = frase.indexOf('azul');
var ultpos = 0;
var resultado = '';
while (pos >= 0) {
    resultado += frase.substring(ultpos, pos) + 'verde';
    ultpos = pos + 4;
    pos = frase.indexOf('azul', pos + 4);
}
if (ultpos < frase.length) {
    resultado += frase.substring(ultpos, frase.length);
}

O código acima procura pela ocorrência da palavra “azul” na nossa frase e, enquanto houver alguma, vai montando uma outra String com a palavra substituída.

Fazendo o teste de desempenho

O primeiro passo para analisar a eficiência das soluções foi utilizar Jsperf. Este site permite a criação de casos de teste comparativos, com quantas variações forem necessárias. Eles podem ser executados por qualquer usuário, em qualquer navegador.

Clique aqui para ver ou executar os testes no jsperf!

Alguns testes foram realizados e o gráfico na data em que escrevo o artigo é o seguinte:

string-replace-performance

Nota: o item “regex2” se refere à expressão regular “nativa” mencionada anteriormente.

Cada novo usuário que executar os testes irá contribuir para a análise, então é provável que logo o gráfico no site Jsperf esteja diferente.

Análise dos resultados

Não temos uma conclusão! Não existe um consenso sobre o método mais rápido!

Em resumo:

  • O método com regex foi o melhor nas versões 7 e 10 do Internet Explorer
  • O método com split e join ganha no Google Chrome
  • E o método com indexOf e substring venceu no Firefox e no Ópera

Minha sugestão, baseada numa análise geral, é usar expressões regulares “nativas”, isto é, sem o RegExp. Note como a barra laranja está sempre em primeiro ou segundo lugar.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Desafio de Programação: algoritmo LCA (Lower Common Ancestor)

Há algum tempo me deparei com uma code question muito interessante sobre árvores. Não, isso não tem a ver com o problema do desmatamento ou com agricultura. Code questions são desafios de programação que frequentemente envolvem conceitos de Estrutura de Dados, Álgebra, Geometria Analítica, Teoria dos Grafos e uma boa capacidade de analisar e resolver problemas. Pena que muitas vezes isso me falta! :b

Code questions podem parecer algo fora da realidade prática, mas na verdade ajudam muito com o raciocínio e, principalmente, no uso correto de estruturas de dados. Diariamente usamos listas, conjuntos, árvores, pilhas e mapas, muitas vezes de forma equivocada. Por falta de treino, frequentemente temos dificuldades de criar lógicas simples e eficientes.

O problema

A referida questão envolvia o conceito de LCA (Lower Common Ancestor – Ancestral comum mais próximo), derivado da Teoria dos Grafos. LCA é o “pai” (ancestral) mais próximo de dois nós de uma árvore.

Se você não entendeu, pense na hierarquia de uma empresa. Temos o presidente como o topo ou raiz da árvore, os gerentes logo abaixo dele e em seguida subgerentes ou subordinados. Se houver um problema entre dois funcionários quaisquer, precisamos descobrir quem pode responder por ambos. Não podemos ir direto ao presidente, nem “pular” a hierarquia, mas devemos ir ao gerente mais baixo da hierarquia que seja chefe diretamente ou indiretamente de ambos.

Bem, na verdade existe um caso onde o LCA e o exemplo da empresa não são equivalentes. Se os dois nós da árvore que estou procurando forem pai e filho, por exemplo, a resposta para o algoritmo seria o próprio pai. Porém, se numa empresa ocorre um problema entre gerente e subordinado, não deveria ser o gerente o responsável por tratar do assunto. Hum… pensando bem, isso se aplica a muitas empresas, sim.

Para entender visualmente uma árvore, considere a imagem abaixo:

grafo-exemplo

Encontrar o ancestral mais comum é simples se cada nó da árvore tiver a informação de quem é seu ancestral. Basta localizar o par de nós e compara os seus pais. Porém, sem essa informação, temos que percorrer a árvore toda armazenando a informação de parentesco antes de fazer essa comparação. Obviamente a code question exigia o caso mais difícil.

Para piorar, a maioria das implementações disponíveis são para Árvores Binárias, isto é, a quantidade de descendentes de cada nó é de no máximo em dois. Isso facilita a solução, mas minha questão era mais complicada, pois cada nó poderia ter vários filhos.

Veja a seguir os atributos de um nó da árvore:

class Node {
    Integer id;
    List<Node> filhos;
}

Para resolver essa code question, precisamos de um algoritmo para encontrar o LCA, tendo como entrada dois nós com a estrutura do código acima.

Solução recursiva

Primeiramente fiz uma implementação recursiva baseada num algoritmo publicado numa resposta do StackOverflow.

O método ficou assim:

Node findClosestCommonAncestor(Node root, Node p, Node q) {
    if (root == null) {
        return null;
    }
    if(root == p || root == q) {
        return root;
    } else {
        Node aux = null;
        //if two children contains p and q, root is the ancestor
        for (Node current : root.children) {
            Node child = findClosestCommonAncestor(current, p, q);
            if (child != null) {
                if (aux == null) {
                    aux = child;
                } else {
                    return root;
                }
            }
        }
        if (aux != null) return aux;
    }
    return null;
}

É possível editar e executar esse código no site ideone.

O algoritmo acima basicamente analisa recursivamente cada nó e verifica se dois de seus filhos são o par de nós p e q. Quando um nó puder responder por ambos, temos a resposta.

Solução iterativa

Soluções recursivas são boas, e em alguns casos podem ser as mais eficientes. Porém, é sempre um desafio pensar numa solução iterativa, isto é, usando apenas laços e estruturas como pilhas.

A Wikipedia contém uma página sobre Tree Traversal que inclui pseudo-códigos para iterar sobre árvore binárias usando pilhas. Existem várias formas de iterar sobre uma árvore. Por exemplo, o código abaixo é do Pre-order Traversal, que significa percorrer os nós começando do nó mais à esquerda até acabar no mais da direita:

iterativePreorder(node)
  parentStack = empty stack
  while (not parentStack.isEmpty() or node ≠ null)
    if (node ≠ null)
      visit(node) #1
      parentStack.push(node) #2
      node = node.left
    else
      node = parentStack.pop() #3
      node = node.right

Anotei três pontos (#1, #2, #3), os quais irei comentar mais adiante.

Entretanto, para a resolução do nosso problema não estamos limitados a dois descendentes por nó, então são necessários alguns ajustes:

  1. Uma pilha auxiliar para armazenar o filho atual sendo visitado a cada nível que o algoritmo desce na árvore. Diferente de uma árvore binária, onde basta percorrer o nó da esquerda e depois o da direita, para percorrer um nó com n filhos precisamos de um contador. E como cada filho pode ter m filhos, então deve haver um contador para cada nível da árvore.
  2. Uma segunda pilha auxiliar para armazenar o caminho até o primeiro nó encontrado. Como um dos objetivos do algoritmo é encontrar dois nós, devemos armazenar o caminho até o primeiro e continuar até encontrar o segundo.

Um pseudo-código para encontrar o ancestral mais próximo dos nós p e q, dada a raiz node, ficou assim:

findClosestCommonAncestor(node, p, q)
  parentStack = empty stack
  childIndexStack = empty stack
  firstNodePath = null
  while (not parentStack.isEmpty() or node ≠ null)
    if (node ≠ null)

      #1
      if (node == p || node == q)
        if (firstNodePath ≠ null)
          parentStack.add(node)
          int n = min(parentStack.length, firstNodePath.length)
          for i = (n - 1)..0
            if (parentStack(i) == firstNodePath(i))
              return parentStack(i)
          return null
        else
          firstNodePath = copy parentStack
          firstNodePath.push(node)

      #2
      if (not empty node.children)
        parentStack.push(node)
        childIndexStack.push(0)
        node = node.children(0)
      else
        node = null

    else

      #3
      node = parentStack.peek()
      i = childIndexStack.pop() + 1
      if (i >= node.children.length)
        node = null
        parentStack.pop()
      else
        node = node.children(i)
        childIndexStack.push(i)

Certamente ficou mais complexo, mas o conceito é basicamente o mesmo do anterior. Note que também marquei neste algoritmo três pontos, pois são análogos ao anterior. Vejamos:

  • #1 Este é o bloco onde o valor do nó atual é processado. O visit(node) do primeiro algoritmo foi substituído por um bloco que verifica se um dos nós foi encontrado. Caso tenha encontrado o primeiro nó ele salva a pilha atual. Caso tenha encontrado os dois ele compara as pilhas, item a item, procurando pelo pai mais próximo.
  • #2 O algoritmo inicial adiciona o nó atual na pilha e avança para o filho da esquerda. O segundo algoritmo generaliza para n filhos avançando para o primeiro filho (0).
  • #3 O algoritmo inicial desempilha um nó e avança para o filho da direita. O segundo algoritmo generaliza avançando para o próximo filho (anterior + 1).

O código em Java ficou assim:

class Node {
    List<Node> children = new ArrayList<Node>();
    Integer id;
    Node(Integer id) {
        this.id = id;
    }
}
Node findClosestCommonAncestor(Node node, Node p, Node q) {
    Stack<Node> parentStack = new Stack<Node>();
    Stack<Integer> childIndexStack = new Stack<Integer>();
    Stack<Node> firstNodePath = null;
    while (!parentStack.empty() || node != null) {
        if (node != null) {
            if (node == p || node == q) {
                if (firstNodePath != null) {
                    parentStack.add(node);
                    int n = Math.min(parentStack.size(), firstNodePath.size());
                    for (int i = n - 1; i >= 0; i--) {
                        if (parentStack.get(i) == firstNodePath.get(i)) {
                            return parentStack.get(i); 
                        }
                    }
                    return null;
                } else {
                    firstNodePath = new Stack<Node>();
                    firstNodePath.setSize(parentStack.size());
                    Collections.copy(firstNodePath, parentStack);
                    firstNodePath.push(node);
                }
            }
            if (!node.children.isEmpty()) {
                parentStack.push(node);
                childIndexStack.push(0);
                node = node.children.get(0);
            } else {
                node = null;
            }
        } else {
            node = parentStack.peek();
            Integer i = childIndexStack.pop() + 1;
            if (i >= node.children.size()) {
                node = null;
                parentStack.pop();
            } else {
                node = node.children.get(i);
                childIndexStack.push(i);
            }
        }
    }
    return null;
}    

A versão completa do código Java está disponível para edição e teste no ideone.com.

Considerações

Se você é um leitor ocasional, não se preocupe se não entendeu nada dos códigos acima. Isso não é algo que você pega em cinco minutos. Por exemplo, eu passei alguns dias pensando na solução iterativa. É necessário estar “imerso” no problema.

Este artigo pode parecer dissonante em relação ao conteúdo do site, mas aprendi recentemente que estudar algoritmos deve fazer parte do aperfeiçoamento de todo desenvolvedor sério. Para usar tecnologias de ponta e de alto nível isso é essencial. Na verdade, não conhecer a teoria da computação é o principal motivo pelo qual temos dificuldades em entender e aplicar melhores soluções.

Você acha que empresas como Google, Facebook ou Amazon conseguem atender milhões de usuários simultânea e ininterruptamente usando implementações padrão de mercado? Os profissionais que mantêm grandes estruturas funcionando são os que entendem profundamente os fundamentos da computação, não APIs básicas de linguagens de programação.

Por muito tempo eu mesmo menosprezei esse tipo de conhecimento, considerando que não agregaria muito à minha carreira. Eu queria algo de “alto nível” e a teoria parecia perda de tempo. Estava errado!

O mercado de trabalho brasileiro ajuda a criar essa ilusão. Veja as vagas publicadas por aí. Eles pedem lógica, Inglês e uma lista infindável de frameworks. Então você sai freneticamente aprendendo várias coisas que, na verdade, são mais do mesmo.

Então um dia você acorda e percebe que é mais um programador medíocre que só sabe uma lista de frameworks. E pior, descobre que na prática as empresas nem mesmo valorizam esse conhecimento.


Este artigo foi baseado numa questão do StackOverflow em Português!

Quanto tempo gastar com testes?

software-testing

Ao planejar um projeto, quanto tempo deve ser reservado para os testes? Não seria suficiente calcular uma porcentagem em relação ao tempo estimado para desenvolvimento? É preciso mesmo planejar isso?

No silver bullets

Não existe um método mágico e correto para estimar o tempo gasto com testes, assim como não há uma solução mágica para o problema da estimação de software.

A verdade é que mesmo especialista em testes sugerem chutar! A princípio, use um fator mágico arbitrário para determinar o tempo reservado para testes. No decorrer do projeto, ajuste a proporção conforme sua produtividade e o nível de qualidade exigido.

Valores “mágicos”

Brooks, autor do famoso livro The Mythical Man-Month, descreve no capítulo 2 sua “regra” para planejar uma tarefa:

  • 30% para planejamento
  • 20% para codificação
  • 25% para testes de componentes e testes antecipados de sistema
  • 25% para testes de sistema e de integração geral

Os valores originais estão em frações, mas adaptei para porcentagem para facilitar o entendimento. Note que os testes ocupam metade do tempo de desenvolvimento (50%), o equivalente a 2,5 vezes o tempo de codificação.

Tenho notado em minhas atividades individuais que o tempo de testes varia de 1 a 1,5 vezes o tempo de desenvolvimento, incluindo testes unitários efetivos antes da codificação cobrindo cenários com valores limite e excepcionais. Além disso, é necessário mais 1 a 1,5 vezes o tempo de desenvolvimento para testes de integração após a codificação individual.

Em resumo, posso dizer que o tempo total de testes, quando feitos adequadamente, varia entre 2 a 3 vezes o tempo de desenvolvimento. Minha observação pessoal corresponde às observações de Brooks.

Pressa: e se não testarmos?

A dura realidade é que nem todos se dão ao luxo de separar tempo suficiente para testar. O termo “luxo” aqui foi usado com um pouco de ironia, porque a verdade é que, em última análise, isso é uma questão cultural da empresa e do indivíduo. Não, não tem tanto a ver com o cliente, porque a qualidade do sistema não é responsabilidade dele.

É responsabilidade dos profissionais de TI fazer o que for necessário para garantir o sucesso do projeto de software para o bem da própria empresa e do cliente, mesmo que as demais áreas não compreendam isso. Se a qualidade exigida não for financeiramente viável, então o projeto como um todo não está bem formulado ou simplesmente deve ser arquivado.

Tenho algumas considerações importantes que talvez possam ajudar:

  1. O tempo investido em testes unitários antes da codificação efetivamente diminui o tempo de codificação, já que o desenvolvedor tem que considerar cuidadosamente as entradas e saídas. De outra forma, as pessoas tendem a iniciar a codificação de um módulo do programa sem a noção do todo e precisa constantemente revisar o que já fizeram ao se deparar com novos detalhes e requisitos.

  2. Criar testes antes da codificação pode ajudar muito no entendimento do problema, já que o desenvolvedor precisa entender os requisitos para validar os resultados através de asserções.

  3. Em projetos “legados”, que não possuem testes unitários ou automatizados, o tempo gasto com correções tende ao infinito, pois a taxa de erros nunca estabiliza. Sem testes, não podemos mensurar corretamente o impacto das alterações e correções. A cada passo para frente, damos dois para trás. Uma correção pontual pode causar efeitos colaterais não esperados em outras funcionalidades.

  4. Uma premissa falsa que muitos tomam por verdadeira é que se as partes de um sistema forem implementadas corretamente, então não haverá problemas ao juntá-las no final. Isso é sempre um erro. Mesmo grandes desenvolvedores que sempre criam código de qualidade não podem evitar testes de integração, de sistema e de aceitação.

  5. Quanto mais tarde um erro for descoberto, maior o custo para corrigi-lo. Quanto antes os testes entrarem em ação, mais cedo eles podem contribuir para encontrar esses erros e problemas em potencial. Considere a imagem abaixo, extraída do artigo The Incredible Rate of Diminishing Returns of Fixing Software Bugs:

cost-fix-project

Um tempo bem gasto com testes permitirá aos desenvolvedores terem um horizonte visível do que é esperado do sistema.

Sobre estimação de software

Em minha pós-graduação, desenvolvi uma monografia sobre estimação de software. No momento em que escolhi este tema, acreditava que iria encontrar um método mágico para determinar o tempo das atividades de um projeto. Obviamente, logo que comecei a pesquisa percebi que havia acreditado num grande engodo.

Um dos livros mais interessantes de minha bibliografia foi Software Estimation: Demystifying the Black Art de Steve McConnell, cujo título já esclarece muito sobre a essência das estimativas: elas são tentativas de prever o futuro. Estimativas são chutes, simples assim.

A consequência disso é que não existe, e nunca existirá, uma regra definitiva para estimação das atividades de desenvolvimento de software. Na verdade, métodos “matemáticos” de estimação (COCOMO, Function Point) acabam confundindo seus usuários no sentido de que estes acreditam que o resultado terá acurácia garantida, afinal há todas aquelas fórmulas matemáticas complicadas. Aliás, é comum confundirmos acurácia (resultado bom) com precisão (nível de detalhe, casas decimais), mas uma estimativa pode ser muito precisa, porém longe da realidade.

Em decorrência disso, as metodologias ágeis não usam valores absolutos para estimar, como horas e dias, mas story points (pontos de história), que são grandezas relativas que variam de acordo com a equipe, o projeto e maturidade do desenvolvedor.

Os métodos mais modernos também não buscam precisão, isto é, estimar em muito detalhe (horas, por exemplo), já que frequentemente isso diminui a acurácia. Estimar em dias, semanas ou, em alguns casos, meses pode parecer “bruto” demais, porém os resultados são mais realistas. Não concorda? Então pense: quão confiável é uma estimativa de 370 dias e 4 horas? Sinceramente, alguém pode prever tudo o que ocorrerá em praticamente um ano?

Então, cuidado com soluções mágicas que podem tentar lhe vender. Embora algumas técnicas de estimação pareçam melhores, na verdade ninguém pode afirmar absolutamente que determinado método é melhor. Fazer isto seria o mesmo que afirmar que você tem um método para jogar na loteria melhor que outras pessoas, sendo o resultado simplesmente aleatório.

Leia algumas conclusões adicionais sobre estimação de software no artigo Reflexões sobre a natureza do software e das estimativas de software.

Mas isso significa que devemos sempre “chutar” durante o planejamento? Claro que não! Podemos chutar com estilo. 😉

Estimação por analogia

Estimar é prever o futuro. Porém se as atividades de desenvolvimento de um novo projeto são análogas a experiências anteriores, os envolvidos podem ter uma boa ideia do esforço necessário para executá-las.

Isso não acaba com os imprevistos, nem garante um cronograma em dia, mas resultados empíricos mostram que uma técnica adequada e a experiência do planejador contribuem para melhores estimativas.

Uma outra abordagem indicada pelos autores e especialistas em estimação é medir e armazenar a produtividade da equipe para então projetar os resultados futuros. Individualmente, este é um dos principais objetivos do PSP (Personal Software Process ou Processo de Software Pessoal). Um dos pilares do Scrum, a Inspeção, deve permitir acompanhar o progresso e a produtividade da equipe.

Embora a estimação, tanto dos testes quanto das demais atividades do desenvolvimento de software, seja uma tarefa mais de intuição do que um processo científico, em geral observa-se uma melhora na qualidade das estimativas com o uso sadio de dados históricos e analogia das atividades novas com as já executadas.

Uma aplicação prática disso para os teste é muito simples. Imagine um cenário onde você entregou a versão inicial do produto e irá partir para a segunda fase de um projeto de N fases. Se você mediu o tempo gasto com os testes na primeira fase, calcule o desvio com relação ao tempo planejado e aplique-o no planejamento da segunda fase. O processo deve ser repetido a cada fase do projeto.

Note que isso é compatível com o conceito do Cone da Incerteza, o qual afirma que as estimativas são melhores a cada fase do projeto. Considere a imagem abaixo, extraído deste artigo):

cone-da-incerteza

Na medida em que o projeto se desenrola, em tese, podemos analisar com mais certeza o horizonte de sua conclusão. No entanto, a curva não é a mesma para todo projeto e empresa. Os autores afirmam que a curva acima é o caso ótimo e se não houver gerenciamento adequado a incerteza continuará grande mesmo na data de término do projeto!

Estimação em faixas

Isso nos leva a outro conceito: estimar em faixas de valores. Podemos usar esta técnica para estimar o melhor e o pior caso para uma determinada atividade. A diferença entre esses casos corresponde à incerteza atual.

Tenho usado faixas de valores para trabalhos como consultor independente e tem funcionado bem, principalmente em atividades que envolvem algo novo para mim, portanto não sei ao certo quanto tempo será necessário de pesquisa, desenvolvimento e testes.

Num exemplo simples, posso responder que a atividade poderá levar de 8 a 12 horas. Se tudo correr bem, pode ser até até menos. Se tudo der errado, talvez eu tenha que negociar horas a mais. Na maioria das vezes consigo acertar e cobro algo dentro da faixa.

Isso também permite ajustes relacionados a reuniões e telefonemas. Se em um projeto passei duas horas no telefone com o cliente, esse tempo é incluído no tempo da atividade. Se o cliente especificou bem a tarefa e não foi necessário muito tempo de análise, bom para ele, vai pagar menos.

É claro que isso envolve uma relação de confiança por parte dos clientes, mas a ética do profissional cedo ou tarde torna-se patente.

Estimativa ou compromisso?

Outro erro comum no dia-a-dia é confundirmos estimativas com compromisso por parte dos desenvolvedores.

Imagine que uma empresa estima os testes com uma regra mágica de 50% do tempo de desenvolvimento. Os desenvolvedores percebem que estão gastando muito mais do que isso.

Uma reação comum para não “atrasar o cronograma” é simplesmente escrever menos testes do que o planejado, como se a estimativa inicial fosse uma obrigação a ser seguida.

O correto seria revisar a estimativa inicial e não tentar se ajustar a ela. O problema é que na prática…

Frequentemente falta aos gerentes de software firmeza de fazer o pessoal esperar por um bom produto (Brooks)

A qualidade é um fator determinante

Usei a citação acima em um artigo sobre o Triângulo de Ferro que escrevi há algum tempo. Este conceito é importante pois demonstra que a qualidade possui uma relação direta e proporcional com o tempo despendido no projeto.

Isso implica em afirmar que mais qualidade exige mais tempo. Por isso, a decisão de investir em mais ou menos testes no início do projeto influenciará diretamente na qualidade final do produto.

Diminuindo o tempo despendido com testes sem prejudicar a qualidade

O título parece contradizer o que acabei de dizer. Mas, se tomarmos o conceito de separação entre atividades essenciais e acidentais do desenvolvimento como faz Brooks em No Silver Bullets, podemos dizer que, embora não haja como evitar os testes sem diminuir a qualidade, podemos diminuir as dificuldades acidentais da criação deles.

Isso pode ser alcançado de algumas formas:

  • Treinando a equipe para melhorar a produtividade
  • Usando ferramentas mais adequadas que facilitem a criação e execução dos testes
  • Investindo na automação
  • Usando tecnologias (frameworks, plataformas) que facilitem os testes

Enfim, podemos ser mais produtivos fazendo mais testes e menos trabalho repetitivo manual.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Entenda como Zebrar uma tabela com CSS

Colorir alternadamente as linhas de uma tabela é um requisito comum. A abordagem mais comum é usar um código personalizado na view para adicionar estilos alternados. No entanto, é possível fazer isso sem o uso de uma linguagem de back-end como Java, PHP ou .Net.

CSS 3

Podemos usar estilos CSS selecionando os elementos pares e ímpares, como no seguinte exemplo:

/* linhas pares (even) */
.tabela tbody tr:nth-child(even) {
    background-color: #CCC;
}
/* linhas ímpares (odd) */
.tabela tbody tr:nth-child(odd) {
    background-color: #FFF;
}

Continuando com o exemplo, agora só precisamos adicionar a classe tabela ao elemento <table> no HTML:

<table class="tabela">
    <thead>
        ....
    </thead>
    <tbody>
        ....
    </tbody>
</table>

Veja um exemplo funcional no Jsfiddle!

Que bruxaria é essa?

Para quem não conhece a sintaxe do CSS, ou nem sabe que tipo de tecnologia é essa, trata-se de um tipo de linguagem para aplicar primariamente estilos visuais nos elementos de uma página web.

Considere o seguinte exemplo:

seletor {
    atributo1: valor1;
    atributo2: valor2;
}

Esta é a estrutura de uma regra (rule) do CSS. Regras são compostas pelas seguintes partes:

  • Seletor: define quais elementos serão afetados pela regra.
  • Atributo: especifica qual atributo será afetado. Podem haver vários atributos por regra.
  • Valor: o respectivo valor de cada atributo.

Um seletor que começa com um ponto (.), como em .tabela, chama-se seletor de classes. Ele diz ao navegador para aplicar a regra aos elementos que possuem o atributo class com a respectiva classe, como em class="tabela". Um elemento pode conter várias classes, cujos nomes devem ser separados por espaços em branco, como em class="tabela outra-classe".

Voltemos agora ao exemplo do tópico anterior. O seletor.tabela tbody tr:nth-child(even) primeiramente seleciona elementos que contém o atributo class="tabela".

Em seguida, encontramos o trecho tbody. Este é um seletor de tag, isto é, ele seleciona as tags <tbody>, que define o corpo da tabela, de forma que não zebramos o título da mesma. Como isso vem depois de .tabela e é separado por um espaço em branco, incluiremos todos os elementos <tbody> filhos do elemento com class="tabela". O próximo trecho é tr, que seleciona todos os elementos <tr> filhos do elemento <tbody>. Note que, se houver uma tabela dentro de outra, as linhas da tabela mais interna também serão afetadas. Se quiséssemos especificar a seleção apenas dos filhos diretos, poderíamos usar o caractere maior (>), como em .tabela > tbody > tr.

O seletor tr é seguido de um caractere de dois pontos (:). Este é um pseudo-seletor. Ele altera o seletor anterior. Nesse caso o pseudo-seletor nth-child() permite especificar quais elementos do conjunto total de tags <tr> serão realmente incluídos. Dentro do parêntesis, poderíamos especificar um índice numérico. Por exemplo, tr:nth-child(5) iria selecionar apenas a quinta linha da tabela. Porém, usamos os valores especiais odd e even para definir índices ímpares e pares, respectivamente.

Finalmente, aplicamos a cor #CCC (um cinza claro) ao atributo background-color (cor de fundo) às linhas pares. Depois, aplicamos a cor #FFF (branco, em hexadecimal) como cor de fundo das linhas ímpares. Note que o valor #FFF é uma abreviação para #FFFFFF. Na versão com 6 letras, cada dupla de bytes representam uma cor do RGB (Red, Green, Blue).

Ufa! Entendeu? 😉

Se você lê Inglês e quer se aprofundar, recomendo a referência da fundação Mozilla.

Compatibilidade com navegadores antigos

A solução em CSS é muito legal, mas o seletor nth-child não vai funcionar no Internet Explorer 6, 7 e 8. Se precisar manter a compatibilidade com essas versões do navegador, uma alternativa é usar jQuery. O jQuery simula seletores mais novos mesmo em navegadores antigos através de código Javascript.

O seguinte trecho de código aplica a coloração em linhas ímpares e pares logo após o carregamento da página:

$(document).ready(function() {
    //linhas pares, iniciando em zero
    $('.tabela tbody tr:even').css('background-color', '#FFF'); 
    //linhas ímpares iniciando em zero
    $('.tabela tbody tr:odd').css('background-color', '#CCC'); 
});

Note que inverti odd e even. Isso é porque a versão CSS do seletor usa índices baseados em 1 (1, 2, 3, …, N), enquanto a versão jQuery usa índices de vetores Javascript, que são baseados em 0 (0, 1, 2, …, N – 1).

Veja o exemplo funcional da versão em Javascript no Jsfiddle!


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Concorrência e objetos thread-safe

1a

Certamente você já concorreu com outras pessoas em sua vida. Uma vaga na faculdade, um emprego ou mesmo a fila do banco.

Sistemas de software também enfrentam certas restrições e competem por recursos computacionais. Na verdade, cada vez que você acessa um site está concorrendo, de alguma forma, com centenas ou milhares de pessoas. Um dos maiores desafios das empresas que disponibilizam serviços na Internet é atender a todos os acessos simultâneos.

Como um engenheiro de software, pretendo agora fazer uma breve análise microscópica sobre esse problema.

O que pode dar errado?

Em geral, um sistema que visa atender vários usuários ao mesmo tempo irá processar as solicitações paralelamente em vários processos ou threads. O problema começa quando as ações de dois ou mais usuários afetam o mesmo conjunto de dados.

Vamos supor que temos um objeto com um contador compartilhado entre diversas threads:

int contador = 0;
public int incrementa() {
    contador++;
    return contador;
}

Suponha ainda que duas threads T1 e T2 estão rodando em dois processadores de uma mesma CPU. Em um dado momento elas chamam, ao mesmo tempo, o método incrementa() e ocorre a seguinte sequência de execução:

  1. Processo T1 executa a linha contador++ (total = 1)
  2. Processo T2 executa a linha contador++ (total = 2)
  3. Processo T2 retorna o valor de total, ou seja, 2
  4. Processo T1 retorna o valor de total, ou seja, 2

Obviamente isso irá causar um efeito colateral indesejado! 🙁

Note ainda que problemas podem ocorrer mesmo que apenas uma das threads faça modificações e as demais estejam lendo valores, já que elas podem ler dados desatualizados e incorrer em exceções.

Um exemplo básico seria a iteração sobre uma lista. Se uma thread remove um item da lista, outras threads que estão no meio de uma iteração podem acabar em uma exceção ArrayIndexOutOfBoundsException.

Os exemplos são inesgotáveis e os efeitos colaterais os mais bizarros!

Objetos thread-safe

Para resolvermos a situação acima, onde um objeto é acessado por múltiplas threads, precisamos construir um objeto que seja à prova de acesso simultâneo, ou seja, um objeto thread-safe. O que diacho é isso? É um objeto que, em dado contexto de uso, garante o acesso seguro a dados compartilhados por várias threads de forma concorrente sem efeitos colaterais indesejados.

Bem, na verdade, essa é metade da história. O ideal do ponto de vista de desempenho seria que não houvessem acessos concorrentes. Isso significa que, num mundo perfeito, nós conseguiríamos distribuir as tarefas igualmente entre as várias threads e cada uma poderia concluir seu trabalho independentemente, sem uso de recursos compartilhados.

Na prática, é comum precisarmos compartilhar dados, então temos que aplicar um conceito muito popular da vida real: o semáforo.

Em nossas ruas, um semáforo é o que garante que somente os veículos de uma via por vez tenham o direito de atravessar um cruzamento. Em computação, um semáforo nada mais é do que uma variável ou objeto que controla o acesso a um determinado recurso. Com isso, estamos fazendo sincronização do acesso de várias threads, analogamente à sincronização dos vários semáforos de um cruzamento.

Em Java, podemos sincronizar o acesso a um método ou bloco de código. Veja o exemplo:

int contador = 0;
public synchronized int incrementa() {
    contador++;
    return contador;
}

Neste novo exemplo, as duas threads não iriam executar o método incrementa() ao mesmo tempo e no mesmo objeto. Somente uma de cada vez adentraria o bloco com o incremento, evitando o efeito colateral descrito no tópico anterior.

Não se engane! (Ressalvas importantes)

É importante notar que a sincronização no método é equivalente a um bloco synchronized(this), o que significa que o bloqueio é feito na instância do objeto que contém o método. Se houverem duas instâncias, não haverá sincronização.

Um erro muito comum é o desenvolvedor achar que o synchronized irá resolver os problemas de concorrência dos sistemas. Isso não é verdade.

Alguns frameworks web, por exemplo, podem criar várias instâncias das classes para tratar requisições, dependendo de seu escopo, então a sincronização não iria ocorrer como esperado. Além disso, aplicações distribuídas em clusters (dois ou mais servidores) também não seriam sincronizadas mesmo usando singletons.

Mesmo que synchronized fizesse o que alguns esperam, ainda incorreríamos em um grande problema.

Gargalos!

gargalo-e1353923696197

A sincronização resolve alguns problemas, mas pode gerar outros. Se houverem muitos métodos sincronizados, gargalos começarão a surgir no sistema.

Gargalos são pontos do sistema que geram lentidão, pois várias threads precisam ficar esperando sua vez. Quanto mais sincronização fizermos, mais gargalos.

Sincronizando somente o necessário

Para resolver isso, podemos usar uma forma mais adequada de sincronização. No exemplo anterior, usamos um semáforo “global”, isto é, o próprio objeto. Isso faz com que todo acesso ao objeto seja sincronizado por um único semáforo!

A solução para isso é usar semáforos mais específicos de forma que sincronizemos somente o que é necessário. Veja o exemplo:

Integer contador = 0;
Object semaforo = new Object();
public Integer incrementa() {
    synchronized (semaforo) {
        contador++;
        return contador;
    }
}

Este simples exemplo não traz muitas vantagens em si mesmo. Mas considere um cenário onde existam outros métodos nesse objeto que não modificam a mesma variável. Poderíamos criar um semáforo para cada variável, limitando ao máximo o escopo da sincronização.

A boa prática diz que o bloqueio deve ser referente aos dados acessados e não ao objeto que os contém. Isso evita muitos bloqueios desnecessários e pode fazer toda a diferença em cenários um pouco mais complexos.

E se não houver modificações?

Objetos imutáveis são, por natureza, seguros para uso em múltiplas threads, pois não há risco de efeitos colaterais. Eles não precisam ser sincronizados para serem seguramente usados em multithreading e podem ajudar muito na melhoria do desempenho da aplicação.

Este é um dos motivos pelos quais as classes básicas do Java, como String e Integer, são imutáveis. Observe que eu estou falando que não é possível modificar o valor de uma instância dessas classes, não que você não pode atribuir outra instância a uma variável.

Então nunca haverá problemas com objetos imutáveis?

Sim e não! Não é por acaso que a definição de thread-safe fala sobre o contexto de uso.

Apenas para citar um exemplo, suponha que um mapa imutável contenha elementos do tipo ArrayList mutáveis. Se threads diferentes acessarem e modificarem essas listas, estaremos sujeitos novamente a efeitos colaterais indesejados. Então, mesmo uma coleção imutável ou adequadamente sincronizada não cobre todos os casos, pois se ela contém um elemento mutável, ainda que seja um simples POJO, o resultado final não será thread-safe.

É preciso também tomar cuidado com classes do Java que, apesar de aparentarem inocência, na realidade são inerentemente thread-unsafe. Um exemplo é classe SimpleDateFormat, que estende a classe DateFormat, a qual possui um atributo do tipo Calendar, que é mutável. Aliás, ter atributos mutáveis como o Calendar é uma fonte de problemas em potencial.

Em resumo, devemos analisar todos os objetos compartilhados por threads, incluindo seu conteúdo.

Um pouquinho da API do Java

Para tentar ajudar, a API Collections do Java nos mune com várias implementações imutáveis e thread-safe para todos os gostos.

As versões sincronizadas de classes de coleções são encontradas no pacote java.util.concurrent. Eis alguns substitutos para as classes convencionais:

A classe ConcurrentHashMap é uma versão melhorada de mapa sincronizado em relação ao HashTable, suportando manipulação por várias threads, mas sem bloquear as operações de leitura.

O método utilitário Collections.synchronizedMap(map) retorna um wrapper sincronizado de um mapa qualquer. Entretanto, mesmo métodos de leitura são sincronizados, resultando em mais gargalos com relação ao ConcurrentHashMap.

Alguns dos wrappers sincronizados disponíveis são:

public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

Além disso, também existe uma lista de wrappers imutáveis, isto é, que retornam uma versão imutável do objeto:

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);

A referência oficial dos métodos que retornam wrappers imutáveis e sincronizados está aqui.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Which component annotations to use in Spring 3

spring-logo

As the majority of modern frameworks, Spring 3 allows one to declare its components (Spring Beans) via annotations, making XML unnecessary. However, it can cause some confusion about the different annotations available that apparently do the same thing.

Defining where Spring should look for annotations

When we add an annotation to some class, we are putting a meta-information, that is, an arbitrary information that has nothing to do the code or the properties of the class, like its name, modifiers, etc.

However, Spring (as any other framework) needs to find and read annotated classes in order they can effectively be used.

This is done through XML configuration:

<context:component-scan 
    base-package="com.package.project.service,
                  com.package.project.controller"/>

Also through the annotation @ComponentScan:

@Configuration
@ComponentScan({
    "com.package.project.service",
    "com.package.project.controller"})
public class SpringConfiguration { ... }

In the above example, we defined in which packages Spring should search for classes. It’ll look for classes with component annotations in these packages and in their subpackages.

Grouping components with stereotypes

Firstly, there is the generic @Component. This annotation makes one class a bean available for dependency injection to the remaining components of the system.

On the other hand, there are more specific annotations that allow us to mark beans with stereotypes, as in UML. This way, the components can be grouped by their “type” or behavior.

If one class is annotated with @Service you can just suppose it contains business methods, high level transactions or some logic related to the application model.

The annotation @Repository makes it obvious that the component implements Repository design patter (which is not the same of DAO, even though they are somehow analogous).

Finally, the annotation @Controller associates a component with a MVC controller.

The benefits

At first it looks like frills, but actually there are a few advantages:

  • It helps separating the logical layers of the application.
  • It enables Aspect Oriented Programming (AOP), like Spring Data JPA module, which dynamically “implements” interfaces annotated with @Repository.
  • It allows you treat separately exceptions that are specific of a layer, using again the example of data access layer (@Repository), where Spring will translate specific database driver exceptions into standardized classes.
  • You can also create any new feature specific to a layer. Just use your imagination and creativity. 😉

Considerations

The performance of “component-scan” is my main reservation about annotations as a new standard against the traditional and verbose XMLs. If we configure a broad package, that is, containing many classes, it can slows down the application startup, since Spring will take some time to list all classes and check them for annotations. Maybe mapping your beans with XML will be better if startup time is an important requirement for you.


This article was based on my Answer on StackOverflow in Portuguese!

Quais anotações usar nos componentes do Spring 3

spring-logo

Como praticamente todos os frameworks modernos, o Spring 3 permite a declaração de componentes (Spring Beans) via anotação, tornando o uso de XML desnecessário. Porém, é comum haver confusão sobre as diferentes anotações que parecem fazer a mesma coisa.

Definindo quais classes o Spring deve considerar

Quando adicionamos uma anotação em uma classe, estamos colocando ali uma meta-informação, isto é, uma informação arbitrária que vai além do código executado e das características da classe como nome, modificador de acesso, etc.

Entretanto, o Spring (ou qualquer outro framework) precisa encontrar e ler a classe com a anotação para que ela possa efetivamente ser usada.

Isso é feito através de configuração XML:

<context:component-scan 
    base-package="br.com.pacote.projeto.service,
                  br.com.pacote.projeto.controller"/>

Ou ainda através da anotação de configuração @ComponentScan:

@Configuration
@ComponentScan({
    "br.com.pacote.projeto.service",
    "br.com.pacote.projeto.controller"})
public class ConfiguracaoSpring { ... }

Nos exemplos acima, especificamos os pacotes onde o Spring irá procurar por classes. Ele irá vasculhar qualquer classe com as anotações de componentes que estão nesses pacotes ou em subpacotes.

Agrupando componentes com estereótipos

Primeiro, existe uma anotação genérica @Component. Com essa anotação, uma classe passa a ser um bean disponível para injeção de dependências nos demais componentes do sistema.

Por outro lado, há também anotações mais específicas que possibilitam marcar as classes com estereótipos, assim como na UML. Dessa forma, os componentes podem ser agrupados por seu “tipo” ou comportamento.

Se uma classe é anotada com @Service pode-se facilmente pressupor que ela contém regras de negócio, transações ou lógica relacionada ao modelo da aplicação.

Se a anotação é @Repository, fica óbvio que a classe implementa o padrão de projeto Repository (que não o mesmo que DAO, mas é de alguma forma análogo).

Por fim, se a anotação é @Controller podemos associar a classe diretamente a um controlador do padrão MVC.

Alguns benefícios

Embora tudo isso a princípio pareça apenas um mero enfeite, é possível listar algumas vantagens:

  • Ajuda na separação lógica de camadas da aplicação.
  • Possibilita a utilização de Programação Orientada a Aspecto (AOP – Aspect Oriented Programming), como usado módulo Spring Data JPA, o qual “gera” dinamicamente a implementação de interfaces anotadas com @Repository.
  • Permite o tratamento pontual de exceções lançadas por camadas específicas, novamente com o exemplo da camada de acesso a dados (@Repository), onde o Spring irá traduzir as exceções de cada driver de banco de dados para classes padronizadas.
  • Você também pode criar qualquer funcionalidade que se aplique por camada, basta usar um pouco a imaginação. 😉

Considerações

Minha única ressalva com relação ao uso das anotações como novo padrão em detrimento dos tradicionais e “verbosos” XML’s é com relação ao desempenho da configuração component-scan. Se definirmos um pacote muito abrangente, isto é, que contém muitas classes, o Spring irá levar um tempo considerável listando as classes a procura das anotações. Mapear os beans em XML ajuda se o tempo de inicialização da aplicação for um requisito importante.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Página 9 de 16

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.