Рейтинговые книги
Читем онлайн Философия Java3 - Брюс Эккель

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 79 80 81 82 83 84 85 86 87 ... 132

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

Простая параметризация

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

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

//• generics/Holderl java

class Automobile {}

public class Holderl {

private Automobile a;

public Holderl(Automobile a) { this.a = a; } Automobile get() { return a; }

} ///:-

Однако такой «контейнер» получается не слишком универсальным — он не может использоваться только для одного типа. Конечно, было бы неудобно создавать новый класс для каждого типа, который нам встретится в программе.

До выхода Java SE5 можно было бы хранить в классе Object:

II: generics/Holder2.java

public class Holder2 { private Object a;

public Holder2(0bject a) { this a = a; } public void set(Object a) { this.a = a; }

public Object get О { return a; } public static void main(String[] args) {

Holder2 h2 = new Holder2(new AutomobileO); Automobile a = (Automobile)h2 get(); h2.set("He Automobile"); String s = (String)h2:get();

h2.set(l); // Автоматически упаковывается в Integer Integer x = (Integer)h2.get(),

}

} ///:-

Теперь класс Holder2 может хранить все, что угодно, — в приведенном примере один объект Holder2 используется для хранения трех разных типов данных.

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

Итак, вместо Object в определении класса было бы удобнее использовать некий условный заменитель, чтобы отложить выбор до более позднего момента. Для этого после имени класса в угловых скобках указывается параметр типа, который при использовании заменяется фактическим типом. В нашем примере это будет выглядеть так (Т — параметр типа):

II: generics/Holder3.java

public class Holder3<T> { private T a;

public Holder3(T a) { this.a = a; } public void set(T a) { this.a = a; } public T getО { return a; } public static void main(String[] args) { Holder3<Automobile> h3 =

new Holder3<Automobile>(new AutomobileO); Automobile a = h3.get(); // Преобразование не требуется // h3.set("He Automobile"); // Ошибка // h3.set(1); // Ошибка

}

} ///:-

При создании Holder3 необходимо указать тип объектов, хранящихся в контейнере, в угловых скобках, как в main(). В дальнейшем в контейнер можно будет помещать объекты только этого типа (или производного, так как принцип заменяемости работает и для параметризованных типов). А при извлечении вы автоматически получаете объект нужного типа.

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

Кортежи

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

Концепция нескольких объектов, «упакованных» в один объект, называется кортежем (tuple). Получатель объекта может читать элементы, но не может добавлять их (эта концепция еще называется объектом передачи данных).

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

//: net/mi ndvi ew/uti1/TwoTuple.java

package net.mindview.util;

public class TwoTuple<A,B> { public final A first; public final В second;

public TwoTuple(A а, В b) { first = a; second = b; } public String toStringO {

return "(" + first + " + second + ")";

}

} ///:-

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

При первом чтении может показаться, что такая архитектура нарушает общие принципы безопасности программирования на Java. Разве first и second не должны быть объявлены приватными, а обращения к ним осуществляться только из методов getFirst() и getSecond()? Подумайте, какая безопасность реализуется в этом случае: клиент может читать объекты и делать с прочитанными значениями все, что пожелает, но не может изменить first и second. Фактически объявление final делает то же самое, но короче и проще.

Кортежи большей длины создаются посредством наследования. Добавить новый параметр типа несложно:

//: net/mi ndvi ew/uti1/ThreeTuple.java

package net.mi ndvi ew.uti1;

public class ThreeTuple<A.B,C> extends TwoTuple<A,B> { public final С third; public ThreeTuple(A а, В b, С с) { super(a. b); third = c:

}

public String toStringO {

return "(" + first + " + second + ", " + third +")";

}

// net/mi ndvi ew/uti1/FourTuple. java package net.mindview.util;

public class FourTuple<A,B,C,D> extends ThreeTuple<A,B,C> { public final D fourth; public FourTuple(A а, В b. С с. D d) { super(a. b, c), fourth = d;

}

public String toStringO {

return "(" + first + ", " + second + " + third + " + fourth + ")";

}

} Hill . net/mi ndvi ew/uti1/Fi veTuple.java package net.mindview util;

