Программирование Android Java: куда подевались беззнаковые типы? Wed, May 24 2017  

Поделиться

нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Java: куда подевались беззнаковые типы? Печать
Добавил(а) microsin   

[Что произошло с беззнаковыми типами на Java?]

В языках наподобие C и C++ имеется множество целых типов различного размера: int, char, short, long (char не совсем точно целый тип, но он может использоваться и как целый тип, и много программистов используют его для целых чисел малого размера). На большинстве 32-битных систем целые числа разного размера соответствуют 1 байту, 2, 4 и 8 байтам. Однако не делайте предположение, что это всегда так, иначе допустите ошибку. С другой стороны Java (как это изначально предусмотрено для переносимости платформы на разное железо) гарантирует, что независимо от условий выполнения 'byte' всегда соответствует 1 байту, 'short' 2 байтам, 'int' 4 байтам и 'long' 8 байтам. Однако C также предоставляет беззнаковые 'unsigned' типы для каждого из этих целых, чего не делает Java. Иногда это невероятно раздражает, что Java не имеет дело с беззнаковыми типами, соответствующими огромному числу аппаратных интерфейсов (USB, Ethernet и т. д.), сетевых протоколов и форматов файлов, которые используют беззнаковые числа.

Исключение Java делает, предоставляя тип 'char', в виде 2-байтного представления unicode, в отличие от типа 'char' на языке C, который соответствует 1 байту кода ASCII. Java 'char' также может использоваться как unsigned short, например он представляет числа в диапазоне 0 .. 2^16. Единственный глюк с этими числами может произойти, когда Вы попытаетесь присвоить char типу short, и если Вы попробуете распечатать char, то получите результат в виде символа unicode вместо представления целой величины. Если Вам нужно напечатать значение char, то сначала приведите его тип к int.

Таким образом, что теперь делать, когда у нас нет беззнаковых типов? Скорее всего, Вам такая ситуация не может нравиться... Ответ заключается в том, чтобы использовать signed типы, которые по размеру больше, чем оригинальный тип unsigned. Например, используйте short для хранения байта unsigned, используйте long для хранения unsigned int (и используйте char для хранения unsigned short). О да, это весьма тупое и неэффективное решение, так как понадобится ровно в 2 раза больше памяти, чем можно было бы обойтись с беззнаковыми типами, но в действительности другого способа нет. Обратите внимание также на доступ к числам типа long - не гарантируется, что он будет атомарным - несмотря на то, что если Вы используете несколько потоков, то Вам так или иначе потребуется синхронизация.

[Как получить и передать значения, которые находятся в форме unsigned?]

Если кто-то отправляет Вам набор байтов по сети (или Вы читаете их с диска, или читаете дескрипторы USB, или делаете что-то еще с массивом чисел) то они возможно будут содержать некоторые unsigned числа, и Вам нужно предпринять какие-то гимнастические трюки, чтобы преобразовать их в типы Java более крупного размера.

Одной из проблем при этом может произойти из за порядка следования друг за другом байт (этот порядок называют endianness [5]), но в этот момент мы вероятно предполагаем (или надеемся), что независимо от того, что Вы пытаетесь читать, это нечто находится в порядке байт 'network byte order', это же 'big endian' и это же стандартное Java endianness.

Чтение из байт в сетевом порядке (network byte order)

Предположим, что мы работаем с массивом байт, и хотим как можно проще прочитать unsigned byte, затем unsigned short, и затем unsigned int.

short anUnsignedByte = 0;
char anUnsignedShort = 0;
long anUnsignedInt = 0;
int firstByte = 0;
int secondByte = 0;
int thirdByte = 0;
int fourthByte = 0;
byte buf[] = getMeSomeData();
 
// Проверка: есть ли у нас нужное количество байт?
if(buf.length < (1 + 2 + 4)) 
  doSomeErrorHandling();int index = 0;
firstByte = (0x000000FF & ((int)buf[index])); index++; anUnsignedByte = (short)firstByte;
firstByte = (0x000000FF & ((int)buf[index])); secondByte = (0x000000FF & ((int)buf[index+1])); index = index+2; anUnsignedShort = (char) (firstByte << 8 | secondByte);
firstByte = (0x000000FF & ((int)buf[index])); secondByte = (0x000000FF & ((int)buf[index+1])); thirdByte = (0x000000FF & ((int)buf[index+2])); fourthByte = (0x000000FF & ((int)buf[index+3])); index = index+4; anUnsignedInt = ((long) (firstByte << 24 | secondByte << 16 | thirdByte << 8 | fourthByte)) & 0xFFFFFFFFL;

Это выглядит сложноватым, однако все просто. Чтобы понять, как это работает, посмотрите сначала на операцию

0x000000FF & (int)buf[index]

Здесь происходит поразрядная операция И (AND), с помощью которой байт signed преобразуется в int - это делается для того, чтобы вытереть все данные, кроме младших 8 бит. Поскольку Java рассматривает byte как signed, то если беззнаковое число станет > 127, то будет установлен бит знака (числа закодированы в формате дополнения до 2) и число в Java станет отрицательным. Когда мы преобразуем это число в int, то биты от 0 до 7 буду такими же, как в беззнаковом байте, а биты от 8 до 31 будут установлены в 1. Таким образом, операция AND с константой 0x000000FF очищает эти биты 31..8. Обратите внимание, что если Вы будете использовать компактную форму записи наподобие

