2.6. «Дырявые» абстракции

Очень важно уметь пользоваться стандартной библиотекой языка программирования. Это существенно повышает качество исходного кода, как внутреннего показателя качества. Уметь пользоваться – значит представлять себе, как работает библиотека, понимать, как и где лучше использовать тот или иной класс или метод.

Это справедливо для любых механизмов, которые используются: механизмов языка программирования, операционной системы или чего-либо еще.

С среде разработчиков программного обеспечения существует «закон дырявых абстракций» [11], грозящих каждому, кто недостаточно хорошо понимает механизмы работы своих инструментов. Закон гласит, что любой механизм, упрощающий что-либо (абстрагирующий сложность какого-либо действия от программиста), на самом деле не до конца скрывает свою реализацию. Если не знать, как это работает, можно столкнуться с очень непонятными проблемами, которые вообще-то нужно предвидеть заранее.

Яркий пример «дырявой» абстракции – сборка мусора в Java. Логично предположить, что раз программное обеспечение разрабатывается на Java, Вам не стоит беспокоиться о распределении памяти – виртуальная машина все сделает за Вас. Однако некоторые проблемы все же остаются: абстракция дает течь.

Одна из таких проблем – утечки памяти (memory leaks). Дело в том, что ненужные объекты уничтожаются, только если на них нет ссылок. Если описывается, например класс стека, используя массив (не нужно этого делать, такой класс есть в стандартной библиотеке; пример взят из [5]), то есть риск в операции pop допустить следующую ошибку:

public Object pop() {
                    return stack[top––];
                    }

Ошибка в том, что в той ячейке массива, из которой «удалили» объект, осталась ссылка на него. Этот объект не будет удален, пока в эту ячейку массива не запишут что-то другое или пока сам стек не будет освобожден. Правильнее было бы писать так:

public Object pop() {
    Object result = stack[top];
    stack[top] = null; // Обнулить ссылку, не удерживать возвращаемый объект
    top––;
    return result;
}

Итак, замечательный сборщик мусора не спасет, если не известно, как он работает, и не отслеживается обнуление ссылок — абстракция оказалась дырявой. Такая же проблема может возникнуть при неаккуратном кэшировании промежуточных результатов вычислений и во многих других случаях.

Другой пример: конкатенация строк (также приводится в [12]). Есть массив строк, нужно объединить все строки из этого массива в одну. Напишем так:

String result = "";
for (int  i = 0; i < array.length; i++) {
    result += array[i];
}

Все работает. Но очень медленно. Кроме того, создается очень много лишних объектов. Дело в том, что строки в Java неизменяемы, т.е. нельзя приписать одну строку в конец другой, не создавая нового объекта. Из-за этого на каждом шаге цикла создается новый объект String, в который копируется содержимое обеих строк. Таким образом, трудоемкость этого цикла очень велика, к тому же создаются ненужные объекты. Абстракция снова дала течь: нужно думать о том, как это работает.

Для решения этой задачи нужно использовать специальный класс StringBuffer, представляющий изменяемые строки.

StringBuffer buffer = new StringBuffer();
for (int  i = 0; i < array.length; i++) {
    buffer.append(array[i]);
}

Здесь не создается лишних объектов, а добавление новой строки в буфер занимает время, пропорциональное длине этой строки.

Еще одной дырявой абстракцией являются вещественные числа с плавающей точкой: потому что невозможно получить без округления ответ 0.1, используя тип double в Java?

Итак, закон «дырявых» абстракций: любая нетривиальная абстракция в какой-то мере является «дырявой».

Необходимо всегда думать о том, как реализована та или иная абстракция.