Вопрос

Учитывая 2 toString() реализации ниже, какая из них предпочтительнее:

public String toString(){
    return "{a:"+ a + ", b:" + b + ", c: " + c +"}";
}

или

public String toString(){
    StringBuilder sb = new StringBuilder(100);
    return sb.append("{a:").append(a)
          .append(", b:").append(b)
          .append(", c:").append(c)
          .append("}")
          .toString();
}

?

Что еще более важно, учитывая, что у нас есть только 3 свойства, это может не иметь значения, но в какой момент вы бы переключились с + связать с StringBuilder?

Это было полезно?

Решение

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

Что еще более важно, учитывая, что у нас есть только 3 свойства, это может не иметь значения разница, но в какой момент вы переключаетесь с concat на builder?

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

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

Ключ заключается в том, пишете ли вы единую конкатенацию в одном месте или накапливаете ее с течением времени.

В приведенном вами примере нет смысла явно использовать StringBuilder .(Посмотрите на скомпилированный код для вашего первого случая.)

Но если вы создаете строку, напримервнутри цикла используйте StringBuilder .

Чтобы уточнить, предполагая, что hugeArray содержит тысячи строк, закодируйте следующим образом:

...
String result = "";
for (String s : hugeArray) {
    result = result + s;
}

очень расточительно расходует время и память по сравнению с:

...
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
    sb.append(s);
}
String result = sb.toString();

Я предпочитаю:

String.format( "{a: %s, b: %s, c: %s}", a, b, c );

... потому что она короткая и читабельная.

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

Я согласен, что если вам придется выводить много параметров, эта форма может сбить с толку (как сказано в одном из комментариев).В этом случае я бы переключился на более удобочитаемую форму (возможно, используя Тострингбилдер из apache-commons - взято из ответа Мэтта б) и снова игнорируйте производительность.

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

public class Main
{
    public static void main(String[] args)
    {
        long now = System.currentTimeMillis();
        slow();
        System.out.println("slow elapsed " + (System.currentTimeMillis() - now) + " ms");

        now = System.currentTimeMillis();
        fast();
        System.out.println("fast elapsed " + (System.currentTimeMillis() - now) + " ms");
    }

    private static void fast()
    {
        StringBuilder s = new StringBuilder();
        for(int i=0;i<100000;i++)
            s.append("*");      
    }

    private static void slow()
    {
        String s = "";
        for(int i=0;i<100000;i++)
            s+="*";
    }
}

Результатом является:

slow elapsed 11741 ms
fast elapsed 7 ms

Проблема в том, что to += append к строке восстанавливает новую строку, поэтому это стоит примерно линейно длине ваших строк (сумма обоих).

Итак - к вашему вопросу:

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

У меня также было столкновение с моим боссом по поводу того, использовать ли append или +. Поскольку они используют Append (я все еще не могу понять, как они говорят каждый раз, когда создается новый объект).Поэтому я подумал провести кое-какие исследования и разработки.Хотя мне нравится объяснение Майкла Боргвардта, но я просто хотел показать объяснение, если кому-то действительно нужно будет знать в будущем.

/**
 *
 * @author Perilbrain
 */
public class Appc {
    public Appc() {
        String x = "no name";
        x += "I have Added a name" + "We May need few more names" + Appc.this;
        x.concat(x);
        // x+=x.toString(); --It creates new StringBuilder object before concatenation so avoid if possible
        //System.out.println(x);
    }

    public void Sb() {
        StringBuilder sbb = new StringBuilder("no name");
        sbb.append("I have Added a name");
        sbb.append("We May need few more names");
        sbb.append(Appc.this);
        sbb.append(sbb.toString());
        // System.out.println(sbb.toString());
    }
}

и разборка вышеуказанного класса получается как

 .method public <init>()V //public Appc()
  .limit stack 2
  .limit locals 2
met001_begin:                                  ; DATA XREF: met001_slot000i
  .line 12
    aload_0 ; met001_slot000
    invokespecial java/lang/Object.<init>()V
  .line 13
    ldc "no name"
    astore_1 ; met001_slot001
  .line 14

met001_7:                                      ; DATA XREF: met001_slot001i
    new java/lang/StringBuilder //1st object of SB
    dup
    invokespecial java/lang/StringBuilder.<init>()V
    aload_1 ; met001_slot001
    invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
g/StringBuilder;
    ldc "I have Added a nameWe May need few more names"
    invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
g/StringBuilder;
    aload_0 ; met001_slot000
    invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\
