18 Dec 2012

Hibernate. Аннотации против XML

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

Хотя в интернетах можно найти много споров по поводу того какой подход лучше: аннотации или XML - они в большей своей степени субъективны и зависят от личных религиозных соображений. Хотелось бы здесь описать объективные причины когда и почему один из способов может быть лучше или хуже с практической стороны.
Итак, плюсы аннотаций:

  • Это движение к стандартизации если говорить про JPA-аннотации. Это может быть неправдой если вы используете Hibernate аннотации, и вам это придется делать если маппинг будет хотя бы чуточку сложный. Однако это все равно ближе к стандарту нежели XML (конечно есть JPA XML, однако никогда не видел его применения на практике, поэтому речь здесь веду именно об hbm.xml).

  • С аннотациями проще и быстрей начинать проект, ибо не нужны доп файлы. Во время старта итак много всяких артефактов и контекстов нужно создавать, а тут еще и hbm.xml файлы.

Минусы аннотаций:

  • Аннотациями вы “загрязняете” свои классы. Это может быть неправдой если у вас достаточно простой маппинг и, особенно, когда имена сущностей совпадают с именами таблиц (что редкость..). Представьте что вам еще нужно JAXB, Lucene маппинги на сущностях делать. В какой-то момент эти все аннотации трудно будет читать.

  • Вам придется писать HQL запросы прямо в Java-классах, что неизбежно приведет к конкатенации оных. В общем читабельность таких запросов ниже плинтуса.

В то же время минусы XML:

  • Появляются те самые доп файлы.

  • Формат ниразу не стандартный, и это затруднит переход к другим JPA реализациям.

Ну и наконец плюсы XML:

  • Оставляет классы чистыми аки совесть младенца. Это особенно приятно в случае со сложными маппингами.

  • Там можно оставлять длинные HQL запросы, и они не так будут смущать глаз.

  • XML проще для начинающих, потому как есть XSD/DTD, автоподсказки и т.п. С аннотациями же нужно точно знать что куда можно ставить, и в редких случаях нагуглить решение совсем не тривиально.

  • Есть возможность замапить одну и ту же Java сущность на несколько таблиц. Пример: представьте что у вас есть продукт (StoreItem) у которого может быть набор свойств. Эти свойства могут быть самыми разными: картинка, цвет, масса и пр. Все они должны иметь определения (хотя бы название -> значение). Причем значения еще могут иметь какие-то умолчания. Итого, появляется таблица PROPERTY_DEFINITION в которую помещаются все возможные свойства и их значения. Ну и конечно есть таблица со значениями этих свойств для конкретных продуктов ITEM_PROPERTY. Когда создается StoreItem мы копируем какие-то свойства из PROPERTY_DEFINITION и помещаем их в ITEM_PROPERIY, на которую уже ссылается конкретный StoreItem. Итого мы имеем класс Property, который может маппиться на обе таблицы. Аннотации не смогут дать такую гибкость, в то время как XML может использовать entity-name.

  • Если вы делите общие классы между несколькими проектами помещая их в jar-файлы, вполне возможно, что проекты захотят по-разному мапить эти сущности на таблицы БД. Иногда это важно и у вас просто не остается выбора кроме как использовать XML (прошу заметить что я не призываю к такому дизайну, и даже наоборот, считаю подход с общими классами неправильным, однако такое случается на практике, и иногда мы просто это получаем в руки).

  • Также бывают случаи, что классы генерируются по XSD или каким-то другим определениям типов. Такие классы могут не находится в репозитории и создаваться во время сборки. Если вы хотите замапить такие классы, у вас не остается выбора кроме как XML. Однако имейте в виду, что вы скорей всего не хотите оказаться в такой ситуации, обычно DB Entities и DTO - это разные классы, иначе рефакторинг одной из сторон будет усложнять жизнь, если делаете так, то только на начальных этапах проекта с заметкой “на рефакторинг”.

  • БД-нейтральность. Если наше приложение должно поддерживать несколько СУБД, на аннотациях мы бы писали два модуля с разными сущностями и разным маппингом, потому как в реальной жизни Hibernate не настолько СУБД-нейтрален, те же стратегии для генерации PK: sequence vs. identity. В случае же с XML мы можем создать два модуля с разными маппингами.

Read more
18 Dec 2012

Шаблон проектирования Одиночка (Singleton Design Pattern)

Бывают такие моменты, когда нужно написать класс, который будет управлять глобальными вещами в системе. Например, это могут быть пулы потоков и соединений к БД, этот объект может управлять реестром, конфигурациями, в общем чем-то глобальным. Можно создать класс со статическими методами, но мы-то ООП-исты(!), объект нам даст значительно большую гибкость. Однако как же запретить создавать несколько объектов одного класса? Очень просто, смотрите и ликуйте в радостной агонии:

public class Singleton1 {
    private static Singleton1 instance = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance(){
        return instance;
    }
}

Как вы могли заметить, конструктор у нас приватный и никто, окромя самих “внутренностей” класса не может его вызвать. Проинициализировав объект instance, мы его возвращаем в статическом методе getInstance(). Теперь сколько бы раз мы не обращались к этому методу, всегда возвратиться один и тот же экземпляр.
Но есть здесь и недостатки:

  1. Статические поля инициализируются при первом обращении к классу, а т.к. в классе могут быть другие статические члены, константы, то обращение к ним вызовет и создание instance. С этим можно конечно смириться, но часто заказчик хочет, чтоб его приложение запускалось мгновенно, а если наш Одиночка - тяжеловес, он много чего дергает и инициализирует, а создаться он может при старте приложения, то это не есть гуд.
  2. В данном случае невозможно отлавливать исключение, если таковые возможны в конструкторе. Если же их не ожидается, то милости просим! Однако бывают случаи…
    Вторую проблему можно решить поместив создание объекта в блок static{..}.

Так же обе проблемы решаются другим способом создания Одиночки:

public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {
    }

    public static Singleton2 getInsance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

Как видите, instance не инициализируется сразу, это происходит в методе getInstance(). То есть при первом обращении к этому методу создастся и наш экземпляр. А при последующих обращениях - вернется созданный в первый раз объект. Это часто называют загрузкой по требованию(load on demand) или ленивой инициализацией(lazy initialization).
“Вот и все!”, скажете вы. Ан-нет, не все так просто. Этот метод годится в однопоточных приложениях, но чаще всего у приложения есть несколько запущенных параллельно потоков. Если два потока в данном случае одновременно дернут метод getInstance(), то они скорей всего инициализируют объект instance дважды. И привести это может к всеобщему хаосу. И мир поглотит тьма..

