openjdk.ruOpenJDK на русском

JEP 326: Raw String Literals (Preview)

Необработанные строковые литералы, версия Preview (предварительная версия)

ОтветственныйJim Laskey
ТипFeature
ОбластьSE
СтатусClosed / Withdrawn
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
Связан сJEP 355: Text Blocks (Preview)
РецензентыAlex Buckley
ОдобренBrian Goetz
Создан2018/01/23 15:40
Обновлён2020/05/01 19:04
Задача8196004

Аннотация

Добавить в язык программирования Java необработанные строковые литералы (raw string literals). Необработанный строковый литерал может занимать несколько строк исходного кода и не интерпретирует ни управляющие последовательности, такие как \n, ни Unicode-экранирования вида \uXXXX.

Обратите внимание: это планировалось как Preview-возможность языка в JDK 12, но было отозвано и в JDK 12 не вошло. В JDK 13 на смену пришли Text Blocks (текстовые блоки) (JEP 355).

Цели

  • Упростить разработчикам следующие задачи:
    • записывать последовательности символов в удобочитаемом виде, без служебных обозначений Java;
    • задавать строки, предназначенные для грамматик, отличных от Java;
    • задавать строки, занимающие несколько строк исходного кода, без специальных обозначений перевода строки.
  • Необработанные строковые литералы должны позволять записывать те же строки, что и традиционные строковые литералы, за исключением платформенно-зависимых символов конца строки.
  • Добавить поддержку в библиотеке, чтобы воспроизводить текущую интерпретацию экранирования в строковых литералах javac и управлять удалением отступов слева.

Что не является целью

  • Не вводить новые операторы для String.
  • Необработанные строковые литералы не поддерживают интерполяцию строк напрямую. Интерполяция может быть рассмотрена в одном из будущих JEP.
  • Никак не менять интерпретацию традиционных строковых литералов, в том числе:
    • не добавлять возможность записи в несколько строк;
    • не добавлять настройку разделителей с помощью повторяющихся открывающих и закрывающих двойных кавычек;
    • не менять обработку управляющих последовательностей.

Мотивация

Во многих языках программирования, включая Java, определены управляющие последовательности для представления символов, которые сложно записать напрямую. Например, управляющая последовательность \n представляет управляющий символ перевода строки ASCII. Чтобы напечатать «hello» и «world» на отдельных строках, можно использовать строку "hello\nworld\n";

System.out.print("hello\nworld\n");

Вывод:

hello
world

Помимо плохой читаемости, этот пример жёстко ориентирован на системы на основе Unix, тогда как в других ОС используются иные представления перевода строки, например \r\n (Windows). В Java для получения подходящей для платформы последовательности перевода строки мы используем более высокоуровневый метод, например println:

System.out.println("hello");
System.out.println("world");

Если «hello» и «world» выводятся с помощью библиотеки GUI, управляющие символы могут вообще не иметь никакого значения.

Признак управляющей последовательности, обратная косая черта, в строковых литералах Java записывается как \\. Такое удвоение обратных косых черт приводит к синдрому наклонённых зубочисток (Leaning Toothpick Syndrome), когда строки становится трудно понимать из-за избытка обратных косых черт. Разработчикам на Java знакомы примеры вроде:

Path path = Paths.get("C:\\Program Files\\foo");

Управляющие последовательности, такие как \" для символа двойной кавычки, также затрудняют понимание при использовании в грамматиках, отличных от Java. Например, для поиска двойной кавычки внутри строки требуется:

Pattern pattern = Pattern.compile("\\\"");

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

Реальному коду на Java, в который часто встраиваются фрагменты других программ (SQL, JSON, XML, регулярные выражения и т. д.), нужен механизм для записи строк буквально, как есть, без специальной обработки Unicode-экранирования, обратной косой черты или переводов строки.

Этот JEP предлагает новый вид литерала — необработанный строковый литерал, в котором не действуют ни экранирование Java, ни спецификации Java о символах конца строки. Он даёт последовательности символов, которые во многих случаях удобнее читать и сопровождать, чем существующий традиционный строковый литерал.

