Вступление


Что вы знаете о обработке строк в Java? Как много этих знаний и насколько они углублены и актуальны? Давайте попробуем вместе со мной разобрать все вопросы, связанные с этой важной, фундаментальной и часто используемой частью языка. Наш маленький гайд будет разбит на две публикации:

  1. String, StringBuffer, StringBuilder (реализация строк)
  2. Pattern, Matcher (регулярные выражения)

Реализация строк на Java представлена тремя основными классами: String, StringBuffer, StringBuilder. Давайте поговорим о них.

String


Строка — объект, что представляет последовательность символов. Для создания и манипулирования строками Java платформа предоставляет общедоступный финальный (не может иметь подклассов) класс java.lang.String. Данный класс является неизменяемым (immutable) — созданный объект класса String не может быть изменен. Можно подумать что методы имеют право изменять этот объект, но это неверно. Методы могут только создавать и возвращать новые строки, в которых хранится результат операции. Неизменяемость строк предоставляет ряд возможностей:

  • использование строк в многопоточных средах (String является потокобезопасным (thread-safe) )
  • использование String Pool (это коллекция ссылок на String объекты, используется для оптимизации памяти)
  • использование строк в качестве ключей в HashMap (ключ рекомендуется делать неизменяемым)

Создание


Мы можем создать объект класса String несколькими способами:

1. Используя строковые литералы:

String habr = "habrahabr";

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

System.out.print("habrahabr"); // создали объект и вывели его значение

2. С помощью конструкторов:

String habr = "habrahabr";
char[] habrAsArrayOfChars = {'h', 'a', 'b', 'r', 'a', 'h', 'a', 'b', 'r'};
byte[] habrAsArrayOfBytes = {104, 97, 98, 114, 97, 104, 97, 98, 114};
 
String first = new String();
String second = new String(habr);

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

String third = new String(habrAsArrayOfChars); // "habrahabr"
String fourth = new String(habrAsArrayOfChars, 0, 4); // "habr"

Конструкторы могут формировать объект строки с помощью массива символов. Происходит копирование массива, для этого используются статические методы copyOf и copyOfRange (копирование всего массива и его части (если указаны 2-й и 3-й параметр конструктора) соответственно) класса Arrays, которые в свою очередь используют платформо-зависимую реализацию System.arraycopy.

String fifth = new String(habrAsArrayOfBytes, Charset.forName("UTF-16BE")); // кодировка нам явно не подходит "?????"

Можно также создать объект строки с помощью массива байтов. Дополнительно можно передать параметр класса Charset, что будет отвечать за кодировку. Происходит декодирование массива с помощью указанной кодировки (если не указано — используется Charset.defaultCharset(), который зависит от кодировки операционной системы) и, далее, полученный массив символов копируется в значение объекта.

String sixth = new String(new StringBuffer(habr));
String seventh = new String(new StringBuilder(habr));

Ну и наконец-то конструкторы использующие объекты StringBuffer и StringBuilder, их значения (getValue()) и длину (length()) для создания объекта строки. С этими классами мы познакомимся чуть позже.

Приведены примеры наиболее часто используемых конструкторов класса String, на самом деле их пятнадцать (два из которых помечены как deprecated).

Длина


Важной частью каждой строки есть ее длина. Узнать ее можно обратившись к объекту String с помощью метода доступа (accessor method) length(), который возвращает количество символов в строке, например:

public static void main(String[] args) {
    String habr = "habrahabr";
    // получить длину строки
    int length = habr.length();
    // теперь можно узнать есть ли символ символ 'h' в "habrahabr"
    char searchChar = 'h';
    boolean isFound = false;
    for (int i = 0; i < length; ++i) {
        if (habr.charAt(i) == searchChar) {
            isFound = true;
            break; // первое вхождение
        }
    }
    System.out.println(message(isFound)); // Your char had been found!
    // ой, забыл, есть же метод indexOf
    System.out.println(message(habr.indexOf(searchChar) != -1)); // Your char had been found!
}

private static String message(boolean b) {
    return "Your char had" + (b ? " " : "n't ") + "been found!";
}

Конкатенация