Третий вариант Одиночки позволяет избежать проблем с синхронизацией - просто пометить метод getInstance() модификатором synchronized:

public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }

Однако и здесь есть рифы под водой - синхронизированные методы снижают производительность, обращение к ним будет выполнятся в пару раз медленней, нежели к обычному методу. Плюс если приложение рассчитано на тысячи пользователей одновременно, многие потоки будут просто ждать пока же один единственный счастливчик не выйдет из метода. В общем, этот вариант никогда не слукавит, но и производительность приложения может снизиться и, возможно, даже ощутимо.

Рассмотрим теперь один из самых популярных способов создания синхронизированного Одиночки с ленивой загрузкой:

public static Singleton4 getInstance() {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }

Здесь две проверки на null! Называется это Double-Checked Locking. Теперь при первом обращении к методу несколько потоков могут пройти первую проверку на null, но во второй блок зайдет и инициализирует объект только один единственный поток! И синхронизация здесь может понадобиться лишь при первом обращении к методу. Ну что, мы убили всех на свете зайцев: и исключения теперь можем отлавливать, и синхронизация без потерь производительности есть, и про ленивую загрузку не забыли. Весь мир живет в ладу и мире, нет обиженных и обездоленных. Если вы в своих приложениях уже использовали такую конструкцию, то я вас поздравляю: вы допустили ошибку :) Эта ошибка может никогда не возникнуть, а может возникнуть раз в год, однако найти и отловить ее практически невозможно не зная того факта, что double-checked locking в Java - это проблема, которую не решить. И сразу крики: “Что за нелепость?! Да что здесь такого может произойти?!”. Так вот, минимум что может произойти вот что: в следствии оптимизаций JVM поля класса могут быть еще не все инициализированы, а ссылка уже может быть присвоена нашему instance, и второй поток поглядит, что instance уже не null, дорога свободна, гуляй-резвись поточная душа - возьмет объект, не ведая, что тот еще сырой. Но это не бага. При создании объекта сначала выделяется под него память, потом ссылке присваивается значение, а потом только вызывается конструктор. “На каждую хитрую задницу найдется свой винт!”, с лихвой воскликните вы и проблему эту решите каким-нибудь интересным способом, однако “на каждый винт найдется дупа с лабиринтом”, скажет вам JVM и обнаружится еще несколько фактов, о которых вы не знали. Если кого-то заинтересовало это и кто-то захочет попробовать свои силы, ознакомьтесь сначала с Java Memory Model + Double Check Logging.

Проблему эту может решить JDK 1.5 и выше с помощью ключевого слова volatile, которое синхронизирует обращение к объектам: private static volatile Singleton5 instance; А наш getInstance() останется без изменений. Однако жил-был такой серьезный дядька как Allen Holub, который заметил, что на мультипроцессорных машинах volatile приводит к серьезным проблемам с производительность. К тому же не во всех JVM volatile реализовано полноценно. Детальней читайте в статье выше и/или гуглите.

В общем, проблема с double-checked locking нерешима и в следующих версиях JVM не собирается исправляться. Во всяком случае, в обозримом будущем. Посему вернемся на землю грешную и сделаем выводы, что пока у нас есть только один вариант сделать безопасную синхронизацию и ленивую загрузку одновременно: это ключевое слово synchronized.

Приведу так же возможный пример с синхронизацией и lazy initialization, однако без возможности отлавливать исключения.

public class Singleton6 {
    private Singleton6() {
    }

    private static class Handler {
        private static Singleton6 instance = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Handler.instance;
    }
}

Здесь у нас есть внутренний статический класс, его поле будет инициализировано при первом обращении к нему, а также обеспечена синхронизация при создании объекта тем, что это статический класс. Называется этот способ решением Билла Пью(Bill Pugh) “Initialization on Demand Holder”. Детальней - Java спецификация и google.com. При необходимости мы можем создавать объект в блоке static{...}, тогда у нас будет так же возможность перехватывать исключения:
private static class Handler {
        private static Singleton6 instance;
		static{
		    try{
	            instance = new Singleton6();
			} catch(SomeException e){
			    blah-blah
			}
		}
    }

Этого примера я, кстати, в многоуважаемом гугле не нашел(может просто плохо искал), придумал его наш Vlad, посему все почести ему :)

Последнее, самое интересное, но мало кем используемое решение:

public enum Singleton7 {
    INSTANCE;

    private Singleton7() {
    }

    public static Singleton7 getInstance(){
        return INSTANCE;
    }
}

Как видите это не что иное, как enum. По сути enum - это тот же класс, так что его можно использовать в таких целях, однако это так же может сбить с толку читателей вашего кода, ведь енум - есть енум, так что решать все же вам. С другой стороны enum можно легко сериализовать-десериализовать, в то время как обычный объект-Одиночку - не так просто привести к консенсусу, нужно сначала поплясать с бубнами, чтоб добиться такой десериализации, при которой не будет создано несколько объектов “одиночек”. Правда кому это может понадобиться я не в курсе, но, говорят, случается..

В общем из всех этих способов вы вольны выбирать любой, который подходит под вашу конкретную ситуацию. Однако полноценно все возможности предоставляет только способ Билла Пью со статическим блоком.

Теперь о подводных камнях Одиночки.

  1. Невозможность наследования. Приватный конструктор + статический метод бьют по демографии классов - размножать детей Одиночки мы не можем и для пущей уверенности можем пометить класс как final. Да и если вы захотите расширять Одиночку - задумайтесь, ибо у вас скорей всего кривой дизайн.
  2. Одиночка нарушает принцип “single class - single responsibility”, т.к. отвечает он и за свое создание, и за первоначально доверенные цели.
  3. В тестах нельзя подсунуть классу mock на Singleton. Почему? См. пункт 1. Люди, использующие мок-фреймворки для написания тестов, поймут о чем речь.
  4. Когда Одиночка не так уж и одинок…
    4.1 Если приложение использует не одну JVM, то у вас могут создаться несколько экземпляров Одиночки. Обращаю на этот факт внимание пользователей EJB, Jini, RMI.
    4.2 Если в приложении используются несколько Class Loader’ов, то каждый из них загрузит по одному экземпляру Одиночки. Так, например, некоторые серверы приложений(application server) имеют на каждый сервлет по Class Loader’у. Если у вас возникла ситуация с несколькими загрузчиками классов, то ручное управление ими может помочь, а так же может помочь отказ от Одиночки :)
    4.3 Как уже было сказано выше, если неправильно разобраться с сериализацией, то в конце концов получите несколько объектов Singleton’a.
  5. Если вы все еще пользуетесь JDK 1.2(аминь!), то лучше не шутите с Одиночкой, т.к. в Garbage Collector’е той версии есть бага - он собирает все объекты, на которые нет внешних ссылок. То есть то, что в самом классе ссылка на объект есть, его волнует в последнюю очередь, - объект он скушает все одно.

