Почему == оператор сравнения значений строк не попал в Java?

50

Каждый компетентный Java-программист знает, что вам нужно использовать String.equals () для сравнения строки, а не ==, потому что == проверяет равенство ссылок.

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

Для сравнения, оператор C # == проверяет равенство значений для строки s. И если вам действительно нужно проверить равенство ссылок, вы можете использовать String.ReferenceEquals.

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

Есть ли какая-то конкретная причина, почему это не реализовано в Java?

l46kok
источник
12
Возможно, вы захотите взглянуть на Scala, где ==есть равенство объектов и равенство eqссылок ( ofps.oreilly.com/titles/9780596155957/… ).
Джорджио
Просто как примечание, и это может вам не помочь, но, насколько я помню, вы можете сравнить строковые литералы с '=='
Kgrover
10
@Kgrover: можно, но это просто удобный побочный продукт равенства ссылок и того, как Java агрессивно оптимизирует литералы соответствия строк в ссылках на один и тот же объект. Другими словами, это работает, но по неправильным причинам.
tdammers
1
@aviv ==оператор отображается только в том Equalsслучае, если ==оператор был реализован таким образом. Поведение по умолчанию для ==такое же, как ReferenceEquals(на самом деле, ReferenceEqualsопределяется как объектная версия ==)
Патрик Хуизинга,
3
Это следствие дизайнерских решений, которые имеют большой смысл во многих других сценариях. Но так как вы, кажется, знаете это и в любом случае спрашиваете об этом, я чувствую себя обязанным ответить на ваш вопрос: почему должен быть вариант использования для сравнения ссылок String?
Джейкоб Райл

Ответы:

90

Я предполагаю, что это просто последовательность или «принцип наименьшего удивления». String - это объект, поэтому было бы удивительно, если бы его обрабатывали иначе, чем другие объекты.

В то время, когда появилась Java (~ 1995), просто иметь что-то подобное Stringбыло для большинства программистов полной роскошью, которые привыкли представлять строки в виде массивов с нулевым символом в конце. Stringповедение сейчас такое, какое было тогда, и это хорошо; незначительное изменение поведения в дальнейшем может иметь неожиданные, нежелательные эффекты в рабочих программах.

В качестве примечания вы можете использовать, String.intern()чтобы получить каноническое (интернированное) представление строки, после чего можно будет проводить сравнения ==. Стажировка занимает некоторое время, но после этого сравнения будут действительно быстрыми.

Дополнение: в отличие от некоторых ответов, речь идет не о поддержке перегрузки операторов . +Оператор (конкатенация) работает на Stringс , даже если Java не поддерживает оператор перегрузки; это просто обрабатывается как особый случай в компиляторе, разрешаясь в StringBuilder.append(). Точно так же ==можно было бы рассматривать как особый случай.

Тогда зачем удивлять особым случаем, +а не чем ==? Потому что +просто не компилируется, когда применяется к не- Stringобъектам, так что это быстро становится очевидным. Различное поведение в ==будет гораздо менее очевидно и , таким образом , гораздо более удивительно , когда он ударяет вас.

Joonas Pulakka
источник
8
Особые случаи добавляют удивления.
Blrfl
17
Струны были роскошью в 1995 году? В самом деле?? Посмотрите на историю компьютерных языков. Количество языков, у которых в то время был какой-то тип строки, намного превосходило бы количество языков, которых не было. Сколько языков, кроме С и его потомков, использовали массивы с нулевым символом в конце?
WarrenT
14
@WarrenT: Конечно, некоторые (если не большинство) языков имели какой-либо тип строки, но Unicode-совместимые строки, собираемые мусором, были новинкой в ​​1995 году, я думаю. Например, Python представил строки Unicode с версией 2.0, 2000 год. Выбор неизменяемости также был спорным выбором в то время.
Joonas Pulakka
3
@JoonasPulakka Тогда, возможно, вы должны отредактировать свой ответ, чтобы сказать это. Потому что в своем нынешнем виде часть «полной роскоши» вашего ответа совершенно неверна.
svick
1
Стажировка имеет свою цену: вы получаете строку, которая никогда не будет освобождена. (Ну, нет, если вы не используете свой собственный интернирующий движок, который вы можете выбросить.)
Donal Fellows
32