g/StringBuilder;
    invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
    astore_1 ; met001_slot001
  .line 15
    aload_1 ; met001_slot001
    aload_1 ; met001_slot001
    invokevirtual java/lang/String.concat(Ljava/lang/String;)Ljava/lang/Strin\
g;
    pop
  .line 18
    return //no more SB created
met001_end:                                    ; DATA XREF: met001_slot000i ...

; ===========================================================================

;met001_slot000                                ; DATA XREF: <init>r ...
    .var 0 is this LAppc; from met001_begin to met001_end
;met001_slot001                                ; DATA XREF: <init>+6w ...
    .var 1 is x Ljava/lang/String; from met001_7 to met001_end
  .end method
;44-1=44
; ---------------------------------------------------------------------------


; Segment type: Pure code
  .method public Sb()V //public void Sb
  .limit stack 3
  .limit locals 2
met002_begin:                                  ; DATA XREF: met002_slot000i
  .line 21
    new java/lang/StringBuilder
    dup
    ldc "no name"
    invokespecial java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    astore_1 ; met002_slot001
  .line 22

met002_10:                                     ; DATA XREF: met002_slot001i
    aload_1 ; met002_slot001
    ldc "I have Added a name"
    invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
g/StringBuilder;
    pop
  .line 23
    aload_1 ; met002_slot001
    ldc "We May need few more names"
    invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
g/StringBuilder;
    pop
  .line 24
    aload_1 ; met002_slot001
    aload_0 ; met002_slot000
    invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\
g/StringBuilder;
    pop
  .line 25
    aload_1 ; met002_slot001
    aload_1 ; met002_slot001
    invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
    invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\
g/StringBuilder;
    pop
  .line 28
    return
met002_end:                                    ; DATA XREF: met002_slot000i ...


;met002_slot000                                ; DATA XREF: Sb+25r
    .var 0 is this LAppc; from met002_begin to met002_end
;met002_slot001                                ; DATA XREF: Sb+9w ...
    .var 1 is sbb Ljava/lang/StringBuilder; from met002_10 to met002_end
  .end method
;96-49=48
; ---------------------------------------------------------------------------

Из приведенных выше двух кодов вы можете видеть, что Майкл прав.В каждом случае создается только один объект SB.

Начиная с Java 1.5, простая конкатенация в одну строку с "+" и StringBuilder.append() генерируют точно такой же байт-код.

Поэтому для удобства чтения кода используйте "+".

2 исключения :

  • многопоточная среда :Буфер строк
  • конкатенация в циклах :StringBuilder/Строковый буфер

Используя последнюю версию Java (1.8), дизассемблирование(javap -c) показывает оптимизацию, введенную компилятором. + также sb.append() будет сгенерирован очень похожий код.Однако будет полезно проверить поведение, если мы используем + в цикле for.

Добавление строк с использованием + в цикле for

Java - Язык:

public String myCatPlus(String[] vals) {
    String result = "";
    for (String val : vals) {
        result = result + val;
    }
    return result;
}

Байт-код:(for отрывок из цикла)

12: iload         5
14: iload         4
16: if_icmpge     51
19: aload_3
20: iload         5
22: aaload
23: astore        6
25: new           #3                  // class java/lang/StringBuilder
28: dup
29: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
32: aload_2
33: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload         6
38: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: astore_2
45: iinc          5, 1
48: goto          12

Добавление строк с помощью stringbuilder.append

Java - Язык:

public String myCatSb(String[] vals) {
    StringBuilder sb = new StringBuilder();
    for(String val : vals) {
        sb.append(val);
    }
    return sb.toString();
}

Байт-код:(for отрывок из цикла)

17: iload         5
19: iload         4
21: if_icmpge     43
24: aload_3
25: iload         5
27: aaload
28: astore        6
30: aload_2
31: aload         6
33: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: pop
37: iinc          5, 1
40: goto          17
43: aload_2

Есть немного вопиющая разница хотя.В первом случае, где + был подержанным, новым StringBuilder создается для каждой итерации цикла for, и сгенерированный результат сохраняется путем выполнения toString() звоните (с 29 по 41).Таким образом, вы генерируете промежуточные строки, которые вам действительно не нужны при использовании + оператор в for петля.

В Java 9 версия 1 должна быть быстрее, потому что она преобразуется в invokedynamic звони.Более подробную информацию можно найти в JEP-280:

Идея состоит в том, чтобы заменить весь танец добавления StringBuilder простым динамическим вызовом java.lang.invoke.StringConcatFactory, который будет принимать значения при необходимости конкатенации.

Apache Commons-Lang имеет Тострингбилдер класс, который очень прост в использовании.Он отлично справляется как с обработкой логики добавления, так и с форматированием того, как вы хотите, чтобы выглядела ваша toString .

