Пять вещей, которые вы не знали о... API коллекций Java. Часть 2

Изменяемые объекты, которых нужно остерегаться

Коллекции™ можно использовать везде, но используйте их «спустя рукава». Коллекции полны тайн, которые могут доставить вам проблемы при неправильном отношении. В этой статье цикла 5 вещей Тед Ньювард раскроет сложную и изменяемую сторону API коллекций Java и подскажет, как получить больше от Iterable, HashMap и SortedSet, не внося ошибок в свой код.

Тед Ньювард, Глава, Neward & Associates

Тед Ньювард - глава Neward & Associates, где он консультирует, руководит, обучает и внедряет Java, .NET, XML Services и другие платформы. Он проживает возле Сиэтла, штат Вашингтон.



22.12.2011

Об этой серии статей

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

Классы коллекций из пакета java.util проектировались для улучшения производительности Java за счет замены массивов. Как вы узнали из предыдущей статьи, они очень гибкие, поощряют адаптацию и расширение любыми способами в угоду хорошему и понятному коду.

Коллекции – это очень мощные, однако изменяемые классы: используйте их аккуратно и злоупотребляйте ими на свой страх и риск.

1. Списки – это не массивы

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

Ключом для различения коллекций и массивов является понимание разницы между порядком и позицией. Например, List – это интерфейс, сохраняющий порядок, в котором элементы были помещены в коллекцию, как показано в листинге 1.

Листинг 1. Изменяемые ключи
import java.util.*;

public class OrderAndPosition
{
    public static <T> void dumpArray(T[] array)
    {
        System.out.println("=============");
        for (int i=0; i<array.length; i++)
            System.out.println("Position " + i + ": " + array[i]);
    }
    public static <T> void dumpList(List<T> list)
    {
        System.out.println("=============");
        for (int i=0; i<list.size(); i++)
            System.out.println("Ordinal " + i + ": " + list.get(i));
    }
    
    public static void main(String[] args)
    {
        List<String> argList = new ArrayList<String>(Arrays.asList(args));

        dumpArray(args);
        args[1] = null;
        dumpArray(args);
        
        dumpList(argList);
        argList.remove(1);
        dumpList(argList);
    }
}

При удалении из коллекции List третьего элемента, находящиеся "сзади" него элементы сдвигаются к началу, чтобы заполнить пустые ячейки. Ясно, что это поведение коллекции отличается от поведения массива. (Фактически само удаление элемента из массива отличается от удаления из коллекции List – "удаление" элемента из массива означает запись в его индексную ячейку новой ссылки или значения null).


2. Iterator, ты меня удивляешь!

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

Однако для тех, кто ищет, Iterator хранит в себе два секрета.

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

Во-вторых, у интерфейса Iterator есть (возможно, более мощный) родственник. ListIterator, доступный только в коллекциях List. Он поддерживает добавление и удаление элементов коллекции List во время обхода, а также двунаправленное перемещение по коллекции.

Двунаправленное перемещение может быть особенно полезным в таких сценариях, как повсеместно распространенная "прокрутка множества результатов", отображение 10 из множества результатов, извлеченных из базы данных или другой коллекции. Также ее можно использовать для обхода коллекции или списка "сзади", вместо того чтобы пытаться все сделать «спереди». Для обхода коллекции List в обратном порядке намного легче применять ListIterator, чем передавать в метод List.get() целочисленные убывающие счетчики.


3. Не все объекты с интерфейсом Iterable происходят из коллекций

Разработчики на Ruby и Groovy любят хвастаться тем, как они могут обойти с помощью итератора текстовый файл и вывести его содержимое в консоль с помощью одной строки кода. В большинстве случаев, говорят они, на то же самое в Java требуются десятки строк кода: нужно открыть FileReader, затем BufferedReader, затем создать цикл while() и вызывать в нем метод getLine() до тех пор, пока он не вернет null. И, конечно же, все это приходится делать в блоке try/catch/finally, чтобы обрабатывать исключения и закрывать в конце файловый дескриптор.

Это может показаться странным аргументом, однако в нем есть некоторый смысл.

Разработчики (и немало Java-разработчиков), возможно, не знают, что объекты с интерфейсом Iterable не обязательно должны происходить из коллекций. Вместо этого Iterable может создать Iterator, знающий, как получить следующий элемент «из воздуха», а не просто взяв его из уже существующей коллекции (листинг 2).

Листинг 2. Обходим файл
// FileUtils.java
import java.io.*;
import java.util.*;

public class FileUtils
{
    public static Iterable<String> readlines(String filename)
    	throws IOException
    {
    	final FileReader fr = new FileReader(filename);
    	final BufferedReader br = new BufferedReader(fr);
    	
    	return new Iterable<String>() {
    		public <code>Iterator</code><String> iterator() {
    			return new <code>Iterator</code><String>() {
    				public boolean hasNext() {
    					return line != null;
    				}
    				public String next() {
    					String retval = line;
    					line = getLine();
    					return retval;
    				}
    				public void remove() {
    					throw new UnsupportedOperationException();
    				}
    				String getLine() {
    					String line = null;
    					try {
    						line = br.readLine();
    					}
    					catch (IOException ioEx) {
    						line = null;
    					}
    					return line;
    				}
    				String line = getLine();
    			};
    		}	
    	};
    }
}

//DumpApp.java
import java.util.*;

public class DumpApp
{
    public static void main(String[] args)
        throws Exception
    {
        for (String line : FileUtils.readlines(args[0]))
            System.out.println(line);
    }
}