Джеймс Гослинг , создатель Java, объяснил это еще в июле 2000 года :

Я исключил перегрузку операторов как личный выбор, потому что видел, как слишком много людей злоупотребляют этим в C ++. В последние пять-шесть лет я проводил много времени, опрашивая людей на предмет перегрузки операторов, и это действительно увлекательно, потому что сообщество разбито на три части: вероятно, от 20 до 30 процентов населения считают перегрузку операторов как порождение дьявола; кто-то сделал что-то с перегрузкой операторов, что просто пометило их галочкой, потому что они использовали как + для вставки списка, и это делает жизнь по-настоящему запутанной. Большая часть этой проблемы проистекает из того факта, что есть только приблизительно полдюжины операторов, которые вы можете ощутимо перегружать, и все же есть тысячи или миллионы операторов, которые люди хотели бы определить - так что вы должны выбрать,

Росс Паттерсон
источник
50
Ах, да, старое оправдание «давайте притупим острый инструмент, чтобы не повредить себя».
Blrfl
22
@Blrfl: Если инструмент создает больше проблем, чем решает, это не очень хороший инструмент. Конечно, решение о том, так ли это с перегрузкой операторов, может обернуться очень долгим обсуждением.
Джорджио
15
-1. Это не отвечает на вопрос вообще. Java делает имеет перегрузку операторов. ==Оператор перегружен для объектов и примитивов. +Оператор перегружен byte, short, int, long, float, double, Stringи , вероятно , несколько других , я забыл. Было бы вполне возможно перегрузку ==для Stringа.
Йорг Миттаг
10
@Jorg - нет, это не так. Перегрузку оператора невозможно определить на уровне пользователя. В компиляторе действительно есть некоторые особые случаи, но это вряд ли подходит
AZ01
9
@Blrfl: Я не возражаю против того, чтобы oafs ранили себя. Когда они случайно высовывают мне глаза, я раздражаюсь.
Джонас
9

Согласованность в языке. Наличие оператора, который действует по-другому, может быть удивительным для программиста. Java не позволяет пользователям перегружать операторы - поэтому равенство ссылок является единственным разумным значением для ==объектов.

В Java:

  • Между числовыми типами ==сравнивает числовое равенство
  • Между логическими типами, ==сравнивает логическое равенство
  • Между объектами ==сравнивает ссылочную идентичность
    • Используйте .equals(Object o)для сравнения значений

Вот и все. Простое правило и просто определить, что вы хотите. Это все описано в разделе 15.21 JLS . Он состоит из трех подразделов, которые легко понять, реализовать и обдумать.

После того, как вы разрешите перегрузку== , точное поведение не является чем-то, что вы можете посмотреть на JLS, указать пальцем на конкретный элемент и сказать «вот как это работает», код может стать трудным для рассуждения. Точное поведение ==может быть удивительным для пользователя. Каждый раз, когда вы видите это, вы должны вернуться и проверить, что это на самом деле означает.

Поскольку Java не допускает перегрузку операторов, необходимо иметь тест на равенство значений, который можно переопределить базовым определением. Таким образом, это было предписано этими вариантами дизайна. ==в Java тестирует числовые значения для числовых типов, логическое равенство для логических типов и ссылочное равенство для всего остального (который может переопределять .equals(Object o)все, что они хотят для равенства значений).

Это не вопрос «есть ли вариант использования для конкретного последствия этого проектного решения», а скорее «это проектное решение для облегчения этих других вещей, это его следствие».

