Странное поведение в Java 8 с ByteBuffer и BitSet

Я новичок в Java и начал реализовывать отправитель UDP с помощью BitSet и ByteBuffer, по какой-то причине я получаю поведение, которого не ожидал.

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.BitSet;

public class Main
{
    public static void main(String[] args) {
        
        ByteBuffer out = ByteBuffer.allocate(2);
        BitSet byt = new BitSet(8);
        byt.set(0, true);
        byt.set(1, false);
        out.put(byt.toByteArray());
        byt.set(0, true);
        byt.set(1, false);
        byt.set(2, true);
        out.put(byt.toByteArray());
        
        System.out.println("First byte is " + out.array()[0]+ " second is " + out.array()[1]);
    }
}

где я получаю результат

First byte is 1 second is 5

что я считаю неправильным, поскольку порядок байтов неправильный

Когда я пытаюсь запустить это:

public class Main
{
    public static void main(String[] args) {
        
        ByteBuffer out = ByteBuffer.allocate(2);
        BitSet byt = new BitSet(8);
        byt.set(0, false);
        byt.set(1, false);
        out.put(byt.toByteArray());
        byt.set(0, true);
        byt.set(1, false);
        byt.set(2, true);
        out.put(byt.toByteArray());
        
        System.out.println("First byte is " + out.array()[0]+ " second is " + out.array()[1]);
    }
}

Вывод изменится на

First byte is 5 second is 0

Я считаю, что это правильный ответ.

Обратите внимание, что изменение произошло только в строке 7, при этом порядок байтов также изменился.

Быстрый тест здесь и здесь

Я новичок в Java. Так что все это может быть большим недоразумением. Спасибо, в любом случае!

BitSet имеет прямой порядок байтов, поэтому последний битовый шаблон — 00000101 (5).
g00se 18.09.2023 18:40

Почему вы думаете, что «порядок байтов неправильный»? Ожидаете ли вы, что при добавлении байта в буфер этот байт появится в более высокой позиции (большем индексе)? javadoc для allocate() : «Позиция нового буфера будет равна нулю» и для put(): «Записывает заданный байт в этот буфер в текущей позиции, а затем увеличивает позицию».

user85421 18.09.2023 19:02

И, кстати, в Java вы можете легко записывать двоичные литеральные числа, например 0b0101 (или (byte)0b0101, если необходимо, в виде байта), нет необходимости использовать BitSet

user85421 18.09.2023 19:11
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
3
53
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я считаю, что это правильный ответ.

Нисколько; вы неверно истолковываете то, что видите, ваш тест не особенно хорошо написан, чтобы это продемонстрировать.

Вот что на самом деле происходит:

BitSet byt = new BitSet(8);
byt.set(0, false);
byt.set(1, false);
out.put(byt.toByteArray());

Это ничего не пишет. Добавьте System.out.println(out.position());, чтобы увидеть это в действии.

Это потому, что спецификация BitSet полностью игнорирует размер вашего битового набора (8 в new BitSet(8) не влияет на то, что делает .toByteArray()), вместо этого он проверяет самый старший 1 бит в вашем битовом наборе и округляет его до ближайшего целого числа, делящегося на 8, и именно столько бит излучается. Учитывая, что ваш второй фрагмент содержит нулевые байты, для его представления достаточно «0 бит», и, следовательно, .toByteArray() послушно создает массив нулевой длины. И buffer.put(someZeroLenArray); послушно делает именно то, о чем вы его просите, а именно: ничего не делать и успешно. Не вносите изменений в байтовый буфер и не перемещайте позицию.'.

Так что вы:

  • Ничего не пишите. Даже 8 нулевых битов — буквально ничего.
  • Напишите «5»
  • Распечатайте первый байт (который печатает 5)
  • Выведите второй, который еще не установлен и поэтому по умолчанию печатает 0.

Вы явно думаете, что цифра «5», которую вы видите, принадлежит второму BitSet, а 0, который вы видите, — из первого.

Порядок байтов правильный (например, порядок байтов, который вы получаете, равен [A] именно тому, что вы получаете в спецификации Java, а [B] соответствует всем вариантам использования порядка байтов, в частности, включая сетевые стандарты, за исключением чипов Intel); ваши ожидания относительно того, какой порядок байтов вы ожидаете, неверны, и все, что вам нужно сделать, это скорректировать это.

В более общем плане порядок байтов здесь даже не применяется: порядок байтов — это концепция, которая применяется, когда вы пишете вещи длиной более 8 бит, но никогда этого не делаете.

Это пример порядка байтов:

