Tratamento de dados binários: BIG_ENDIAN e LITTLE_ENDIAN

Sistema binário

Antes de esquentar, será feita uma breve introdução sobre o sistema binário e o pilar fundamental da computação – o bit.


Um bit pode armazenar apenas uma informação 0 ou 1, ou desligado e ligado, ou mesmo falso e verdadeiro. Ele compõe o sistema binário, em que existem apenas dois símbolos para representar uma informação.

 

De forma análoga ao sistema decimal, podemos representar números maiores do que a quantidade de símbolos simplesmente considerando a posição do símbolo como representativa do seu valor. Por exemplo: no número 34 o símbolo 3 vale mais que o símbolo 4, pois está em uma posição que o traduz para 30 unidades, enquanto o 4 está na posição mais a direita, e é traduzido para 4 unidades.

 

Então para o sistema binário o valor 2 pode ser representado pelos símbolos 10, o valor 3 como 11, o quatro como 100, e assim por diante. Cada vez que quisermos multiplicar o valor por 2 basta adicionar um 0 no final.

 

Se usarmos 8 bits, temos um byte, que pode representar até 28 valores distintos, ou 256 valores. Se o símbolo 00000000 representa o valor 0, o 11111111 representa o valor 255. Para representar o 256 já seria necessário outro bit.

 

Dessa noção surge o conceito de representação de informações.

 

Tipos de dados

256 valores distintos não precisam representar necessariamente um valor inteiro contíguo. Podem representar caracteres, como a tabela ASCII (em que o A é o 65º símbolo, o B é o 66º, e assim por diante), ou cores, ou qualquer outro tipo de informação que caiba nesse comprimento de dados.

 

Se for necessário representar valores além de 255, vimos que pode-se usar bits extras, ou mesmo outros bytes. 2 bytes são suficientes para representar até 216 valores distintos, ou 65.536 valores.

 

Mas e para representar números negativos? Dentre as maneiras possíveis de se fazer, pode-se usar um bit para representar o sinal do número – positivo ou negativo. Assim, em quatro bits, o 0101 seria o 5, e o 1101 seria o -5, por exemplo. Mas o problema é que nessa representação (chamada de ‘complemento de um’) existem dois zeros: o 0000 e o 1000!

 

Atualmente a representação chamada ‘complemento de dois’ é a mais usada – a regra é parecida com a do ‘complemento de um’, mas os números negativos são derivados dos números positivos invertidos e adicionados a 1. Por exemplo, o 5 continua sendo o 0101, mas o -5 é o 0101 invertido (1010) somado a um, que resulta em 1011. A consequencia é que com 16 bits (ou 2 bytes) podemos representar os mesmos 65.536 valores, ou de -32.768 até 32.767. 


Tipos de dados em Java

Com a proliferação das linguagens de programação e aos diferentes tipos de uso dos hardwares e softwares, surgiram muitos tipos de representação de dados. A linguagem C, por exemplo, possui os tipos inteiros signed (com sinal) e unsigned (sem sinal). Assim com 8 bits (1 byte) pode-se representar de 0 a 255 com um unsigned byte ou de -128 a 127 com um signed byte.

 

Em Java todos os tipos inteiros são signed, ou seja, possuem sinal. São eles:

 

byte

short

int

long

 

Todos eles utilizam a representação ‘complemento de dois’.

 

Little Endians e Big Endians

A representação de um número por meio de bytes pode parecer simples a primeira vista, mas por uma questão de arquitetura de processadores e desempenho, nem sempre um número composto de vários bytes é armazenado na sequencia. O byte mais significativo, popularmente conhecido como MSB (most significant byte) pode ser o primeiro ou o último.

 

O que isso quer dizer? Um valor inteiro que usa 2 bytes para a sua representação pode ter os seus bytes armazenados como AB ou como BA, onde A é o mais significativo (aquele que possui mais valor), e B o menos significativo (aquele que possui menor valor). O número 256, por exemplo, pode ser visto de duas formas:

 

00000001 00000000

00000000 00000001

 

O primeiro caso é mais intuitivo – cada posição à esquerda é uma ordem de significância maior, como estamos acostumados a ler os números naturais. Chamamos essa representação de Big Endian, onde o bit mais significativo é o primeiro a ser armazenado.

 

O segundo caso é o chamado Little Endian, adotado pela plataforma Intel e pelos arquivos binários no Windows 95, por exemplo. Nesse caso o byte mais significativo (o MSB) é armazenado por último. Se lermos esse valor da forma errada, podemos interpretá-lo como valendo 1, mas na verdade esse 1 pertence ao byte mais significativo (que vale 28 ou 256 vezes mais que o primeiro byte).

 

Armazenamento de informações

Existem dois tipos de arquivos que o programador deve acostumar-se a trabalhar: os arquivos texto e os arquivos binários.


Arquivos texto são compostos por uma seqüência de caracteres, ou texto plano. Eles podem ser lidos por um editor simples, e geralmente são arquivos com extensão .txt; arquivos XML,  HTML, CSS; ou mesmo arquivos-fonte de linguagens de programação, como os de extensão .java, .c, .cpp, .pas, e assim por diante.

 