public class FiveTuple<A,B,C,D,E> extends FourTuple<A,В,C.D> { public final E fifth;

public FiveTuple(A а. В b. С с. D d. E e) { super(a, b. c. d); fifth = e.

}

public String toStringO {

return "(" + first + " + second + " +

third + " + fourth + " + fifth + ")";

}

} ///-

Чтобы воспользоваться этими классами, достаточно определить кортеж нужной длины как возвращаемое значение функции, а затем создать и вернуть его командой return:

II: generics/TupleTest.java i mport net.mi ndvi ew.uti1.*;

class Amphibian {} class Vehicle {}

public class TupleTest {

static TwoTuple<String.Integer> fO {

// Автоматическая упаковка преобразует int в Integer: return new TwoTuple<String.Integer>("hi", 47);

}

static ThreeTuple<Amphibian,String,Integer> g() {

return new ThreeTuple<Amphibian, String, Integer>( new AmphibianO. "hi", 47);

}

static

FourTuple<Vehicle,Amphibian,String,Integer> hO { return

new FourTuple<Vehicle,Amphibian,String.Integer>(

new VehicleO, new AmphibianO, "hi". 47);

}

static

FiveTuple<Vehicle.Amphibian.String,Integer.Double> kO { продолжение & return new

Fi veTuple<Vehi cle,Amphi bi an,Stri ng,Integer,Double>(

new VehicleO, new AmphibianO. "hi", 47, 11.1);

}

public static void main(String[] args) {

TwoTuple<String.Integer> ttsi = f(); System.out.pri ntln(ttsi);

// ttsi first = "there"; // Ошибка компиляции- final System.out.pri ntln(g()); System.out.println(hO); System, out. println(kO);

}

} /* Output: (hi. 47)

([email protected], hi, 47) ([email protected], [email protected], hi, 47) ([email protected], [email protected], hi, 47, 11.1) *///:-

Спецификация final для public-полей предотвращает их изменение после конструирования (поэтому попытка выполнения команды ttsi.first="there" приводит к ошибке).

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

Класс стека

Давайте рассмотрим менее тривиальный пример: реализацию традиционного стека. В главе И была приведена реализация стека на базе LinkedList. В этом примере класс LinkedList уже содержал все методы, необходимые для создания стека. Класс стека строился объединением одного параметризованного класса (Stack<T>) с другим параметризованным классом (LinkedList<T>). Этот пример показывает, что параметризованный тип — такой же тип, как и все остальные (за некоторыми исключениями, о которых речь пойдет позже):-

Вместо того, чтобы использовать LinkedList, мы также могли реализовать собственный механизм хранения связанного списка:

//: generics/LinkedStack java

// Стек, реализованный на базе внутренней структуры

public class LinkedStack<T> {

private static class Node<U> { U item; Node<U> next;

NodeO { item = null; next = null; } Node(U item, NodeO next) { this.item = item; this next = next;

}

boolean end() { return item == null && next == null; }

}

private Node<T> top = new Node<T>(); // Предохранитель public void push(T item) {

top = new Node<T>(item, top);

}

public Т popО {

Т result = top item; if( itop.endO)

top = top.next; return result;

}

public static void main(String[] args) {

LinkedStack<String> Iss = new LinkedStack<String>(); for(String s : "Phasers on stun!".split(" ")) Iss.push(s);

String s;

while((s = Iss.popO) != null) System.out.printin(s);

}

} /* Output-

stun!

on

Phasers *///:-

Внутренний класс Node тоже является параметризованным и имеет собственный параметр типа.

Для определения наличия элементов в стеке в этом примере используется предохранитель (end sentinel). Он создается при конструировании LinkedStack, а затем при каждом вызове push() новый объект Node<T> создается и связывается с предыдущим Node<T>. При вызове рор() всегда возвращается top.item, после чего текущий объект Node<T> уничтожается и происходит переход к следующему — если только текущим элементом не является предохранитель; в этом случае переход не выполняется. При повторных вызовах рор() клиент будет получать null, что свидетельствует об отсутствии элементов в стеке.

1 ... 79 80 81 82 83 84 85 86 87 ... 132
На этой странице вы можете бесплатно читать книгу Философия Java3 - Брюс Эккель бесплатно.
Похожие на Философия Java3 - Брюс Эккель книги

Оставить комментарий