В связи со всеми вышеуказанными недостатками Одиночки, его часто относят к антипаттерну, другие же оспаривают это мнение и говорят, что им просто нужно уметь правильно пользоваться. Те же Gang of Four(корифеи ООП) до сих пор от него не отказались, хотя с момента издания их книги о шаблонах прошло много времени и многое можно было переосмыслить. Однако и противники, и приверженцы сходятся в одном: если есть возможность не применять этот шаблон, - не применяйте, чаще всего можно найти более правильное решение.

Вроде все, что хотел сказать и что нашел по этому поводу учел, будут замечания, говорите - исправлюсь и голову склоню в поклоне.

Singleton JT Webinar

Read more
18 Dec 2012

Логирование в Hibenrate + MySQL. Конфигурация Log4j/logback

Как логировать генерируемые Hibernate’ом запросы? Как логировать параметры запросов? Может ли лог Hibernate’a лгать?

Если вы работаете с Hibernate, вам скорей всего понадобится видеть, то ли для оптимизации, то ли для дебага какие запросы и зачем он генерирует. Следующие свойства следует описать в hibernate.cfg.xml ну или передать во время конфигурирования SessionFactory.

Самое важное, это возможность увидеть SQL генерируемый Хибом:

<property name="show_sql">true</property>

Однако запросы появятся только в консоли. Дабы по-настоящему логировать SQL запросы, нужно указать следующий логер в log4j: org.hibernate.SQL=TRACE. Обратите внимание на заглавные буквы SQL.

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

<property name="use_sql_comments">true</property>

А следующая опция будет форматировать выводимый запрос, иначе запрос будет в одну строку:

<property name="format_sql">true</property>

А такой логер нужно настроить чтоб Hibernate логировал значения передаваемые в PreparedStatement вместо ? и :name

log4j.logger.org.hibernate.type = TRACE

А вот так будет выглядеть файл конфигурации для logback (groovy-версия):
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.core.ConsoleAppender

import static ch.qos.logback.classic.Level.*

appender("consoleAppender", ConsoleAppender) {
    encoder(PatternLayoutEncoder) {
        pattern = "[%d{HH:mm:ss.SSS}] [%thread] [%-5level] [%logger{36}] - %msg%n"
    }
}

/**
 * This would allow us to see parameters passed into prepared statements
 */
logger("org.hibernate.type.descriptor.sql.BasicBinder", TRACE)
/**
 * We don't want to include the whole org.hibernate.type package to get rid of noise, 
 * thus we need to include necessary classes explicitly.
 */
logger("org.hibernate.type.EnumType", TRACE)
/**
 * Shows executed SQL statements. This one is better than show_sql because the latter can log only to console. Note,
 * capitalized SQL letters, it's important.
 */
logger("org.hibernate.SQL", TRACE)
root(INFO, ["consoleAppender"])

DB/Driver Logging

Однако и это не все. Иногда Hibernate не в состоянии логировать запрос как есть, потому как он генерируется самим драйвером, так например происходит во время Batch операций. Для того, чтоб увидеть такие запросы по-настоящему (Hibernate их будет выводить как и другие запросы), нужно смотреть на логи БД или JDBC Driver’a. Рассмотрим пример MySQL. Увидеть запросы можно либо заглянув в /var/log/mysql/mysql.log (это может зависить от версии и настроек MySQL Server’a), либо указав в строке соединения параметры логирования:

jdbc.url=jdbc:mysql://localhost/hib_training?characterEncoding=UTF-8\
  &rewriteBatchedStatements=true\
  &logger=com.mysql.jdbc.log.StandardLogger\
  &profileSQL=true

Стандарный логер (он будет использоваться по умолчанию) может вас не удовлетворить потому как он будет выводить сообщения не синхронно с самим Hibernate. По этим или другим причинам вы можете настроить com.mysql.jdbc.log.Slf4JLogger либо com.mysql.jdbc.log.Jdk14Logger.

Read more
18 Dec 2012

