Почему Java не предлагает перегрузку операторов?

Переходя от C++ к Java, очевидный вопрос без ответа состоит в том, почему Java не включает перегрузку операторов?

не Complex a, b, c; a = b + c; намного проще чем Complex a, b, c; a = b.add(c);?

Есть ли известная причина для этого, действительные аргументы для запрета перегрузки операторов? Причина произвольна или потеряна во времени?

18 ответов

Решение

Предполагая, что вы хотите перезаписать предыдущее значение объекта, на который ссылается a, тогда должна быть вызвана функция-член.

Complex a, b, c;
// ...
a = b.add(c);

В C++ это выражение говорит компилятору создать три (3) объекта в стеке, выполнить сложение и скопировать результирующее значение из временного объекта в существующий объект a,

Тем не менее, в Java, operator= не выполняет копирование значений для ссылочных типов, и пользователи могут создавать только новые ссылочные типы, а не типы значений. Так что для пользовательского типа с именем ComplexНазначение означает копирование ссылки на существующее значение.

Рассмотрим вместо этого:

b.set(1, 0); // initialize to real number '1'
a = b; 
b.set(2, 0);
assert( !a.equals(b) ); // this assertion will fail

В C++ это копирует значение, поэтому сравнение будет неравным. В Java operator= выполняет эталонное копирование, поэтому a а также b теперь ссылаются на то же значение. В результате сравнение будет производить "равно", так как объект будет сравнивать равный самому себе.

Разница между копиями и ссылками только добавляет путаницы в перегрузку операторов. Как упомянул @Sebastian, Java и C# оба должны иметь дело со значением и ссылочным равенством отдельно - operator+ вероятно, будет иметь дело с ценностями и объектами, но operator= уже реализован для работы со ссылками.

В C++ вы должны иметь дело только с одним видом сравнения за раз, так что это может быть менее запутанным. Например, на Complex, operator= а также operator== оба работают над значениями - копируют значения и сравнивают значения соответственно.

Есть много постов с жалобами на перегрузку операторов.

Я чувствовал, что должен прояснить концепции "перегрузки операторов", предлагая альтернативную точку зрения на эту концепцию.

Код запутывает?

Этот аргумент является ошибкой.

Обфускация возможна на всех языках...

Код в 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 ;
}

... даже в стандартных интерфейсах Java

Для другого примера, давайте посмотрим 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 и Java имеют (жестко запрограммированную) перегрузку операторов

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

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

Просто вы, как программист C (по историческим причинам) или Java (по личным причинам, см. Ниже), не можете предоставить свой собственный.

В C++ перегрузка операторов не является обязательной...

В C++ перегрузка операторов для встроенных типов невозможна (и это хорошо), но пользовательские типы могут иметь пользовательские перегрузки операторов.

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

Правда в том, что, как и toString(), clone(), equals() методы предназначены для Java (то есть квазистандартно-подобные), перегрузка операторов C++ является настолько большой частью C++, что становится такой же естественной, как исходные операторы C или ранее упомянутые методы Java.

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

... но этим нельзя злоупотреблять

Перегрузка оператора должна стремиться соблюдать семантику оператора. Не вычитать в + оператор (как в "не вычитать в add функция ", или" вернуть дерьмо в clone Метод ").

Перегрузка броска может быть очень опасной, потому что она может привести к неясностям. Таким образом, они действительно должны быть зарезервированы для четко определенных случаев. Что касается && а также ||, никогда не перегружайте их, если вы действительно не знаете, что делаете, так как вы потеряете оценку короткого замыкания, которую имеют собственные операторы && а также || наслаждаться.

Итак... Хорошо... Тогда почему это невозможно в Java?

Потому что Джеймс Гослинг сказал так:

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

Джеймс Гослинг. Источник: http://www.gotw.ca/publications/c_family_interview.htm

Пожалуйста, сравните текст Гослинга выше со Страуструпом ниже:

Многие дизайнерские решения C++ коренятся в моей неприязни к тому, чтобы заставлять людей делать что-то определенным образом [...] Часто у меня возникало искушение запретить функцию, которая мне лично не нравилась, я воздерживался от этого, потому что не думал, что имею право навязывать свои взгляды другим.

