Глубокое погружение в Java String

Java задняя часть JVM исходный код

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

Java String — один из наиболее часто используемых классов в программировании на Java и самый простой класс, предоставляемый JDK. Поэтому я решил начать с класса String и копнуть немного глубже, чтобы получить хорошее начало.

Определение класса и члены класса

При открытии исходного кода String в JDK следует в первую очередь обратить внимание на определение класса String.

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence

ненаследуемый и неизменяемый

Любой, кто писал на Java, знает, что когдаfinalКогда ключевое слово украшает класс, это означает, что этот класс не может быть унаследован. Таким образом, класс String не может быть унаследован извне. В это время нам может быть любопытно, дизайнер String Зачем проектировать его так, чтобы он не наследовался. Я нашел это на ZhihuВопросы и обсуждения, Я думаю, что первый ответ довольно ясен. Строка как самый основной ссылочный тип данных в Java, наиболее важным моментом является неизменность, поэтому используйтеfinalПросто дляНаследование запрещено ломает неизменную природу String.

Реализуйте неизменность класса, а не только с помощьюfinalКласс модификации настолько прост.Как вы можете видеть из исходного кода, String на самом деле является инкапсуляцией массива символов, а массив символов является закрытым и не предоставляется. Любой метод, который изменяет массив символов, поэтому после инициализации объект String не может быть изменен.

Сериализация

Из приведенного выше определения класса мы видим, что String реализует интерфейс сериализации.Serializable, поэтому String поддерживает сериализацию и десериализацию. Что такое сериализация объектов Java? Я полагаю, что у многих новичков в Java, таких как я, есть этот вопрос.Углубленный анализ сериализации и десериализации JavaЭтот отрывок из статьи Хорошо объяснил.

Платформа Java позволяет нам создавать повторно используемые объекты Java в памяти, но в целом Эти объекты могут существовать только во время работы JVM. То есть время жизни этих объектов не будет больше времени жизни JVM. Но в реальных приложениях Может потребоваться возможность сохранения (сохранения) указанного объекта после остановки JVM и повторного чтения сохраненного объекта в будущем. Сериализация объектов Java может помочь нам реализовать эту функцию. При сериализации объектов Java при сохранении объекта его состояние сохраняется в виде набора байтов, и в дальнейшем эти байты собираются в объект. Следует отметить, что сериализация объекта сохраняет «состояние» объекта, то есть его переменные-члены. Отсюда видно, что сериализация объектов не обращает внимания на статические переменные в классе. В дополнение к сериализации объектов при сохранении объектов сериализация объектов также используется при использовании RMI (удаленный вызов метода) или при передаче объектов по сети. Java Serialization API предоставляет стандартный механизм обработки сериализации объектов, и этот API прост и удобен в использовании.

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

serialVersionUID— номер версии сериализации. Java использует этот UID для определения согласованности потока байтов во время десериализации и локального класса. Если они совпадают, они считаются согласованными. Может быть десериализован и выдает исключение, если отличается.

serialPersistentFieldsЭто определение гораздо реже предыдущего, и, вероятно, предполагается, что оно связано с членами класса при сериализации. Для того, чтобы понять смысл этого поля, я гуглю и Baidu, а также Только что нашел документацию JDK для классаObjectStreamFieldнебольшое описание,A description of a Serializable field from a Serializable class. An array of ObjectStreamFields is used to declare the Serializable fields of a class.Общая идея заключается в том, что этот класс используется для описания сериализованного поля сериализованного класса, Если вы определяете массив этого класса, вы можете объявить поля класса, которые необходимо сериализовать. Но я до сих пор не нашел конкретного использования и функции этого класса. Позже я более подробно рассмотрел определение этого поля, а такжеserialVersionUIDОн также должен определять различные правила через определенные имена полей, а затем я напрямую искал ключевые слова.serialPersistentFields, и, наконец, нашел свою конкретную роль. который,Настройка сериализации по умолчанию включает ключевые словаtransientи статические имена полейserialPersistentFields,transientИспользуется для указания того, какое поле не должно сериализоваться по умолчанию,serialPersistentFieldsИспользуется для указания того, какие поля должны быть сериализованы по умолчанию. Если также определеноserialPersistentFieldsа такжеtransient,transientбудет игнорироваться.Я проверил это на себе, и это действительно работает.

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

Сортируемый

Класс String также реализуетComparableинтерфейс,Comparable<T>Интерфейс имеет только один методpublic int compareTo(T o), реализация этого интерфейса означает, что класс поддерживает сортировку, готовые к использованиюCollections.sortилиArrays.sortтакие методы, как сортировка списка или массива объектов этого класса.

В String мы также можем видеть такую ​​статическую переменную,

 public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }

Как видно из приведенного выше исходного кода, этот статический член является реализациейComparatorЭкземпляр класса, который реализует интерфейс для сравнения размера двух строк String без учета регистра.

ТакComparableа такжеComparatorВ чем разница и связь? В то же время, почему String нужно реализовать и то, и другое?