Пример с путями к файлам

Традиционные строковые литералы Необработанные строковые литералы
Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar");
Runtime.getRuntime().exec(`"C:\Program Files\foo" bar`);

Пример с несколькими строками

Традиционные строковые литералы Необработанные строковые литералы
String html = "<html>\n" +
              "    <body>\n" +
              "		    <p>Hello World.</p>\n" +
              "    </body>\n" +
              "</html>\n";
String html = `<html>
<body>
<p>Hello World.</p>
</body>
</html>
`;

Пример с регулярным выражением

Традиционные строковые литералы Необработанные строковые литералы
System.out.println("this".matches("\\w\\w\\w\\w"));
System.out.println("this".matches(`\w\w\w\w`));

Вывод:

true

Пример с другим языком

Традиционные строковые литералы Необработанные строковые литералы
String script = "function hello() {\n" +
                "   print(\'\"Hello World\"\');\n" +
                "}\n" +
                "\n" +
                "hello();\n";
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval(script);
String script = `function hello() {
print('"Hello World"');
}

hello();
`;
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); Object obj = engine.eval(script);

Вывод:

"Hello World"

Пример с базой данных

Традиционные строковые литералы Необработанные строковые литералы
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
               "WHERE `CITY` = ‘INDIANAPOLIS'\n" +
               "ORDER BY `EMP_ID`, `LAST_NAME`;\n";
String query = ``SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = ‘INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;
``;

Описание

Необработанный строковый литерал — это новая форма литерала.

Literal:
  IntegerLiteral
  FloatingPointLiteral
  BooleanLiteral
  CharacterLiteral
  StringLiteral
  RawStringLiteral
  NullLiteral

RawStringLiteral:
  RawStringDelimiter RawInputCharacter {RawInputCharacter} RawStringDelimiter

RawStringDelimiter:
    ` {`}

Необработанный строковый литерал состоит из одного или нескольких символов, заключённых в последовательности обратных апострофов ` (\u0060) (обратная кавычка, гравис). Необработанный строковый литерал открывается последовательностью из одного или нескольких обратных апострофов. Необработанный строковый литерал закрывается, когда встречается последовательность обратных апострофов той же длины, что и открывающая. Любая другая последовательность обратных апострофов считается частью тела строки.

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

Символы в необработанном строковом литерале никогда не интерпретируются, за исключением CR и CRLF — платформенно-зависимых символов конца строки. Последовательности CR (\u000D) и CRLF (\u000D\u000A) всегда преобразуются в LF (\u000A). Такое преобразование даёт наименее неожиданное поведение на разных платформах.

Традиционные строковые литералы поддерживают два вида экранирования: Unicode-экранирования вида \uxxxx и управляющие последовательности, такие как \n. Ни один из этих видов экранирования в необработанных строковых литералах не обрабатывается: составляющие экранирование символы используются как есть. Это означает, что обработка Unicode-экранирований отключается, когда лексический анализатор встречает открывающий обратный апостроф, и снова включается, когда он встречает закрывающий обратный апостроф. Для единообразия Unicode-экранирование \u0060 нельзя использовать вместо открывающего обратного апострофа.

Ниже приведены примеры необработанных строковых литералов:

`"`                // a string containing " alone
``can`t``          // a string containing 'c', 'a', 'n', '`' and 't'
`This is a string` // a string containing 16 characters
`\n`               // a string containing '\' and 'n'
`\u2022`           // a string containing '\', 'u', '2', '0', '2' and '2'
`This is a
two-line string`   // a single string constant

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

В файле class строковая константа не хранит сведений о том, получена ли она из необработанного строкового литерала или из традиционного строкового литерала.

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

Экранирование

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

public String unescape()

будет преобразовывать каждую последовательность символов, начинающуюся с \ и имеющую то же написание, что и экранирование, определённое в JLS (Unicode-экранирование или управляющая последовательность), в символ, который представляет эта последовательность.

Примеры (b0–b3 равны true):

