Работа с ArrayStoreException
Object[] o = "a;b;c".split(";");
o[0] = 42;
бросает
java.lang.ArrayStoreException: java.lang.Integer
в то время как
String[] s = "a;b;c".split(";");
Object[] o = new Object[s.length];
for (int i = 0; i < s.length; i++) {
o[i] = s[i];
}
o[0] = 42;
не делает.
Есть ли другой способ справиться с этим исключением без создания временного String[]
массив?
3 ответа
В Java массив также является объектом.
Вы можете поместить объект подтипа в переменную супертипа. Например, вы можете положить String
возражать в Object
переменная.
К сожалению, определение массива в Java как-то не работает. String[]
считается подтипом Object[]
, но это неправильно! Для более подробного объяснения читайте о "ковариации и контравариантности", но суть в этом следующая: тип следует рассматривать как подтип другого типа, только если этот подтип выполняет все обязательства надтипа. Это означает, что если вы получаете объект подтипа вместо объекта супертипа, вы не должны ожидать поведения, противоречащего контракту супертипа.
Проблема в том, что String[]
поддерживает только часть Object[]
контракт. Например, вы можете прочитать Object
значения из Object[]
, И вы также можете прочитать Object
значения (которые оказываются String
объекты) из String[]
, Все идет нормально. Проблема с другой частью контракта. Вы можете поставить любой Object
в Object[]
, Но вы не можете поставить любой Object
в String[]
, Следовательно, String[]
не следует считать подтипом Object[]
, но спецификация Java говорит, что это так. И, таким образом, мы имеем такие последствия.
(Обратите внимание, что подобная ситуация возникла снова с родовыми классами, но на этот раз она была решена правильно. List<String>
не является подтипом List<Object>
; и если вы хотите иметь общий супертип для них, вам нужно List<?>
, который только для чтения. Так должно быть и с массивами; но это не так. И из-за обратной совместимости, уже слишком поздно ее менять.)
В вашем первом примере String.split
функция создает String[]
объект. Вы можете положить его в Object[]
переменная, но объект остается String[]
, Вот почему он отвергает Integer
значение. Вы должны создать новый Objects[]
массив и скопировать значения. Вы могли бы использовать System.arraycopy
функция для копирования данных, но вы не можете избежать создания нового массива.
Нет, нет способа избежать копирования массива split
возвращается.
Массив, который split
возвращается на самом деле String[]
и Java позволяет назначить это переменной типа Object[]
, Это все еще действительно String[]
тем не менее, поэтому, когда вы пытаетесь сохранить что-то еще, чем String
в нем вы получите ArrayStoreException
,
Для справочной информации см. 4.10.3. Подтипирование среди типов массивов в спецификации языка Java.
Это результат какой-то сделки со стороны разработчиков Java много месяцев назад. Хотя это может показаться странным, эта функциональность важна для многих методов, таких как Arrays.sort
(который также вызывается в Collections.sort
). По сути, любой метод, который принимает Object[] в качестве параметра, перестал бы работать так, как предполагалось, если бы X[] (где X - некоторый подкласс Object) не считался подтипом. Возможно, что массивы можно было переработать так, чтобы при определенных обстоятельствах они были доступны только для чтения, например, но тогда возникает вопрос "когда?".
С одной стороны, создание массивов, которые были переданы в метод в качестве аргументов только для чтения, может препятствовать способности кодера вносить изменения на месте. С другой стороны, создание исключения, когда массив передается в качестве аргумента, все еще позволяет кодировщику вносить недопустимые изменения, такие как сохранение строки, когда массив Integer является тем, что было передано вызывающей стороной.
Но результат произнесения "Integer[] (например) не является подтипом Object[]" - это кризис, когда нужно создать отдельный метод для Object[] и Integer[]. Расширяя такую логику, мы можем также сказать, что должен быть создан отдельный метод для String[], Comparable[] и т. Д. Каждый тип массива требует отдельного метода, даже если эти методы в противном случае были бы абсолютно одинаковыми.
Это именно та ситуация, для которой у нас есть полиморфизм.
Хотя здесь допускается полиморфизм, но, к сожалению, он позволяет попытаться незаконно сохранить значение в массиве, и ArrayStoreException
выбрасывается, если такой случай происходит. Тем не менее, это небольшая цена, которую нужно заплатить, и ее можно избежать ArrayIndexOutOfBoundsException
,
ArrayStoreException
в большинстве случаев может быть легко предотвращено двумя способами (хотя вы не можете контролировать то, что делают другие).
1)
Не пытайтесь хранить объекты в массиве, не зная, что это фактический тип компонента. Когда массив, с которым вы работаете, был передан в метод, вы не обязательно знаете, откуда он берется, поэтому вы не можете предполагать, что он безопасен, если класс типа компонента не является окончательным (то есть без подклассов).
Если массив возвращается из метода, как в вопросе выше, познакомьтесь с методом. Возможно ли, что фактический тип является подклассом возвращаемого типа? Если это так, вы должны принять это во внимание.
2)
Когда вы впервые инициализируете массив, с которым работаете локально, используйте форму X[] blah = new X[...];
или же X[] blah = {...};
или (по состоянию на Java 10) var blah = new X[...];
, Затем любая попытка сохранить не-X значение в этом массиве приведет к ошибке компилятора. То, что вы не должны говорить, это Y[] blah = new X[...];
где X является подклассом Y.
Если у вас есть массив, как в приведенном выше вопросе, где вы хотите хранить компоненты неправильного типа, то, как предлагают другие, вы должны либо создать новый массив правильного типа, и скопировать информацию в...
Object[] o = Arrays.copyOf(s, s.length, Object[].class); //someone demonstrate System.arrayCopy. I figure I show another way to skin cat. :p
o[0] = 42;
или вы должны каким-то образом преобразовать компоненты, которые вы хотите сохранить, в соответствующий тип.
s[0] = String.valueOf(42);
Обратите внимание, что 42!= "42", поэтому при принятии решения о том, какой путь выбрать, следует учитывать, как это повлияет на остальную часть вашего кода.
Я просто хотел бы закончить заметкой о дженериках (как указано в предыдущем ответе). Дженерики на самом деле способны удивлять ничего не подозревающего кодера. Рассмотрим следующий фрагмент кода (модифицированный здесь).
import java.util.List;
import java.util.ArrayList;
public class UhOh {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
WildcardFixed.foo(list);
list.add(6);
System.out.println(list); // ¯\_(ツ)_/¯ oh well.
int i = list.get(0); //if we're going to discuss breaches of contract... :p
}
}
class WildcardFixed /*not anymore ;) */ {
static void foo(List<?> i) {
fooHelper(i);
}
private static <T> void fooHelper(List<T> l) {
l.add((T)Double.valueOf(2.5));
}
}
Дженерики, дамы и господа.:п
Конечно, есть и другие варианты, например, вы реализуете свой собственный метод split, который напрямую возвращает массив Object. Я не уверен, хотя, что на самом деле беспокоит вас с временным массивом String?
Кстати, вы можете сократить свой код на несколько строк, используя System.arrayCopy вместо реализации собственного цикла для копирования элементов массива:
System.arrayCopy(s, 0, o, 0, s.length);