Преимуществом этого подхода является то, что здесь содержимое всего файла не хранится в памяти. Однако нужно предостеречь, что в данной реализации на файловом дескрипторе не вызывается метод close(). (Это можно исправить, добавив закрытие для случая, когда readLine() возвращает null, однако это не поможет в случае, если Iterator не обойдет файл до конца).


4. Знайте об изменяемом hashCode()

Map– это замечательная коллекция, приносящая нам изящество наборов пар ключ–значение, которые часто можно встретить в таких языках, как Perl. JDK предоставляет отличную реализацию типа Map, называемую HashMap, внутри которой используются хеш-таблицы для обеспечения быстрого поиска значений по ключу. Однако здесь кроется тонкая проблема: ключи, хеш-коды которых зависят от содержимого изменяемых полей, подвержены ошибке, которая может свести с ума даже самого снисходительного к огрехам Java разработчика.

Если предположить, что в объекте Person из листинга 3 имеется типичный метод hashCode(), в котором для вычисления хеш-кода используются не финальные поля firstName, lastName и age, то вызов метода get() интерфейса Map может вернуть значение null.

Листинг 3. Изменяемый hashCode() приводит к ошибке
// Person.java
import java.util.*;

public class Person
    implements Iterable<Person>
{
    public Person(String fn, String ln, int a, Person... kids)
    {
        this.firstName = fn; this.lastName = ln; this.age = a;
        for (Person kid : kids)
            children.add(kid);
    }
    
    // ...
    
    public void setFirstName(String value) { this.firstName = value; }
    public void setLastName(String value) { this.lastName = value; }
    public void setAge(int value) { this.age = value; }
    
    public int hashCode() {
        return firstName.hashCode() & lastName.hashCode() & age;
    }

    // ...

    private String firstName;
    private String lastName;
    private int age;
    private List<Person> children = new ArrayList<Person>();
}


// MissingHash.java
import java.util.*;

public class MissingHash
{
    public static void main(String[] args)
    {
        Person p1 = new Person("Ted", "Neward", 39);
        Person p2 = new Person("Charlotte", "Neward", 38);
        System.out.println(p1.hashCode());
        
        Map<Person, Person> map = new HashMap<Person, Person>();
        map.put(p1, p2);
        
        p1.setLastName("Finkelstein");
        System.out.println(p1.hashCode());
        
        System.out.println(map.get(p1));
    }
}

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


5. equals() vs Comparable

Путешествуя по документации, Java-разработчики часто встречаются с типом SortedSet (и его единственной в JDK реализацией – TreeSet). Так как SortedSet является единственной коллекцией пакета java.util, предлагающим любое поведение сортировки, разработчики начинают использовать ее, не вникая слишком подробно в ее детали. Рассмотрим листинг 4.

Листинг 4. SortedSet, я так рад, что нашел тебя!
import java.util.*;

public class UsingSortedSet
{
    public static void main(String[] args)
    {
        List<Person> persons = Arrays.asList(
            new Person("Ted", "Neward", 39),
            new Person("Ron", "Reynolds", 39),
            new Person("Charlotte", "Neward", 38),
            new Person("Matthew", "McCullough", 18)
        );
        SortedSet ss = new TreeSet(new Comparator<Person>() {
            public int compare(Person lhs, Person rhs) {
                return lhs.getLastName().compareTo(rhs.getLastName());
            }
        });
        ss.addAll(perons);
        System.out.println(ss);
    }
}

Поработав немного с этим кодом, можно обнаружить одну из главных особенностей коллекции Set: она не позволяет хранить дубликаты. Эта особенность описана в Javadoc для Set. Множество (Set) является "коллекцией, которая не содержит дубликатов. Более формально, множества не содержат таких пар элементов e1 и e2, таких что e1.equals(e2), и не более одного элемента null".

Однако, похоже, что это не совсем так – хотя ни один из объектов Person из листинга 4 не равен другому (согласно реализации метода equals() класса Person), при распечатке оказывается, что в TreeSet представлено только три объекта.

Несмотря на озвученную природу множества, в коллекции TreeSet для сравнения объектов не используется метод equals(). Эта коллекция требует, чтобы либо объекты напрямую реализовывали интерфейс Comparable, либо чтобы в момент создания ей был передан Comparator. В дальнейшем для сравнения в коллекции используется либо метод compareTo, предоставляемый интерфейсом Comparable, либо метод compare, предоставляемый Comparator.

Поэтому объекты, хранимые во множестве, будут иметь два потенциальных способа определения равенства: ожидаемый метод equals() и метод Comparable/Comparator, зависящий от контекста того, кто спрашивает.

Что еще хуже, недостаточно просто объявить, что два объекта должны быть идентичны, так как сравнение в целях сортировки – это не то же самое, что сравнение в целях определения равенства: вполне допустимо считать два объекта Person равными при сортировке по фамилии, но не равными при сравнении их содержимого.

При реализации множества всегда гарантируйте, что различие между equals() и Comparable.compareTo(), возвращающим 0, всегда понятно. А также это различие должно быть ясно отражено в вашей документации.


В заключение

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

Итак, мы уже проникли в глубь коллекций, однако еще не достигли "золотого рудника" – коллекций параллельного доступа (Concurrent Collections), появившихся в Java 5. В следующих пяти советах нашего цикла мы рассмотрим пакет java.util.concurrent.


Загрузка

ОписаниеИмяРазмер
Исходный кодj-5things3-src.zip15 KБ

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=782408
ArticleTitle=Пять вещей, которые вы не знали о... API коллекций Java. Часть 2
publish-date=12222011