boolean b0 = `\n`.equals("\\n");
boolean b1 = `\n`.unescape().equals("\n");
boolean b2 = `\n`.length == 2;
boolean b3 = `\n`.unescape().length == 1;

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

Инструментам также будет предоставлена возможность выполнять обратное преобразование в экранирование. В класс String будет добавлен ещё и следующий метод:

public String escape()

который будет преобразовывать все символы меньше ' ' в Unicode-экранирования или символьные управляющие последовательности, символы больше '~' — в Unicode-экранирования, а символы ", ', \ — в управляющие последовательности.

Примеры (b0–b3 равны true):

boolean b0 = "\n".escape().equals(`\n`);
boolean b1 = `•`.escape().equals(`\u2022`);
boolean b2 = "•".escape().equals(`\u2022`);
boolean b3 = !"•".escape().equals("\u2022");

Кодировка исходного кода

Если исходный файл содержит символы не из ASCII, убедитесь, что в командной строке javac указана правильная кодировка (см. javac -encoding). Другой вариант — записать в необработанной строке соответствующие Unicode-экранирования, а затем с помощью одного из описанных выше библиотечных методов преобразовать их в нужные символы не из ASCII.

Управление отступами

Один из вопросов, связанных с многострочными строками, — форматировать ли строку от левого края (как в heredoc) или, в идеале, согласовывать её с отступами окружающего кода. Тогда возникает вопрос, как управлять этим случайным отступом.

Например, одни разработчики могут писать так:

String s = `
this is my
    embedded string
`;

а другим может не нравиться стиль с выступом влево, и они будут встраивать строку относительно отступа кода:

String html = `
                       this is my
                           embedded string
                      `;

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

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

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

public String align()

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

Пример:

String html = `
                       <html>
                           <body>
                               <p>Hello World.</p>
                           </body>
                       </html>
                  `.align();
    System.out.print(html);

Вывод:

<html>
    <body>
        <p>Hello World.&</p>
    </body>
</html>

Кроме того, общее управление отступами будет обеспечивать следующий метод экземпляра String:

public String indent(int n)

где n задаёт число пробельных символов, добавляемых в каждую строку или удаляемых из неё; положительное n добавляет n пробелов (U+0020), а отрицательное n удаляет n пробельных символов.

Пример:

String html = `
                       <html>
                           <body>
                               <p>Hello World.</p>
                           </body>
                       </html>
                  `.align().indent(4);
    System.out.print(html);

Вывод:

<html>
        <body>
            <p>Hello World.&</p>
        </body>
    </html>

В тех случаях, когда align() не подходит разработчику, мы ожидаем, что преобладающим вариантом будет align().indent(n). Поэтому будет предоставлен дополнительный вариант align:

public String align(int n)

где n — отступ, применяемый к строке после выравнивания.

Пример:

String html = `
                       <html>
                           <body>
                               <p>Hello World.</p>
                           </body>
                       </html>
                  `.align(4);
    System.out.print(html);

Вывод:

<html>
        <body>
            <p>Hello World.&</p>
        </body>
    </html>

Настраиваемое управление отступами будет обеспечивать метод экземпляра строки:

<R> R transform​(Function<String,​R> f)

где переданная функция f вызывается со строкой this в качестве аргумента.

Пример:

public class MyClass {
    private static final String MARKER= "| ";
    public String stripMargin(String string) {
        return lines().map(String::strip)
                      .map(s -> s.startsWith(MARKER) ? s.substring(MARKER.length()) : s)
                      .collect(Collectors.joining("\n", "", "\n"));
    }

    String stripped = `
                          | The content of
                          | the string
                      `.transform(MyClass::stripMargin);
    System.out.print(stripped);

Вывод:

The content of
the string

Следует отметить, что опасения насчёт размера class-файлов и влияния этого решения на время выполнения снимаются возможностями свёртки констант из JEP 303.

Альтернативы

Выбор разделителей

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

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

  • Открывающий разделитель должен ясно указывать, что далее следует тело необработанного строкового литерала.