0xFF & buf[index]

Java будет подразумевать, что старшие биты константы 0xFF равны 0, и оператор & автоматически преобразует байт в int.

Следующая вещь, которую Вы увидите здесь, это оператор сдвига << , побитовый оператор сдвига влево. Он сдвигает последовательность бит числа влево столько раз, сколько указано в правой части оператора. Например, если int foo = 0x000000FF, то (foo << 8) == 0x0000FF00, и (foo << 16) == 0x00FF0000.

Последняя часть в этой головоломке это оператор |, побитная операция ИЛИ (OR). Предположим, что мы загрузили два байта в отдельные числа int, так что получили 0x00000012 и 0x00000034. Теперь, чтобы получить 16-битное число из этих байт, нужно сдвинуть один из них влево на 8 бит, получить 0x00001200 и 0x00000034, и затем склеить их друг с другом. Это как раз и делает операция OR, получится 0x00001200 | 0x00000034 = 0x00001234. Это число можно хранить как беззнаковое целое в типе Java 'char'.

Подобным образом поступают, когда нужно сохранить unsigned int, для этого преобразуют число в тип long. В этом случае нужно сделать операцию побитного AND с числом 0xFFFFFFFFL (обратите внимание на суффикс L, который говорит Java, что это целый тип 'long').

Запись байт в network byte order

Предположим, что нужно записать значения, которые мы прочитали выше, в тот же самый буфер. Ранее мы читали буфер как unsigned byte, затем unsigned short, и затем unsigned int, а теперь нужно записать туда же в обратном порядке (по некой тайной причине) сначала unsigned int, затем unsigned short, и наконец unsigned byte.

buf[0] = (anUnsignedInt & 0xFF000000L) >> 24;
buf[1] = (anUnsignedInt & 0x00FF0000L) >> 16;
buf[2] = (anUnsignedInt & 0x0000FF00L) >> 8;
buf[3] = (anUnsignedInt & 0x000000FFL);
buf[4] = (anUnsignedShort & 0xFF00) >> 8; buf[5] = (anUnsignedShort & 0x00FF);
buf[6] = (anUnsignedByte & 0xFF);

[Подробнее о порядке байт (endianness)]

Что это вообще значит, и почему об этом нужно заботиться? Что означают термины Endianness и Network Byte Order?

Endianness означает, в каком порядке следуют байты друг за другом при хранении чисел, размер которых превышает 1 байт. Java использует порядок байт 'big endian', также известный как 'network byte order'. Процессоры Intel x86 являются архитектурой с порядком байт 'little endian' (за исключением программы Java, работающей на чипе Intel). Файл данных, созданный в системе x86, будет вероятно (но необязательно) закодирован в little endian. Файл данных, созданный программой Java, вероятно (но необязательно) будет закодирован в big endian. Любая система может выводить данные в любом формате, каком захочет, так что возможно что Вы будете с этим работать, и будьте осторожны при записи данных в порядке 'network byte order', что то же самое что и 'big endian'.

"Byte order" или "Endianness" относятся к тому, как какой-то отдельный компьютер хранит числа в памяти. Обычно компьютер бывает либо "big endian", либо "little endian".

Это нужно принимать во внимание, поскольку если Вы предположите, что данные записаны в формате 'big endian', и напишете свой код соответственно, а дата на само деле должны быть в формате 'little endian', то на выходе получится мусор. То же самое произойдет и наоборот, когда Вы предположите 'little endian' для данных, которые на самом деле 'big endian'.

Каждое число, независимо от того, представлено ли оно как десятичные цифры в виде 500000007 или как байты, например как то же самое число в шестнадцатеричном виде 0x1DCD6507, может считаться строкой цифр. И эта строка цифр может считаться имеющей начало или левый край, и конец, или правый край. В английском и русском языках считается, что первая цифра в числе всегда соответствует самому старшему разряду (или считается наиболее значимой) - например цифра 5 в числе 500000007 имеет действительное значение 500000000. Последняя цифра в числе всегда самая маленькая по весу (наименее значимая) - например цифра 7 в числе 500000007 представляет значение 7.

Когда мы говорим об Endianness, или о порядке следования байт, то имеем в виду то же самое, когда записываем цифры в определенном порядке. Когда мы пишем самую значимую цифру первой, и затем за ней менее значимую и так далее, пока не дойдем до последней, наименее значимой цифры? Или мы должны начать записывать с самой маленькой по значению цифры? В English мы всегда пишем самую значимую цифру первой, так что English может считаться архитектурой "big endian" (наверное есть человеческие языки, которые делают все наоборот).

