Почему Java не предлагает перегрузку операторов?
При переходе с C ++ на Java возникает очевидный вопрос без ответа: почему Java не включает перегрузку операторов?
Не Complex a, b, c; a = b + c;
намного проще, чем Complex a, b, c; a = b.add(c);
?
Есть ли известная причина для этого, действительные аргументы в пользу того, чтобы не разрешать перегрузку операторов? Причина произвольная или затеряна во времени?
Переведено автоматически
Ответ 1
Есть много сообщений с жалобами на перегрузку оператора.
Я почувствовал, что должен прояснить концепцию "перегрузки операторов", предложив альтернативную точку зрения на эту концепцию.
Этот аргумент является ошибочным.
Запутать код в C или Java с помощью функций / методов так же легко, как в C ++ с помощью перегрузок операторов:
// C++
T operator + (const T & a, const T & b) // add ?
{
T c ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
// Java
static T add (T a, T b) // add ?
{
T c = new T() ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
/* C */
T add (T a, T b) /* add ? */
{
T c ;
c.value = a.value - b.value ; /* subtract !!! */
return c ;
}
Для другого примера давайте посмотрим на Cloneable
интерфейс в Java:
Предполагается, что вы клонируете объект, реализующий этот интерфейс. Но вы могли бы солгать. И создать другой объект. На самом деле, этот интерфейс настолько слаб, что вы могли бы возвращать объект совсем другого типа, просто ради удовольствия:
class MySincereHandShake implements Cloneable
{
public Object clone()
{
return new MyVengefulKickInYourHead() ;
}
}
Поскольку Cloneable
интерфейсом можно злоупотреблять / запутывать, следует ли запретить его на тех же основаниях, на которых предполагается перегрузка операторов C ++?
Мы могли бы перегрузить toString()
метод MyComplexNumber
класса, чтобы он возвращал строго определенное время суток. Следует ли также запретить toString()
перегрузку? Мы могли бы саботировать MyComplexNumber.equals
, чтобы заставить его возвращать случайное значение, изменять операнды ... и т.д. и т.п. и т.п..
В Java, как и в C ++ или любом другом языке, программист должен соблюдать минимум семантики при написании кода. Это означает реализацию add
функции, которая добавляет, и Cloneable
метода реализации, который клонирует, и ++
оператора, который увеличивает.
Теперь, когда мы знаем, что код можно саботировать даже с помощью нетронутых методов Java, мы можем спросить себя о реальном использовании перегрузки операторов в C ++?
Ниже мы сравним для разных случаев "один и тот же" код на Java и C ++, чтобы иметь представление о том, какой стиль кодирования более понятен.
// C++ comparison for built-ins and user-defined types
bool isEqual = A == B ;
bool isNotEqual = A != B ;
bool isLesser = A < B ;
bool isLesserOrEqual = A <= B ;
// Java comparison for user-defined types
boolean isEqual = A.equals(B) ;
boolean isNotEqual = ! A.equals(B) ;
boolean isLesser = A.comparesTo(B) < 0 ;
boolean isLesserOrEqual = A.comparesTo(B) <= 0 ;
Пожалуйста, обратите внимание, что A и B могут быть любого типа в C ++, при условии, что предусмотрены перегрузки операторов. В Java, когда A и B не являются примитивами, код может стать очень запутанным, даже для примитивоподобных объектов (BigInteger и т.д.)...
// C++ container accessors, more natural
value = myArray[25] ; // subscript operator
value = myVector[25] ; // subscript operator
value = myString[25] ; // subscript operator
value = myMap["25"] ; // subscript operator
myArray[25] = value ; // subscript operator
myVector[25] = value ; // subscript operator
myString[25] = value ; // subscript operator
myMap["25"] = value ; // subscript operator
// Java container accessors, each one has its special notation
value = myArray[25] ; // subscript operator
value = myVector.get(25) ; // method get
value = myString.charAt(25) ; // method charAt
value = myMap.get("25") ; // method get
myArray[25] = value ; // subscript operator
myVector.set(25, value) ; // method set
myMap.put("25", value) ; // method put
В Java мы видим, что для выполнения каждым контейнером одного и того же действия (доступа к его содержимому через индекс или идентификатор) у нас есть другой способ сделать это, что сбивает с толку.
В C ++ каждый контейнер использует один и тот же способ доступа к своему содержимому благодаря перегрузке операторов.
В приведенных ниже примерах используется Matrix
объект, найденный по первым ссылкам, найденным в Google для "Java Matrix object" и "C ++ Matrix object":
// C++ YMatrix matrix implementation on CodeProject
// http://www.codeproject.com/KB/architecture/ymatrix.aspx
// A, B, C, D, E, F are Matrix objects;
E = A * (B / 2) ;
E += (A - B) * (C + D) ;
F = E ; // deep copy of the matrix
// Java JAMA matrix implementation (seriously...)
// http://math.nist.gov/javanumerics/jama/doc/
// A, B, C, D, E, F are Matrix objects;
E = A.times(B.times(0.5)) ;
E.plusEquals(A.minus(B).times(C.plus(D))) ;
F = E.copy() ; // deep copy of the matrix
И это касается не только матриц. Классы BigInteger
и BigDecimal
Java страдают от той же сбивающей с толку многословности, тогда как их эквиваленты в C ++ столь же понятны, как встроенные типы.
// C++ Random Access iterators
++it ; // move to the next item
--it ; // move to the previous item
it += 5 ; // move to the next 5th item (random access)
value = *it ; // gets the value of the current item
*it = 3.1415 ; // sets the value 3.1415 to the current item
(*it).foo() ; // call method foo() of the current item
// Java ListIterator<E> "bi-directional" iterators
value = it.next() ; // move to the next item & return the value
value = it.previous() ; // move to the previous item & return the value
it.set(3.1415) ; // sets the value 3.1415 to the current item
// C++ Functors
myFunctorObject("Hello World", 42) ;
// Java Functors ???
myFunctorObject.execute("Hello World", 42) ;
// C++ stream handling (with the << operator)
stringStream << "Hello " << 25 << " World" ;
fileStream << "Hello " << 25 << " World" ;
outputStream << "Hello " << 25 << " World" ;
networkStream << "Hello " << 25 << " World" ;
anythingThatOverloadsShiftOperator << "Hello " << 25 << " World" ;
// Java concatenation
myStringBuffer.append("Hello ").append(25).append(" World") ;
Хорошо, в Java вы тоже можете использовать MyString = "Hello " + 25 + " World" ;
... Но, подождите секунду: это перегрузка операторов, не так ли? Разве это не обман???
:-D
Один и тот же универсальный код, модифицирующий операнды, должен использоваться как для встроенных модулей / примитивов (у которых нет интерфейсов в Java), так и для стандартных объектов (у которых может не быть правильного интерфейса) и пользовательских объектов.
Например, вычисление среднего значения двух значений произвольных типов:
// C++ primitive/advanced types
template<typename T>
T getAverage(const T & p_lhs, const T & p_rhs)
{
return (p_lhs + p_rhs) / 2 ;
}
int intValue = getAverage(25, 42) ;
double doubleValue = getAverage(25.25, 42.42) ;
complex complexValue = getAverage(cA, cB) ; // cA, cB are complex
Matrix matrixValue = getAverage(mA, mB) ; // mA, mB are Matrix
// Java primitive/advanced types
// It won't really work in Java, even with generics. Sorry.
Теперь, когда мы увидели справедливые сравнения между кодом C ++, использующим перегрузку операторов, и тем же кодом на Java, мы можем обсудить "перегрузку операторов" как концепцию.
Даже за пределами информатики существует перегрузка операторов: например, в математике перегружены такие операторы, как +
, -
, *
и т.д.
Действительно, значение +
, -
, *
и т.д. Меняется в зависимости от типов операндов (чисел, векторов, квантовых волновых функций, матриц и т.д.).
Большинство из нас в рамках наших научных курсов изучали несколько значений операторов в зависимости от типов операндов. Тогда они показались нам непонятными?
Это самая важная часть перегрузки операторов: как в математике или физике, операция зависит от типов ее операндов.
Итак, знайте тип операнда, и вы будете знать результат операции.
В C реальное поведение оператора меняется в зависимости от его операндов. Например, добавление двух целых чисел отличается от добавления двух double или даже одного целого числа и одного double. Существует даже целая область арифметики указателей (без приведения вы можете добавить к указателю целое число, но вы не можете добавить два указателя ...).
В Java нет арифметики указателей, но кто-то все же обнаружил, что конкатенация строк без +
оператора была бы достаточно нелепой, чтобы оправдать исключение из кредо "перегрузка операторов - зло".
Просто вы, как программист на C (по историческим причинам) или Java (по личным причинам, см. Ниже), не можете предоставить свой собственный.
В C ++ перегрузка операторов для встроенных типов невозможна (и это хорошо), но у пользовательских типов могут быть пользовательские перегрузки операторов.
Как уже говорилось ранее, в C ++, в отличие от Java, пользовательские типы не считаются гражданами языка второго сорта по сравнению со встроенными типами. Итак, если встроенные типы имеют операторы, пользовательские типы тоже должны иметь их.
Правда в том, что, подобно тому, как методы toString()
, clone()
, equals()
предназначены для Java (т.е. квазистандартные), перегрузка операторов C ++ является настолько неотъемлемой частью C ++, что становится такой же естественной, как исходные операторы C или ранее упомянутые методы Java.
В сочетании с шаблонным программированием перегрузка операторов становится хорошо известным шаблоном проектирования. Фактически, вы не сможете продвинуться далеко в STL без использования перегруженных операторов и перегрузки операторов для вашего собственного класса.
При перегрузке оператора следует стремиться уважать семантику оператора. Не вычитать в +
операторе (как в "не вычитать в add
функции" или "возвращать дерьмо в clone
методе").
Перегрузка приведения может быть очень опасной, потому что она может привести к неоднозначностям. Поэтому их действительно следует приберечь для четко определенных случаев. Что касается &&
и ||
, никогда не перегружайте их, если вы действительно не знаете, что делаете, так как вы потеряете оценку короткого замыкания, которой пользуются собственные операторы &&
и ||
.
Because James Gosling said so:
I left out operator overloading as a fairly personal choice because I had seen too many people abuse it in C++.
James Gosling. Source: http://www.gotw.ca/publications/c_family_interview.htm
Please compare Gosling's text above with Stroustrup's below:
Many C++ design decisions have their roots in my dislike for forcing people to do things in some particular way [...] Often, I was tempted to outlaw a feature I personally disliked, I refrained from doing so because I did not think I had the right to force my views on others.
Bjarne Stroustrup. Source: The Design and Evolution of C++ (1.3 General Background)
Some objects would greatly benefit from operator overloading (concrete or numerical types, like BigDecimal, complex numbers, matrices, containers, iterators, comparators, parsers etc.).
In C++, you can profit from this benefit because of Stroustrup's humility. In Java, you're simply screwed because of Gosling's personal choice.
The reasons for not adding operator overloading now in Java could be a mix of internal politics, allergy to the feature, distrust of developers (you know, the saboteur ones that seem to haunt Java teams...), compatibility with the previous JVMs, time to write a correct specification, etc..
So don't hold your breath waiting for this feature...
Yeah...
While this is far from being the only difference between the two languages, this one never fails to amuse me.
Apparently, the C# folks, with their "every primitive is a struct
, and a struct
derives from Object", got it right at first try.
Despite all the FUD against used defined operator overloading, the following languages support it: Kotlin, Scala, Dart, Python, F#, C#, D, Algol 68, Smalltalk, Groovy, Raku (formerly Perl 6), C++, Ruby, Haskell, MATLAB, Eiffel, Lua, Clojure, Fortran 90, Swift, Ada, Delphi 2005...
So many languages, with so many different (and sometimes opposing) philosophies, and yet they all agree on that point.
Food for thought...
Ответ 2
James Gosling likened designing Java to the following:
"Есть такой принцип переезда, когда вы переезжаете из одной квартиры в другую. Интересный эксперимент заключается в том, чтобы упаковать свою квартиру и разложить все по коробкам, затем переехать в следующую квартиру и ничего не распаковывать, пока это вам не понадобится. Итак, вы готовите свой первый обед и достаете что-то из коробки. Затем, примерно через месяц или около того, вы используете это, чтобы в значительной степени выяснить, какие вещи в вашей жизни вам действительно нужны, а затем вы берете все остальное - забываете, как сильно оно вам нравится или насколько оно классное - и просто выбрасываете это. Удивительно, как это упрощает вашу жизнь, и вы можете использовать этот принцип во всех видах проблем дизайна: не делайте что-то только потому, что это круто или просто потому, что это интересно. "
Вы можете прочитать контекст цитаты здесь
В принципе, перегрузка операторов отлично подходит для класса, который моделирует какую-либо точку, валюту или комплексное число. Но после этого у вас быстро заканчиваются примеры.
Другим фактором было злоупотребление этой функцией в C ++ разработчиками, перегружающими операторы, такие как '&&', '||', операторы приведения и, конечно, 'new'. Сложность, возникающая в результате сочетания этого с передачей по значению и исключениями, хорошо описана в книге " Исключительный C ++" .
Ответ 3
Ознакомьтесь с Boost.Единицы измерения: текст ссылки
Он обеспечивает анализ измерений с нулевыми накладными расходами за счет перегрузки операторов. Насколько понятнее это может стать?
quantity<force> F = 2.0*newton;
quantity<length> dx = 2.0*meter;
quantity<energy> E = F * dx;
std::cout << "Energy = " << E << endl;
на самом деле выводило бы "Энергия = 4 Дж", что правильно.
Ответ 4
Разработчики Java решили, что перегрузка операторов доставляет больше проблем, чем того стоит. Вот так просто.
В языке, где каждая объектная переменная фактически является ссылкой, перегрузка операторов сопряжена с дополнительной опасностью быть совершенно нелогичной - по крайней мере, для программиста на C ++. Сравните ситуацию с перегрузкой ==
оператора равенства в C # и Object.Equals
и Object.ReferenceEquals
.