Бьярне Страуструп. Источник: разработка и развитие C++ (1.3 Общая информация)

Будет ли перегрузка операторов полезной для Java?

Некоторые объекты могут значительно выиграть от перегрузки операторов (конкретные или числовые типы, такие как BigDecimal, комплексные числа, матрицы, контейнеры, итераторы, компараторы, парсеры и т. Д.).

В C++ вы можете воспользоваться этим преимуществом благодаря скромности Страуструпа. В Java вы просто облажались из-за личного выбора Гослинга.

Может ли он быть добавлен в Java?

Причинами отсутствия добавления перегрузки операторов сейчас в Java могут быть сочетание внутренней политики, аллергия на эту функцию, недоверие разработчиков (вы знаете, диверсантов, которые, похоже, преследуют команды Java...), совместимость с предыдущими JVM, время написать правильную спецификацию и т.д..

Так что не ждите, пока эта функция...

Но они делают это на C#!!!

Да уж...

Хотя это далеко не единственное различие между двумя языками, этот не может не удивить меня.

Видимо, люди C#, с их "каждый примитив является struct и struct происходит от объекта ", понял все правильно с первой попытки.

И они делают это на других языках!!!

Несмотря на все FUD против используемой определенной перегрузки операторов, ее поддерживают следующие языки: Scala, Dart, Python, F#, C#, D, Algol 68, Smalltalk, Groovy, Perl 6, C++, Ruby, Haskell, MATLAB, Eiffel, Lua, Clojure, Fortran 90, Swift, Ada, Delphi 2005...

Так много языков, так много разных (и иногда противоположных) философий, и все же они все согласны с этим.

Пища для размышлений...

Джеймс Гослинг сравнил проектирование Java со следующим:

"Есть такой принцип при переезде, когда вы переезжаете из одной квартиры в другую. Интересный эксперимент состоит в том, чтобы упаковать свою квартиру и положить все в коробки, затем переехать в следующую квартиру и ничего не распаковывать, пока она вам не понадобится. Вы готовите свой первый прием пищи, и вы вытаскиваете что-то из коробки. Затем, через месяц или около того, вы использовали это, чтобы в значительной степени выяснить, какие вещи в вашей жизни вам действительно нужны, а затем вы берете остальную часть вещи - забудьте, насколько вам это нравится или насколько это круто - и вы просто выбрасываете это. Удивительно, как это упрощает вашу жизнь, и вы можете использовать этот принцип во всех вопросах дизайна: не делать вещи только потому, что они Вы крутые или просто потому, что они интересные ".

Вы можете прочитать контекст цитаты здесь

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

Другим фактором было злоупотребление функцией в C++ разработчиками, перегружающими такие операторы, как '&&', '||', операторы приведения и, конечно, 'new'. Сложность, возникающая из-за того, что это сочетается с передачей по значению и исключениями, хорошо освещена в книге, посвященной C++.

Проверьте Boost.Units: текст ссылки

Он обеспечивает анализ измерений с нулевыми накладными расходами посредством перегрузки операторов. Насколько яснее это можно получить?

quantity<force>     F = 2.0*newton;
quantity<length>    dx = 2.0*meter;
quantity<energy>    E = F * dx;
std::cout << "Energy = " << E << endl;

на самом деле будет выводить "Энергия = 4 Дж", что правильно.

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

В языке, где каждая переменная объекта на самом деле является ссылкой, перегрузка оператора создает дополнительную опасность быть нелогичной - по крайней мере, для программиста C++. Сравните ситуацию с перегрузкой оператора C# == и Object.Equals а также Object.ReferenceEquals (или как там это называется).

Groovy перегружен оператором и работает в JVM. Если вы не возражаете против производительности (которая становится меньше с каждым днем). Это автоматически на основе имен методов. например, "+" вызывает метод "плюс (аргумент)".

