Каковы различия между "универсальными" типами в C++ и Java?
У Java есть дженерики, а C++ обеспечивает очень сильную модель программирования с template
s. Итак, в чем же разница между обобщениями C++ и Java?
12 ответов
Между ними есть большая разница. В C++ вам не нужно указывать класс или интерфейс для универсального типа. Вот почему вы можете создавать действительно универсальные функции и классы с оговоркой более свободной типизации.
template <typename T> T sum(T a, T b) { return a + b; }
Приведенный выше метод добавляет два объекта одного типа и может использоваться для любого типа T, для которого доступен оператор "+".
В Java вы должны указать тип, если вы хотите вызывать методы для переданных объектов, например:
<T extends Something> T sum(T a, T b) { return a.add ( b ); }
В C++ универсальные функции / классы могут быть определены только в заголовках, поскольку компилятор генерирует разные функции для разных типов (с которыми он вызывается). Таким образом, сборка идет медленнее. В Java компиляция не имеет серьезных проблем, но Java использует технику, называемую "стирание", в которой универсальный тип стирается во время выполнения, поэтому во время выполнения Java фактически вызывает...
Something sum(Something a, Something b) { return a.add ( b ); }
Так что общее программирование на Java не очень полезно, это всего лишь небольшой синтаксический сахар, чтобы помочь с новой конструкцией foreach.
РЕДАКТИРОВАТЬ: мнение выше о полезности было написано моложе себя. Обобщения Java помогают, конечно, с безопасностью типов.
Java Generics сильно отличается от шаблонов C++.
В основном в C++ шаблоны представляют собой прославленный набор препроцессоров / макросов (Примечание: поскольку некоторые люди не могут понять аналогию, я не говорю, что обработка шаблонов - это макрос). В Java они в основном являются синтаксическим сахаром для минимизации шаблонного преобразования объектов. Вот довольно приличное введение в шаблоны C++ против обобщений Java.
Чтобы уточнить этот момент: когда вы используете шаблон C++, вы в основном создаете еще одну копию кода, как если бы вы использовали #define
макро. Это позволяет вам делать такие вещи, как иметь int
параметры в определениях шаблонов, которые определяют размеры массивов и тому подобное.
Java не работает так. В Java все объекты выходят из java.lang.Object, поэтому, прежде чем Generics, вы бы написали код, подобный этому:
public class PhoneNumbers {
private Map phoneNumbers = new HashMap();
public String getPhoneNumber(String name) {
return (String)phoneNumbers.get(name);
}
...
}
потому что все типы коллекций Java использовали Object в качестве базового типа, чтобы вы могли помещать в них что угодно. Java 5 катится и добавляет дженерики, чтобы вы могли делать такие вещи, как:
public class PhoneNumbers {
private Map<String, String> phoneNumbers = new HashMap<String, String>();
public String getPhoneNumber(String name) {
return phoneNumbers.get(name);
}
...
}
И это все, что является универсальным Java: обертки для приведения объектов. Это потому, что Java Generics не улучшены. Они используют стирание типа. Это решение было принято, потому что Java Generics появился настолько поздно, что они не хотели нарушать обратную совместимость (Map<String, String>
можно использовать всякий раз, когда Map
называется). Сравните это с.Net/C#, где стирание типов не используется, что приводит ко всем видам различий (например, вы можете использовать примитивные типы и IEnumerable
а также IEnumerable<T>
не имеют никакого отношения друг к другу).
И класс, использующий дженерики, скомпилированные с помощью компилятора Java 5+, можно использовать в JDK 1.4 (при условии, что он не использует никаких других функций или классов, требующих Java 5+).
Вот почему Java Generics называют синтаксическим сахаром.
Но это решение о том, как сделать дженерики, имеет глубокие последствия настолько, что возникли (превосходные) часто задаваемые вопросы по Java Generics, чтобы ответить на многие, многие вопросы, которые возникают у людей о дженериках Java.
Шаблоны C++ имеют ряд функций, которых нет в Java Generics:
Использование аргументов примитивного типа.
Например:
template<class T, int i> class Matrix { int T[i][i]; ... }
Java не позволяет использовать аргументы примитивного типа в обобщениях.
Использование аргументов типа по умолчанию - это одна из функций, которую мне не хватает в Java, но для этого есть причины обратной совместимости;
- Java позволяет ограничивать аргументы.
Например:
public class ObservableList<T extends List> {
...
}
Действительно нужно подчеркнуть, что вызовы шаблонов с разными аргументами действительно являются разными типами. Они даже не разделяют статических членов. В Java это не так.
Помимо различий с дженериками, для полноты приведем базовое сравнение C++ и Java (и еще одного).
И я также могу предложить мышление на Java. Как программист на C++, многие понятия, такие как объекты, уже будут второй натурой, но есть небольшие различия, поэтому может быть целесообразно иметь вводный текст, даже если вы просматриваете части.
Многое из того, что вы узнаете при изучении Java, - это все библиотеки (обе стандартные - то, что входит в JDK- и нестандартные, которые включают в себя часто используемые вещи, такие как Spring). Синтаксис Java более многословен, чем синтаксис C++, и не имеет большого количества функций C++ (например, перегрузка операторов, множественное наследование, механизм деструктора и т. Д.), Но это не делает его строго подмножеством C++.
C++ имеет шаблоны. В Java есть дженерики, которые выглядят как шаблоны C++, но они очень разные.
Шаблоны работают, как следует из названия, предоставляя компилятору шаблон (подождите его...), который он может использовать для генерации безопасного кода, заполняя параметры шаблона.
Обобщения, как я их понимаю, работают наоборот: параметры типа используются компилятором для проверки того, что код, использующий их, безопасен для типов, но полученный код генерируется без типов вообще.
Думайте о шаблонах C++ как о действительно хорошей макросистеме, а обобщения Java как о инструменте для автоматической генерации типов типов.
Еще одна особенность, которая есть в шаблонах C++, которых нет в обобщениях Java, - это специализация. Это позволяет вам иметь различную реализацию для определенных типов. Таким образом, вы можете, например, иметь высоко оптимизированную версию для int, но при этом иметь общую версию для остальных типов. Или вы можете иметь разные версии для указателей и не указателей типов. Это удобно, если вы хотите работать с разыменованным объектом, когда вручаете указатель.
Это великолепное объяснение этой темы в Java Generics and Collections. Автор Maurice Naftalin, Philip Wadler. Я очень рекомендую эту книгу. Цитировать:
Обобщения в Java напоминают шаблоны в C++. ... Синтаксис намеренно похож, а семантика сознательно отличается.... С точки зрения семантики, дженерики Java определяются стиранием, а шаблоны C++ - расширением.
Пожалуйста, прочитайте полное объяснение здесь.
(источник: oreilly.com)
По сути, шаблоны AFAIK, C++ создают копию кода для каждого типа, в то время как шаблоны Java используют точно такой же код.
Да, вы можете сказать, что шаблон C++ эквивалентен общему понятию Java (хотя правильнее было бы сказать, что дженерики Java по своей сути эквивалентны C++)
Если вы знакомы с механизмом шаблонов C++, вы можете подумать, что дженерики похожи, но сходство поверхностно. Дженерики не генерируют новый класс для каждой специализации и не допускают "шаблонного метапрограммирования".
от: Java Generics
Обобщения Java (и C#) кажутся простым механизмом замены типов во время выполнения.
Шаблоны C++ представляют собой конструкцию времени компиляции, которая дает вам возможность изменить язык в соответствии с вашими потребностями. На самом деле это чисто функциональный язык, который компилятор выполняет во время компиляции.
Еще одним преимуществом шаблонов C++ является специализация.
<typename T> T sum(T a, T b) { return a + b; }
<typename T> T sum(T* a, T* b) { return (*a) + (*b); }
Special sum(const Special& a, const Special& b) { return a.plus(b); }
Теперь, если вы вызовете sum с помощью указателей, будет вызван второй метод, если вы вызовете sum с не указательными объектами, будет вызван первый метод, а если вы вызовете sum() со специальными объектами, будет вызван третий. Я не думаю, что это возможно с Java.
Ответ ниже взят из книги "Взломать решения для интервьюирования в коде" главы 13, которая, на мой взгляд, очень хорошая.
Реализация дженериков Java коренится в идее "стирания типов": этот метод исключает параметризованные типы, когда исходный код транслируется в байт-код виртуальной машины Java (JVM). Например, предположим, что у вас есть код Java ниже:
Vector<String> vector = new Vector<String>();
vector.add(new String("hello"));
String str = vector.get(0);
Во время компиляции этот код переписывается в:
Vector vector = new Vector();
vector.add(new String("hello"));
String str = (String) vector.get(0);
Использование дженериков Java не сильно изменило наши возможности; это только сделало вещи немного красивее. По этой причине дженерики Java иногда называют "синтаксическим сахаром:".
Это сильно отличается от C++. В C++ шаблоны по сути являются прославленным набором макросов, при этом компилятор создает новую копию кода шаблона для каждого типа. Доказательством этого является тот факт, что экземпляр MyClass не будет использовать статическую переменную совместно с MyClass. Однако экземпляры типа MyClass будут использовать статическую переменную.
/*** MyClass.h ***/
template<class T> class MyClass {
public:
static int val;
MyClass(int v) { val v;}
};
/*** MyClass.cpp ***/
template<typename T>
int MyClass<T>::bar;
template class MyClass<Foo>;
template class MyClass<Bar>;
/*** main.cpp ***/
MyClass<Foo> * fool
MyClass<Foo> * foo2
MyClass<Bar> * barl
MyClass<Bar> * bar2
new MyClass<Foo>(10);
new MyClass<Foo>(15);
new MyClass<Bar>(20);
new MyClass<Bar>(35);
int fl fool->val; // will equal 15
int f2 foo2->val; // will equal 15
int bl barl->val; // will equal 35
int b2 bar2->val; // will equal 35
В Java статические переменные являются общими для всех экземпляров MyClass независимо от параметров различных типов.
Шаблоны Java и шаблоны C++ имеют ряд других отличий. Они включают:
- Шаблоны C++ могут использовать примитивные типы, такие как int. Java не может и должна вместо этого использовать Integer.
- В Java вы можете ограничить параметры типа шаблона определенным типом. Например, вы можете использовать обобщенные элементы для реализации CardDeck и указать, что параметр типа должен расширяться из CardGame.
- В C++ может быть создан экземпляр параметра type, тогда как Java не поддерживает это.
- В Java параметр типа (т. Е. Foo в MyClass) нельзя использовать для статических методов и переменных, поскольку они будут разделены между MyClass и MyClass. В C++ эти классы различны, поэтому параметр типа может использоваться для статических методов и переменных.
- В Java все экземпляры MyClass, независимо от параметров их типа, имеют одинаковый тип. Параметры типа стираются во время выполнения. В C++ экземпляры с разными параметрами типов являются разными типами.
Я подведу итоги в одном предложении: шаблоны создают новые типы, общие ограничения ограничивают существующие типы.
@Keith:
Этот код на самом деле неправильный и кроме мелких глюков (template
пропущен, синтаксис специализации выглядит иначе), частичная специализация не работает на шаблонах функций, только на шаблонах классов. Код, однако, будет работать без частичной специализации шаблона, вместо этого используя простую старую перегрузку:
template <typename T> T sum(T a, T b) { return a + b; }
template <typename T> T sum(T* a, T* b) { return (*a) + (*b); }
Шаблоны - это не что иное, как макросистема. Синтаксис сахар. Они полностью раскрываются перед фактической компиляцией (или, по крайней мере, компиляторы ведут себя так, как если бы это было так).
Пример:
Допустим, мы хотим две функции. Одна функция берет две последовательности (список, массивы, векторы, что угодно) чисел и возвращает их внутреннее произведение. Другая функция принимает длину, генерирует две последовательности этой длины, передает их первой функции и возвращает ее результат. Уловка в том, что мы можем ошибиться во второй функции, так что эти две функции на самом деле не имеют одинаковую длину. Нам нужен компилятор, чтобы предупредить нас в этом случае. Не когда программа запущена, а когда она компилируется.
В Java вы можете сделать что-то вроде этого:
import java.io.*;
interface ScalarProduct<A> {
public Integer scalarProduct(A second);
}
class Nil implements ScalarProduct<Nil>{
Nil(){}
public Integer scalarProduct(Nil second) {
return 0;
}
}
class Cons<A implements ScalarProduct<A>> implements ScalarProduct<Cons<A>>{
public Integer value;
public A tail;
Cons(Integer _value, A _tail) {
value = _value;
tail = _tail;
}
public Integer scalarProduct(Cons<A> second){
return value * second.value + tail.scalarProduct(second.tail);
}
}
class _Test{
public static Integer main(Integer n){
return _main(n, 0, new Nil(), new Nil());
}
public static <A implements ScalarProduct<A>>
Integer _main(Integer n, Integer i, A first, A second){
if (n == 0) {
return first.scalarProduct(second);
} else {
return _main(n-1, i+1,
new Cons<A>(2*i+1,first), new Cons<A>(i*i, second));
//the following line won't compile, it produces an error:
//return _main(n-1, i+1, first, new Cons<A>(i*i, second));
}
}
}
public class Test{
public static void main(String [] args){
System.out.print("Enter a number: ");
try {
BufferedReader is =
new BufferedReader(new InputStreamReader(System.in));
String line = is.readLine();
Integer val = Integer.parseInt(line);
System.out.println(_Test.main(val));
} catch (NumberFormatException ex) {
System.err.println("Not a valid number");
} catch (IOException e) {
System.err.println("Unexpected IO ERROR");
}
}
}
В C# вы можете написать почти то же самое. Попробуйте переписать его на C++, и он не скомпилируется, жалуясь на бесконечное расширение шаблонов.
Я хотел бы процитировать здесь любую разницу:
Основное различие между C++ и Java заключается в их зависимости от платформы. В то время как C++ является платформенно-зависимым языком, Java - платформенно-независимым языком.
Вышеупомянутое утверждение является причиной того, почему C++ может предоставлять истинные универсальные типы. Хотя в Java есть строгая проверка, и поэтому они не позволяют использовать дженерики так, как это позволяет C++.