public void toString() {
     ToStringBuilder tsb =  new ToStringBuilder(this);
     tsb.append("a", a);
     tsb.append("b", b)
     return tsb.toString();
}

Вернет вывод, который выглядит как com.blah.YourClass@abc1321f[a=whatever, b=foo].

Или в более сжатой форме с использованием цепочки:

public void toString() {
     return new ToStringBuilder(this).append("a", a).append("b", b").toString();
}

Или, если вы хотите использовать отражение для включения каждого поля класса:

public String toString() {
    return ToStringBuilder.reflectionToString(this);
}

Вы также можете настроить стиль toString, если хотите.

По соображениям производительности использование += (String конкатенация) не рекомендуется.Причина, по которой это:Java - Язык String является неизменяемым, каждый раз, когда выполняется новая конкатенация, новый String создается (новый отпечаток пальца уже отличается от старого в пуле строк ).Создание новых строк оказывает давление на GC и замедляет работу программы:создание объекта обходится дорого.

Приведенный ниже код должен сделать его более практичным и в то же время понятным.

public static void main(String[] args) 
{
    // warming up
    for(int i = 0; i < 100; i++)
        RandomStringUtils.randomAlphanumeric(1024);
    final StringBuilder appender = new StringBuilder();
    for(int i = 0; i < 100; i++)
        appender.append(RandomStringUtils.randomAlphanumeric(i));

    // testing
    for(int i = 1; i <= 10000; i*=10)
        test(i);
}

public static void test(final int howMany) 
{
    List<String> samples = new ArrayList<>(howMany);
    for(int i = 0; i < howMany; i++)
        samples.add(RandomStringUtils.randomAlphabetic(128));

    final StringBuilder builder = new StringBuilder();
    long start = System.nanoTime();
    for(String sample: samples)
        builder.append(sample);
    builder.toString();
    long elapsed = System.nanoTime() - start;
    System.out.printf("builder - %d - elapsed: %dus\n", howMany, elapsed / 1000);

    String accumulator = "";
    start = System.nanoTime();
    for(String sample: samples)
        accumulator += sample;
    elapsed = System.nanoTime() - start;
    System.out.printf("concatenation - %d - elapsed: %dus\n", howMany, elapsed / (int) 1e3);

    start = System.nanoTime();
    String newOne = null;
    for(String sample: samples)
        newOne = new String(sample);
    elapsed = System.nanoTime() - start;
    System.out.printf("creation - %d - elapsed: %dus\n\n", howMany, elapsed / 1000);
}

Результаты пробега приведены ниже.

builder - 1 - elapsed: 132us
concatenation - 1 - elapsed: 4us
creation - 1 - elapsed: 5us

builder - 10 - elapsed: 9us
concatenation - 10 - elapsed: 26us
creation - 10 - elapsed: 5us

builder - 100 - elapsed: 77us
concatenation - 100 - elapsed: 1669us
creation - 100 - elapsed: 43us

builder - 1000 - elapsed: 511us
concatenation - 1000 - elapsed: 111504us
creation - 1000 - elapsed: 282us

builder - 10000 - elapsed: 3364us 
concatenation - 10000 - elapsed: 5709793us
creation - 10000 - elapsed: 972us

Не принимая во внимание результаты для 1 конкатенации (JIT еще не выполнял свою работу), даже для 10 конкатенаций снижение производительности имеет значение;для тысяч конкатенаций разница огромна.

Уроки, извлеченные из этого очень быстрого эксперимента (легко воспроизводимого с помощью приведенного выше кода):никогда не используйте += для объединения строк вместе, даже в самых простых случаях, когда требуется несколько конкатенаций (как уже было сказано, создание новых строк в любом случае обходится дорого и оказывает давление на GC).

Сделайте метод toString настолько читабельным, насколько это возможно!

Единственное исключение из этого правила в моей книге - если вы можете доказать для меня это потребляет значительные ресурсы :) (Да, это означает профилирование)

Также обратите внимание, что компилятор Java 5 генерирует более быстрый код, чем рукописный подход "StringBuffer", используемый в более ранних версиях Java.Если вы используете "+", это и будущие улучшения предоставляются бесплатно.

Похоже, ведутся некоторые споры о том, по-прежнему ли необходимо использовать StringBuilder с текущими компиляторами.Поэтому я подумал, что отдам свои 2 цента опыта.

У меня есть JDBC результирующий набор из 10 тысяч записей (да, мне нужны все они в одном пакете.) Использование оператора + на моем компьютере занимает около 5 минут с Java 1.8.Используя stringBuilder.append("") для одного и того же запроса требуется меньше секунды.

