A compilação é uma via de mão única. É um processo irreversível. Muita coisa se perde durante a compilação.
O que os descompiladores TENTAM fazer é recuperar algo que seja o mais parecido possível com o código-fonte original. Mas, como há coisas que se perdem, não há descompilação perfeita.
Uma coisa que interfere bastante são as otimizações que o compilador faz. Por vezes o compilador pode eliminar código que ele detecta que não é alcançável. Ele pode reutilizar variáveis locais. Ele pode transformar um for em um while ou em um do-while ou vice-versa. Ele pode inverter condições em ifs e trocar o bloco “then”* pelo bloco “else” para melhorar o desempenho. Ele também vai a otimizar concatenações de Strings e expressões matemáticas. Às vezes o copilador pode inserir instruções break, continue ou return em lugares onde não haviam se ele achar que isso pode melhorar o desempenho sem mudar o comportamento do código.
Por vezes o compilador também criará atributos, métodos ou construtores ocultos (chamados de sintéticos). Um exemplo de atributo oculto é a referência que toda classe interna não-static tem para a classe externa. Retorno covariante é implementado com o uso de alguns métodos sintéticos sobrecarregados.
O compilador também perde o nome de variáveis locais e de atributos. As annotations de variáveis locais são perdidas também. Boa parte dos generics é perdida por causa do type-erasure. Algumas poucas annotations de métodos, construtores, atributos e classes também são perdidas (aquelas que não tem @Retention(RetentionPolicy.RUNTIME) ou @Retention(RetentionPolicy.CLASS)).
Comentários (inclusive javadocs), indentação e formatação do código obviamente são perdidos.
O compilador também explora algumas vantagens que são permitidas pela máquina virtual mas não são pela linguagem. Por exemplo, a JVM permite que haja dois ou mais métodos com o mesmo nome e os mesmos parâmetros, mas a linguagem java não (para a sobrecarga ocorrer, os parâmetros tem que variar). Outra complicação é que a linguagem exige que a chamada ao construtor da superclasse seja a primeira instrução de um construtor, mas a JVM não exige isso.
Daí, os descompiladores burros simplesmente convertem o que o compilador fez direto para a linguagem (o que as vezes resulta em um código muito feio e nem sempre compilável). Descompiladores mais inteligentes tentam desfazer algumas otimizações e/ou maluquices que os compiladores criam. Por fim, há diversos tipos de compiladores diferentes que geram arquivos .class diferentes a partir de um mesmo arquivo .java, daí alguns descompiladores funcionam melhor com certos compiladores do que com outros.
- ok, java não tem uma palavra chave “then”. Aqui me refiro ao bloco logo após ao “)” do if.
EDIT: Como dito pelo thingol, ainda há ofuscadores de código. Existem ferramentas (às vezes embutidas dentro dos compiladores) que tentam dificultar ao máximo o trabalho de descompiladores criando uma bagunça bem grande no .class.