Некоторые люди говорят, что перегрузка операторов в Java приведет к запутыванию. Неужели эти люди когда-нибудь останавливались, чтобы посмотреть на какой-нибудь Java-код, выполняющий некоторые базовые математические операции, такие как увеличение финансовой стоимости на процент с помощью BigDecimal? .... многословие такого упражнения становится его собственной демонстрацией запутывания. По иронии судьбы, добавление перегрузки операторов в Java позволило бы нам создать наш собственный класс Currency, который сделал бы такой математический код элегантным и простым (менее запутанным).

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

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

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

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

+1 за добавление перегрузки операторов в Java 8.

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

По крайней мере, я думаю, что в этом причина. В любом случае, я на вашей стороне.:)

Сказать, что перегрузка оператора приводит к логическим ошибкам типа, что оператор не соответствует логике операции, это все равно, что ничего не сказать. Тот же тип ошибки произойдет, если имя функции не подходит для логики работы - так в чем же решение: отбросьте возможность использования функции!? Это комичный ответ - "Не подходит для логики работы", каждое имя параметра, каждый класс, функция или что-либо может быть логически неуместным. Я думаю, что эта опция должна быть доступна на респектабельном языке программирования, и те, кто считает, что это небезопасно - эй, нет, оба говорят, что вы должны его использовать. Давайте возьмем C#. Они свалили указатели, но эй - есть заявление "небезопасный код" - программа на ваш страх и риск.

Предполагая, что Java является языком реализации, тогда a, b и c будут ссылками на тип Complex с начальными значениями null. Также предполагая, что Complex является неизменяемым как упомянутый BigInteger и аналогичный неизменяемый BigDecimal, я думаю, что вы имеете в виду следующее, поскольку вы присваиваете ссылку на Complex, возвращаемую при добавлении b и c, а не сравниваете эту ссылку с a.

Не является:

Complex a, b, c; a = b + c;

намного проще чем:

Complex a, b, c; a = b.add(c);

Альтернативы нативной поддержке перегрузки операторов Java

Поскольку в Java нет перегрузки операторов, вот несколько альтернатив, на которые вы можете посмотреть:

  1. Используйте другой язык. Groovy и Scala имеют перегрузку операторов и основаны на Java.
  2. Используйте java-oo, плагин, который включает перегрузку операторов в Java. Обратите внимание, что он НЕ зависит от платформы. Кроме того, он имеет много проблем и не совместим с последними выпусками Java (то есть Java 10). ( Оригинальный источник Stackru)
  3. Используйте JNI, собственный интерфейс Java или альтернативы. Это позволяет вам писать методы на C или C++ (может быть, другие?) Для использования в Java. Конечно, это также не зависит от платформы.

Если кто-то знает о других, пожалуйста, прокомментируйте, и я добавлю его в этот список.

Иногда было бы неплохо иметь перегрузку операторов, классы друзей и множественное наследование.

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

// a = b + c
Complex a, b, c; a = b.add(c);

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

Перегрузка операторов приводит к путанице и создает крутую кривую обучения для новых программистов. Таким образом, они просто исключили перегрузку операторов из программы.

Это не веская причина для запрета, а практическая причина:

Люди не всегда используют это ответственно. Посмотрите на этот пример из библиотеки Python:

>>> IP()
<IP |>
>>> IP()/TCP()
<IP frag=0 proto=TCP |<TCP |>>
>>> Ether()/IP()/TCP()
<Ether type=0x800 |<IP frag=0 proto=TCP |<TCP |>>>
>>> IP()/TCP()/"GET / HTTP/1.0\r\n\r\n"
<IP frag=0 proto=TCP |<TCP |<Raw load='GET / HTTP/1.0\r\n\r\n' |>>>
>>> Ether()/IP()/IP()/UDP()
<Ether type=0x800 |<IP frag=0 proto=IP |<IP frag=0 proto=UDP |<UDP |>>>>
>>> IP(proto=55)/TCP()
<IP frag=0 proto=55 |<TCP |>>

Вот объяснение:

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

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

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

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

Хотя язык Java напрямую не поддерживает перегрузку операторов, вы можете использовать подключаемый модуль компилятора Manifold в любом проекте Java, чтобы включить его. Он поддерживает Java 8–13 (текущая версия Java) и полностью поддерживается в IntelliJ IDEA.

Другие вопросы по тегам