Виды тестирования (Types of testing)

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

  • Модульные тесты. Их задача - протестировать отдельный модуль логики в полной изоляции - как от окружения, так и от других модулей. Это не обязательно должен быть один класс, это может быть их небольшая агломерация - например, в случае с одним главным классом и с несколькими вспомогательными package-private классами.
    Для того, чтоб отделить тестируемый модуль (SUT, system under test) от других модулей (DOC, dependent-on class) и от внешней среды, обычно приходят на помощь mock-фреймворки, которые подменяют все остальное на манекены-обманки (перечень библиотек можно найти внизу). Например, если у нас есть парсер HTML страничек, нам не обязательно для него лезть на реальный сайт, качать страницу и отдавать, мы можем сохранить где-то эту страницу в тестах и постоянно использовать во время тестирования. В таком случае класс доступа к внешнему ресурсу мокается и возвращает нужный контент когда его просит SUT.
    Почему они важны? Потому что они могут протестировать все - каждое условие в классе, каждый цикл, каждый метод. Таких тестов как правило больше всего.
    Существует некая классификация описанная в XUnit Test Patterns (краткое пояснение можно найти в блоге Фаулера). Мое личное мнение - эти названия высосаны из пальца и использовать их не стоит. Терминология в тестировании и так путает, а тут еще появляются Dummy, Fake, Mock, которые слишком близки по семантическому смыслу, чтоб их серьезно рассматривать в качестве инструмента для общения. Да и уровень модульного тестирования и так слишком низок, чтоб моки нужно было делить на типы. На моей практике эти термины нужны были только во время собеседований.

  • Компонентные тесты. Термин используется не так часто, однако мне очень понравился. В данном случае имеется в виду компонент системы. Для этого не нужно запускать все приложение. Обычно в наших приложениях мы делим все на слои. Такие компонентые тесты могут инициализировать все слои, мокать внешние ресурсы, и тестировать целый кусок логики. Плюсы таких тестов: а) тестируют не класс, а настоящую предметную логику б) очень быстрые по сравнению с системными тестами. Идеальные компонентные тесты - те, в которых все внешние ресурсы ограничены, однако иногда проще что-то оставить, например, если это flow использующее JMS внутри единого приложения, то сам JMS можно оставить (заменив его на ActiveMQ например), а вот обращение к внешним системам уже придется мокать, иначе мы будем тестировать и их, и в таком случае слишком много времени будет тратиться на поддержку.
    Бывает компонентными тестами называют модульные. Ну и по правде говоря компонентом вы можете назвать что угодно, так что аккуратно с этим термином.

  • Интеграционные тесты (System Integration Test, SIT). Этот термин самый перегруженный из всех, интеграционным тест могут назвать в случаях: когда просто используется внешний ресурс; когда используется агломерация классов (в нашем случае это Компонентные); когда запускается все приложение и т.п. Дайте людям повод, они назовут этот тест интеграционным. Я стараюсь не использовать по возможности этот термин, однако бывают семантически подходящие случаи: это когда вы на самом деле тестируете интеграцию с другими системами. Пример: приложение А следит за обновлениями в приложении Б и дергает приложение С. Все эти системы могут общаться по разным протоколам: JAX-RS, JAX-WS, JMS, Protobuf, нам нужно протестировать это общение потому как протокол с одной стороны мог поменяться в то время как с другой стороны он остался прежним, и тогда приложение в целом не работоспособно. Собственно этим и занимаются интеграционные тесты, они тестируют точки соприкосновения систем. Заметьте, что они не должны тестировать саму бизнес логику, иначе вы, опять же, будете тестировать несколько приложений, а вам этот геморрой не нужен.

  • Системные тесты. Это тесты, иже нуждаются в запущенном приложении. Их можно как автоматизировать, так и предоставлять работу ручным тестеровщикам. Проблема этих тестов: их трудно писать и автоматизировать потому как все окружение должно быть поднято; они медленные; их трудно поддерживать, потому как любой чих может их сломать. Таких тестов должно быть мало по сравнению с модульными и компонентными, они должны тестировать основные функции приложения и не вдаваться в детали.
    Эти тесты должны быть максимально независимыми друг от друга. Например, если у вас есть сайт где можно зарегистрироваться, то в идеале каждый тест должен регистрировать нового пользователя, это важно для распараллеливания их выполнения когда окажется что они проходят полдня.


Еще несколько терминов, хоть и ортогональных вышеприведенным (потому как они одновременно могут быть и системными например), однако очень используемых:

  • Sanity тесты - базовые системные тесты, которые определяют а работоспособно ли приложение вообще. Удобно использовать при каждом деплое дабы автоматизированно определить запустилось ли приложение удачно. Включает в себя проверку только самых-самых базовых функций системы. Вот один из примеров реализации написанный на питоне - он просто опрашивает стартовую страницу, если та отвечает HTTP 200/201, значит приложение стартовало, если же там коды ошибок, значит доставка сайта упала и сборку в Jenkins’e нужно помечать красным.

  • Smoke тесты - судя по классификации ISTQB - это то же самое, что и Sanity. Однако многие выделяют их в разные категории. Бывает говорят, что Smoke - это регрессионное тестирование главных функций дабы определить имеет ли смысл дальше тестировать или приложение уже серьезно повреждено (начинает дымиться), в то время как Sanity говорит работает ли приложение вообще. Другие трактуют эти термины в точности наоборот (хотя семантически приведенный вариант больше похож на правду). В своей команде вы просто определитесь сами что вы как называете и имейте в виду что вас могут неверно понять.

  • Story Acceptance Tests. Когда вы сформировали требования, вы можете определить тесты (и иногда даже сразу их написать), которые будут определять удовлетворяет ли функционал заведенным требованиям. Очень круто если заказчик их сам пишет, однако такая возможность редка. Если такие тесты не проходят, значит фича не является реализованной. Очень удобно такие тесты писать в виде Given some state, when doing something new, then we find a new state.

  • User Acceptance Tests - это когда вы садите настоящих пользователей (даете доступ заказчику) и просите покликать, ожидая какую-то обратную связь. Это тот этап, когда вы можете определить а то ли вообще хотел заказчик.

  • Регрессионные тесты (Regression Tests). Ваши приемочные тесты, после того как отслужили свое, и ваша фича сдана заказчику, смогут продолжать служить в форме регрессионных тестов. Такие тесты определяют не сломался ли старый функционал во время реализации новых фич или после рефакторинга. Важность этих тестов нельзя преувеличить. Если вы их автоматизировали, вы сможете делать частые релизы. Не имея же автоматизированной регрессии, вы будете вынуждены перетестировать многое вручную перед каждым релизом. Автоматизированная регрессия позволяет вам чувствовать себя уверенным человеком :)

  • Нагрузочные тесты. Определяют как ваше приложение ведет себя под нагрузкой и собственно какую нагрузку они могут выдержать. Достаточно сложные тесты, особенно учитывая, что сама нагрузка - вещь многоликая. Можно тестировать пропускную способность (напр., количество транзакций в секунду), максимальную нагрузку (когда приложение уходит в себя насовсем), однако чаще всего нас будет интересовать capacity (дам конфетку тому кто переведет это на русский :)) - количество одновременных транзакций при которых приложение будет себя вести удовлетворительно (для этого вы ставите изначально порог, к примеру, каждая страница не должна загружаться дольше чем 1 сек).

  • Тесты на выносливость (Longevity tests) - это подтип нагрузочных тестов. Отличается тем, что вы просто помещаете ваше приложение под постоянную среднюю нагрузку эдак на месяц и не перезагружаете его. Такие тесты помогают определить, например, есть ли у вас утечки памяти.


Обзор библиотек для тестирования:

  • JUnit, TestNG для запуска тестов и проверки результатов. Очень понравился Groovy JUnit, о нем можно послушать на одной из JTalks Тех Сессий.

  • Mockito, EasyMock - самые популярные библиотеки для создания моков

  • PowerMock - позволяет подменять static, final, private методы, такая библиотека помогает в legacy системах, трудно поддающихся рефакторингу

  • Awaitility - позволяет удобно работать в случаях, когда нужно ждать ответ некоторое время. Сразу оговорюсь, что это очень не эффективно, намного эффективней использовать какие-то callback’и.

  • Rest Assured, Restito - для написания тестов, работающих с REST API. Первый может тестировать REST сервер, то бишь слать запросы вашему сервису; а второй может запускать mock-сервис и тестировать как ваш REST клиент шлет запросы другим системам.

  • JBehave, Cucumber (Ruby) - фреймворки позволяющие описывать тесты в виде Given/When/Then (так называемые Specification By Example). По опыту с первым скажу, что вещь на практике не такая удобная, более подробно в Рецензии на JBehave.