Так что разница огромная.Внутри цикла StringBuilder это намного быстрее.

Смотрите пример ниже:

//java8
static void main(String[] args) {
    case1();//str.concat
    case2();//+=
    case3();//StringBuilder
}

static void case1() {
    List<Long> savedTimes = new ArrayList();
    long startTimeAll = System.currentTimeMillis();
    String str = "";
    for (int i = 0; i < MAX_ITERATIONS; i++) {
        long startTime = System.currentTimeMillis();
        str = str.concat(UUID.randomUUID()+"---");
        saveTime(savedTimes, startTime);
    }
    System.out.println("Created string of length:"+str.length()+" in "+(System.currentTimeMillis()-startTimeAll)+" ms");
}

static void case2() {
    List<Long> savedTimes = new ArrayList();
    long startTimeAll = System.currentTimeMillis();
    String str = "";
    for (int i = 0; i < MAX_ITERATIONS; i++) {
        long startTime = System.currentTimeMillis();
        str+=UUID.randomUUID()+"---";
        saveTime(savedTimes, startTime);
    }        
    System.out.println("Created string of length:"+str.length()+" in "+(System.currentTimeMillis()-startTimeAll)+" ms");
}

static void case3() {
    List<Long> savedTimes = new ArrayList();
    long startTimeAll = System.currentTimeMillis();
    StringBuilder str = new StringBuilder("");
    for (int i = 0; i < MAX_ITERATIONS; i++) {
        long startTime = System.currentTimeMillis();
        str.append(UUID.randomUUID()+"---");
        saveTime(savedTimes, startTime);
    }        
    System.out.println("Created string of length:"+str.length()+" in "+(System.currentTimeMillis()-startTimeAll)+" ms");

}

static void saveTime(List<Long> executionTimes, long startTime) {
        executionTimes.add(System.currentTimeMillis()-startTime);
        if(executionTimes.size()%CALC_AVG_EVERY == 0) {
            out.println("average time for "+executionTimes.size()+" concatenations: "+
                    NumberFormat.getInstance().format(executionTimes.stream().mapToLong(Long::longValue).average().orElseGet(()->0))+
                    " ms avg");
            executionTimes.clear();
        }
}

Выходной сигнал:

среднее время для 10000 конкатенаций:среднее значение 0,096 мс
среднее время для 10000 конкатенаций:среднее значение 0,185 мс
среднее время для 10000 конкатенаций:среднее значение 0,327 мс
среднее время для 10000 конкатенаций:среднее значение 0,501 мс
среднее время для 10000 конкатенаций:среднее значение 0,656 мс
Созданная строка длиной: 1950000 в 17745 мс
среднее время для 10000 конкатенаций:среднее значение 0,21 мс
среднее время для 10000 конкатенаций:среднее значение 0,652 мс
среднее время для 10000 конкатенаций:среднее значение 1,129 мс
среднее время для 10000 конкатенаций:среднее значение 1,727 мс
среднее время для 10000 конкатенаций:среднее значение 2,302 мс
Созданная строка длиной: 1950000 в 60279 мс
среднее время для 10000 конкатенаций:среднее значение 0,002 мс
среднее время для 10000 конкатенаций:среднее значение 0,002 мс
среднее время для 10000 конкатенаций:среднее значение 0,002 мс
среднее время для 10000 конкатенаций:среднее значение 0,002 мс
среднее время для 10000 конкатенаций:среднее значение 0,002 мс
Созданная строка длиной: 1950000 в 100 мс

С увеличением длины строки увеличивается и время конкатенации.
Вот где находится StringBuilder это определенно необходимо.
Как вы видите, конкатенация: UUID.randomUUID()+"---", на самом деле не влияет на время.

P.S.: Я не думаю, что Когда использовать StringBuilder в Java на самом деле это дубликат этого.
В этом вопросе говорится о toString() который в большинстве случаев не выполняет конкатенации огромных строк.

Конкатенация строк с точки зрения производительности с использованием '+' обходится дороже, поскольку для этого приходится создавать совершенно новую копию String, поскольку строки в java неизменяемы.Это играет особую роль, если конкатенация происходит очень часто, например:внутри цикла.Ниже приведено то, что предлагает моя ИДЕЯ, когда я пытаюсь сделать такую вещь:

enter image description here

Общие Правила:

  • В рамках назначения одной строки использование конкатенации строк вполне допустимо.
  • Если вы выполняете цикл для создания большого блока символьных данных, выберите StringBuffer .
  • Использование += в строке всегда будет менее эффективным, чем использование StringBuffer , поэтому это должно вызывать предупреждение, но в некоторых случаях полученная оптимизация будет незначительной по сравнению с проблемами с удобочитаемостью, поэтому руководствуйтесь своим здравым смыслом.