ByteBuffer bb = ByteBuffer.allocate(4);
bb.putInt(1);
System.out.println("At pos 0: " + bb.get(0)); // 0
System.out.println("At pos 3: " + bb.get(3)); // 1
bb = ByteBuffer.allocate(4);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(1);
System.out.println("At pos 0: " + bb.get(0)); // 1
System.out.println("At pos 3: " + bb.get(3)); // 0

Другими словами, учитывая, что «int» требует записи 4 байтов, какой из 4 байтов, составляющих int, мы пишем первым — тот, у которого самые младшие цифры (1 здесь — «1» в терминах int — это 00000000001 из конечно), или тот, у которого больше всего (0)? При использовании LITTLE_ENDIAN первой печатается цифра 1.

Вставленный вами фрагмент кода не имеет порядка байтов. Это ни младший, ни большой порядковый номер. Биты имеют формат с прямым порядком байтов, но биты с прямым порядком байтов — это то, как это делается для всех систем и всех процессоров, о которых сегодня уместно говорить.

Ответ принят как подходящий

BitSet.toByteArray() создает массив байтов минимальной длины, необходимой для представления BitSet. Вы можете прочитать в документах:

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/BitSet.html#toByteArray()

byte[] bytes = s.toByteArray();
then bytes.length == (s.length()+7)/8 and
s.get(n) == ((bytes[n/8] & (1<<(n%8))) != 0)
for all n < 8 * bytes.length.

и s.length():

Возвращает «логический размер» этого BitSet: индекс старшего установленного бита в BitSet плюс один. Возвращает ноль, если BitSet не содержит установленных битов.

Итак, по сути, если вы храните в своем битовом наборе только false, toByteArray() вернет пустой массив.

ByteBuffer будет заполнять байты по мере их ввода. Таким образом, порядок хранения байтов совпадает с порядком, в котором вы вызываете метод .put.

Теперь давайте последуем примеру вашего кода:

Изначально ваш ByteByffer размером 2:

| x | x |
  0   1

х означает, что он пустой

Затем вы создаете BitSet: [true, false, false, false, false, false, false, false] Это массив: [1]. Затем вы помещаете его в свой ByteBuffer:

| 1 | x |
  0   1

в первой записи у вас сейчас 1.

Затем вы создаете второй битовый набор: [true, false, true, false, false, false, false, false], который равен [5] (2^0 + 2^2 = 1 + 4 = 5). Затем вы помещаете его в BytBuffer:

| 1 | 5 |
  0   1

Итак, ваш вывод: «Первый байт равен 1, второй — 5». И это правильно.

что я считаю неправильным, поскольку порядок байтов неправильный

Я не думаю, что ваша проблема как-то связана с порядком байтов: если вы ожидали увидеть «Первый байт равен 5, второй равен 1», то измените порядок добавления байтов в ByteBuffer.

Теперь давайте последуем второму примеру:

Изначально ваш ByteByffer размером 2:

| x | x |
  0   1

затем BitSet с [false, false, false, false, false, false, false, false], который равен 0 в двоичном формате. Но BitSet.toByteArray создаст пустой массив, поскольку BitSet не имеет установленных битов.

Итак, в ByteBuffe нет никаких изменений, он все еще пуст:

| x | x |
  0   1

Второй набор бит — [true, false, true, false, false, false, false, false], то есть [5], после помещения в ByteBuffer:

| 5 | x |
  0   1

Здесь, поскольку первый BitSet ничего не добавил, мы видим только результат добавления второго байта.

Итак, ваша проблема связана с тем, как BitSet создает массив при использовании toByteArray(). Вы можете отказаться от использования BitSet и использовать какой-либо другой класс, некоторые подсказки см. в этом SO: Использование Java BitSet и byte[]

[редактировать] исправить второй пример можно вручную, принимая во внимание, что нулевой массив может быть возвращен BitSet.toByteBuffer и добавлением массива [0] вручную. Конечно, то же самое можно сделать и для второго байта.

    ByteBuffer out = ByteBuffer.allocate(2);
    BitSet byt = new BitSet(8);
    byt.set(0, false);
    byt.set(1, false);
    
    byte[] byteArr = byt.toByteArray();
    if (byteArr.length == 0) {
        out.put((byte)0);
    } else {
        out.put(byteArr);
    }
    
    byt.set(0, true);
    byt.set(1, false);
    byt.set(2, true);
    
    byteArr = byt.toByteArray();
    out.put(byteArr);
    
    System.out.println("First byte is " + out.array()[0]+ " second is " + out.array()[1]);

Другие вопросы по теме