Naturalmente, a sua representação também varia dependendo da plataforma ou aplicação: caracteres podem ter apenas 1 byte ou 2 bytes, podendo ser codificado em ASCII, EBCDIC ou Unicode; e as convenções sobre alguns caracteres, como quebra de linha, podem variar também (ex: em Windows é o caracter 13, em Unix é o caracter 10 seguido do 13, e no Mac OS 9 ou anteriores era apenas o caracter 10). Mais detalhes sobre arquivos-texto serão assunto para outro artigo.

 

Já os arquivos binários podem ser vistos como uma seqüência de bytes, e cada byte pode representar um determinado tipo de informação. Pode ser uma seqüência de inteiros, um inteiro seguido por valores de ponto flutuante, ou mesmo um formato pré-definido, como imagens BMP, GIF, JPG; arquivos executáveis, músicas MP3, sons WAVE, arquivos compactados, vídeos, entre muitos outros.  

 

A leitura de um arquivo binário exige não só conhecer o significado de cada byte, mas também se eles estão agrupados para representar números no formato com sinal ou sem sinal, e/ou se esses números usam a representação Big Endian ou Little Endian.

 

 

Conversão de formatos em Java

Em Java todos os tipos de dados são Big Endian, ou seja, o bit mais significativo é o primeiro a ser armazenado. Essa representação é encapsulada para o usuário, então independente da plataforma onde a JVM estiver rodando, os arquivos binários Java, os sockets e representações internas serão Big Endian.

 

Porém, quando se deseja utilizar um programa Java para ler dados binários gerados por uma outra plataforma, ou mesmo pela rede, e se esses dados forem unsigned (sem sinal), ou do tipo little endian (bit mais significativo por último), é necessária fazer uma conversão de formatos. O mesmo se for desejado escrever dados em Java para serem lidos em plataformas que utilizam outro tipo de representação.

 

Existem diversas maneiras de se fazer isso em Java. Uma maneira intuitiva é ler os bytes e reorganizá-los no formato desejado. O pacote java.nio possui a classe ByteBuffer que pode ser usada para ler bytes, e a ordem dos bytes pode ser modificada pelos atributos ByteBuffer.BIG_ENDIAN e ByteBuffer.LITTLE_ENDIAN.

 

Uma maneira que julgo ser mais intuitiva é estender as classes DataInputStream, DataOutputStream ou RandomAccessFile, e criar métodos para trabalhar com o formato desejado. Dessa forma o programador pode adequar essas classes à sua necessidade.

 

Leitura de dados LITTLE_ENDIAN

Vamos considerar um exemplo simples: um arquivo binário composto por uma seqüência de bytes, onde cada 2 bytes contíguos representa um short (inteiro de 2 bytes). O primeiro número diz a quantidade de números no arquivo (com exceção dele próprio). Ex:

 

3

10

-15

228

 

Se os shorts a serem lidos forem do tipo BIG_ENDIAN e com sinal, ou seja, do mesmo tipo entendido pela plataforma Java, bastaria o seguinte procedimento para imprimir os números:

 

try {

FileInputStream fis = new FileInputStream (“C:\\shorts.bin”);

DataInputStream dis = new DataInputStream(fis);

 

short num = dis.readShort();

for(int i=0; i<num; i++)

System.out.println(dis.readShort());

 

} catch (IOException e) {

             System.err.println("IOException : " + e);

}

finally {

f.close();

}

 

Se os números seguirem a representação BIG_ENDIAN, mas forem sem sinal (unsigned), então a própria classe DataInputStream pode ser usada: ela possui os métodos readUnsignedByte() e readUnsignedShort(). Em Java, um short pode armazenar valores de -32.768 até 32.767, mas um unsigned short pode variar entre 0 e 65.535. Logo, o retorno do método readUnsignedShort() não pode ser um short, mas sim um int. O mesmo raciocínio vale para o método readUnsignedByte(), que retorna um int por conveniência, mas poderia retornar um short, embora não um byte!

 

Então a leitura de shorts BIG_ENDIAN sem sinal ficaria assim:

 

...

for(int i=0; i<num; i++)

System.out.println(dis.readUnsignedShort());

...

 

Agora vamos supor que os shorts do arquivo estejam armazenados na representação LITTLE_ENDIAN. Isso quer dizer, como já explicado anteriormente, que ao invés de seus bytes estarem na ordem ABABABAB, ela está na ordem BABABABA, onde A é o byte com maior significância.

 

No caso da leitura de um short LITTLE_ENDIAN e com sinal, as classes wrapper Short, Integer e Long oferecem os métodos reverseBytes, o que facilita a matemática (atenção: apenas a partir da JDK 5.0!):

 

            public int readShortLittleEndian() throws IOException {

                        return Short.reverseBytes(this.readShort());

            }

 

Se o short LITTLE_ENDIAN for sem sinal (unsigned), então é preciso fazer uma simples operação binária para converter o número para o desejado:

 

            public int readUnsignedShortLittleEndian() throws IOException {

                        int i = Short.reverseBytes(this.readShort());

                        return i & 0xffff;

            }

 

 

Na próxima parte do artigo, serão disponibilizados outros tipos de conversão tanto de leitura quanto de escrita de valores do tipo LITTLE_ENDIAN, além dos arquivos binários em vários formatos, e referências adicionais para o assunto.

 

 

 

 

Comments