Конкатенация — операция объединения строк, что возвращает новую строку, что есть результатом объединения второй строки с окончанием первой. Операция для объекта String может быть выполнена двумя способами:

1. Метод concat

String javaHub = "habrhabr".concat(".ru").concat("/hub").concat("/java");
System.out.println(javaHub); // получим "habrhabr.ru/hub/java"
// перепишем наш метод используя concat
private static String message(boolean b) {
    return "Your char had".concat(b ? " " : "n't ").concat("been found!");
}

Важно понимать, что метод concat не изменяет строку, а лишь создает новую как результат слияния текущей и переданной в качестве параметра. Да, метод возвращает новый объект String, поэтому возможны такие длинные «цепочки».

2. Перегруженные операторы "+" и "+="

String habr = "habra" + "habr"; // "habrahabr"
habr += ".ru"; // "habrahabr.ru"

Это одни с немногих перегруженных операторов в Java — язык не позволяет перегружать операции для объектов пользовательских классов. Оператор "+" не использует метод concat, тут используется следующий механизм:

String habra = "habra";
String habr = "habr";
// все просто и красиво
String habrahabr = habra + habr;
// а на самом деле
String habrahabr = new StringBuilder()).append(habra).append(habr).toString(); // может быть использован StringBuffer

Используйте метод concat, если слияние нужно провести только один раз, для остальных случаев рекомендовано использовать или оператор "+" или StringBuffer / StringBuilder. Также стоит отметить, что получить NPE (NullPointerException), если один с операндов равен null, невозможно с помощью оператора "+" или "+=", чего не скажешь о методе concat, например:

String string = null;
string += " habrahabr"; // null преобразуется в "null", в результате "null habrahabr"
string = null;
string.concat("s"); // логично что NullPointerException

Форматирование


Класс String предоставляет возможность создания форматированных строк. За это отвечает статический метод format, например:

String formatString = "We are printing double variable (%f), string ('%s') and integer variable (%d).";
System.out.println(String.format(formatString, 2.3, "habr", 10));
// We are printing double variable (2.300000), string ('habr') and integer variable (10).

Методы


Благодаря множеству методов предоставляется возможность манипулирования строкой и ее символами. Описывать их здесь нет смысла, потому что Oracle имеет хорошие статьи о манипулировании и сравнении строк. Также у вас под рукой всегда есть их документация. Хотелось отметить новый статический метод join, который появился в Java 8. Теперь мы можем удобно объединять несколько строк в одну используя разделитель (был добавлен класс java.lang.StringJoiner, что за него отвечает), например:

String hello = "Hello";
String habr = "habrahabr";
String delimiter = ", ";

System.out.println(String.join(delimiter, hello, habr));
// или так
System.out.println(String.join(delimiter, new ArrayList<CharSequence>(Arrays.asList(hello, habr))));
// в обоих случаях "Hello, habrahabr"

Это не единственное изменение класса в Java 8. Oracle сообщает о улучшении производительности в конструкторе String(byte[], *) и методе getBytes().

Преобразование


1. Число в строку

int integerVariable = 10;
String first = integerVariable + ""; // конкатенация с пустой строкой
String second = String.valueOf(integerVariable); // вызов статического метода valueOf класса String
String third = Integer.toString(integerVariable); // вызов метода toString класса-обертки

2. Строку в число

String string = "10";
int first = Integer.parseInt(string); 
/* 
   получаем примитивный тип (primitive type) 
   используя метод parseXхх нужного класса-обертки,
   где Xxx - имя примитива с заглавной буквы (например parseInt) 
*/
int second = Integer.valueOf(string); // получаем объект wrapper класса и автоматически распаковываем


StringBuffer


Строки являются неизменными, поэтому частая их модификация приводит к созданию новых объектов, что в свою очередь расходует драгоценную память. Для решения этой проблемы был создан класс java.lang.StringBuffer, который позволяет более эффективно работать над модификацией строки. Класс является mutable, то есть изменяемым — используйте его, если Вы хотите изменять содержимое строки. StringBuffer может быть использован в многопоточных средах, так как все необходимые методы являются синхронизированными.

Создание