  • Вероятность появления закрывающего разделителя в теле строки должна быть низкой. Если закрывающий разделитель всё же нужен в теле строки, правила его встраивания должны быть ясными и простыми. Встраивание должно выполняться без использования экранирования.

Мы исходим из того, что выбор разделителя строкового литерала ограничен тремя символами кавычек из Latin1: одинарной кавычкой, двойной кавычкой и обратной кавычкой (backtick). Любой другой вариант ухудшил бы ясность и не согласовывался бы с традиционными строковыми литералами.

Тем не менее необработанный строковый литерал нужно отличать от традиционного строкового литерала. Например, двойную кавычку можно было бы сочетать с другими символами или произвольными фразами и получить своего рода составной разделитель для необработанных строковых литералов. Например, $"xyz"$ или abcd"xyz"abcd. Такие составные разделители удовлетворяют базовым требованиям, но не дают ясного и простого способа встроить закрывающий разделитель. Кроме того, в случае произвольных фраз возникает соблазн придавать фразе смысловое значение, что предвещает появление ещё одной индустрии, подобной аннотациям Java.

Можно использовать повторение кавычек: """xyz""". Здесь нужно действовать осторожно, чтобы избежать неоднозначности. Пример: "" + x + "" можно разобрать как конкатенацию традиционного строкового литерала с переменной и ещё одним традиционным строковым литералом или как необработанный строковый литерал для строки " + x + " из семи символов.

Преимущество обратной кавычки в том, что её не нужно перепрофилировать. Мы также можем избежать неоднозначности, которую создают повторение кавычек и пустая строка. С точки зрения Java Language Specification это новый разделитель. Он удовлетворяет всем требованиям к разделителям, включая простое правило встраивания.

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

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

Многострочные традиционные строковые литералы

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

Другие языки

Java остаётся одним из немногих современных языков программирования, которые не поддерживают необработанные строки на уровне языка.

Следующие языки программирования поддерживают необработанные строковые литералы, и мы изучили в них разделители и использование необработанных и многострочных строк: C, C++, C#, Dart, Go, Groovy, Haskell, JavaScript, Kotlin, Perl, PHP, Python, R, Ruby, Scala и Swift. Также были рассмотрены представления строк в инструментах Unix bash, grep и sed.

Многострочные литералы можно было бы получить просто, изменив спецификацию Java так, чтобы разрешить CR и LF в теле традиционного строкового литерала в двойных кавычках. Однако использование двойной кавычки подразумевает, что escape-последовательности должны интерпретироваться.

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

Разделители

Язык/инструмент

"""..."""

Groovy, Kotlin, Python, Scala, Swift

`...`

Go, JavaScript

@"..."

C#

R"..."

Groovy (старый стиль)

R"xxx(...)xxx"

C/C++

%(...)

Ruby

qq{...}

Perl

В Python, Kotlin, Groovy и Swift для обозначения необработанных строк выбраны тройные двойные кавычки. Этот выбор отражает связь с существующими строковыми литералами.

В Go и JavaScript используется обратная кавычка. При таком выборе используется символ, который редко встречается в строках. Для документов Markdown это не идеально, но покрывает большинство случаев.

Уникальный метатег, например @"..." в C#, даёт функциональность, схожую с предлагаемыми здесь обратными кавычками. Однако @ в Java наводит на мысль об аннотациях. Кроме того, использование ещё одного метатега ограничивает его применение для будущих целей.

Heredoc

Альтернатива кавычкам для необработанных строк — документы «here» (heredoc). Heredoc впервые появились в командных оболочках Unix и перешли в такие языки программирования, как Perl. У heredoc есть заполнитель и маркер конца. Заполнитель указывает, куда в коде вставляется строка, а также задаёт маркер конца. Маркер конца следует за телом строки. Например:

System.out.println(<<HTML);
<html>
    <body>
        <p>Hello World.</p>
    </body>
</html>
HTML

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

Тестирование

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

Следует добавить негативные тесты для проверки граничных случаев с символами конца строки и концом единицы компиляции.

Следует добавить тесты для методов обработки escape-последовательностей и управления отступами.

Следует добавить тесты, которые проверяют, что можно встраивать Java в Java и Markdown в Java.