Read more
18 Dec 2012

Шаблон проектирования Команда(Command Design Pattern)

Рассмотрим для начала небольшой пример.
Допустим мы разрабатываем класс, который будет включать/выключать лампу, класс Switch:

public class Switch {
    private Light light;
    public Switcher(Light light){
        this.light = light;
    }
    public void flipUp(){
        light.turnOn();
    }
    public void flipDown(){
        light.turnOff();
    }
}

Обычный класс, которому в конструктор передают объект light, его мы включаем в одном методе, выключаем в другом. Но тут произошло следующее: наше приложение стало очень популярным и мы получили заказ сделать то же самое, но теперь с телевизором. Мы конечно хотим переиспользовать старый код, дабы не придумывать все сначала. Поэтому нам приходится лезть в старый код и его модифицировать:

public class Switcher {
    private TV tv;
    public Switcher(TV tv){
        this.tv = tv;
    }
    public void tvOn(){
        tv.on();
    }
    public void tvOff(){
        tv.off();
    }
}

Однако один из принципов ОО Проектирования гласит, что класс должен быть открыть для расширения, однако закрыт для изменений. В данном случае мы нарушаем этот принцип: каждый раз, если измениться прибор, который нужно включать или выключать, нам нужно лезть в уже готовый класс, изменять его, внося в него новые ошибки..
Здесь и приходит на помощь шаблон проектирования Команда. Во всех реализациях этого шаблона есть абстракный базовый класс(или интерфейс). Его реализуют все конкретные команды. Вот он, этот интерфейс:
public interface Command {
    public void execute();
}

Приведу некоторые классы-команды:
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOff();
    }
}

public class TvOnCommand implements Command {
    private TV tv;

    public TvOnCommand(TV tv) {
        this.tv = tv;
    }

    public void execute() {
        tv.on();
    }
}

Теперь у нас есть классы, которые могут сами включать-выключать то ли телевизор, то ли лампу. Наш старый Switch теперь выглядит так:
public class Switch {
    private Command onCommand;
    private Command offCommand;

    public Switch(Command onCommand, Command offCommand) {
         this.onCommand = onCommand;
         this.offCommand = offCommand;
    }

    public void on() {
         onCommand.execute();
    }

    public void off() {
         offCommand.execute();
    }
}

Как видите, теперь в конструктор ему передаются команды, которые сами выполняют все действия, Switch теперь общается только с ними и говорит когда им нужно включить/выключить какой-то прибор. Таким образом этот класс ничего не знает ни о каких приборах, теперь единственное, что нам нужно будет сделать, чтоб он заработал с другим прибором, - это создать новые команды для нового прибора и передать их Switch’у. Все, больше не нужно изменять старую, работающую реализацию, достаточно просто добавить новую. Наш код больше не нарушает Opend/Closed Principle. Это и есть шаблон проектирования Команда.
Типичный пример для Команды - это устройство ресторана:
Покупатель(Client) делает заказ(command object), передает его Официантке(invoker), та, в свою очередь, идет к повору, клеет у него где-нибудь на стенке, этот заказ, Повар(receiver) потом видит заказ и делает еду. В данном случае, как видите, Официантка ничего не знает о Поваре, ее дело - отнести заказ куда надо. В этом и есть соль Команды: он отделяет объект, который вызывает действие от объекта, который производит это действие. Кстати, Client, Invoker, Command Object & Reciever - это стандартные имена действующих лиц в этом шаблоне.

Теперь поговорим о том, как осуществить undo-операции с помощью Команды. На самом деле это тривиальный процесс, нам всего лишь нужно сделать действие, противоположное прямому. Для начала добавим метод undo() в базовый интерфейс:

public interface Command {
    public void execute();
	public void undo();
}

Теперь реализуем его для LightOffCommand:
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOff();
    }
	
	public void undo(){
		light.turnOn();
	}
}

Как видите, ничего сложного! Конечно, наш пример уж слишком простой, однако для осуществления undo-операции хватает, как правило:

  1. Сохранения предыдущего состояния объекта. Однако это может быть довольно-таки ресурсо-емкой операцией, например, если мы создаем графический редактор, мы не можем сохранять всю графику после каждого действия.
  2. Выполнение алгоритма, противоположного прямому. То есть если произошла арифметическая операция +10, то нам всего лишь нужно запомнить число 10 и во время undo сделать -10.

Макросы
Макрос - это какой-то набор команд, который можно повторять или отменять. В том же MS Word можно создать макрос, который будет копировать какой-то текст, а потом вставлять его в другое место(текста, разумеется). Собственно здесь у нас было выполнено две команды: копирование и вставка.
Как создать такой макрос? С помощью Команды - очень просто:

public class Macros implements Command{
	private List<Command> commands;
	
	public void setCommands(List<Command> commands){
		this.commands = commands;
	}
	
	public void execute(){
		for(Command next: commands){
			next.execute();
		}
	}
}

Как вы видите, в данном примере макрос - это тоже Команда. Просто его execute() будет вызывать соответствующие методы у других Команд. Таким образом можно “записать” этот макрос и повторять переданную ему последовательность команд в строго определенном порядке.
Операция отмены в данном случае будет так же тривиальной:
public class Macros implements Command{
	private List<Command> commands;
	
	public void setCommands(List<Command> commands){
		this.commands = commands;
	}
	
	public void execute(){
		for(Command next: commands){
			next.execute();
		}
	}
	
	public void undo(){
		for(int i = commands.size() - 1; i >= 0; i--){
			commands.get(i).undo();
		}
	}
}

Все элементарно: мы просто вызываем undo у каждой команды, которая имеется в макросей. В обратном порядке, конечно же.
Многие из нас, когда приходят домой, включают свет при входе, а позже и телевизор(или компьютер ;)). Эти серые будни у нас повторяются ежедневно, посему можно было бы придумать кнопочку, которая включает нам сразу все, что нам нужно, вместо того, чтоб делать это поотдельности. Тогда наш выключатель по сути не изменится, хотя добавилась теперь возможность выключать по средством всего одной команды:
public class Switch {
    private Command onCommand;