Существует четыре способа создания объекта класса StringBuffer. Каждый объект имеет свою вместимость (capacity), что отвечает за длину внутреннего буфера. Если длина строки, что хранится в внутреннем буфере, не превышает размер этого буфера (capacity), то нет необходимости выделять новый массив буфера. Если же буфер переполняется — он автоматически становиться больше.

StringBuffer firstBuffer = new StringBuffer(); // capacity = 16
StringBuffer secondBuffer = new StringBuffer("habrahabr"); // capacity = str.length() + 16
StringBuffer thirdBuffer = new StringBuffer(secondBuffer); // параметр - любой класс, что реализует CharSequence
StringBuffer fourthBuffer = new StringBuffer(50); // передаем capacity

Модификация


В большинстве случаев мы используем StringBuffer для многократного выполнения операций добавления (append), вставки (insert) и удаления (delete) подстрок. Тут все очень просто, например:

String domain = ".ru";
// создадим буфер с помощью String объекта
StringBuffer buffer = new StringBuffer("habrahabr"); // "habrahabr"
// вставим домен в конец
buffer.append(domain); // "habrahabr.ru"
// удалим домен
buffer.delete(buffer.length() - domain.length(), buffer.length()); // "habrahabr"
// вставим домен в конец на этот раз используя insert
buffer.insert(buffer.length(), domain); // "habrahabr.ru"

Все остальные методы для работы с StringBuffer можно посмотреть в документации.

StringBuilder


StringBuilder — класс, что представляет изменяемую последовательность символов. Класс был введен в Java 5 и имеет полностью идентичный API с StringBuffer. Единственное отличие — StringBuilder не синхронизирован. Это означает, что его использование в многопоточных средах есть нежелательным. Следовательно, если вы работаете с многопоточностью, Вам идеально подходит StringBuffer, иначе используйте StringBuilder, который работает намного быстрее в большинстве реализаций. Напишем небольшой тест для сравнения скорости работы этих двух классов:

public class Test {
    public static void main(String[] args) {
        try {
            test(new StringBuffer("")); // StringBuffer: 35117ms.
            test(new StringBuilder("")); // StringBuilder: 3358ms.
        } catch (java.io.IOException e) {
            System.err.println(e.getMessage());
        }
    }
    private static void test(Appendable obj) throws java.io.IOException {
        // узнаем текущее время до теста 
        long before = System.currentTimeMillis();
        for (int i = 0; i++ < 1e9; ) {
            obj.append("");
        }
        // узнаем текущее время после теста 
        long after = System.currentTimeMillis();
        // выводим результат
        System.out.println(obj.getClass().getSimpleName() + ": " + (after - before) + "ms.");
    }
}

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