В примере выше значение 500000007 в шестнадцатеричном виде будет 0x1DCD6507. И если мы разобьем это число на отдельные байты, то получим байты со значениями 0x1D, 0xCD, 0x65 и 0x07. Десятичные значения этих байт буду соответственно 29, 205, 101 и 7. Самый старший из этих байт 29, он представляет число 29 * 256 * 256 * 256 = 486539264. Второй по значимости байт 205, он представляет число 205 * 256 * 256 = 13434880. Третий байт 101 представляет число 101 * 256 = 25856, и наименее значимый байт 7 представляет число 7 * 1 = 7. Все эти числа в сумме 486539264 + 13434880 + 25856 + 7 = 500000007.

Когда компьютер сохраняет эти 4 байта в памяти, скажем по адресам 2056, 2057, 2058 и 2059, то встает вопрос - как от будет эти байты сохранять? Он может положить 29 по адресу 2056, 205 в 2057, 101 в 2058 и 7 в 2059, в таком же порядке, как пишутся числа на English. Если это так, что архитектура компьютера считается "big endian". Однако компьютеры с другой архитектурой могут поступить наоборот, и сохранить 7 в 2056, 101 в 2057, 205 в 2058 и 29 в 2059. В этом случае компьютер имеет архитектуру "little endian".

Обратите внимание, что это также относится и к тому, как компьютер сохраняет 2-байтовые значения short и 8 -байтовые long. Также помните, что самый старший байт часто называют "Most Significant Byte" (MSB) и самый младший называют "Least Significant Byte" (LSB), так что Вы часто будете видеть эти фразы или их акронимы.

На чистом Java порядок байт всегда один и тот же, так что можно забыть об используемой платформе, пока Вы пишете только на Java.

Но что получится, когда мы работаем с данными, которые были сгенерированы в других языках программирования? Теперь на это необходимо обратить внимание. Вы должны быть уверены, что декодируете байты в том же порядке, в каком они были изначально закодированы, и точно так же должны убедиться, что кодируете данные так, как они будут потом декодироваться. Порядок байт должен быть задан в спецификации API или платформы, с форматом данных которой работаете.

Большая проблема состоит в том, что нужно постоянно помнить текущий порядок байт, и знать порядок байт, в котором Вы байты читаете. Если эти порядки не совпадают, то нужно корректно переорганизовать данные, или в случае работы с беззнаковыми числами, как было показано выше, Вам нужно удостовериться, что помещаете нужные байты в корректные части чисел int/short/long.

[При чем тут Network Byte Order?]

Когда был разработан протокол Интернета Internet Protocol (IP), порядок байт "big endian" был обозначен как "network order". Все числовые значения в заголовках пакета IP сохранены в порядке "network order". Порядок байт на компьютере, который создает пакеты, назвали "host order", хотя он мог бы быть тем же самым, что и в "network order", если архитектура компьютера использует "big endian". Вот почему порядок байт Java называют как "network order", потому что и Java, и сетевые протоколы используют порядок байт "big endian".

[Почему в Java не предоставлена поддержка типов unsigned?]

Хороший вопрос. Это всегда казалось несколько странным, в особенности если принять во внимание широкое использование беззнаковых чисел в сетевых протоколах. Неужели разработчики решили, что беззнаковые типы - это лишнее усложнение, и их мало кто использует? Вот цитата интервью [3] с James Gosling, которая может подсказать о причине такого поступка:

> Q: Программисты часто говорят об удобствах и неудобствах при программировании на "простом языке". Что эта фраза значит для Вас, действительно ли [C/C++/Java] является простым в Вашем представлении?
...
> Gosling: Для меня как разработчика языка слово "простой" в действительности означает нечто, что я ожидал от Java. На самом деле можно сказать, что в C/C++ есть отдельные моменты, которые никто в действительности не понимает. Если опросить разработчика C по поводу unsigned, то Вы скоро обнаружите, что разработчики C фактически не понимают то, что происходит с числами без знака, и какая арифметика при этом работает. Вещи наподобие этой делают C сложным. Языковая часть Java, как мне кажется, очень проста.

Вот еще один источник [4]:

Кое что про Oak ...
Heinz Kabutz 15 июля 2003
...
> Пытаясь заполнить свои пробелы знаний истории Java, я начал изучать сайт Sun, и случайно наткнулся на язык Oak (спецификация Oak version 0.2). Oak был оригинальным именем того языка, что сейчас известен как Java, и это было самое старое руководство, доступное для Oak (т. е. Java).
...
> Unsigned integer values (Section 3.1)
> В спецификации говорится: "Четыре целых типа с шириной 8, 16, 32 и 64 бита, являются знаковыми (signed), за исключением того, когда они снабжены префиксом-модификатором unsigned.
>
> На боковой панели сказано: "Поддержка unsigned пока не реализована; может быть, что это никогда не будет сделано".

Evangelos Haleplidis написал набор классов, которые можно использовать для упрощения чтения беззнаковых типов на Java [1].

[Ссылки]

1. Java and unsigned int, unsigned short, unsigned byte, unsigned long, etc. site:darksleep.com.
2. Java Unsigned Bytes site:sites.google.com.
3. The C Family of Languages: Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling site:www.gotw.ca.
4. Doing Things with Java that Should Not Be Possible site:www.artima.com.
5. Порядок следования байт (endianness).

 

Добавить комментарий


Защитный код
Обновить

Top of Page