    public Switch(Command onCommand) {
         this.onCommand = onCommand;
    }

    public void on() {
         onCommand.execute();
    }

    public void off() {
         offCommand.undo();
    }
}

Вот только команды ему будут передаваться непростые. Это наш вышеприведенный макрос, немного видоизмененный, правда:
public class Macros implements Command{
	private List<Command> commands;
	
	public Macros(List<Command> commands){
		this.commands = commands;
	}
	
	public void execute(){
		for(Command next: commands){
			next.execute();
		}
	}
	
	public void undo(){
		for(int i = commands.size() - 1; i >= 0; i--){
			commands.get(i).undo();
		}
	}
}

А вот как мы теперь это все используем:
public class Main{
	public static void main(String []args){
		TV tv = new TV();
		Light light = new Light();
	
		Command tvOn = new TvOnCommand(tv);
		
		Command lightOn = new LightOnCommand(light);
		
		List<Command> onCommands = new ArrayList<Command>();
		commands.add(tvOn);
		commands.add(lightOn);
		
		Macros onMacros = new Macros(onCommands);
		
		Switcher switcher = new Switcher(onMacros);
	}
}

Все, теперь наш выключатель работает не просто с каким-то прибором, а с целой серией приборов. При этом он даже об этом не подозревает.

Теперь коротко о применениях Команды..

Transactional behavior
Часто бывают такие ситуации, чтоб какой-то процесс или выполнился полностью, или вообще не выполнился. Это так называемые транзакции. Допустим, вы разрабатываете какой-то инсталлятор, он должен устанавливать ваше приложение, однако, если произошла какая-то проблема, он должен откатить свои изменения до 0.
Это решается следующим образом:

  1. Каждый этап установки - это Команда, которая имеет работоспособный undo().
  2. Все команды храняться в стеке(можно в макросе).
  3. При поломке, делаем undo() для всех команд, как это было в случае вышеприведенного макроса.
    Собственно это и все, что я хотел сказать по этой теме :)

Wizards
Все мы сталкивались с wizard’ами для установки ПО, для его конфигурации и тому подобное. Команда и здесь сует свой нос! Дело в том, что в этих wizard’ах есть кнопка Назад, которая возвращает на предыдущий этап, причем данные из этого этапа не потерялись - они заполнились на этой формочке наново. Как же это сделать удобно для разработчика?
А следует это делать так: данный из каждого этапа помещаются в соответствующую Команду, при этом никакого выполнения команды конечно же не происходит. При нажатии кнопки “Ходiмо Узад” данные из команды вытягиваются и суются снова на форму. Когда мы дошли до конца, где есть кнопочка “Ну усьо, приехали”, для всех команд выполняется execute(). Подробней смотрим пункт про транзакции.

Command & SWING
Дабы привести пример Команды в жизни, обратимся к SWING(вообще штука, полностью построенная на шаблонах. Респект разработчикам Sun’a). В SWING Команду можно встретить в иерархии классов, начало которым берет Action - это класс, который добавляется, к примеру, в Меню или в Тулбар кнопочки, таким образом она обеспечивает одинаковую команду для нескольких компонентов. При изменении команды, изменятся все подвязанные к ней компоненты. Однако Команда она потому, что у нее есть метод actionPerformed() - аналог наших execute(). Стоит, конечно, заметить, что в данном случае мы наблюдаем переплетение двух шаблонов - Наблюдателя(Observer) и всем знакомой Команды. Вот яркий пример, когда можно запутаться в шаблонах, если расчитывать, что в одном месте может присутствовать всего один шаблон :)

Command vs. Multi-threading
Часто, для снижения нагрузки на основной поток, команды раздают другим потокам. Делается это так: наш Invoker имеет ссылку на пул потоков, проходится по нескольким потокам и раздает все Команды, которые у него имеются. В данном случае поток даже не знает(во всяком случае не должен) о том, что он выполняет - он просто обворачивает команду и выполняет ее. Замечу, что вполне работоспособный сценарий, когда не Invoker передает потокам команды, а есть какой-то объект-посредник, который отбирает у Invoker’a объекты команд и передает потокам. В данном случае нужно так же не переборщить, т.к. не забываем, что создание потока - далеко не малопотребительная операция.

Logging
В данном случае мы говорим не про обычный вывод сообщений в лог. В данном контексте, логирование - это запись этапов работы приложения с целью его последующего восстановления. Чтоб добиться такого эффекта, нужно разбить выполнение приложения на какие-то команды, которые можно записывать на HDD(хотя бы той же сериализацией). Тогда, если приложение записывало каждый свой шаг таким вот последовательным логированием, а потом тетя Клава пролила на проводку немножко мыльной воды, и сервер упал, мы можем восстановить состояние нашего детища. Как? Да обычно - читаем эти команды с HDD, создаем их объекты и проходимся по execute() каждой команды последовательно. Вот, собственно, и вся магия. Хотя на самом деле все так просто лишь в теории - добиться такой архитектуры в крупных приложениях очень тяжело.

Заключительная часть..

Command, Action etc.
Часто возникает путанница с Командой из-за названий. Во-первых, Command и Action - это одно и то же, и не верьте тем злодеям, кто разъединяет эти два понятия. Во-вторых, Action в Struts & commandObject в Spring MVC - это не примеры шаблона Команды, это просто такие названия. Этим злодеям, которые утверждают обратное, тоже верить не следует.

  1. Client, Source, Invoker.
  2. Command Object, Action Object, Event Object.
  3. Receiver, Target.

Это примеры названий, который могут быть примером Команды, а могут и не быть по причине существования синонимов и омонимов(так что все претензии - к филологам, они всему виной). Так что будьте осторожны в терминологии, связанной с Командой. Принятые слоглашения ключевых объектов данного шаблона - это Client, Invoker, Command object, Receiver.

Правильный Command. Best Practice

  1. Command Object не должен делать все вместо получателя - слишком много обязанностей для него. При нарушении этого принципа нарушается SRP-принцип, а так же усложняется процесс создания undo() операции. Лучшая практика показывает, что Command Object должен быть настолько прост, насколько это возможно.
  2. Invoker не должен знать о Reciever’e. Главное, что предоставляет нам данный шаблон, - это отвязка Invoker’a от Reciever’а. То есть первый вообще не должен знать о последнем. Это изюминка Команды и если нарушается этот принцип, не называйте свою конструкцию Командой.
  3. Invoker не должен знать о конкретных классах Команд. Если Invoker знает какую команду он дергает, значит он знает слишком много(таких персонажей обычно убивают в фильмах). То есть по сути Вызыватель будет знать о том, что делает команда, а это неправильно, он должен ведать только о базовом, абстрактном классе.