Строковое интернирование , является одним из таких примеров этого. Согласно JLS 3.10.5 все строковые литералы интернированы. Другие строки интернируются, если .intern()на них ссылаются . Это "foo" == "foo"правда, является следствием дизайнерских решений, принятых для минимизации использования памяти литералами String. Кроме того, интернирование String - это то, что находится на уровне JVM и имеет небольшую открытость для пользователя, но в подавляющем большинстве случаев не должно касаться программиста (а сценарии использования для программистов не были что-то, что было в списке для дизайнеров при рассмотрении этой функции).

Люди будут указывать на это +и +=перегружены для строки. Однако это не здесь и не там. Остается, что если ==значение String (и только String) имеет значение равенства, то для равенства ссылок потребуется другой метод (который существует только в String). Кроме того, это излишне усложнит методы, которые принимают Object и ожидают, что ==будут вести себя одинаково и .equals()вести себя другим, что потребует от пользователей особого случая для всех этих методов для String.

Согласованный контракт для ==объектов - это только ссылочное равенство, и оно .equals(Object o)существует для всех объектов, которые должны проверяться на равенство значений. Усложнение этого усложняет слишком много вещей.


источник
спасибо, что нашли время ответить. Это было бы отличным ответом на другие вопросы, с которыми я связан. К сожалению, это не подходит для этого вопроса. Я дополню ОП пояснениями на основе комментариев. Я ищу больше случаев использования, когда пользователь языка хотел бы иметь ложные отрицания при сравнении строк. Язык обеспечивает эту функцию как последовательность, теперь я хотел бы, чтобы мы пошли дальше. Возможно, подумать об этом от дизайнера нового языка, это нужно? (к сожалению, нет lang-design.SE)
Anonsage
3
@ Anonage это не ложный минус. Они не один и тот же объект. Это все, что он говорит. Я также должен указать, что в Java 8 new String("foo") == new String("foo")может быть истина (см. Дедупликация строк ).
1
Что касается языкового дизайна, CS.SE рекламирует, что это может быть там по теме.
Ох спасибо! Я опубликую свои будущие вопросы о дизайне lang там. :) И, да, к сожалению, «ложноотрицательный» не самый точный способ описать мой вопрос и то, что я ищу ... Мне нужно написать больше слов, чтобы люди не могли угадать, что я пытаюсь сказать.
Anonsage
2
«Согласованность в языке» также помогает с дженериками
Брендан
2

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

Я не эксперт в C #, но разработчики этого языка, похоже, настроили его так, что каждый примитив является a, structа каждый struct- объектом. Поскольку C # допускает перегрузку операторов, такая компоновка позволяет любому классу, а не просто Stringзаставить себя работать «ожидаемым» образом с любым оператором. C ++ допускает то же самое.

Blrfl
источник
1
«Java не поддерживает перегрузку операторов, что означает, что == применяется только к примитивным типам или ссылкам. Все остальное требует вызова метода.»: Можно добавить, что, если ==подразумевается равенство строк, нам потребуются другие обозначения для равенства ссылок.
Джорджио
@ Джорджио: Точно. Смотрите мой комментарий к ответу Гилада Наамана.
Blrfl
Хотя это можно решить статическим методом, который сравнивает ссылки двух объектов (или оператора). Как в C #, например.
Гилад Нааман
@GiladNaaman: Это была бы игра с нулевой суммой, потому что она вызывает противоположную проблему с тем, что есть у Java сейчас: равенство будет для оператора, и вам придется вызывать метод для сравнения ссылок. Кроме того, вам нужно будет наложить требование, чтобы все классы реализовывали что-то, к чему можно привязаться ==. Это эффективно добавляет перегрузку операторов, что будет иметь огромное влияние на способ реализации Java.
Blrfl
1
@Blrfl: Не совсем. Всегда будет определенный способ сравнения reference ( ClassName.ReferenceEquals(a,b)), а также ==оператор и Equalsметод по умолчанию, указывающие на ReferenceEquals.
Гилад Нааман
2

Это было сделано по-другому на других языках.

В Object Pascal (Delphi / Free Pascal) и C # оператор равенства определен для сравнения значений, а не ссылок, при работе со строками.