Здесь не будем расширять первый вопрос.Comparableявляется внутренней реализацией класса, класс может быть реализован только один раз, иComparatorэто внешняя реализация, которую можно передать без изменения В случае с самим классом добавьте в класс дополнительные функции сортировки. Таким образом, мы также можем реализовать String для StringComparatorиспользовать, см.Разница между Comparable и ComparatorЭта статья.

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

метод класса

Строковые методы можно условно разделить на следующие категории.

  • Метод строительства
  • Функциональный подход
  • заводской метод
  • стажер метод

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

стажер метод

Пул строковых констант

Строка — это один из основных типов Java, и вы можете использовать литералы для создания объектов, таких какString s = "hello". Конечно, вы также можете использоватьnewдля создания объекта String, Но я редко вижу такой способ письма.Со временем я привык к первому способу письма, но я не знал, что за этим стоит много знаний. В следующем фрагменте кода можно увидеть разницу между ними.

public class StringConstPool {
    public static void main(String[] args) {
        String s1 = "hello world";
        String s2 = new String("hello world");
        String s3 = "hello world";
        String s4 = new String("hello world");
        String s5 = "hello " + "world";
        String s6 = "hel" + "lo world";
        String s7 = "hello";
        String s8 = s7 + " world";
        
        System.out.println("s1 == s2: " + String.valueOf(s1 == s2) );
        System.out.println("s1.equals(s2): " + String.valueOf(s1.equals(s2)));
        System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
        System.out.println("s1.equals(s3): " + String.valueOf(s1.equals(s3)));
        System.out.println("s2 == s4: " + String.valueOf(s2 == s4));
        System.out.println("s2.equals(s4): " + String.valueOf(s2.equals(s4)));
        System.out.println("s5 == s6: " + String.valueOf(s5 == s6));
        System.out.println("s1 == s8: " + String.valueOf(s1 == s8));
    }
}
/* output
s1 == s2: false
s1.equals(s2): true
s1 == s3: true
s1.equals(s3): true
s2 == s4: false
s2.equls(s4): true
s5 == s6: true
s1 == s8: false
 */

Как видно из вывода этого кода,equalsВсе результаты сравнения верны, потому что Stringequalsзначение сравнения (по умолчанию для объектов ObjectequalsРеализация заключается в сравнении ссылок, Строка переопределяет этот метод).==Сравнивает ссылки двух объектов и возвращает результат, если ссылки совпадают.true, иначе возвратfalse.s1==s2: falseа такжеs2==s4: falseобъяснилnewОбъект должен создать новую ссылку для возврата.s1==s3: trueЭто доказывает, что объекты, созданные с использованием литералов с одними и теми же литералами, получат одинаковые ссылки.

s5 == s6на самом деле иs1 == s3То же самое и с JVM, потому что простые операции над такими константами выполняются уже на этапе компиляции. мы можем использоватьjavapДекомпилируйте файл класса, чтобы увидеть После компиляции.

➜ ~ javap -c StringConstPool.class
Compiled from "StringConstPool.java"
public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
  public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello world
       2: astore_1
       3: return
}

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

s1 == s8Ситуация немного сложнее, s8 получается операцией переменных, поэтому его значение нельзя вычислить напрямую во время компиляции. И Java не может перегружать операторы, поэтому мы также в исходном коде JDK Соответствующие улики не были найдены. Все декомпилируется, и мы будем декомпилировать, чтобы увидеть, действительно ли компилятор влияет на это.

public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
  public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String  world
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_2
      23: return
}

По результатам декомпиляции можно обнаружить, что переменная операция String на самом деле состоит изStringBuilderосуществленный,s8 = s7 + " world"Код эквивалентен(new StringBuilder(s7)).append(" world").toString().Stringbuilderявляется изменяемым классом, черезappendМетоды иtoStringОбъединить два объекта String в новый объект String, так что нетрудно понять, почемуs1 == s8 : false.

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

Что делает метод стажера

Многое из того, что я сказал выше, не касается темыinternметод, тоinternКаков эффект от метода? Сначала посмотрите на исходный код.

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

В Oracle JDK,internметодnativeКлючевое слово оформлено и не реализовано, что означает, что эта часть скрыта от реализации. Как вы можете видеть из комментариев, этот метод делает, если константный пул Если текущая строка существует в пуле констант, текущая строка будет возвращена напрямую.Если такой строки в пуле констант нет, строка будет помещена в пул констант, а затем возвращена. Функцию этого метода можно понять, введя аннотации. Докажите это еще на нескольких примерах.

public class StringConstPool {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = new String("hello");
        String s3 = s2.intern();
        System.out.println("s1 == s2: " + String.valueOf(s1 == s2));
        System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
    }
}
/* output
s1 == s2: false
s1 == s3: true
*/

Здесь легко понятьinternНа самом деле обычные строковые объекты также связаны с пулом констант.

КонечноinternПринципы реализации и лучшие практики также необходимо понять и изучить.Эта статья технической команды MeituanУглубленный анализ String#internОчень подробно и подробно, рекомендую к прочтению.