Ну, собственно, это и весь рассказ.

Литература:

А также многие другие тропинки гугла.

Read more
18 Dec 2012

PreparedStatement (подготовленные запросы)

NB: хотя здесь и ведется повествование про PreparedStatement, большая часть оного правдива и для CallableStatement.
Как, наверное, большинству известно в JDBC можно создавать разные виды Statement’ов включая обычные Statement, PreparedStatement и CallableStatement. Здесь мы обсудим что такое PreparedStatement и зачем он нужен.
Итак, начнем с того, что СУБД при поступлении в них запросов, проверяют их синтаксис, разбирают (soft parse), оптимизируют (hard parse) и создают некий query plan – то как на самом деле уже СУБД будет выполнять запрос, какие операции она будет при этом делать.
СУБД умеют кешировать выполненные запросы, то бишь если взять запрос: select * from books. И выполнять его несколько раз, то на каком-то этапе СУБД начнет его кешировать. Кеш во многих системах – это мапа, что значит, что у нее есть ключ и значение. В данном случае в качестве ключа будет SQL запрос (а точнее его хеш), а в качестве значения – разобранный план. Когда СУБД получает очередной запрос с тем же текстом, она проверяет кеш и если там уже есть скомпилированный запрос, то она использует его вместо того, чтоб наново его разбирать (на самом деле периодически СУБД доделывают hard parse, чтоб оптимизировать запрос еще больше).
Заметьте, что в качестве ключа используется полностью тело запроса. Это значит, что запрос:
select * from books where id=1 и запрос select * from books where id=2 – не являются одинаковыми и оба будут компилироваться, что значит что мы не можем кешировать один и тот же запрос только из-за того, что параметры каждый раз разные! Эту проблему решает PreparedStatement, который имеет вид: select * from books where id=? В данном случае СУБД закеширует запрос и будет только лишь подставлять новые параметры вместо знака вопроса. Это позволяет существенно ускорить обработку запросов. Разные СУБД конечно же по-разному реализуют кеш и время, когда он будет задействован (что часто основывается на статистике или просто на указаном пользователем значении).
Что происходит под капотом:

  1. Когда выполняется connection.prepareStatement("some query") драйвер обращается к СУБД для подготовки запроса*, которая возвращает обратно идентификатор запроса (его хеш как правило) и еще некоторые данные, такие как количество параметров в запросе.
  2. При вызове executeQuery() драйвер отсылает лишь идентификатор запроса и параметры, СУБД по ID находит уже разобранный запрос и выполняет его.

Но прозорливый читатель сразу заметит, что при следующем выполнении connection.prepareStatement() снова произойдет сначала вызов prepare к БД, затем собственно вызов для выполнения конкретного запроса. Во-первых, это два вызова по сети, что не хорошо с точки зрения производительности. Во-вторых, хоть СУБД и закешировало запрос и не будет его второй раз разбирать, у нас уже есть ID запроса после первого раза, зачем нам снова лазить в БД за ним, если его можно где-то сохранить и переиспользовать? Собственно так это и реализовано в большинстве драйверов:
Connection#prepareStatement(String sql) – здесь и происходит вся магия, по переданной в метод строке драйвер проверяет объект в своем внутреннем (не СУБД!) кеше, и, если тот там есть, возвращает его; если нет – создает новый.** Это называется неявным кешированием (implicit cache)***.
Идем дальше: PreparedStatement#close() - собственно этот метод и не оправдывает своего имени в данном случае – он не закрывает на самом деле statement, а помещает его как раз таки в кеш. Физически PreparedStatement закрывается только в случае а) если соединение с БД было закрыто б) когда кеш достигает своего максимальной вместимости и нужно освобождать его от старых и малоиспользуемых statement’ов в) если кеш отключен г) если кеш не поддерживается драйвером :)
Но это еще не все. В большистве случаев приложения работают не напрямую с соединениями, создавая и закрывая их, а с пулами соединений (например, DBCP, C3P0), которые сами их создают и предоставляют вашему коду. Так вот, если в обычном случае PreparedStatements привязаны к одному соединению и не могут быть переиспользованы, то пулы позволяют каждому соединению использовать подготовленные запросы других соединений, что означает, что производительность вырастет еще больше.
Опишу пункты, которые нужно знать при работе с подготовленными запросами в MySQL (многое из этого подходит и для других СУБД):

  • Запросы должны точно совпадать (запросы со словами USERS и users - будут считаться разными) - это правда для всех СУБД****

  • Не всегда PreparedStatement кешируются с первого раза, часто их нужно выполнить по несколько раз.

  • Соединения к разным MySQL серверам, или использующие два разных протокола, или даже просто - два одинаковых соединения с разными кодировками по умолчанию, - они все будут использовать разные кеши, которые не будут переиспользоваться другими соедиениями.

  • Запрос не должен начинаться с пробелов (если честно, то не уверен, что это правда для MySQL, но уже сил нет читать документацию :) Для PostgreSQL это так).

  • Подзапросы и запросы с UNION не кешируются.

  • Запросы внутри хранимых процедур не кешируются.

  • MySQL (не драйвер, а сам сервер) до версии 5.1.17 не кеширует запросы, у версий выше есть тоже свои “пасхальные яйца”, из-за который невозможно кешировать запрос, поэтому читайте обязательно документацию.

  • Обязательно установите свойство cachePrepStmts в true, ибо по умолчанию оно отключено! Используйте параметры соединения, такие как prepStmtCacheSize и prepStmtCacheSqlLimit для конфигурации MySQL драйвера.

Какие еще плюшки нам дает PreparedStatement?

Кроме улучшения производительности, подготовленные запросы защищают от SQL Injections. Чтобы было совсем просто понять суть, пример будет очень простым и глупым. Допустим, есть функциональность на форуме, такая как “удалить пользователя”. Мы вводим в поле его имя и нажимаем на кнопку Submit. Передается запрос на сервер и мы работаем с обычным Statement, ну и для создания запроса используем конкатенацию:

Sring query = "delete from users where username=" + username;