В частности, в Pascal string - это примитивный тип (одна из вещей, которые мне действительно нравятся в Pascal, получение NullreferenceException только из-за неинициализированной строки просто раздражает) и имеют семантику копирования при записи, что делает (в большинстве случаев) строковые операции очень дешево (другими словами, заметно только после того, как вы начнете объединять многомегабайтные строки).

Итак, это решение для разработки языка для Java. Когда они разрабатывали язык, они следовали пути C ++ (например, Std :: String), поэтому строки - это объекты, что является ИМХО, чтобы компенсировать отсутствие в C реального типа строк вместо того, чтобы делать строки примитивными (какими они являются).

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

Фабрицио Араужо
источник
как это отвечает на заданный вопрос?
Комнат
Смотрите последнее предложение (которое я выделил в соответствующем абзаце при редактировании).
Fabricio Araujo
1
ИМХО, Stringдолжен был быть примитивный тип в Java. В отличие от других типов, компилятор должен знать о String; кроме того, операции над ним будут достаточно распространены, так что для многих типов приложений они могут создавать узкое место в производительности (что может быть облегчено встроенной поддержкой). Типичный string[нижний регистр] будет иметь объект, выделенный в куче для хранения его содержимого, но никакой «нормальной» ссылки на этот объект не будет нигде; таким образом, это может быть однонаправленное Char[]или Byte[]не обязательно быть Char[]косвенным через другой объект.
суперкат
1

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

Класс «String» не является примитивом, поэтому он не имеет перегрузки для «==» и использует по умолчанию сравнение адреса объекта в памяти компьютера.

Я не уверен, но я думаю , что в Java 7 или 8 оракул сделал исключение в компиляторе , чтобы признать в str1 == str2качествеstr1.equals(str2)

Гилад Нааман
источник
«Я не уверен, но я думаю, что в Java 7 или 8 оракул сделал исключение в компиляторе, чтобы распознать str1 == str2 как str1.equals (str2)»: я не удивлюсь: Oracle, кажется, менее обеспокоен с минимализмом, чем было у Солнца.
Джорджио
2
Если это правда, это очень уродливый хак, поскольку это означает, что теперь есть один класс, который язык обрабатывает по-другому, и нарушает код, который сравнивает ссылки. : - @
Blrfl
1
@WillihamTotland: рассмотрим противоположный случай. В настоящее время, если я создаю две строки, s1и s2и дать им такое же содержание, они проходят равенство ( s1.equals(s2)сравнение) , но не такое же ссылка ( ==) для сравнения , потому что они два разных объекта. Изменение семантики ==для обозначения равенства приведет s1 == s2к оценке того, trueгде она использовалась для оценки false.
Blrfl
2
@Brlfl: Хотя это действительно так, на первый взгляд это кажется исключительно плохой вещью, на которую можно положиться, поскольку строки являются неизменяемыми, внутренними объектами.
Виллихам Тотланд
2
@Giorgio: «Java 7 или 8 Oracle сделали исключение в компиляторе, чтобы распознать str1 == str2 как str1.equals (str2)» Нет, спецификация языка Java гласит: « Справочные операторы равенства : « Если операнды оператора равенства как ссылочного типа, так и нулевого типа, тогда операция является объектным равенством. " Это все, ребята. Я не нашел ничего нового в ранней версии Java 8 .
Дэвид Тонхофер
0

Похоже, что Java была разработана, чтобы поддерживать фундаментальное правило, согласно которому ==оператор должен быть допустимым в любое время, когда один операнд может быть преобразован в тип другого, и должен сравнивать результат такого преобразования с не преобразованным операндом.