Комментарии (68)


  1. leventov
    20.06.2015 22:30
    -1

    Это перевод? Отметьте, откуда


    1. tobilko Автор
      20.06.2015 22:33

      это мой оригинальный текст


      1. pleha
        21.06.2015 20:36
        +2

        Мне всегда было интересно хоть кто-то нашел применение StringBuffer. Не очень понятно в каком случае он может пригодиться. Попеременно дописывать какие-то строки и потом их куда-то выводить. Получается в такой строке порядок должен быть не очень важен. Могу подумать только о логировании и чем-то подобном, но для этого есть масса других средств.


        1. vladimir_dolzhenko
          21.06.2015 20:55

          именно StringBuffer? или в целом подход StringBuilder/StringBuffer?


          1. leventov
            21.06.2015 21:33
            +1

            Конечно, именно StringBuffer. StringBuilder-то один из самых часто используемых классов в стандартной библиотеке.


            1. vladimir_dolzhenko
              21.06.2015 22:07
              +2

              У меня только одно объяснение: по историческим причинам и в силу того, что java дает обратную совместимость

               * @since       1.5
               */
              public final class StringBuilder
              


               * @since   JDK1.0
               */
               public final class StringBuffer
              


  1. hmpd
    20.06.2015 23:25
    +7

    String second = new String(habr); // аналогично second = habr;

    Разве аналогично?
    1. String second = new String(habr) — создается новый объект second с тем же содержимым, что и в объекте habr. При этом second и habr ссылаются на разные объекты.
    2. String second = habr — копируется ссылка на объект habr, то есть second и habr ссылаются на один объект.
    Верно?


    1. tobilko Автор
      20.06.2015 23:48

      Опечатался, спасибо. Все верно отметили.
      Хотелось бы дополнить и сказать, что все ссылки на string объекты хранится в String Pool и перед созданием строки с помощью литерала проверяется нет ли эквивалентной строки в пуле, если нет — добавляется, иначе просто получаем ссылку на уже готовый объект. В случаи с new, новый объект создаться в любом случаи, независимо от пула.


      1. hmpd
        20.06.2015 23:53

        Вам спасибо за статью! Давайте продолжение.

        (Чисто языковой комментарий: слово «что», конечно, заменяет слово «который», но не без потерь: оно не передает род заменяемого слова, поэтому текст воспринимается хуже. Пример:
        «если длина строки, что хранится...» — к чему здесь относится «что»: к длине или к строке? Конкретно в этом случае подойдет причастие:
        «длина строки, хранящаяся...» или
        «длина строки, хранящейся...»)


        1. tobilko Автор
          21.06.2015 00:25

          спасибо, исправил :)


  1. apangin
    20.06.2015 23:29
    +6

    А теперь запустите тот же тест с -XX:BiasedLockingStartupDelay=0:

    StringBuffer: 5590ms.
    StringBuilder: 5493ms.
    

    И чего в итоге померили?

    Также изменения коснулись hashCode (новый алгоритм hashing)
    Оппа! Как такое может быть, если алгоритм хеширования для String строго прописан в спецификации?


    1. apangin
      20.06.2015 23:55
      +3

      А при добавлении третьей строчки у меня вообще весело получается:

          test(new StringBuffer("")); // StringBuffer: 5020ms.
          test(new StringBuilder("")); // StringBuilder: 14041ms.
          test(new StringBuilder("")); // StringBuilder: 10716ms.
      


      1. vladimir_dolzhenko
        21.06.2015 19:42

        Учитывая цикл 1e9 итераций и что StringBuilder/StringBuffer расширяются — может произойти GC, наверняка может случится и JIT — словом, что именно происходило в фоне — неизвестно, так, что можно гадать на кофейной гуще, что же именно мерялось.


    1. tobilko Автор
      20.06.2015 23:59

      Алгоритм действительно не изменился (где-то прочитал, но не проверял тогда), спасибо.


  1. uthark
    21.06.2015 06:16
    +1

    Вдобавок к вышеперечисленному, хочу добавить, что одним из важных аспектов, почему String объявлен как final — это безопасность. Например, когда загружаем класс, то имя класса передается в виде строки. Если бы строки были не финальные, то вредоносный код мог бы изменить имя класса и, таким образом, загрузить неправильный класс. Например, вместо java.io.FileReader был бы загружен com.malicious.FileRemover. Также можно было бы сломать контракт equals/hashCode.

    Для обхода всех этих проблем, возможно, был бы введен класс java.lang.SafeString, который бы помог предотвратить эти проблемы, но попутно создал бы новых (например, конвертацию из SafeString в String и обратно, замусоривание API). Но так как это один из базовых классов, то было принято решение сделать класс String финальным.


  1. gurinderu
    21.06.2015 11:04
    +3

    В принципе не плохая статья, но ни слова о String pool, злом методе intern и злом методе substring(до версии 1.7.0_06)


    1. vladimir_dolzhenko
      21.06.2015 19:13
      +3

      Просто оставлю это здесь


      1. gurinderu
        21.06.2015 23:24
        -1

        А я это видел и итак знаю)


        1. vladimir_dolzhenko
          21.06.2015 23:27

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


  1. ASuprun
    21.06.2015 11:15
    +1

    Маленькое уточнение по поводу оператора "+". Как с казано в документации к Oracle JDK 6, 7 и 8, реализация этого оператора сделана с использованием StringBuilder:

    The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(or StringBuffer) class and its append method. String conversions are implemented through the method toString, defined by Object and inherited by all classes in Java.
    (ссылка на v8)

    Что также отражено в спецификации:
    An implementation may choose to perform conversion and concatenation in one step to avoid creating and then discarding an intermediate String object. To increase the performance of repeated string concatenation, a Java compiler may use the StringBuffer class or a similar technique to reduce the number of intermediate String objects that are created by evaluation of an expression.

    For primitive types, an implementation may also optimize away the creation of a wrapper object by converting directly from a primitive type to a string.
    (ссылка)

    Эта тема также поднималась на StackOverflow и там был детально описан процесс конкатенации (ссылка).


    1. 23derevo
      21.06.2015 15:05

      Как сказано в документации к Oracle JDK 6, 7 и 8


      это не документация к Oracle JDK. Это javadoc, то есть, документация к стандарту Java SE (6, 7, 8).


    1. tobilko Автор
      21.06.2015 15:11

      String habr = new StringBuilder(String.valueOf("habra")).append("habr").toString();
      

      это уточнения было в статьи


      1. vladimir_dolzhenko
        21.06.2015 19:09
        +3

        Объясните — откуда взялось это убожество String.valueOf?

        public String s(String v, String f){
            return v + f;
        }
        


        и его bytecode
         public java.lang.String s(java.lang.String, java.lang.String);
            descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
            flags: ACC_PUBLIC
            Code:
              stack=2, locals=3, args_size=3
                 0: new           #2                  // class java/lang/StringBuilder
                 3: dup
                 4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
                 7: aload_1
                 8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
                11: aload_2
                12: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
                15: invokevirtual #5                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
                18: areturn
              LineNumberTable:
                line 3: 0
        


        т.е + это синтетический сахар, а на деле

        public String s(String v, String f){
            return new StringBuilder().append(v).append(f).toString();
        }
        

        и никаких String.valueOf — не вводите людей в заблуждение.

        И вы уверены, что в следующем методе используется конкатенация?
        public String q(){
        return "ha" + "br";
        }


        Данный строковый литерал может быть вычислен уже на стадии компиляции и javac подставляет уже результат
        Constant pool:
           #2 = String             #20            // habr
        
         public java.lang.String q();
            descriptor: ()Ljava/lang/String;
            flags: ACC_PUBLIC
            Code:
              stack=1, locals=1, args_size=1
                 0: ldc           #2                  // String habr
                 2: areturn
              LineNumberTable:
                line 3: 0
        


        Что может привести к неожиданным результатам:
        К примеру, вы используете константу из сторонней библиотеки, которая скажем возвращает ее версию в виде строки, н-р «v1». Если вы обновите эту библиотеку (которая возвращает «v2») без пересборки класса, который ее использует — результат будет по прежнему «v1» ибо именно это значение было подставлено на стадии компиляции.

        Hack, который позволяет обходить данные грабли — использование метода intern — как например тут. По сути строковый литерал уже находится в пуле строк и никакого выигрыша/проигрыша нет, кроме того, что теперь javac не может (по крайней мере пока) вычислить значение на стадии компиляции и уже будет честно загружать класс и его константу.


        1. tobilko Автор
          21.06.2015 23:01

          String habrahabr = new StringBuilder()).append(habra).append(habr).toString();
          


          вы правы, исправил


        1. 23derevo
          21.06.2015 23:01

          поддерживаю. Проверить очень легко: скомпирировать конкатенацию, а потом декомпилировать.
          Даже в байткод руками лазать не надо :)


          1. vladimir_dolzhenko
            21.06.2015 23:05
            +2

            скомпирировать… декомпилировать.


            Настоящим пацанам нужна только консолька, vi, javac да javap для этого


  1. vladimir_dolzhenko
    21.06.2015 11:18
    +6

    Напишем небольшой тест для сравнения скорости работы этих двух классов


    Возьми вольтметр и измерь!


    1. tobilko Автор
      21.06.2015 13:47
      -2

      предложите свой вариант


      1. 23derevo
        21.06.2015 15:08
        +6

        Прежде, чем писать любые перфомансные тесты и юзать всякие System.currentTimeMillis(), мы с vladimir_dolzhenko настоятельно рекомендуем Вам ознакомиться вот с этой лекцией:


        1. tobilko Автор
          21.06.2015 15:21
          +1

          спасибо, посмотрю, перепишу тест :)


          1. vladimir_dolzhenko
            21.06.2015 16:20
            +3

            Всецело согласен с Лешей 23derevo, но стоит добавить, что одной лекцией не стоит ограничится, чтобы понять как же измерять и пользоваться jmh, но вектор правильный.

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

            Т.е в однопоточном случае разница между StringBuilder и StringBuffer будет в пределах погрешности измерений (после того, как JIT свернет). Это следует учитывать, когда будете писать jmh-тест.


  1. apangin
    21.06.2015 22:46
    +2

    Используйте метод concat, если слияние нужно провести только один раз, для остальных случаев рекомендовано использовать или оператор "+" или StringBuffer / StringBuilder.
    Никогда не используйте concat. Обычный "+" или StringBuilder всегда будет эффективней, даже всего для двух строк. Просто потому, что JVM оптимизирует "+" специальным образом (это JVM intrinsic), а concat — нет (это обычный Java метод).


    1. vladimir_dolzhenko
      21.06.2015 22:49

      Можно подробнее про JVM intrinsic  используемый в +?

      Как бы байткод всегда показывает цепочку new StringBuilder().append()...toString() и не знал, что intrinsic может действовать над цепочкой


      1. apangin
        22.06.2015 00:57
        +5

        Именно так. HotSpot JVM распознаёт паттерн new StringBuilder().append()...toString() (или то же самое со StringBuffer), и компилирует это как одно целое. Регулируется опцией -XX:+OptimizeStringConcat, по умолчанию включено.


        1. vladimir_dolzhenko
          22.06.2015 00:58

          Спасибо большое!


        1. 23derevo
          14.07.2015 12:57
          +1

          vladimir_dolzhenko, apangin, lany
          зацените шипилёвский JEP в тему: openjdk.java.net/jeps/8085796


          1. lany
            14.07.2015 13:03
            +1

            Ага, я ниже в комментарии писал про это =)


            1. 23derevo
              14.07.2015 13:08

              ага, каюсь, пропустил :)


    1. 23derevo
      21.06.2015 23:16

      в теории ведь никто не мешает concat реализовать как JVM intrinsic?


      1. vladimir_dolzhenko
        21.06.2015 23:42

        Представляешь как было бы… фантастично-феерично иметь pluggable intrinsic?


        1. 23derevo
          21.06.2015 23:57

          а чего там представлять? Есть такая штука — OpenJDK. Она некоторыми и используется именно так, как ты хочешь: Taobao JVM (слайды 34-37)


          1. vladimir_dolzhenko
            22.06.2015 00:06

            Все клева конечно, только доклад 2011.
            Почему-то на ум приходит академическая разработка RedHat под названием Shenandoah — вызов Zing VM и С4 — но только больше о них ничего не слышно.


            1. 23derevo
              22.06.2015 00:13

              погоди, тут другой случай :)

              Как я понял — это просто кастомизированная OpenJDK (classlib, jvm), работающая внутри Taobao. Они делают определенные изменения и оптимизации, заточенные под их конкретные юзкейсы (ворклоады, железо, сценарии и т.п.). И как я понимаю, именно поэтому они не всегда коммитят назад в OpenJDK — их изменения не всем подойдут.


              1. vladimir_dolzhenko
                22.06.2015 00:50
                +1

                Я вообще о том, что ежели продукт пошел — то должны быть упоминания и развитие, кроме данного доклада.

                Но все равно было бы варенье-с-маслом видеть pluggable intrinsic в java-по-умолчанию


                1. 23derevo
                  22.06.2015 02:23
                  +3

                  Ну вот смотри, у нас в Одноклассниках тоже есть свои сборки OpenJDK патченные — и classlib и JVM. Мы об этом упоминаем периодически, но чтобы прямо кричать об этом на каждом углу…

                  Многие, кстати, патчат. Например, тот же гугл. Посмотри доклады Jeremy Manson с JVMLS 2013 и JVMLS 2014. И тоже об этом мало в интернете есть. Потому что это больше не инфоповод и не рокит саенс.