Что такое raw-тип и почему мы не должны его использовать?
Вопросы:
- Что такое raw-типы в Java и почему я часто слышу, что их не следует использовать в новом коде?
- Какова альтернатива, если мы не можем использовать необработанные типы, и как это лучше?
Переведено автоматически
Ответ 1
Что такое raw-тип?
Спецификация языка Java определяет необработанный тип следующим образом:
JLS 4.8 Необработанные типы
Необработанный тип определяется как один из:
Ссылочный тип, который формируется путем использования имени универсального объявления типа без сопровождающего списка аргументов типа.
Тип массива, тип элемента которого является raw-типом.
Тип, не являющийся
static
членом необработанного типаR
, который не наследуется от суперкласса или суперинтерфейсаR
.
Вот пример для иллюстрации:
public class MyType<E> {
class Inner { }
static class Nested { }
public static void main(String[] args) {
MyType mt; // warning: MyType is a raw type
MyType.Inner inn; // warning: MyType.Inner is a raw type
MyType.Nested nest; // no warning: not parameterized type
MyType<Object> mt1; // no warning: type parameter given
MyType<?> mt2; // no warning: type parameter given (wildcard OK!)
}
}
Здесь, MyType<E>
это параметризованный тип (JLS 4.5). В разговорной речи этот тип обычно называют просто MyType
для краткости, но технически это так MyType<E>
.
mt
имеет необработанный тип (и генерирует предупреждение о компиляции) по первому маркеру в приведенном выше определении; inn
также имеет необработанный тип по третьему маркеру.
MyType.Nested
это не параметризованный тип, хотя это тип-член параметризованного типа MyType<E>
, потому что это static
.
mt1
, и mt2
оба объявлены с фактическими параметрами типа, поэтому они не являются необработанными типами.
Что такого особенного в необработанных типах?
По сути, необработанные типы ведут себя точно так же, как и до появления дженериков. То есть следующее полностью законно во время компиляции.
List names = new ArrayList(); // warning: raw type!
names.add("John");
names.add("Mary");
names.add(Boolean.FALSE); // not a compilation error!
Приведенный выше код работает просто отлично, но предположим, что у вас также есть следующее:
for (Object o : names) {
String name = (String) o;
System.out.println(name);
} // throws ClassCastException!
// java.lang.Boolean cannot be cast to java.lang.String
Теперь мы сталкиваемся с проблемами во время выполнения, потому что names
содержит что-то, что не является типом instanceof String
.
Предположительно, если вы хотите names
содержать только String
, вы могли бы, возможно, все же использовать raw-тип и вручную проверять каждый add
самостоятельно, а затем вручную приводить к String
каждому элементу из names
. Еще лучше, однако, НЕ использовать raw-тип и позволить компилятору делать всю работу за вас, используя возможности Java generics.
List<String> names = new ArrayList<String>();
names.add("John");
names.add("Mary");
names.add(Boolean.FALSE); // compilation error!
Конечно, если вы ДЕЙСТВИТЕЛЬНО хотите names
разрешить a Boolean
, то вы можете объявить его как List<Object> names
, и приведенный выше код будет скомпилирован.
See also
How's a raw type different from using <Object>
as type parameters?
The following is a quote from Effective Java 2nd Edition, Item 23: Don't use raw types in new code:
Just what is the difference between the raw type
List
and the parameterized typeList<Object>
? Loosely speaking, the former has opted out generic type checking, while the latter explicitly told the compiler that it is capable of holding objects of any type. While you can pass aList<String>
to a parameter of typeList
, you can't pass it to a parameter of typeList<Object>
. There are subtyping rules for generics, andList<String>
is a subtype of the raw typeList
, but not of the parameterized typeList<Object>
. As a consequence, you lose type safety if you use raw type likeList
, but not if you use a parameterized type likeList<Object>
.
To illustrate the point, consider the following method which takes a List<Object>
and appends a new Object()
.
void appendNewObject(List<Object> list) {
list.add(new Object());
}
Generics in Java are invariant. A List<String>
is not a List<Object>
, so the following would generate a compiler warning:
List<String> names = new ArrayList<String>();
appendNewObject(names); // compilation error!
If you had declared appendNewObject
to take a raw type List
as parameter, then this would compile, and you'd therefore lose the type safety that you get from generics.
See also
How's a raw type different from using <?>
as a type parameter?
List<Object>
, List<String>
, etc are all List<?>
, so it may be tempting to just say that they're just List
instead. However, there is a major difference: since a List<E>
defines only add(E)
, you can't add just any arbitrary object to a List<?>
. On the other hand, since the raw type List
does not have type safety, you can add
just about anything to a List
.
Consider the following variation of the previous snippet:
static void appendNewObject(List<?> list) {
list.add(new Object()); // compilation error!
}
//...
List<String> names = new ArrayList<String>();
appendNewObject(names); // this part is fine!
The compiler did a wonderful job of protecting you from potentially violating the type invariance of the List<?>
! If you had declared the parameter as the raw type List list
, then the code would compile, and you'd violate the type invariant of List<String> names
.
A raw type is the erasure of that type
Back to JLS 4.8:
It is possible to use as a type the erasure of a parameterized type or the erasure of an array type whose element type is a parameterized type. Such a type is called a raw type.
[...]
The superclasses (respectively, superinterfaces) of a raw type are the erasures of the superclasses (superinterfaces) of any of the parameterizations of the generic type.
The type of a constructor, instance method, or non-
static
field of a raw typeC
that is not inherited from its superclasses or superinterfaces is the raw type that corresponds to the erasure of its type in the generic declaration corresponding toC
.
In simpler terms, when a raw type is used, the constructors, instance methods and non-static
fields are also erased.
Take the following example:
class MyType<E> {
List<String> getNames() {
return Arrays.asList("John", "Mary");
}
public static void main(String[] args) {
MyType rawType = new MyType();
// unchecked warning!
// required: List<String> found: List
List<String> names = rawType.getNames();
// compilation error!
// incompatible types: Object cannot be converted to String
for (String str : rawType.getNames())
System.out.print(str);
}
}
When we use the raw MyType
, getNames
becomes erased as well, so that it returns a raw List
!
JLS 4.6 continues to explain the following:
Type erasure also maps the signature of a constructor or method to a signature that has no parameterized types or type variables. The erasure of a constructor or method signature
s
is a signature consisting of the same name ass
and the erasures of all the formal parameter types given ins
.The return type of a method and the type parameters of a generic method or constructor also undergo erasure if the method or constructor's signature is erased.
The erasure of the signature of a generic method has no type parameters.
The following bug report contains some thoughts from Maurizio Cimadamore, a compiler dev, and Alex Buckley, one of the authors of the JLS, on why this sort of behavior ought to occur: https://bugs.openjdk.java.net/browse/JDK-6400189. (In short, it makes the specification simpler.)
If it's unsafe, why is it allowed to use a raw type?
Here's another quote from JLS 4.8:
The use of raw types is allowed only as a concession to compatibility of legacy code. The use of raw types in code written after the introduction of genericity into the Java programming language is strongly discouraged. It is possible that future versions of the Java programming language will disallow the use of raw types.
Effective Java 2nd Edition also has this to add:
Given that you shouldn't use raw types, why did the language designers allow them? To provide compatibility.
The Java platform was about to enter its second decade when generics were introduced, and there was an enormous amount of Java code in existence that did not use generics. It was deemed critical that all this code remains legal and interoperable with new code that does use generics. It had to be legal to pass instances of parameterized types to methods that were designed for use with ordinary types, and vice versa. This requirement, known as migration compatibility, drove the decision to support raw types.
In summary, raw types should NEVER be used in new code. You should always use parameterized types.
Are there no exceptions?
Unfortunately, because Java generics are non-reified, there are two exceptions where raw types must be used in new code:
- Class literals, e.g.
List.class
, notList<String>.class
instanceof
operand, e.g.o instanceof Set
, noto instanceof Set<String>
See also
Ответ 2
What are raw types in Java, and why do I often hear that they shouldn't be used in new code?
Raw-types are ancient history of the Java language. In the beginning there were Collections
and they held Objects
nothing more and nothing less. Every operation on Collections
required casts from Object
to the desired type.
List aList = new ArrayList();
String s = "Hello World!";
aList.add(s);
String c = (String)aList.get(0);
While this worked most of the time, errors did happen
List aNumberList = new ArrayList();
String one = "1";//Number one
aNumberList.add(one);
Integer iOne = (Integer)aNumberList.get(0);//Insert ClassCastException here
The old typeless collections could not enforce type-safety so the programmer had to remember what he stored within a collection.
Generics where invented to get around this limitation, the developer would declare the stored type once and the compiler would do it instead.
List<String> aNumberList = new ArrayList<String>();
aNumberList.add("one");
Integer iOne = aNumberList.get(0);//Compile time error
String sOne = aNumberList.get(0);//works fine
Для сравнения:
// Old style collections now known as raw types
List aList = new ArrayList(); //Could contain anything
// New style collections with Generics
List<String> aList = new ArrayList<String>(); //Contains only Strings
Более сложный сопоставимый интерфейс:
//raw, not type save can compare with Other classes
class MyCompareAble implements CompareAble
{
int id;
public int compareTo(Object other)
{return this.id - ((MyCompareAble)other).id;}
}
//Generic
class MyCompareAble implements CompareAble<MyCompareAble>
{
int id;
public int compareTo(MyCompareAble other)
{return this.id - other.id;}
}
Обратите внимание, что невозможно реализовать CompareAble
интерфейс с compareTo(MyCompareAble)
необработанными типами.
Почему вы не должны их использовать:
- Любой тип,
Object
хранящийся в aCollection
, должен быть приведен, прежде чем его можно будет использовать - Использование generics позволяет проверять время компиляции
- Использование необработанных типов - это то же самое, что сохранение каждого значения в виде
Object
Что делает компилятор: Дженерики обратно совместимы, они используют те же классы java, что и raw-типы. Волшебство происходит в основном во время компиляции.
List<String> someStrings = new ArrayList<String>();
someStrings.add("one");
String one = someStrings.get(0);
Будет скомпилирован как:
List someStrings = new ArrayList();
someStrings.add("one");
String one = (String)someStrings.get(0);
Это тот же код, который вы бы написали, если бы использовали raw-типы напрямую. Думал, я не уверен, что происходит с CompareAble
интерфейсом, я предполагаю, что он создает две compareTo
функции, одна принимает MyCompareAble
, а другая принимает Object
и передает ее первой после приведения к ней.
Каковы альтернативы необработанным типам: используйте дженерики
Ответ 3
Необработанный тип - это имя универсального класса или интерфейса без каких-либо аргументов типа. Например, учитывая универсальный класс Box:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
Чтобы создать параметризованный тип Box<T>
, вы указываете фактический аргумент типа для параметра формального типа T
:
Box<Integer> intBox = new Box<>();
Если фактический аргумент type опущен, вы создаете необработанный тип Box<T>
:
Box rawBox = new Box();
Следовательно, Box
это необработанный тип универсального типа Box<T>
. Однако непатентованный класс или тип интерфейса не является необработанным типом.
Необработанные типы появляются в устаревшем коде, потому что многие классы API (например, классы коллекций) не были универсальными до JDK 5.0. При использовании необработанных типов вы, по сути, получаете предгенеричное поведение - a Box
дает вам Object
s . Для обратной совместимости разрешено присваивать параметризованный тип своему необработанному типу:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
Но если вы присвоите raw-тип параметризованному типу, вы получите предупреждение:
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
Вы также получите предупреждение, если используете необработанный тип для вызова универсальных методов, определенных в соответствующем универсальном типе:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
Предупреждение показывает, что необработанные типы обходят проверки универсального типа, откладывая перехватывание небезопасного кода до времени выполнения. Следовательно, вам следует избегать использования необработанных типов.
В разделе "Удаление типов" содержится дополнительная информация о том, как компилятор Java использует необработанные типы.
Непроверенные сообщения об ошибках
Как упоминалось ранее, при смешивании устаревшего кода с универсальным вы можете столкнуться с предупреждающими сообщениями, подобными следующему:
Примечание: Example.java использует непроверенные или небезопасные операции.
Примечание: Перекомпилируйте с помощью -Xlint: для получения подробной информации флажок снят.
Это может произойти при использовании более старого API, который работает с raw-типами, как показано в следующем примере:
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
Термин "непроверенный" означает, что компилятор не располагает достаточной информацией о типах для выполнения всех проверок типов, необходимых для обеспечения безопасности типов. Предупреждение "непроверено" по умолчанию отключено, хотя компилятор выдает подсказку. Чтобы увидеть все предупреждения "непроверено", перекомпилируйте с помощью -Xlint:непроверено .
Перекомпиляция предыдущего примера с помощью -Xlint:unchecked раскрывает следующую дополнительную информацию:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning
Чтобы полностью отключить непроверенные предупреждения, используйте флаг -Xlint:-непроверенный. @SuppressWarnings("unchecked")
Аннотация подавляет непроверенные предупреждения. Если вы не знакомы с @SuppressWarnings
синтаксисом, смотрите Аннотации.
Исходный код: Учебные пособия по Java
Ответ 4
"Необработанный" тип в Java - это класс, который не является универсальным и имеет дело с "необработанными" объектами, а не с типобезопасными параметрами универсального типа.
Например, до того, как были доступны Java generics, вы бы использовали класс collection, подобный этому:
LinkedList list = new LinkedList();
list.add(new MyObject());
MyObject myObject = (MyObject)list.get(0);
Когда вы добавляете свой объект в список, не имеет значения, к какому типу относится этот объект, и когда вы получаете его из списка, вы должны явно привести его к ожидаемому типу.
Используя generics, вы удаляете фактор "неизвестно", потому что вы должны явно указать, какой тип объектов может быть включен в список:
LinkedList<MyObject> list = new LinkedList<MyObject>();
list.add(new MyObject());
MyObject myObject = list.get(0);
Обратите внимание, что в generics вам не нужно приводить объект, исходящий из вызова get, коллекция предварительно определена для работы только с MyObject . Сам этот факт является основным движущим фактором для generics. Он преобразует источник ошибок во время выполнения во что-то, что можно проверить во время компиляции.