Это правило едва ли уникально для Java, но оно имеет некоторые далеко идущие (и, к сожалению, IMHO) эффекты на разработку других связанных с типом аспектов языка. Было бы чётче указывать поведение в ==отношении определенных комбинаций типов операндов и запрещать комбинации типов X и Y, где x1==y1и x2==y1не подразумевалось бы x1==x2, но языки редко делают это [в соответствии с этой философией, double1 == long1либо нужно было бы указать, double1не является точным представлением long1или отказывается компилировать;int1==Integer1должно быть запрещено, но должны быть удобные и эффективные, не бросающие средства, средства проверки, является ли объект целым в штучной упаковке с определенным значением (сравнение с чем-то, что не является целым в штучной упаковке, должно просто возвращаться false)].

Что касается применения ==оператора к строкам, если бы Java запретила прямые сравнения между операндами типа Stringи Object, она вполне могла бы избежать неожиданностей в поведении ==, но не было бы такого поведения, которое она могла бы реализовать для таких сравнений, которые не были бы удивительными. Наличие двух строковых ссылок, сохраняемых в типе, Objectведет себя иначе, чем ссылки, хранящиеся в типе, Stringбыло бы гораздо менее удивительно, чем если бы любое из этих поведений отличалось от такового при законном сравнении смешанного типа. Если String1==Object1это законно, это будет означать, что единственный способ поведения String1==String2и Object1==Object2сопоставления String1==Object1состоит в том, чтобы они соответствовали друг другу.

Supercat
источник
Я должен что-то упустить, но IMHO ==для объектов должен просто вызывать (null-safe) equals и что-то еще (например, ===или System.identityEqual) должно использоваться для сравнения идентификаторов. Смешивание примитивов и объектов изначально было бы запрещено (до 1.5 не было автобокса), а затем можно было бы найти какое-то простое правило (например, unll-safe unbox, затем приведение, затем сравнение).
Maaartinus
@maaartinus: Хороший языковой дизайн должен использовать отдельные операторы равенства для равенства значений и ссылок. Хотя я согласен, что концептуально было бы возможно иметь int==Integerоператор return, falseесли значение Integerравно нулю, и иначе сравнивать значения, этот подход был бы непохожим на поведение ==во всех других обстоятельствах, где он безоговорочно приводит оба операнда к одному и тому же типу до сравнивая их. Лично мне интересно, было ли введено автоматическое распаковывание, чтобы позволить int==Integerповедение, которое не было бессмысленным ...
суперкат
... поскольку автобоксирование intи сравнение ссылок было бы глупо [но не всегда терпело неудачу]. В противном случае я не вижу причин, позволяющих неявное преобразование, которое может завершиться неудачей с NPE.
суперкат
Я думаю, что моя идея последовательна. Просто помните, что в лучшем мире ==не имеет ничего общего identityEquals. +++ "отдельные операторы равенства для равенства значений и ссылок" - но какие? Я бы рассмотреть как примитивные ==и equalsкак делать значение сравнения в том смысле , что equalsсмотрит на значении ссылки. +++ Если ==подразумевается equals, тогда int==IntegerСЛЕДУЕТ делать автобокс и сравнивать ссылки с использованием нулевых безопасных равных. +++ Боюсь, моя идея на самом деле не моя, а только то, что делает Котлин.
Maaartinus
@maaartinus: Если бы ==никогда не проверялось равенство ссылок, то он мог бы разумно выполнить нулевой безопасный тест на равенство значений. Тот факт , что она делает тест равенства ссылок, однако, сильно ограничивает , как он может обрабатывать смешанные сравнения ссылки / значения без непоследовательности. Также обратите внимание, что в Java зафиксировано представление о том, что операторы переводят оба операнда в один и тот же тип, а не приводят к особому поведению, основанному на комбинациях задействованных типов. Например, 16777217==16777216.0fвозвращает, trueпотому что выполняет преобразование с потерями первого операнда в float, в то время как ...
суперкат
0

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

Address oldAddress;
Address newAddress;
... populate values ...
if (oldAddress==newAddress)
... etc ...

Я могу иметь или не иметь функцию равенства в таких случаях. Если я это сделаю, функция equals может сравнить все содержимое обоих объектов. Часто это просто сравнивает некоторый идентификатор. «A и B - это ссылки на один и тот же объект», а «A и B - это два разных объекта с одинаковым содержанием», это, конечно, две совершенно разные идеи.

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