Вот такой хороший блог Джона Скита вокруг этой темы.

Могу ли я указать, что если вы собираетесь выполнить итерацию по коллекции и использовать StringBuilder, вы можете захотеть проверить Общий язык Apache Lang и StringUtils.присоединиться() (с разными вкусами) ?

Независимо от производительности, это избавит вас от необходимости создавать StringBuilders и циклы for для того, что кажется миллионный время.

Я сравнил четыре различных подхода, чтобы сравнить производительность.Я точно не знаю, что происходит с gc, но для меня важно время.Компилятор здесь является важным фактором.Я использовал jdk1.8.0_45 под платформой window8.1.

concatWithPlusOperator = 8
concatWithBuilder = 130
concatWithConcat = 127
concatStringFormat = 3737
concatWithBuilder2 = 46


public class StringConcatenationBenchmark {

private static final int MAX_LOOP_COUNT = 1000000;

public static void main(String[] args) {

    int loopCount = 0;
    long t1 = System.currentTimeMillis();
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithPlusOperator();
        loopCount++;
    }
    long t2 = System.currentTimeMillis();
    System.out.println("concatWithPlusOperator = " + (t2 - t1));

    long t3 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithBuilder();
        loopCount++;
    }
    long t4 = System.currentTimeMillis();
    System.out.println("concatWithBuilder = " + (t4 - t3));

    long t5 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithConcat();
        loopCount++;
    }
    long t6 = System.currentTimeMillis();
    System.out.println("concatWithConcat = " + (t6 - t5));

    long t7 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatStringFormat();
        loopCount++;
    }
    long t8 = System.currentTimeMillis();
    System.out.println("concatStringFormat = " + (t8 - t7));

    long t9 = System.currentTimeMillis();
    loopCount = 0;
    while (loopCount < MAX_LOOP_COUNT) {
        concatWithBuilder2();
        loopCount++;
    }
    long t10 = System.currentTimeMillis();
    System.out.println("concatWithBuilder2 = " + (t10 - t9));
}

private static void concatStringFormat() {
    String s = String.format("%s %s %s %s ", "String", "String", "String", "String");
}

private static void concatWithConcat() {
    String s = "String".concat("String").concat("String").concat("String");
}

private static void concatWithBuilder() {
    StringBuilder builder=new StringBuilder("String");
    builder.append("String").append("String").append("String");
    String s = builder.toString();
}

private static void concatWithBuilder2() {
    String s = new StringBuilder("String").append("String").append("String").append("String").toString();
}

private static void concatWithPlusOperator() {
    String s = "String" + "String" + "String" + "String";
}
}

Вот что я проверил в Java8

  • Использование конкатенации строк
  • Использование StringBuilder

    long time1 = System.currentTimeMillis();
    usingStringConcatenation(100000);
    System.out.println("usingStringConcatenation " + (System.currentTimeMillis() - time1) + " ms");
    
    time1 = System.currentTimeMillis();
    usingStringBuilder(100000);
    System.out.println("usingStringBuilder " + (System.currentTimeMillis() - time1) + " ms");
    
    
    private static void usingStringBuilder(int n)
    {
        StringBuilder str = new StringBuilder();
        for(int i=0;i<n;i++)
            str.append("myBigString");    
    }
    
    private static void usingStringConcatenation(int n)
    {
        String str = "";
        for(int i=0;i<n;i++)
            str+="myBigString";
    }
    

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

usingStringConcatenation 29321 ms
usingStringBuilder 2 ms

Я думаю, нам следует использовать подход добавления StringBuilder.Причина в том, что

  1. Объединение строк будет каждый раз создавать новый объект string (поскольку String является неизменяемым объектом) , поэтому оно создаст 3 объекта.

  2. С помощью String builder будет создан только один объект[StringBuilder является изменяемым], и к нему будет добавлена дополнительная строка.

Для простых строк, подобных этой, я предпочитаю использовать

"string".concat("string").concat("string");

По порядку, я бы сказал, что предпочтительным методом построения строки является использование StringBuilder, String#concat(), затем перегруженного оператора + .StringBuilder - это значительное увеличение производительности при работе с большими строками, точно так же, как использование оператора + приводит к значительному снижению производительности (экспоненциально значительному снижению по мере увеличения размера строки).Единственная проблема с использованием .concat() заключается в том, что он может выдавать NullPointerExceptions .

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top