Запомните раз и навсегда, что это плохо! Если какой-то злоумышленник в поле на форме введет следующую строку: vasia' or 'a'='a, это приведет к печальным последствиям. Результирующий запрос будет следующим:
delete from users where username='vasia' or 'a'='a'

Т.к. ‘a’ всегда ровняется ‘a’, то выражение в where всегда будет true и в результате запроса удалятся все записи из таблицы. Для избежания подобного, нужно заескейпить входящую строку. Это значит, что все входящие символы, если они представляют собой что-то, что для СУБД представялется значащим символом (например, кавычки), будут заменены на какую-то другую комбинацию символов. Делать это можно самому, можно использовать уже существующие методы/библиотеки, но в конце концов запрос выходит приблизительно такой:
delete from users where username='vasya\' or \'a\'=\'a'

То есть все кавычки в строке заменены на ', что для MySQL будет значит “воспринимай ковычку как часть строки”. Однако зачем нам это делать самим, когда все уже сделано для нас с помощью PreparedStatement? Используя его запрос будет выглядет следующим образом:
delete from users where username=?

А затем мы укажем параметр: preparedStatement.setString(1, username) И все переданное сюда будет восприниматься исключительно как текст, СУБД сама все заескейпит.

Пишите свои вопросы и комментарии, ибо чует душа - сложно и запутано написано, будем переписывать если так оно и окажется.


* Некоторые драйвера, иже не поддерживают пред-компиляцию, отсылают запрос только на этапе executeQuery().
** Заметьте, что при создании обычного Statement, никакой строки в объект соединения не передается, что значит, что они создаются каждый раз наново.
*** На самом деле некоторые JDBC драйвера (такие как Oracle) могут кешировать и обычные statement’ы. В случае Oracle JDBC Driver для этого нужно дергать implementation-specific API и оно не будет столь же эффективно, ну и плюс там есть свои заморочки. Это называется explicit statement caching.
**** Конечно может и не для всех, все я не смотрел, но для 3 СУБД из проверенных - это правда.

PS: огромной спасибо нашему Vlad’у за его блог пост по заказу, который очень помог в написании данной статьи.
PPS: Обсуждение на тему: PreparedStatements. Both DBMS & Java caching

Read more
18 Dec 2012

Primitive types, autoboxing, caching

Здесь мы обсудим детально работу с примитивами, их обертками, а также - их кешированием в Java.
Итак, во-первых, в Java есть примитивы, такие как int, double, а есть их аналоги - Integer, Double. Созданы они скорей всего для работы с generics (Map<Integer, Double>), а возможно и просто, чтоб можно было использовать примитивы как объекты, но это не суть важно. А важно то, что при работе с обертками есть несколько незаметных на первый взгляд нюансов.
Когда мы говорим про развертывание (unboxing):

Integer boxed = new Integer(10);
int primitive = boxed;
на самом деле происходит следующее: primitive = boxed.intValue(). Таким образом если наш boxed будет null, то выбросится NullPointerException (NPE). То же самое и с тем же Double, только там метод соответственно называется doubleValue()]
Когда происходит обертывание (boxing):
int primitive = 10;
Integer boxed = primitive;
на самом деле происходит следующее: Integer boxed = Integer.valueOf(10) то же самое будет и с любой другой оберткой, вызовется их valueOf(). И здесь важный нюанс, если вы заглянете в вышеприведенный метод, то окажется, что он не просто создает объект класса Integer, а смотрит является ли переданное в него число больше -127 или меньше какой-то верхней границы, если же число и взаправду входит в этот промежуток, то новый объект создаваться не будет! Это значит, что:
Integer one = 1;
Integer copyOfOne = 1;
one.equals(copyOfOne) == true
one == copyOfOne == true!
Ссылки равны, это произошло, потому как метод valueOf() вернул тот же объект. Однако:
Integer thousand = 1000;
Integer copyOfThousand = 1000;
thousand.equals(copyOfThousand) == true
thousand == copyOfThousand == false!
И это может привести к неразберихе. Запомните, что существует такой кеш, и, кстати, его можно изменять при запуске приложения. Задать верхнюю границу можно с помощью параметра: -XX:AutoBoxCacheMax=1000, который доступен в Sun JDK начиная с какого-то обновления 6й версии. Такой кеш существует так же и для Character. В классе Boolean все еще проще, там кеш постоянен и задается всегда одинаково, с помощью констант Boolean.TRUE, Boolean.FALSE. Ну и соответственно:
Boolean a = true;
Boolean b = true;
Boolean c = Boolean.TRUE;
Boolean d = new Boolean(true);
a == b == c != d == true
Остальные обертки (Double, Float) не работают с кешем, потому как они не точные и тут уже не ясно какие цифры используются чаще всего.
Также важно понимать, что все классы-обертки являются неизменяемыми (immutable), что значит, что их нельзя изменять после создания (у них нет setter’ов). Нужно заметить в таком случае, что это может привести к не эффективной работе с обертками, например:
Integer a = 10;
a = a + 1;
Во второй строке будет создан новый объект Integer, теперь объект не тот, что был раньше. То же самое произойдет и просто при:
a++. Каждый раз при инкременте, будет создан новый объект Integer и он будет присвоен ссылке а.

Теперь поговорим о чуть другом - о классе String. Он не является оберткой, однако несет с собой похожие нюансы. Итак, во-первых, строки в Java - тоже неизменяемы (для изменяемых нужно применять StringBuilder, StringBuffer). Таким образом:

String a = "a";
a += "b";
Вторая строка создаст уже новый объект и присвоит его ссылке а. Во-вторых, у строк также имеется свой кеш (его чаще называют пулом строк), который заполняется всеми строками-константами (в данном случае конснанта - это захардкодженная строка), то есть:
System.out.println("a"). Кроме того, что это создаст объект со значением “а”, этот объект еще и поместится в кеш. Из этого следует:
String a = "a";
Strinb b = "a";
String c = new String("a");
a.equals(b) == true
a.equals(c) == true
a == b == true!
a == c == false!
Также динамическую (не-константа) строку можно поместить в кеш с помощью интернирования (сейчас увидите откуда взялось такое слово):
String a = new String("a");
String b = "a";
String stringFromCache = a.intern();
b == stringFromCache == true
В данном случае не много толку было от интернирования, т.к. строка “а” и так была в константах, однако это просто демонстрация того, как можно положить строку в кеш.
Более потробно о строка и их эффективном использовании можно (и нужно, обязательно прочитайте!) прочитать в теме Правильные и неправильные примеры работы со String.

Read more