Integer three=new Integer(3);
Integer triangle=new Integer(3);
if (three==triangle) ...

Конечно, это возвращает ложь, но я вижу, что кто-то думает, что это должно быть правдой.

Но как только вы скажете, что == сравнивает дескрипторы ссылок, а не содержимое объектов в целом, создание особого случая для строк может привести к путанице. Как сказал кто-то еще здесь, что, если вы хотите сравнить дескрипторы двух объектов String? Будет ли какая-то специальная функция, чтобы сделать это только для строк?

И что насчет ...

Object x=new String("foo");
Object y=new String("foo");
if (x==y) ...

Это ложно, потому что это два разных объекта, или истина, потому что это строки, содержимое которых одинаково?

Так что да, я понимаю, как программисты смущаются этим. Я сделал это сам, я имею в виду написать if myString == "foo", когда я имел в виду myString.equals ("foo"). Но, за исключением изменения смысла оператора == для всех объектов, я не вижу, как его решить.

сойка
источник
Обратите внимание, что другие современные языки в JVM, такие как Scala, используют ==для обозначения «равных строк».
Андрес Ф.
@AndresF. (пожимает плечами) В Java «<» означает «меньше чем», а в XML «открывает тег». В VB «=» может означать «равно», а в Java он используется только для присваивания. И т.д. Неудивительно, что разные языки используют разные символы для обозначения одного и того же символа или один и тот же символ для обозначения разных вещей.
Jay
Это был не твой ответ. Это был просто комментарий. Я просто указывал, что более современный язык, чем Java, позволил изменить смысл ==, как вы упоминали в конце.
Андрес Ф.
@AndresF. И в моем ответе не было ничего хорошего, просто я сказал, что разные языки по-разному подходят к этим вопросам. :-) Мне действительно нравится, как VB обрабатывает это ... пауза для шипения и шиков от ненавистников VB ... "=" всегда сравнивает значение (для системных типов), примитив или объект. «Есть» сравнивает ручки двух объектов. Это кажется мне более интуитивным.
Jay
Конечно. Но Scala намного ближе к Java, чем Visual Basic. Мне нравится думать, что дизайнеры Scala поняли, что использование Java ==было подвержено ошибкам.
Андрес Ф.
0

Это действительно вопрос для Strings, а не только для строк, но и для других неизменных объектов , представляющих некоторые «ценность», например Double, BigIntegerи даже InetAddress.

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

  • Пусть компилятор узнает обо всех этих классах-значениях и способах сравнения их содержимого. Если бы это было всего несколько классов из java.langпакета, я бы это учел, но это не распространяется на такие случаи, как InetAddress.

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

  • Удалите открытые конструкторы и используйте статические методы, возвращающие экземпляры из пула, всегда возвращающие один и тот же экземпляр для одного и того же значения. Чтобы избежать утечек памяти, вам нужно что-то вроде SoftReferences в пуле, которого не было в Java 1.0. И теперь, чтобы сохранить совместимость, String()конструкторы больше не могут быть удалены.

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

Для меня читаемость кода является наиболее важной, и программист на Java знает, что операторы имеют фиксированное значение, определенное в спецификации языка, тогда как методы определены в некотором коде, и их значение нужно искать в Javadoc метода. Я хотел бы остаться с этим различием, даже если это означает, что сравнения строк не смогут использовать ==оператор.

Есть только один аспект сравнений Java, который меня раздражает: эффект автобокса и -бокса. Это скрывает различие между примитивом и типом обертки. Но когда вы сравниваете их ==, они ОЧЕНЬ разные.

    int i=123456;
    Integer j=123456;
    Integer k=123456;
    System.out.println(i==j);  // true or false? Do you know without reading the specs?
    System.out.println(j==k);  // true or false? Do you know without reading the specs?
Ральф Клеберхофф
источник