Лучший подход?
Swing достаточно мощный; он может дать очень много при использовании всего нескольких строк. Примеры, показанные в этой книге, достаточно просты, и с целью обучения есть смысл писать их руками. Вы на самом деле можете выбрать комбинацию простых компоновок. Однако, с некоторой точки зрения, это теряет смысл для ручного проектирования GUI форм — это становится слишком сложным, и вы теряете время при программировании. Разработчики Java и Swing ориентировали язык и библиотеку на использование инструментов поддержки построителей GUI, которые были созданы с целью ускорения создания и приобретения опыта программирования. Как только вы поймете, что происходит с компоновкой и как работать с событиями (описано далее), то не особенно важно, что вы на самом деле знаете детали того, как располагать компоненты в ручную — позвольте соответствующему инструменту сделать это за вас (Java, помимо всего, предназначена для увеличения продуктивности программиста).
Максимум рычагов управления библиотеками
Наибыстрейший способ создания программы - это использование кода, который уже написан: библиотеки. Главная цель Java - это создание легких в использовании библиотек. Это выполнено путем преобразования библиотек в новые типы данных (классы), так что введение в библиотеку означает добавление нового типа в языке. Так как компилятор Java заботится о том, как используется библиотека — гарантирую правильную инициализацию и очистку и, убеждаясь, что функции вызываются правильно — вы можете сфокусироваться на том, что вы хотите сделать с помощью библиотеки, а не на том, как вы делаете это.
Манипуляции с ссылками
Передавая ссылку другому методу в качестве параметра, новая ссылка будет продолжать указывать на тот же самый объект. Следующий простейший пример наглядно это демонстрирует:
//: Приложение а:PassReferences.java
// Передача ссылок.
public class PassReferences { static void f(PassReferences h) { System.out.println("h внутри f(): " + h); } public static void main(String[] args) { PassReferences p = new PassReferences(); System.out.println("p внутри main(): " + p); f(p); } } ///:~
В этом примере при выводе результатов на экран автоматически вызывается метод toString(), а PassReferences наследуется непосредственно из класса Object, без переопределения метода toString(). Таким образом, при распечатке названия класса объекта и его адреса (не ссылки, а физического адреса по которому размещается объект) используется метод toString() класса Object. Результат работы примера:
p внутри main(): PassReferences@1653748 h внутри f(): PassReferences@1653748
Как вы видете, p и h ссылаются на один и тот же объект. Это более эффективно чем дублирование самого объекта PassReferences лишь для передачи параметра методу, но в то же время сопряжено с серьезными проблемами.
Массивы
Наиболее необходимое введение в массивы в последнем разделе Главы 4, которая показывает, как вам определить и проинициализировать массив. Хранение объектов - это основная тема этой главы, а массивы - это просто один способ хранить объекты. Но есть несколько других способов хранения объектов, так что же делает массивы особенными?
Есть две проблемы, которые отличают массивы от других типов контейнеров: эффективность и тип. Массив является наиболее эффективным способом, из тех, которые обеспечивает Java, для хранения объектов и доступа к ним в случайном порядке (на самом деле, речь идет о ссылках на объекты). Массив - это простая линейная последовательность, которая делает быстрым доступ к элементам, но вы расплачиваетесь за эту скорость: когда вы создаете массив объектов, его размер фиксирован и не может изменяться в течение всей продолжительности жизни этого массива объектов. Вы можете согласиться создать массив определенного размера, а затем, если вы выйдите за пределы, создадите новый и переместите все ссылки из старого массива в новый. Такое поведение заключено в класс ArrayList, который будет изучен позже в этой главе. Однако, потому что превышение этого размера не всегда одинаково, ArrayList менее эффективен, чем массив.
Контейнерный класс vector в C++ знает тип объектов, которые он хранит, но он имеет другие недостатки по сравнению с массивами в Java: метод vector'а из С++ operator[] не делает проверки границ, так что вы можете выйти за пределы [44]. В Java есть проверка границ не зависимо от того используете ли вы массив или контейнер, вы получите RuntimeException, если вы выйдите за границы. Как вы выучили в Главе 10, этот тип исключения указывает на ошибку программы, и поэтому у вас нет необходимости выполнять проверку в вашем коде. С другой стороны, объяснением того, что vector из С++ не проверяет границы при каждом доступе, может стать скорость доступа, в Java вы имеете постоянную проверку на превышение пределов все время и для массивов и для контейнеров.
Другие основные контейнерные классы, которые мы изучим в этой главе, List, Set и Map, все имеют дело с объектами, как будто они не имеют определенного типа. То есть, они трактуются как тип Object - корневой класс для всех классов в Java. С одной точки зрения, это работает прекрасно: вам необходимо построить только один контейнер и все объекты будут подходить для этого контейнера. (За исключением примитивов, они могут быть помещены в контейнер как константы при использовании классов-оболочек Java для примитивных типов, или как переменные значения при использовании ваших собственных классов-оболочек.) Это второй момент, когда массив лучше общих контейнеров: когда вы создаете массив, вы создаете его для содержания определенного типа. Это значит, что вы имеете проверку типа во время компиляции для предотвращения помещения неправильного типа или ошибку типа при извлечении. Конечно, Java предохранит вас от посылки объекту неподходящего сообщения и во время компиляции и во время выполнения. Так что вы, так или иначе, не рискуете, это даже лучше, что компилятор направляет вас, это быстрее при выполнении и при этом меньше вероятность, что конечный пользователь будет удивлен, получив исключение.
Для эффективности и проверке типа ценной попыткой будет использование массива, если это возможно. Однако когда вы пробуете решить общую проблему, массив может быть слишком ограничен. После обзора массивов остаток этой главы будет посвящен контейнерным классам, имеющимся в Java.
Массивы - первоклассные объекты
Независимо от типа массива, с которым вы работаете, идентификатор массива на самом деле указывает на действительные объекты, создаваемые в куче. Это объект, который содержит ссылки на другие объекты, и он может быть создан либо косвенным образом, как часть синтаксиса инициализации массива, либо явно с помощью выражения new. Частью объекта массива (фактически, только поле или метод, к которому вы можете получить доступ) является член length с доступом только для чтения, который говорит вам, сколько элементов можно хранить в объекте массива. Синтаксис ‘[]’ - это просто способ доступа, который вы имеете к объекту массива.
Следующий пример показывает различные способы, которыми может быть инициализирован массив, и как ссылки на объект могут быть присвоены различным объектам массива. Он также показывает, что массив объектов и массив примитивов почти идентичны в использовании. Отличие только в том, что массив объектов хранит ссылки, в то время как массив примитивов хранит значения примитивов напрямую.
//: c09:ArraySize.java
// Инициализация & пере присвоение массивов.
class Weeble {} // Немного мистическое создание
public class ArraySize { public static void main(String[] args) { // Массивы объектов:
Weeble[] a; // Null - ссылки
Weeble[] b = new Weeble[5]; // Null - ссылки
Weeble[] c = new Weeble[4]; for(int i = 0; i < c.length; i++) c[i] = new Weeble(); // Групповая инициализация:
Weeble[] d = { new Weeble(), new Weeble(), new Weeble() }; // Динамическая групповая инициализация:
a = new Weeble[] { new Weeble(), new Weeble() }; System.out.println("a.length=" + a.length); System.out.println("b.length = " + b.length); // Ссылки внутри массива автоматически
// инициализируются значением null:
for(int i = 0; i < b.length; i++) System.out.println("b[" + i + "]=" + b[i]); System.out.println("c.length = " + c.length); System.out.println("d.length = " + d.length); a = d; System.out.println("a.length = " + a.length);
// Массив примитивов:
int[] e; // Null - ссылка
int[] f = new int[5]; int[] g = new int[4]; for(int i = 0; i < g.length; i++) g[i] = i*i; int[] h = { 11, 47, 93 }; // Ошибка компиляции: переменная e не инициализирована
//!System.out.println("e.length=" + e.length);
System.out.println("f.length = " + f.length); // Примитивы внутри массива
// автоматически инициализируются нулем:
for(int i = 0; i < f.length; i++) System.out.println("f[" + i + "]=" + f[i]); System.out.println("g.length = " + g.length); System.out.println("h.length = " + h.length); e = h; System.out.println("e.length = " + e.length); e = new int[] { 1, 2 }; System.out.println("e.length = " + e.length); } } ///:~
Вот что программа выдает на выходе:
b.length = 5 b[0]=null
b[1]=null
b[2]=null
b[3]=null
b[4]=null
c.length = 4 d.length = 3 a.length = 3 a.length = 2 f.length = 5 f[0]=0 f[1]=0 f[2]=0 f[3]=0 f[4]=0 g.length = 4 h.length = 3 e.length = 3 e.length = 2
Массив a изначально это просто null-ссылка, и компилятор предохраняет вас от работы с этой ссылкой, пока вы правильно не инициализируете ее. Массив b инициализирован и указывает на массив ссылок Weeble, но никакие реальные объекты не помещаются в этот массив. Однако вы все равно спросить размер массива, так как b указывает на допустимый объект. Здесь мы получаем небольшой недостаток: вы не можете определить, сколько элементов на самом деле есть в массиве, так как length говорит нам о том, сколько элементов могут быть помещены в массив; то есть, размер массива объектов - это не число элементов реально хранящихся в нем. Однако когда создается массив объектов, его ссылки автоматически инициализируются значением null, так что вы можете проверить, имеется ли в определенной ячейке массива объект, просто проверив ее на null. Аналогично массив примитивов автоматически инициализируется нулями для числовых типов: (char)0 для char и false для boolean.
Массив c показывает создание массива объектов, за которым следует присвоение объектов Weeble для всех ячеек массива. Массив d показывает синтаксис “групповой инициализации”, которая является причиной того, что массив объектов создается (косвенным образом с помощью new в куче, так же как и массив c) и инициализируется объектами Weeble, и все это в одной инструкции.
О следующей инициализации массива можно думать, как о “динамической групповой инициализации”. Групповая инициализация, использованная для d, должна использоваться в точке определения d, но со вторым синтаксисом вы можете создавать и инициализировать объекты где угодно. Например, предположим, есть метод hide( ), который принимает массив объектов Weeble. Вы можете вызвать его, сказав:
hide(d);
но вы можете также динамически создать массив, который вы хотите передать в качестве аргумента:
hide(new Weeble[] { new Weeble(), new Weeble() });
В некоторых ситуациях этот новый синтаксис обеспечивает более удобный способ для написания кода.
Выражение:
a = d;
показывает, как вы можете получить ссылку, которая присоединена к массиву объектов, и присвоить ее другому массиву объектов, также как вы делаете это с другими типами ссылок на объекты. Теперь и a, и d указывают на один и тот же массив объектов в куче.
Вторая часть ArraySize.java показывает, что массив примитивов работает точно так же, как и массив объектов, за исключением того, что массив примитивов содержит значения примитивов напрямую.
Массивы в Java
Фактически, все языки программирования поддерживают массивы. Использование массивов в C и C++ рискованно, поскольку эти массивы всего лишь блоки памяти. Если программа обращается к массиву вне пределов этого блока, или использует память до инициализации (общая ошибка программирования), получится непредсказуемый результат.
Одна из главных целей Java - это безопасность, так что многие проблемы, надоедающие программистам в C и C++, не повторяются в Java. Java массив гарантированно инициализируется и нельзя получить доступ вне его пределов. Цена такой проверки диапазона - выделение дополнительной памяти к каждому массиву, так же как и за проверку индексов во время выполнения, но предположение, что это безопасно и повышает продуктивность, стоит расходов.
Когда вы создаете массив объектов, на самом деле вы создаете массив ссылок, а каждая из этих ссылок автоматически инициализируется специальным значением, имеющим собственное ключевое слово: null. Когда Java видит null, он распознает, что опрашиваемая ссылка не указывает на объект. Вы должны присвоить объект каждой ссылке, прежде чем использовать ее, и, если вы попробуете использовать ссылку, которая все еще null, о проблемах вы узнаете во время выполнения. Таким образом, типичные ошибки при работе с массивами предотвращены в Java.
Вы также можете создать массив примитивов. Опять компилятор гарантирует инициализацию, поскольку он заполняет нулями память для этого массива.
Массивы более подробно будут рассмотрены в следующих главах.
Математические операторы
Основные математические операторы те же, что и допустимые в большинстве языков программирования: сложение (+), вычитание (-), деление (/), умножение (*) и остаток от деления (%, которое производит остаток от целочисленнгого деления). Целочисленное деление в результате выполняет отсечение, а не округление.
Java также использует стенографическую запись для одновременного выполнения операции и присвоения. Это обозначается оператором, следующим за знаком равенства и совместимо со всеми операциями языка (когда это имеет смысл). Например, для добавления 4 к переменной x и присвоения результата x, используйте: x += 4.
Этот пример показывает использование математических операторов:
//: c03:MathOps.java
// Демонстрация математических операторов.
import java.util.*;
public class MathOps { // Создает стенографию, чтобы меньше печатать:
static void prt(String s) { System.out.println(s); } // стенография для печати строки и int:
static void pInt(String s, int i) { prt(s + " = " + i); } // стенография для печати строки и float:
static void pFlt(String s, float f) { prt(s + " = " + f); } public static void main(String[] args) { // Создает генератор случайных чисел,
// принимающий текущее время по умолчанию:
Random rand = new Random(); int i, j, k; // '%' ограничивает максимальное значение величиной 99:
j = rand.nextInt() % 100; k = rand.nextInt() % 100; pInt("j",j); pInt("k",k); i = j + k; pInt("j + k", i); i = j - k; pInt("j - k", i); i = k / j; pInt("k / j", i); i = k * j; pInt("k * j", i); i = k % j; pInt("k % j", i); j %= k; pInt("j %= k", j); // Проверка чисел с плавающей точкой:
float u,v,w; // Также применима к числам двойной точности
v = rand.nextFloat(); w = rand.nextFloat(); pFlt("v", v); pFlt("w", w); u = v + w; pFlt("v + w", u); u = v - w; pFlt("v - w", u); u = v * w; pFlt("v * w", u); u = v / w; pFlt("v / w", u); // следующее также работает для
// char, byte, short, int, long,
// и double:
u += v; pFlt("u += v", u); u -= v; pFlt("u -= v", u); u *= v; pFlt("u *= v", u); u /= v; pFlt("u /= v", u); } } ///:~
Первое, что вы увидите - это несколько стенаграфических методов для мечати: метод prt( ) печатает String, метод pInt( ) печатает String, а следом за ней int, a pFlt( ) печатает String, а следом float. Конечно они в конце концов используют System.out.println( ).
Для генерации чисел программа сначала создает объект Random. Поскольку во время создания не передаются аргументы, Java использует текущее время как источник для генератора случайных чисел. Программа генерирует несколько случайных чисел разных типов с помощью объекта Random, просто вызывая разные методы: nextInt( ), nextLong( ), nextFloat( ) или nextDouble( ).
Оператор остатка от деления, когда он используется с результатом работы генератора случайных чисел, ограничивает результат значением верхней границы операнда минус единица (в этом случае 99).
Меню
Каждый компонент способен содержать меню, включая JApplet, JFrame, JDialog и их потомков, имеющих метод setJMenuBar( ), который принимает JMenuBar (вы можете иметь только один JMenuBar для определенного компонента). Вы добавляете JMenu в JMenuBar, и JMenuItem в JMenu. Каждый JMenuItem может иметь присоединенный к нему ActionListener для сигнализации, что элемент меню выбран.
В отличие от систем, использующих ресурсы, в Java и Swing вы должны в ручную собрать все меню в исходном коде. Вот очень простой пример меню:
//: c13:SimpleMenus.java
// <applet code=SimpleMenus
// width=200 height=75> </applet>
import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
public class SimpleMenus extends JApplet { JTextField t = new JTextField(15); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenu[] menus = { new JMenu("Winken"), new JMenu("Blinken"), new JMenu("Nod") }; JMenuItem[] items = { new JMenuItem("Fee"), new JMenuItem("Fi"), new JMenuItem("Fo"), new JMenuItem("Zip"), new JMenuItem("Zap"), new JMenuItem("Zot"), new JMenuItem("Olly"), new JMenuItem("Oxen"), new JMenuItem("Free") }; public void init() { for(int i = 0; i < items.length; i++) { items[i].addActionListener(al); menus[i%3].add(items[i]); } JMenuBar mb = new JMenuBar(); for(int i = 0; i < menus.length; i++) mb.add(menus[i]); setJMenuBar(mb); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); } public static void main(String[] args) { Console.run(new SimpleMenus(), 200, 75); } } ///:~
Использование оператора остатка от деления в выражении “i%3” распределяет элементы меню на три JMenu. Каждый JMenuItem должен иметь присоединенный к нему ActionListener; здесь один и тот же ActionListener используется везде, но вам обычно нужны индивидуальные слушатели для каждого JMenuItem.
JMenuItem наследует от AbstractButton, так что он имеет поведение, аналогичное кнопке. Сам по себе он обеспечивает элемент, который может быть помещен в выпадающее меню. Есть также три типа наследников от JMenuItem: JMenu для содержания других JMenuItem (так что вы можете иметь каскадированное меню), JCheckBoxMenuItem, которое производит отметки, указывающие, было ли выбрано меню, или нет, и JRadioButtonMenuItem, которое содержит радио кнопки.
В качестве более изощренного примере здесь снова используются вкусы мороженого, используемые для создания меню. Этот пример также показывает каскадируемое меню, мнемонические обозначение клавиш, JCheckBoxMenuItem, и способ, которым вы можете динамически менять меню:
//: c13:Menus.java
// Подменю, checkbox-элементы в меню, перестановка меню,
// мнемоники (горячие клавиши) и команды реакции.
// <applet code=Menus width=300
// height=100> </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
public class Menus extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTextField t = new JTextField("No flavor", 30); JMenuBar mb1 = new JMenuBar(); JMenu f = new JMenu("File"), m = new JMenu("Flavors"), s = new JMenu("Safety"); // Альтернативный подход:
JCheckBoxMenuItem[] safety = { new JCheckBoxMenuItem("Guard"), new JCheckBoxMenuItem("Hide") }; JMenuItem[] file = { new JMenuItem("Open"), }; // Вторая полоса меню меняется на:
JMenuBar mb2 = new JMenuBar(); JMenu fooBar = new JMenu("fooBar"); JMenuItem[] other = { // Добавление горячих клавиш в меню (мнемоник)
// очень просто, но их могут иметь только JMenuItems
// в своем конструкторе:
new JMenuItem("Foo", KeyEvent.VK_F), new JMenuItem("Bar", KeyEvent.VK_A), // Нет горячей клавиши:
new JMenuItem("Baz"), }; JButton b = new JButton("Swap Menus"); class BL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuBar m = getJMenuBar(); setJMenuBar(m == mb1 ? mb2 : mb1); validate(); // Обновление фрейма
} } class ML implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Open")) { String s = t.getText(); boolean chosen = false; for(int i = 0; i < flavors.length; i++) if(s.equals(flavors[i])) chosen = true; if(!chosen) t.setText("Choose a flavor first!"); else
t.setText("Opening "+ s +". Mmm, mm!"); } } } class FL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)e.getSource(); t.setText(target.getText()); } } // Другой способ: вы можете создать другой класс
// для каждого MenuItem. Затем вам
// не нужно следить, кто есть кто:
class FooL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Foo selected"); } } class BarL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Bar selected"); } } class BazL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Baz selected"); } } class CMIL implements ItemListener { public void itemStateChanged(ItemEvent e) { JCheckBoxMenuItem target = (JCheckBoxMenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Guard")) t.setText("Guard the Ice Cream! " + "Guarding is " + target.getState()); else if(actionCommand.equals("Hide")) t.setText("Hide the Ice Cream! " + "Is it cold? " + target.getState()); } } public void init() { ML ml = new ML(); CMIL cmil = new CMIL(); safety[0].setActionCommand("Guard"); safety[0].setMnemonic(KeyEvent.VK_G); safety[0].addItemListener(cmil); safety[1].setActionCommand("Hide"); safety[0].setMnemonic(KeyEvent.VK_H); safety[1].addItemListener(cmil); other[0].addActionListener(new FooL()); other[1].addActionListener(new BarL()); other[2].addActionListener(new BazL()); FL fl = new FL(); for(int i = 0; i < flavors.length; i++) { JMenuItem mi = new JMenuItem(flavors[i]); mi.addActionListener(fl); m.add(mi); // Добавляем разделитель:
if((i+1) % 3 == 0) m.addSeparator(); } for(int i = 0; i < safety.length; i++) s.add(safety[i]); s.setMnemonic(KeyEvent.VK_A); f.add(s); f.setMnemonic(KeyEvent.VK_F); for(int i = 0; i < file.length; i++) { file[i].addActionListener(fl); f.add(file[i]); } mb1.add(f); mb1.add(m); setJMenuBar(mb1); t.setEditable(false); Container cp = getContentPane(); cp.add(t, BorderLayout.CENTER); // Готовим систему к замене меню:
b.addActionListener(new BL()); b.setMnemonic(KeyEvent.VK_S); cp.add(b, BorderLayout.NORTH); for(int i = 0; i < other.length; i++) fooBar.add(other[i]); fooBar.setMnemonic(KeyEvent.VK_B); mb2.add(fooBar); } public static void main(String[] args) { Console.run(new Menus(), 300, 100); } } ///:~
В этой программе я поместил элементы меню в массивы, а затем прошелся по всем массивам, вызывая add( ) для каждого JMenuItem. Это делает добавление или удаление пункта меню менее утомительным.
Эта программа создает не один, а два JMenuBar для демонстрации, что меню может быть заменено во время работы программы. Вы можете посмотреть, как создан JMenuBar из JMenu, и как каждый JMenu создан из JMenuItem, JCheckBoxMenuItem, или даже JMenu (который производит подменю). Когда JMenuBar собран, он может быть установлен в текущей программе с помощью метода setJMenuBar( ). Обратите внимание, что когда нажата кнопка, выполняется проверка какое меню сейчас установлено с помощью вызова метода getJMenuBar( ), а затем помещает другое меню на его место.
Когда проверяете “Open”, обратите внимание, что пробелы и большие буквы - критичны, но Java не сигнализирует об ошибках, если нет совпадений со словом “Open”. Такой род сравнения строк приводи к ошибкам программы.
Пометки и снятие пометки на элементах меню происходит автоматически. Код обработки JCheckBoxMenuItem показывает два разных способа определения, что было помечено: сравнение строк (которое, как упоминалось ранее, является не очень безопасным подходом, хотя вы видите его использование) и сравнение объектов - источников события. Как показано, метод getState( ) может использоваться для проверки состояния. Вы можете также менять состояние JCheckBoxMenuItem с помощью setState( ).
События для меню немного противоречивы и могут запутать: JMenuItem использует ActionListener, а JCheckboxMenuItem использует ItemListener. Объект JMenu может также поддерживать ActionListener, но обычно это бесполезно. В общем случае вы будете присоединять слушатели к каждому JMenuItem, JCheckBoxMenuItem или JRadioButtonMenuItem, но пример показывает ItemListener и ActionListener, присоединенные к различным компонентам меню.
Swing поддерживает мнемоники, или “горячие клавиши”, так что вы можете выбрать все что угодно, унаследованное от AbstractButton (кнопки, элементы меню и т.п.), используя клавиатуру вместо мыши. Это достаточно просто: для JMenuItem вы можете использовать перегруженный конструктор, который принимает второй аргумент, идентифицирующий клавишу. Однако большинство AbstractButton не имеют конструкторов, подобных этому, поэтому есть более общий способ решения проблемы - это использование метода setMnemonic( ). Приведенный выше пример добавляет мнемонику к кнопке и к некоторым из элементов меню; индикатор горячей клавиши в компоненте появляется автоматически.
Вы также можете видеть использование команды setActionCommand( ). Она выглядит немного странной, потому что в каждом случае “команда реакции” точно такая же, как и метка компонента меню. Почему просто не использовать метку вместо этой альтернативной строки? Проблема заключена в интернационализации. Если вы перенесете эту программу на другой язык, вы захотите изменить только метки в меню, и не изменять код (чтобы не вносить новые ошибки). Чтобы выполнить это легче при кодировании, выполняется проверка ассоциированной строки с этим компонентом меню, “команда реакции” может быть неизменной, в то время как метка меню может измениться. Весь код работает с “командой реакции”, так что на него не влияют никакие изменения меток меню. Обратите внимание, что в этой программе не у всех компонент меню проверяется их команда реакции, так что эти элементы не имеют своих установленных команд реакции.
Основная работа происходит в слушателях. BL выполняет обмен JMenuBar. В ML “cмотрят, кто звонил”, при этом подходе берется источник ActionEvent и приводится в типу JMenuItem, затем получается строка команды реакции для передачи ее в каскадную инструкцию if.
Слушатель FL прост, несмотря на то, что он обрабатывает все различные вкусы в меню вкусов. Этот подход полезен, если вы имеете достаточно простую логику, но в общем случае вы будете использовать подход с использованием FooL, BarL и BazL, каждый из которых присоединяется только к одному компоненту меню, так чтобы не нужна было дополнительная логика определения, и вы точно знали, кто вызвал слушателя. Даже с избытком классов, созданных этим способом, внутренний код имеет тенденцию быть меньше, а процесс более понятным.
Вы можете видеть, что код меню быстро становится многозначным и беспорядочным. Это другой случай, где использование построителя GUI является подходящим решением. Хороший инструмент также берет на себя заботы о меню.
Мертвая блокировка
Из-за того, что процесс может быть блокирован и из-за того, что объекты могут иметь synchronized методы, запрещающие процессам доступ к этим объектам до тех пор, пока не будет снята блокировка синхронизации, то возможен случай, когда один процесс ожидает другой процесс, который в свою очередь ожидает третий процесс и так далее, до тех пор пока цепочка не вернется к первому ожидающему процессу. В этом случае мы получаем бесконечный цикл процессов ожидающих друг друга, причем ни один не может продолжить выполнение. Это называется мертвая блокировка (deadlock). Можно утверждать, что это не происходит слишком часто, но когда это произойдет с вашим кодом, то будет очень сложно обнаружить ошибку.
В языке Java не существует специальных средств для того, чтобы помочь предотвратить мертвую блокировку; все действия по предотвращению возложены на программиста и заключаются в аккуратном проектировании. Хотя это и не утешение для того, кто пытается отлаживать программу с такой блокировкой.
Методы, аргументы и возвращаемое значение
До этих пор термин функция использовался для описания поименованной процедуры. Термин, наиболее часто используемый в Java, это метод, как “способ что-то сделать”. Если вы хотите, вы можете продолжать думать в терминах функций. На самом деле, это только семантическое различие, но далее в этой книге будет использоваться “метод”, а не “функция”.
Методы в Java определяют сообщения, которые объекты могут принимать. В этом разделе вы выучите как просто определить метод.
Фундаментальные части метода - это его имя, аргументы, возвращаемое значение и тело. Посмотрите на основную форму:
returnType methodName( /* список аргументов */ ) { /* Тело метода */
}
Возвращаемый тип - это тип значения, которое помещается в память из метода после его вызова. Список аргументов дает типы и имена, чтобы проинформировать вас, что вы должны передать в этот метод. Имя метода и список аргументов вместе уникально идентифицируют метод.
Методы в Java могут создаваться только как часть класса. Метод может быть вызван только для объекта, [21] а этот объект должен быть способен выполнить этот вызов метода. Если вы попытаетесь вызвать неправильный метод для объекта, вы получите сообщение об ошибке во время компиляции. Вы вызываете метод для объекта по имени объекта, за которым следует разделитель (точка), а далее идет имя метода и список его аргументов, как здесь: objectName.methodName(arg1, arg2, arg3). Например, предположим, что вы имеете метод f( ) , который не принимает аргументов и возвращает значение типа int. Тогда, если вы имеете объект с именем a для которого может быть вызван f( ) , вы можете сказать:
int x = a.f();
Тип возвращаемого значения должен быть совместим с типом x.
Этот вызов метода часто называется посылкой сообщения объекту. В приведенном выше примере сообщение - f( ) , а объект - a. Объектно-ориентированное программирование часто резюмирует, как просто “посылку сообщения объекту”.
Мини редактор
Управляющий элемент JTextPane великолепно подходит для редактирования, без больших усилий. Следующий пример делает очень простое использование этого, игнорирую большую часть функциональности класса:
//: c13:TextPane.java
// JTextPane - это маленький редактор.
// <applet code=TextPane width=475 height=425>
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*;
public class TextPane extends JApplet { JButton b = new JButton("Add Text"); JTextPane tp = new JTextPane(); static Generator sg = new Arrays2.RandStringGenerator(7); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(int i = 1; i < 10; i++) tp.setText(tp.getText() + sg.next() + "\n"); } }); Container cp = getContentPane(); cp.add(new JScrollPane(tp)); cp.add(BorderLayout.SOUTH, b); } public static void main(String[] args) { Console.run(new TextPane(), 475, 425); } } ///:~
Кнопка просто добавляет случайно сгенерированный текст. Смысл JTextPane состоит в том, что она позволяет редактировать текст на месте, так что вы увидите и не нужен метод append( ). В этом случае (вероятно, недостаточное использование возможностей JTextPane), текст должен захватываться, изменятся и помещаться назад в панель, используя setText( ).
Как упоминалось ранее, апплет по умолчанию использует компоновку BorderLayout. Если вы добавите что-то в панель без указания детализации, оно просто заполнит центр панели до краев. Однако если вы укажите один из окружающих регионов (NORTH, SOUTH, EAST или WEST), как сделано здесь, компонент поместит себя в этот регион — в этом случае кнопка вмонтирована внизу экрана.
Обратите внимание на встроенные особенности JTextPane, такие как автоматическое разбиение строк. Есть много других особенностей, которые вы можете просмотреть, используя документацию JDK.
Многофайловое хранение с использованием Zip
Библиотека, поддерживающая Zip формат, более обширная. С ее помощью вы можете легко хранить множественные файлы, есть даже отдельные файлы, которые делают легким процесс чтения Zip файла. Библиотека использует стандартный Zip формат, так что он может работать совместно со всеми инструментами, которые доступны в Internet. Следующий пример имеет ту же форму, что и предыдущий, но он обрабатывает столько аргументов командной строки, сколько вы захотите. Кроме того, он показывает использование классов Checksum для подсчета и проверки контрольной суммы для файла. Есть два типа Checksum : Adler32 (который быстрее) и CRC32 (который медленнее, но немного более аккуратный).
//: c11:ZipCompress.java
// Использует компрессию Zip для компрессии любого
// числа файлов, переданных из командной строки.
import java.io.*; import java.util.*; import java.util.zip.*;
public class ZipCompress { // Исключение выбрасывается на консоль:
public static void main(String[] args) throws IOException { FileOutputStream f = new FileOutputStream("test.zip"); CheckedOutputStream csum = new CheckedOutputStream( f, new Adler32()); ZipOutputStream out = new ZipOutputStream( new BufferedOutputStream(csum)); out.setComment("A test of Java Zipping"); // Хотя нет соответствующего getComment().
for(int i = 0; i < args.length; i++) { System.out.println( "Writing file " + args[i]); BufferedReader in = new BufferedReader( new FileReader(args[i])); out.putNextEntry(new ZipEntry(args[i])); int c; while((c = in.read()) != -1) out.write(c); in.close(); } out.close(); // Контрольная сумма действительна только после
// того, как файл будет закрыт!
System.out.println("Checksum: " + csum.getChecksum().getValue()); // Теперь вытянем файлы:
System.out.println("Reading file"); FileInputStream fi = new FileInputStream("test.zip"); CheckedInputStream csumi = new CheckedInputStream( fi, new Adler32()); ZipInputStream in2 = new ZipInputStream( new BufferedInputStream(csumi)); ZipEntry ze; while((ze = in2.getNextEntry()) != null) { System.out.println("Reading file " + ze); int x; while((x = in2.read()) != -1) System.out.write(x); } System.out.println("Checksum: " + csumi.getChecksum().getValue()); in2.close(); // Альтернативный способ для открытия и чтения
// zip файлов:
ZipFile zf = new ZipFile("test.zip"); Enumeration e = zf.entries(); while(e.hasMoreElements()) { ZipEntry ze2 = (ZipEntry)e.nextElement(); System.out.println("File: " + ze2); // ... и вытягиваем данные, как и раньше
} } } ///:~
Для каждого файла, добавляемого в архив, вы должны вызвать putNextEntry( ) и передать ему объект ZipEntry. Объект ZipEntry содержит обширный интерфейс, который позволит вам получить и установить все данные, доступные для этого конкретного включения в ваш Zip файл: имя, компрессированный и не компрессированный размеры, дата, CRC контрольная сумма, дополнительное поле данных, комментарий, метод компрессии и есть ли включаемые директории. Однако, хотя формат Zip имеет возможность установки пароля, это не поддерживается в Zip библиотеке Java. И хотя CheckedInputStream и CheckedOutputStream поддерживают обе контрольные суммы Adler32 и CRC32, класс ZipEntry поддерживает только интерфейс для CRC. Это является ограничением лежащего в основе Zip формата, и это может ограничить вас в использовании быстрого Adler32.
Для извлечения файлов ZipInputStream имеет метод getNextEntry( ), который возвращает следующий ZipEntry, если он существует. В качестве более краткой альтернативы, вы можете читать файл, используя объект ZipFile, который имеет метод entries( ), возвращающий Enumeration из ZipEntries.
Для чтения контрольной суммы вы должны как-то получить доступ к ассоциированному объекту Checksum. Здесь получается ссылка на объекты CheckedOutputStream и CheckedInputStream, но вы также могли просто владеть ссылкой на объект Checksum.
Трудным методом для потоков Zip является setComment( ). Как показано выше, вы можете установить комментарий, когда вы записываете файл, но нет способа получить коментарий в ZipInputStream. Комментарии появились для полной поддержки базиса включение-за-включением только через ZipEntry.
Конечно, вы не ограничены в файлах, когда используете библиотеки GZIP или Zip — вы можете компрессировать все, что угодно, включая данные для посылки через сетевое соединение.
Многомерные массивы
Java позволяет вам легко создавать многомерные массивы:
//: c04:MultiDimArray.java
// Создание многомерных массивов.
import java.util.*;
public class MultiDimArray { static Random rand = new Random(); static int pRand(int mod) { return Math.abs(rand.nextInt()) % mod + 1; } static void prt(String s) { System.out.println(s); } public static void main(String[] args) { int[][] a1 = { { 1, 2, 3, }, { 4, 5, 6, }, }; for(int i = 0; i < a1.length; i++) for(int j = 0; j < a1[i].length; j++) prt("a1[" + i + "][" + j + "] = " + a1[i][j]); // 3-х мерный массив фиксированной длины:
int[][][] a2 = new int[2][2][4]; for(int i = 0; i < a2.length; i++) for(int j = 0; j < a2[i].length; j++) for(int k = 0; k < a2[i][j].length; k++) prt("a2[" + i + "][" + j + "][" + k + "] = " + a2[i][j][k]); // 3-х мерный массив с векторами переменной длины:
int[][][] a3 = new int[pRand(7)][][]; for(int i = 0; i < a3.length; i++) { a3[i] = new int[pRand(5)][]; for(int j = 0; j < a3[i].length; j++) a3[i][j] = new int[pRand(5)]; } for(int i = 0; i < a3.length; i++) for(int j = 0; j < a3[i].length; j++) for(int k = 0; k < a3[i][j].length; k++) prt("a3[" + i + "][" + j + "][" + k + "] = " + a3[i][j][k]); // Массив не примитивных объектов:
Integer[][] a4 = { { new Integer(1), new Integer(2)}, { new Integer(3), new Integer(4)}, { new Integer(5), new Integer(6)}, }; for(int i = 0; i < a4.length; i++) for(int j = 0; j < a4[i].length; j++) prt("a4[" + i + "][" + j + "] = " + a4[i][j]); Integer[][] a5; a5 = new Integer[3][]; for(int i = 0; i < a5.length; i++) { a5[i] = new Integer[3]; for(int j = 0; j < a5[i].length; j++) a5[i][j] = new Integer(i*j); } for(int i = 0; i < a5.length; i++) for(int j = 0; j < a5[i].length; j++) prt("a5[" + i + "][" + j + "] = " + a5[i][j]); } } ///:~
Код, использующийся для печати, использует length, так что он не зависит от того, имеет ли массив фиксированную длину.
Первый пример показывает многомерный массив примитивных типов. Вы ограничиваете каждый вектор в массиве с помощью фигурных скобок:
int[][] a1 = { { 1, 2, 3, }, { 4, 5, 6, }, };
Каждая пара квадратных скобок переносит нас на новый уровень в массиве.
Второй пример показывает трехмерный массив, резервируемый с помощью new. Здесь весь массив резервируется сразу:
int[][][] a2 = new int[2][2][4];
А в третьем примере показано, что каждый вектор массива, создающий матрицу, может быть произвольной длины:
int[][][] a3 = new int[pRand(7)][][]; for(int i = 0; i < a3.length; i++) { a3[i] = new int[pRand(5)][]; for(int j = 0; j < a3[i].length; j++) a3[i][j] = new int[pRand(5)]; }
Первый new создает массив произвольной длиной первого элемента, а остальные элементы не определены. Второй new, внутри цикла for, заполняет элементы, но оставляет третий индекс неопределенным, пока вы не введете третий new.
Вы увидите на выводе, что значения массива автоматически инициализируются нулями, если вы не передадите им явно начальные значения.
Вы можете работать с массивами не примитивных объектов точно таким же образом, что показано в четвертом примере, демонстрирующем возможность помещения множества выражений new в фигурных скобках:
Integer[][] a4 = { { new Integer(1), new Integer(2)}, { new Integer(3), new Integer(4)}, { new Integer(5), new Integer(6)}, };
Пятый пример показывает, как можно построить массив не примитивных объектов по частям:
Integer[][] a5; a5 = new Integer[3][]; for(int i = 0; i < a5.length; i++) { a5[i] = new Integer[3]; for(int j = 0; j < a5[i].length; j++) a5[i][j] = new Integer(i*j); }
i*j - это просто помещает отличное от нуля значение в Integer.
Многопоточность
Фундаментальная концепция в компьютерном программировании - это идея обработки более одной задачи одновременно. Многи проблемы программирования требуют, чтобы программы были способны остановить свое выполнение, заняться какой-то другой проблемой, а затем вернуться к главному процессу. Решение может быть получено несколькими путями. Изначально, программисты со знаниями нижнего уровня машины писали процедуру обработки прерывания и приостановление главного процесса инициировалось аппаратным прерыванием. Хотя это работало хорошо, это было сложно не оперативно, так что это делало перенос программы на новый тип машин медленным и дорогим.
Иногда прерывания необходимы для обработки критичных ко времени задач, но есть большой класс проблем, в которых вы просто пробуете отделить часть проблемы в отдельно работающий процесс, так что вся программа станет легче отзываться. Внутри программы раздельно выполняющие части, называются потоками, а общая концепция называется mмногопоточностью. Общий пример многопоточности - это пользовательский интерфейс. При использовании потоков пользователь может нажать кнопку и получить быстрый ответ, не заставляя ждать, пока программа завершит свою текущую задачу.
Обычно, потоки - это просто способ выделить время у одного процессора. Но если операционная система поддерживает несколько процессоров, каждый поток может выполняться различными процессорами, и они действительно могут работать параллельно. Одно из удобств многопоточности на уровне языка - это то, что программист не должен беспокоится о том, есть ли в системе несколько процессоров или только один. Программа логически разделена на потоки и, если машина имеет более одного процессора, то программа работает быстрее без дополнительных регулировок.
Все это делает использование потоков приятно простым. В этом выгода: разделение ресурсов. Если вы имеете более одного запущенного процесса, которые ожидают доступа к одному и тому же ресурсу, вы имеете проблему. Например, два процесса не могут одновременно посылать информацию на принтер. Для решения проблемы ресурс, который может быть разделен, такой как принтер, должен быть заблокирован, пока он используется. Так процесс блокирует ресурс, завершает задачу, а затем освобождает блокировку, так что кто-то другой может использовать ресурс.
Потоки Java встроены в язык, что делает сложные предметы более простыми. Потоки поддерживается на уровне объектов, так что один исполняющийся поток представлен одним объектов. Java также обеспечивает ограниченное блокирование ресурса. Может быть заблокирована память любого объекта (который, помимо всего, один из видов разделяемых ресурсов), так что только один поток может использовать его одновременно. Это достигается с помощью ключевого слова synchronized. Другие типы ресурсов должны быть явно заблокированы программистом, обычно с помощью создания объекта для представления блокировки, который все потоки должны проверять прежде, чем обратится к ресурсу.
Множественное наследование в Java
Interface это не просто более чистая форма абстрактного класса. Он имее более "высокое" применение. Поскольку interface не имеет реализации всего, что есть в нтем, то нет и массива-хранилища связанного с ним, нет ничего мешающего для комбинации нескольких интерфейсов. И это ценно, поскольку иногда вам требуется нечто: "x есть a и b и c." В C++, этот акт множественных интерфейсов называется множественное наследование, и при этом этот тип тянет за собой "прилипший" багаж, поскольку каждый тип имеет свою реализацию. В Java Вы можете осуществить то же самое, но только один из этих классов может иметь реализацию, так что проблемы, возникающие в C++, не возникают в Java, при комбинировании множества интерфейсов:
В дочерних классах, Вы не можете насильно получить доступ к базовому классу, поскольку он так же абстрактен и монолитен - нерушим (один без абстрактных методов). Если Вы наследуете не от интерфейса, Вы можете наследовать только от него одного. Все остальные из элементов базового класса должны быть интерфейсами. Вы помещаете имена всех интерфейсов после ключевого слова implements и разделяете их при помощи запятых. Вы можете использовать столько интерфейсов, сколько хотите, каждый из них становится независимым типом, к которому Вы в последствии можете привести. Следующий пример демонстрирует комбинирование класса с несколькими интерфейсами для создания нового класса:
//: c08:Adventure.java
// Множество интерфейсов.
import java.util.*;
interface CanFight { void fight(); }
interface CanSwim { void swim(); }
interface CanFly { void fly(); }
class ActionCharacter { public void fight() {} }
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { public void swim() {} public void fly() {} }
public class Adventure { static void t(CanFight x) { x.fight(); } static void u(CanSwim x) { x.swim(); } static void v(CanFly x) { x.fly(); } static void w(ActionCharacter x) { x.fight(); } public static void main(String[] args) { Hero h = new Hero(); t(h); // Treat - CanFight
u(h); // Treat - CanSwim
v(h); // Treat - CanFly
w(h); // Treat - ActionCharacter
} } ///:~
В этом примере, как Вы можете видеть, класс Hero комбинирует конкретный класс ActionCharacter с интерфейсами CanFight, CanSwim и CanFly. Когда Вы комбинируете именованный класс с интерфейсами, как в этом примере, этот класс должен быть указан первым, а только затем интерфейсы, в противном случае компилятор выдаст сообщение об ошибке.
Заметьте, что сигнатура fight( ) та же самая, в интерфейсе CanFight и в классе ActionCharacter, но обратите внимание, что fight( ) не определена в Hero. Правилом для интерфейса является то, что Вы можете наследовать от него, но тогда Вы получите еще один интерфейс. Если же Вы хотите создать объект нового типа, то это должен быть класс со всеми определениями. Даже в силу того, что Hero не предоставляет определения для fight( ), это определение появляется вместе с ActionCharacter. Поскольку оно предоставляется автоматически и есть возможность создать объект от Hero.
В классе Adventure, Вы можете видеть, что в нем есть несколько методов, которые воспринимают в качестве аргументов различные интерфейсы и конкретные классы. Когда объект Hero уже создан, то он может быть передан в качестве параметра в любой их этих методов, что в свою очередь означает, что он может быть приведен к базовому типу к любому из вышеописанных интерфейсов. Поскольку мы рассматриваем путь создания интерфейсов в Java, то на этой дороге программисту не встретится никаких особенных трудностей и не придется специально напрягаться.
Запомните пожалуйста причину использования интерфейсов, кратко ее можно изложить так: возможность приведения к более, чем одному базовому типу. В дополнение вторая причина для использования интерфейсов в том же, в чем и причина использования абстрактных базовых классов: предотвращение создания объектов этого класса программистами и понимания, что это всего лишь интерфейс. Отсюда возникает вопрос: Что Вы должны использовать? Interface или abstract класс? Интерфейс дает вам преимущества абстрактного класса и преимущества интерфейса, так что если нужно создать базовый класс без любых определений методов или переменных, то Вы должны предпочесть абстрактному классу интерфейс. В действительности, если Вы знаете, что что-то собирается стать базовым классом, вашим первым решением должно быть - использовать интерфейс и если только Вы решите, что этот класс должен иметь определения методов и переменных, только тогда Вы должны изменить его на абстрактный класс или если это так необходимо на обычный класс.
Модель событий Swing
В модели событий Swing компоненты могут инициировать (“возбуждать”) события. Каждый тип события представляется разными классами. Когда событие возбуждается событие, оно принимается одним или несколькими “слушателями”, которые реагируют на это событие. Таким образом, источник событий и место, где событие обрабатывается, могут быть разделены. Так как вы обычно используете компоненты Swing, как они есть, то необходимо писать код, вызывающийся в том случае, когда компонент принимает событие, это хороший пример разделения интерфейса и реализации.
Каждый слушатель события - это объект класса, который реализует определенный тип интерфейса слушателя. Как программист, все, что вы делаете - это создаете объект слушателя и регистрируете его в компоненте, который возбуждает событие. Эта регистрация выполняется вызовом метода addXXXListener( ) для компонента, возбуждающего событие, в котором “XXX” представляет тип слушателя события. Вы можете легко узнать, какой тип события может быть обработан, просмотрев имена методов “addListener”, и если вы попробуете слушать неверные события, вы обнаружите ошибку времени компиляции. Позже в этой главе вы увидите, что JavaBeans также использует имена методов “addListener” для определения того, какое событие может обработать Bean.
Поэтому, вся ваша событийная логика переходит в класс слушателя. Когда вы создаете класс-слушатель, главное ограничение в том, что он должен реализовывать подходящий интерфейс. Вы можете создать класс глобального слушателя. Но это та ситуация, в которой внутренние классы более полезны, не только потому, что они обеспечивают логическое группирование ваших классов - слушателей внутри UI или логических бизнес - классов, которые они обслуживают, но и потому (как вы увидите позже), что фактически, объект внутреннего класса хранит ссылку на родительский объект, обеспечивая лучший способ для перекрестного вызова класса и подсистемных границ.
Все дальнейшие примеры этой главы используют модель событий Swing, а оставшаяся часть этого раздела опишет детали этой модели.
Модификация поведения потока
Потоки InputStream и OutputStream адаптируются под определенные требования с использованием “декорирующих” подклассов FilterInputStream и FilterOutputStream. Классы иерархии Reader и Writer продолжают использовать эту идею — но не точно.
В приведенной таблице соответствие произведено с еще большим приближением, чем в предыдущей таблице. Различия происходят из-за организации классов: в то время как BufferedOutputStream является подклассом FilterOutputStream, BufferedWriter не является подклассом FilterWriter (который, не смотря на то, что он является абстрактным, не имеет подклассов и, таким образом, появляется помещенным в любой объект, а здесь упомянуть просто для того, чтобы вы не удивились, увидев его). Однако интерфейсы классов достаточно близки при сравнении.
FilterInputStream | FilterReader |
FilterOutputStream | FilterWriter (абстрактный класс без подклассов) |
BufferedInputStream | BufferedReader (так же имеет readLine( )) |
BufferedOutputStream | BufferedWriter |
DataInputStream | Используйте DataInputStream (За исключением случаев, когда вам нужно использовать readLine( ), в этом случае вы должны использовать BufferedReader) |
PrintStream | PrintWriter |
LineNumberInputStream | LineNumberReader |
StreamTokenizer | StreamTokenizer (Используйте конструктор, принимающий Reader) |
PushBackInputStream | PushBackReader |
Есть одно направление, которое достаточно понятно: Когда вы хотите использовать readLine( ), вы не должны более использовать DataInputStream (при этом вы встретитесь с сообщении об использовании устаревших методов во время компиляции), а вместо этого использовать BufferedReader. Тем не менее, DataInputStream все еще остается “привилегированным” членом библиотеки ввода/вывода.
Чтобы сделать переход к использованию PrintWriter более легким, он имеет конструктор, который принимает любой объект типа OutputStream, наряду с объектами Writer. Однако PrintWriter более не поддерживает форматирование, которое поддерживал PrintStream; интерфейс, фактически, остался тем же.
Конструктор PrintWriter также имеет необязательную опцию для выполнения автоматической выгрузки буферов, которая случается после каждого вызова println( ), если установлен флаг в конструкторе.
Мой собственный список книг
Список в порядке публикации. Не все их книг в данный момент доступны в продаже.
Computer Interfacing with Pascal & C, (Самиздат через Eisys, 1988. Доступна только с www.BruceEckel.com). Введение в электронику с самого начала, когда CP/M все еще была операционной системой и когда DOS только появилась. Я использовал языки высокого уровня и параллельный порт компьютера для создания множества различных электронных проектов. Книга собрана из статей в первом и единственном журнале для которого я писал Micro Cornucopia. (Если перефразировать Larry O’Brien, редактора Software Development Magazine продолжительное время: лучший компьютерный журнал, к котором я когда либо печатался, у них всегда есть план, как вырастить робота в цветочном горшке!) Увы, Micro C пропала еще до возникновения Интернет. Создание этой книги было подведением черты, опыта публикаций.
Using C++, (Osborne/McGraw-Hill, 1989). Одна из первых книг по C++. Эта книга сейчас заменена второй редакцией C++ Inside & Out.
C++ Inside & Out, (Osborne/McGraw-Hill, 1993). Как уже замечено выше - вторая редакция Using C++. C++ в этой книге довольно правилен, но на момент 1992 года и по этому Thinking in C++ призвана заменить ее. Вы можете не только узнать больше об этой книге но и скачать ее целиком с исходными кодами с www.BruceEckel.com.
Thinking in C++, 1st Edition, (Prentice-Hall, 1995).
Thinking in C++, 2nd Edition, Volume 1, (Prentice-Hall, 2000). Доступна загрузка с www.BruceEckel.com.
Black Belt C++, the Master’s Collection, Bruce Eckel, редактор (M&T Books, 1994). Вышло из печати. Сборник глав от различных C++ светил, основанных на их представлениях на конференции разработчиков программного обеспечения, на котором я председательствовал. Обложка этой книги вдохновила меня на создание обложек всех последующих книг.
Thinking in Java, 1st Edition, (Prentice-Hall, 1998). Первая редакция этой книги получила награду Software Development Magazine Productivity, награду Java Developer’s Journal Editor’s Choice и награду JavaWorld Reader’s Choice Award for best book. Доступна для загрузки с www.BruceEckel.com.
[ Предыдущая глава ] [ Краткое оглавление ] [ Содержание ] [ Список ]
Может ли быть внутренний класс перегружен?
Что произойдет, когда Вы создадите внутренний класс, а затем наследуете окружающий класс и переопределите внутренний класс? Что же это такое, что возможно переопределить внутренний класс? Да, это именно так и выглядит, но переопределение внутреннего класса это то же самое, как если бы во внешнем классе был бы другой метод, который ничего бы не делал:
//: c08:BigEgg.java
// Внутренний класс не может быть переопределен
// как метод.
class Egg { protected class Yolk { public Yolk() { System.out.println("Egg.Yolk()"); } } private Yolk y; public Egg() { System.out.println("New Egg()"); y = new Yolk(); } }
public class BigEgg extends Egg { public class Yolk { public Yolk() { System.out.println("BigEgg.Yolk()"); } } public static void main(String[] args) { new BigEgg(); } } ///:~
Конструктор по умолчанию генерируется компилятором и эти вызовы конструктора базового класса тоже. Вы можете думать, что поскольку BigEgg уже создан, то будет использована переопределенная версия Yolk, но в действительности все не так. Вывод будет такой:
New Egg() Egg.Yolk()
Этот пример показывает, что нет никакой дополнительной магии в наследовании от внешнего класса. Два внутренних классов полностью разделены, каждый существует в своем собственном пространстве имен. Но все равно, остается возможность недвусмысленно наследовать от внутреннего класса:
//: c08:BigEgg2.java
// Правильное наследование от внутреннего класса.
class Egg2 { protected class Yolk { public Yolk() { System.out.println("Egg2.Yolk()"); } public void f() { System.out.println("Egg2.Yolk.f()"); } } private Yolk y = new Yolk(); public Egg2() { System.out.println("New Egg2()"); } public void insertYolk(Yolk yy) { y = yy; } public void g() { y.f(); } }
public class BigEgg2 extends Egg2 { public class Yolk extends Egg2.Yolk { public Yolk() { System.out.println("BigEgg2.Yolk()"); } public void f() { System.out.println("BigEgg2.Yolk.f()"); } } public BigEgg2() { insertYolk(new Yolk()); } public static void main(String[] args) { Egg2 e2 = new BigEgg2(); e2.g(); } } ///:~
Теперь BigEgg2.Yolk ясно расширяет Egg2.Yolk и переопределяет его методы. Метод insertYolk( ) позволяет BigEgg2 привести к базовому типу один из его объектов Yolk к ссылке y в Egg2, так что, когда g( ) вызывает y.f( ), то используется переопределенная версия f( ). Вывод же будет такой:
Egg2.Yolk() New Egg2() Egg2.Yolk() BigEgg2.Yolk() BigEgg2.Yolk.f()
Второй вызов Egg2.Yolk( ) это вызов конструктора базового класса из конструктора BigEgg2.Yolk. Вы так же можете видеть, что переопределенная версия f( ) используется, когда вызывается g( ).
Мультимедийный CD ROM
Существует два мультимедийных диска связанных с этой книгой. Один связан непосредственно с книгой Thinking in C, что описывается после предисловия, и знакомит вас с соответствующим синтаксисом языка С, необходимым для понимания Java. Второй, доступный мультимедиа диск, основан на данной книги. Он является отдельным продуктом и содержит полную версию недельного курса "Hands-On Java" (Практический курс Java), а также более 15 часов записанных лекций, одновременно с показом слайдов. Для семинаров, читаемых по данной книге, это лучшее подспорье. Также диск содержит все лекции (с важными замечаниями для привлечения внимания) с пятидневного учебного семинара в стиле "полного погружения".
Второй диск доступен только при заказе непосредственно на сайте www.BruceEckel.com.
Начальная стоимость
Стоимость перехода на Java это больше, чем просто приобретение компилятора Java (компилятор Java от Sun бесплатный, так что это не помеха). Ваши средне- и долгосрочные затраты будут минимизированы, если вы инвестируете их в практику (и, возможно, в руководство вашим первым проектом), а так же, если вы обнаружите и приобретете библиотеки классов, которые решают ваши проблемы прежде, чем попробуете построить самостоятельно. Это затраты реальных денег, которые должны быть вложены в реалистичные планы. В дополнение, здесь есть спрятанные затраты в потере продуктивности, пока идет изучение нового языка и, возможно, новой среды программирования. Практика и руководство, конечно, могут минимизировать их, но члены команды должны преодолевать препятствия сами, чтобы понять новую технологию. Во время этого процесса они могут сделать много ошибок (это будущее, поскольку узнанные ошибки - скорейший путь обучения) и будут менее продуктивны. Даже тогда, с некоторыми проблемами программирования, правильными классами и правильной средой программирования, возможно, быть более продуктивным, пока учите Java (даже с учетом того, что вы делаете много ошибок и пишите мало строчек кода в день), чем если бы вы остались с C.
Нахождение класса
Вы можете задаться вопросом, что необходимо для восстановления объекта из его сериализованного состояния. Например, предположим, вы сериализовали объект и послали его в файл или по сети на другую машину. Может ли программа на другой машине реконструировать объект, используя только содержимое файла?
Лучшим способом для ответа на этот вопрос (как обычно) будет проведение эксперимента. Следующий файл содержится в поддиректории для этой главы:
//: c11:Alien.java
// Сериализуемый класс.
import java.io.*;
public class Alien implements Serializable { } ///:~
Файл, который создает и сериализует объект Alien, содержится в том же директории:
//: c11:FreezeAlien.java
// Создает файл сериализации.
import java.io.*;
public class FreezeAlien { // Выбрасывает исключение на консоль:
public static void main(String[] args) throws IOException { ObjectOutput out = new ObjectOutputStream( new FileOutputStream("X.file")); Alien zorcon = new Alien(); out.writeObject(zorcon); } } ///:~
С точки зрения поимки и обработки исключений, эта программа выбрала быстрый и грязный способ передачи исключений наружу main( ), так что информация о них будет помещаться в командной строке.
Как только программа будет скомпилирована и запущена, скопируйте результирующий файл X.file в поддиректорий, под названием xfiles, где имеется следующий код:
//: c11:xfiles:ThawAlien.java
// Пробуем восстановить сериализованный файл
// без объекта класса, хранимого в файле.
import java.io.*;
public class ThawAlien { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream( new FileInputStream("X.file")); Object mystery = in.readObject(); System.out.println(mystery.getClass()); } } ///:~
Эта программа открывает файл и успешно читает объект mystery. Однако, как только вы попробуете найти что-нибудь об объекте — что требует Class объекта для Alien — виртуальная машина Java (JVM) не сможет найти Alien.class (если он не будет указан в Classpath, чего не должно случится в этом примере). Вы получите ClassNotFoundException. (Еще раз: все свидетельства иной жизни исчезнут прежде, чем доказательства ее существования могут быть проверены!)
Если вы хотите многое сделать после восстановления объекта, который был сериализован, вы должны убедится, что JVM может найти соответствующий .class либо по локальному пути классов, либо где-то в Internet.
Написание исходного кода IDL
Первый шаг состоит в написании IDL описания разрабатываемого сервиса. Это обычно делает программист сервера, который после этого свободен реализовывать сервер на любом языке, для которго существует CORBA IDL компилятор. IDL файл передается программисту клиентской стороны и становится мостом между языками.
Приведенный ниже пример показывает IDL описание для нашего сервера ExactTime:
//: c15:corba:ExactTime.idl
//# Вы должны установить idltojava.exe с
//# java.sun.com и отрегулировать установки для
//# использования вашего локального C препроцессора
//# чтобы откомпилировать этот. //# Смотрите докуметацию на java.sun.com.
module remotetime { interface ExactTime { string getTime(); }; }; ///:~
Это декларация интерфейса ExactTime внутри просранства имен remotetime. Интерфейс состоит из единственного метода, который возвращает текущее время в формате string.
Наследование и finalize( )
Когда Вы используете композицию для создания нового класса, Вы никогда не беспокоитесь о завершении объекта этого класса. Каждый участник - независимый объект и поэтому сборщик мусора не обращают внимание на то, что это член вашего класса. С наследованием Вы должны переопределять finalize( ) в дочерних классах, если у вас есть какие либо специальные очистки, которые должны произойти как часть сбора мусора. Когда Вы переопределяете finalize( ) в наследуемом классе, то важно не забыть вызвать finalize( ) базового класса, поскольку в противном случае финализация базового класса не произойдет. Следующий пример доказывает это утверждение:
//: c07:Frog.java
// Проверка завершения с наследованием.
class DoBaseFinalization { public static boolean flag = false; }
class Characteristic { String s; Characteristic(String c) { s = c; System.out.println( "Creating Characteristic " + s); } protected void finalize() { System.out.println( "finalizing Characteristic " + s); } }
class LivingCreature { Characteristic p = new Characteristic("is alive"); LivingCreature() { System.out.println("LivingCreature()"); } protected void finalize() throws Throwable { System.out.println( "LivingCreature finalize"); // Вызов версии базового класса!
if(DoBaseFinalization.flag) super.finalize(); } }
class Animal extends LivingCreature { Characteristic p = new Characteristic("has heart"); Animal() { System.out.println("Animal()"); } protected void finalize() throws Throwable { System.out.println("Animal finalize"); if(DoBaseFinalization.flag) super.finalize(); } }
class Amphibian extends Animal { Characteristic p = new Characteristic("can live in water"); Amphibian() { System.out.println("Amphibian()"); } protected void finalize() throws Throwable { System.out.println("Amphibian finalize"); if(DoBaseFinalization.flag) super.finalize(); } }
public class Frog extends Amphibian { Frog() { System.out.println("Frog()"); } protected void finalize() throws Throwable { System.out.println("Frog finalize"); if(DoBaseFinalization.flag) super.finalize(); } public static void main(String[] args) { if(args.length != 0 && args[0].equals("finalize")) DoBaseFinalization.flag = true; else
System.out.println("Not finalizing bases"); new Frog(); // Тотчас становится мусором
System.out.println("Bye!"); // Принудительный вызов завершения и очистки:
System.gc(); } } ///:~
Класс DoBaseFinalization
просто содержит флаг, который показывает для каждого класса в иерархии вызывать ли super.finalize( ). Этот флаг устанавливается как аргумент командной строки, так что Вы можете посмотреть поведение с и без вызовов завершения базового класса.
Каждый класс в иерархии так же содержит объект класса Characteristic. Вы увидите, что не обращая внимание на вызов завершителя базового класса объект Characteristic всегда завершается.
Каждое переопределение finalize( ) должно иметь доступ к protected членам класса, поскольку метод finalize( ) в классе Object является protected и компилятор не позволит вам уменьшить права доступа во время наследования. ("Friendly" менее "достижимы" чем protected.)
В Frog.main( ), флаг DoBaseFinalization настраивается и создается единственный объект Frog. Помните, что сборщик мусора и индивидуальное завершение, могут не произойти для отдельного объекта, поэтому, что бы вызвать их насильно вызывается System.gc( ) и оттуда уже завершение. Без завершения базовых классов вывод такой:
Not finalizing bases Creating Characteristic is alive LivingCreature() Creating Characteristic has heart Animal() Creating Characteristic can live in water Amphibian() Frog() Bye! Frog finalize finalizing Characteristic is alive finalizing Characteristic has heart finalizing Characteristic can live in water
Вы можете видеть, что не были вызваны завершители для базовых классов Frog (объекты класса были завершены, как Вы и ожидали). Но если Вы добавите аргумент "finalize" в командную строку, Вы получите:
Creating Characteristic is alive LivingCreature() Creating Characteristic has heart Animal() Creating Characteristic can live in water Amphibian() Frog() bye! Frog finalize Amphibian finalize Animal finalize LivingCreature finalize finalizing Characteristic is alive finalizing Characteristic has heart finalizing Characteristic can live in water
Поскольку элементы объектов завершены в том же порядке, в каком они были созданы, то технически порядок финализации объектов не определен. С базовыми же классами, Вы можете осуществлять контроль над порядком завершения. Следуя форме, которая используется в C++ для деструкторов, Вы должны осуществить завершение дочернего класса раньше, чем завершение базового класса. Потому что финализация дочернего класса может вызвать некоторые методы в базовом классе, которые требуют, что бы компоненты этого базового классы были все еще активны, поэтому Вы не должны уничтожать их принудительно.
Наследование от процесса
Простейшим путем для создания процесса является наследование от класса Thread, который имеет все необходимое для создания и запуска процесса. Наиболее важный метод для Thread это run(), который необходимо переопределить чтобы процесс выполнял то, что вам необходимо. Таким образом, run() есть код, который будет запущен "одновременно" с другими процессами в программе.
Следующий пример создает произвольное количество процессов, отслеживаемые по присвоенному каждому процессу уникальному номеру, сгенерированному static переменной. Метод run()от Thread переопределен и выполняет уменьшение счетчика при каждом проходе цикла и завершает выполнение когда счетчик равен нулю (в том месте когда run() возвращает значение процесс завершается).
//: c14:SimpleThread.java
// Very simple Threading example.
public class SimpleThread extends Thread { private int countDown = 5; private static int threadCount = 0; private int threadNumber = ++threadCount; public SimpleThread() { System.out.println("Making " + threadNumber); } public void run() { while(true) { System.out.println("Thread " + threadNumber + "(" + countDown + ")"); if(--countDown == 0) return; } } public static void main(String[] args) { for(int i = 0; i < 5; i++) new SimpleThread().start(); System.out.println("All Threads Started"); } } ///:~
Метод run( ) практически всегда содержит какой-либо тип цикла, выполняемый до тех пор, пока процесс не станет ненужным, поэтому необходимо предусмотреть условие выхода из цикла (или, как в примере выше, просто return из run()). Часто задача run() в выполнении бесконечного цикла, что означает, что кроме исключительных случаев возникновения какого-либо внешнего события завершающющего run(), цикл будет выполняться вечно.
В вызове main( ) содержится несколько созданных и запущенных процессов. Метод start()класса Thread выполняет специальную инициализацию для процесса и затем вызывает run(). Таким образом необходимые действия: вызов конструктора для создания объекта, затем start() конфигурирует процесс и вызов run(). Если не вызвать start() (что делается в конструкторе, если возможно) процесс никогда не будет запущен.
Результат работы программа для одного из запусков ( которые могут отличаться при каждом запуске) следующий:
Making 1 Making 2 Making 3 Making 4 Making 5 Thread 1(5) Thread 1(4) Thread 1(3) Thread 1(2) Thread 2(5) Thread 2(4) Thread 2(3) Thread 2(2) Thread 2(1) Thread 1(1) All Threads Started Thread 3(5) Thread 4(5) Thread 4(4) Thread 4(3) Thread 4(2) Thread 4(1) Thread 5(5) Thread 5(4) Thread 5(3) Thread 5(2) Thread 5(1) Thread 3(4) Thread 3(3) Thread 3(2) Thread 3(1)
Заметьте, что ни где в данном примере не вызывался sleep() и результат работы показывает, что каждый процесс получил часть процессорного времени для выполнения. Это демонстрирует, что sleep(), пока он полагается на существование процесса для того чтобы выполниться, не задействован ни в разрешение, ни в запрещение процесса. Это просто другой метод.
Можно также видеть, чтопроцессы выполняются не в том же порядке в каком они были запущены. Фактически, порядок, в котором CPU обрабатывает существующие процессы, не определен до тех пор, пока не определены приоритеты, используя setPriority() метод класса Thread.
Когда main() создает объекты Thread он не сохраняет ссылки на них. Обычные объекты могут быть просто игрушкой для сборщика мусора, но не Thread. Каждый Thread "регистрирует" себя сам, и таким образом ссылка на него храниться где-то в другом месте из-за чего сборщик мусора не может его очистить.
Наследование от внутренних классов
Поскольку конструктор внутреннего класса должен быть присоединен к ссылке на окружающий его класс, то наследование может выглядеть несколько запутано, если Вы захотите от него наследовать. Проблема заключается в том, что эта "секретная" ссылка на окружающий объект должна быть инициализирована, а в дочернем объекте нет того объекта, к которому можно было бы "привязаться". Решением здесь является использование синтаксиса явной ассоциации:
//: c08:InheritInner.java
// Наследование внутреннего класса.
class WithInner { class Inner {} }
public class InheritInner extends WithInner.Inner { //! InheritInner() {} // Не компилируется
InheritInner(WithInner wi) { wi.super(); } public static void main(String[] args) { WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); } } ///:~
Вы можете видеть, что InheritInner расширяется только внутренним классом, но не расширяется внешним. Но когда приходит время для создания конструктора, то конструктор по умолчанию не подходит, Вы не можете передать ссылку на окружающий объект. В дополнение Вы должны использовать такой синтаксис
enclosingClassReference.super();
внутри конструктора. При этом предоставляется ссылка и программа будет компилироваться.
Наследование: повторное использование интерфейса
Сама по себе идея заключается в том, что объект является удобным инструментом. Он позволяет вам оформлять данные и функциональность вместе, согласно концепции, так что вы можете представить подходящую идею проблемной области раньше, чем начать усиленно использовать идиому лежащей в основе машины. Эта концепция является фундаментом при программировании с использованием ключевого слова class.
Однако это выглядит довольно жалко, решать все проблемы путем создания класса, а затем усиленно создавать качественно новый класс, который может иметь сходную функциональность. Гораздо лучше, если мы можем взять существующий класс, размножить его и создать дополнения и модификации клона. Это результат, который вы получаете при наследовании, за исключением того, что если класс-оригинал (называемый базовый или супер или родительский класс) меняется, модифицируется и “клон” (называемый производный или наследуемый или sub или наследник класс), отражая эти изменения.
Стрелка на приведенной выше UML диаграмме указывает от класса-наследника на базовый класс. Как вы видите, может быть более одного наследуемого класса.)
Тип - это больше чем описание ограничений для набора объектов. Это также взаимные отношения с другими типами. Два типа могут иметь общие характеристики и черты поведения, но один тип может содержать больше характеристик, чем другой, а может обрабатывать больше сообщений (или обрабатывать их иначе). Наследование выражает эту схожесть между типами, используя концепцию базового типа и наследуемого типа. Базовый тип содержит все характеристики и черты поведения, которые есть у всех типов, наследуемых от этого. Вы создаете базовый тип для образования ядра вашей идеи для некоторых объектов в вашей системе. От базового типа вы образуете другие типы для создания разных способов, которыми данное ядро может быть реализовано.
Например, машина по переработке мусора сортирует кусочки мусора. Базовый тип - “мусор”, а каждый кусочек мусора имеет вес, объем и так далее, и может быть разрезан, расплавлен или растворен. Для этих более специфичных типов мусора наследуются типы, которые могут иметь дополнительные характеристики (бутылки имеют цвет) или черты поведения (алюминий может быть раздавлен, а сталь может магнитится). Вдобавок, некоторые черты поведения могут отличаться (объем бумаги зависит от типа и состояния). Используя наследование, вы можете создать иерархические типы, которые выражают проблему, которую вы пробуете решить в терминах своих типов.
Второй пример - это классический пример с “формой”, возможно, используемый компьютерной системе разработки или в игровых симуляторах. Базовый тип - “форма”, а каждая форма имеет размер, цвет, положение и так далее. Каждая форма может быть нарисована, стерта, перемещена, окрашена и т.д. От нее образуются (наследуются) специфические типы: окружность, квадрат, треугольник и так далее, каждый из которых может иметь дополнительные характеристики и черты поведения. Определенные формы могут быть зеркально отражены, например. Некоторые черты поведения могут отличаться, так если вы хотите посчитать площадь формы. Формы иерархически объединяет и похожие и различные черты форм.
Поиск решения в терминах проблемы чрезвычайно полезно, так как у вас нет необходимости массы промежуточных моделей, чтобы перейти от описания проблемы к описанию решения. У объектов в первичной модели типы иерархические, так что вы прямо переходите от описания системы реального мира к описанию системы в кодах. Несомненно, одно из затруднений людей, работающих с объектно-ориентированной разработкой, в том, что слишком просто пройти от начала до конца. Разум, натренированный на поиск сложных решений часто сначала стопорится на такой простоте.
Когда вы наследуете от существующего типа, вы создаете новый тип. Этот новый тип содержит не все члены существующего типа (private члены спрятаны и недоступны), более важно, это дублирование интерфейсов базового класса. То есть, все сообщения, которые вы можете послать объекту базового класса, вы можете послать их объекту наследуемого класса. Так как мы знаем тип класса, которому мы посылаем сообщения, это означает, что наследуемый класс того же типа, что и базовый класс. В предыдущем примере “окружность - это форма”. Такая эквивалентность типов через наследование - это один из основных шлюзов для понимания значения объектно-ориентированного программирования.
Так как оба класса: базовый и наследованный имеют одинаковый интерфейс, должна быть реализация для работы с этим интерфейсом. Таким образом, должен быть определенный код, который выполняется, когда объект принимает определенное сообщение. Если вы просто наследуете класс и ничего больше не делаете, методы интерфейса базового класса переходят без изменения в наследованный класс. Это означает, что объект наследованного класса имеет не только тот же тип, он имеет такие же черты поведения, что обычно не интересно.
Вы имеете два способа сделать ваш новый класс отличным от оригинального базового класса. Первый достаточно прямой: вы просто добавляете несколько новый функций к наследуемому классу. Это означает, что базовый класс просто не делает столько, сколько вам нужно, так что вы добавляете больше функций. Это простое и примитивное использование наследования, одновременно является законченным решением вашей проблемы. Однако вы должны взглянуть более пристально на возможно, что вашему базовому классу может не хватать этих дополнительных функций. Этот процесс обнаружения и повторения при разработке встречается регулярно в объектно-ориентированном программировании.
Хотя наследование иногда может подразумевать (особенно в Java, где ключевое слово, означающее наследование, это extends), что вы хотите добавить новые функции к интерфейсу, это не всегда так. Второй, наиболее важный способ сделать ваш класс отличным, заключается в изменении поведения существующей функции базового класса. Это называется перегрузкой.
Для перегрузки функции вы просто создаете новое определение для функции в наследуемом классе. Вы говорите, “Я использую ту же функцию интерфейса, но я хочу делать что-то другое для моего нового типа.
Не статическая инициализация экземпляра
Java обеспечивает аналогичный синтаксис для не static переменных для каждого объекта. Вот пример:
//: c04:Mugs.java
// Java "Инициализация экземпляра".
class Mug { Mug(int marker) { System.out.println("Mug(" + marker + ")"); } void f(int marker) { System.out.println("f(" + marker + ")"); } }
public class Mugs { Mug c1; Mug c2; { c1 = new Mug(1); c2 = new Mug(2); System.out.println("c1 & c2 initialized"); } Mugs() { System.out.println("Mugs()"); } public static void main(String[] args) { System.out.println("Inside main()"); Mugs x = new Mugs(); } } ///:~
Вы можете видеть, что предложение инициализации экземпляра:
{ c1 = new Mug(1); c2 = new Mug(2); System.out.println("c1 & c2 initialized"); }
выглядит точно так же, как и предложение статической инициализации, за исключением отсутствия ключевого слова static. Этот синтаксис необходим для поддержки инициализации анонимного внутреннего класса (смотрите Главу 8).
Недостаток неизменных классов
Создание неизменных классов на первый взгляд является элегантным решением. Однако, всякий раз, когда вам понадобится модифицировать новый объект этого типа, вы должны терпеть неудобства, связанные с необходимостью создания нового объекта, а также более частым "сбором мусора". Для каких-то объектов это не составит труда, но для некоторых (таких, как класс String) сопряжено с множеством проблем.
В таком случае хорошим выходом будет создание класса-компаньона, который может изменяться. Тогда, если вам требуется произвести множество изменений, вы можете переключаться на использование редактируемого класса-компаньона, а после завершения всех модификаций вновь работать с неизменным классом.
Пример:
//: Приложение А:Immutable2.java
// Класс-компаньон для внесения изменений
// в неизменный класс.
class Mutable { private int data; public Mutable(int initVal) { data = initVal; } public Mutable add(int x) { data += x; return this; } public Mutable multiply(int x) { data *= x; return this; } public Immutable2 makeImmutable2() { return new Immutable2(data); } }
public class Immutable2 { private int data; public Immutable2(int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable2 add(int x) { return new Immutable2(data + x); } public Immutable2 multiply(int x) { return new Immutable2(data * x); } public Mutable makeMutable() { return new Mutable(data); } public static Immutable2 modify1(Immutable2 y){ Immutable2 val = y.add(12); val = val.multiply(3); val = val.add(11); val = val.multiply(2); return val; } // Это приводит к тому же результату:
public static Immutable2 modify2(Immutable2 y){ Mutable m = y.makeMutable(); m.add(12).multiply(3).add(11).multiply(2); return m.makeImmutable2(); } public static void main(String[] args) { Immutable2 i2 = new Immutable2(47); Immutable2 r1 = modify1(i2); Immutable2 r2 = modify2(i2); System.out.println("i2 = " + i2.read()); System.out.println("r1 = " + r1.read()); System.out.println("r2 = " + r2.read()); } } ///:~
Immutable2 содержит методы, которые, как и ранее, защищали неизменность объекта за счет создания новых объектов в тех случаях, когда требуется его модификация. Эти операции осуществляются методами add() и multiply(). Класс-компаньон Mutable также имеет методы add() и multiply(), но они уже служат не для создания нового объекта, а для его изменения. Кроме того, в классе Mutable есть метод для создания Immutable2 объекта с использованием данных и наоборот.
Два статических метода modify1() и modify2() демонстрируют два различных метода решения одной и той же задачи. В методе modify1() все действия выполняются внутри класса Immutable2 и в процессе работы создаются четыре новых Immutable2 объекта. (и каждый раз при переопределении val предыдущий объект становится мусором).
В методе modify2() первой операцией является Immutable2 y и создание Mutable объекта. (Это напоминает вызов метода clone(), рассмотренный нами ранее, но в то же время при этом создается объект нового типа). Затем объект Mutable используется для многочисленных операций не требующих создания новых объектов. В конце результаты передаются в Immutable2. Итак, вместо четырех новых объектов создаются только два (Mutable и результат Immutable2).
Такой прием имеет смысл использовать в случаях, когда:
Вам нужно использовать неизменные объекты и
Вам необходимо их часто изменять или
Слишком накладно создавать новые неизменные объекты.
Неизмененные классы
Некоторые классы остались неизменными при переходе от Java 1.0 к Java 1.1:
DataOutputStream |
File |
RandomAccessFile |
SequenceInputStream |
DataOutputStream, в основном, используется без изменений, так что для хранения и получения данных в транспортабельном формате, используй те иерархии InputStream и OutputStream.
Неявные объекты
Сервлеты включают классы, которые обеспечивают соответствующие утилиты, такие как HttpServletRequest, HttpServletResponse, Session и т.д. Объекты этих классов встроены в JSP спецификацию и автоматически доступны для использования в вашем JSP коде без написания дополнительных строчек кода Неявные объекты JSP сведены в приведенную ниже таблицу.
Неявная переменная | Тип (javax.servlet) | Описание | Границы | ||||
request | Зависящий от протокола тип, производный от HttpServletRequest | Запрос, который вызывает обращение к службе. | запрос | ||||
response | Зависящий от протокола тип, производный от HttpServletResponse | Ответ на запрос. | страница | ||||
pageContext | jsp.PageContext | Содержимое страницы включает зависящие от реализации особенности и обеспечивает удобные методы и доступ к пространству имен для JSP. | страница | ||||
session | Зависящий от протокола тип, производный от http.HttpSession | Объект сессии, созданный для клиентского запроса. Смотрите объект Session для сервлетов. | сессия | ||||
application | ServletContext | Контекст сервлета получается из конфигурирующего сервлет объекта (e.g., getServletConfig(), getContext( ). | приложение | ||||
out | jsp.JspWriter | Объект, который пишет в выходной поток. | страница | ||||
config | ServletConfig | ServletConfig для этого JSP. | страница | ||||
page | java.lang.Object | Экземпляр класса страницы, обрабатывающей этот запрос. | страница |
Границы видимости каждого объекта могут значительно отличаться. Например, объект session имеет границы видимости, которые превышают страницу, так как он может отвечать за несколько клиентских запросов и страниц. Объект application может предоставить сервис для группы JSP страниц, которые совместно представляют Web приложение.
Некоторые службы CORBA
Это короткое описание того, что делает JavaIDL код (в основном игнорируется часть CORBA кода, зависящая от производителя). Первая строка main( ) запускает ORB, это необходимо потому, что нашему серверу необходимо взаимодействовать с ним. Сразу после инициализации ORB создается серверный объект. На самом деле правильнее называть временный обслуживающий объект (transient servant object): объект, который принимает запросы от клиентов, и чье время жизни равно совпадает с временем жизни породившего процесса. Как только временный бслуживающий объект создан, он регистрируется с помошь ORB, что означает, что ORB знает о его наличи и теперь может перенаправлять к нему запросы.
До этого момента все, что мы имели - это timeServerObjRef - указатель на объект, который известен только внутри текущего серверного процесса. Следующий шаг состоит в присвоении строкового имени эому обслуживающему объекту. Клиент будет использовать имя для нахождения обслуживающего объекта. Мы совершили эту операцию, используя Сервис Указания Имен. Во-первых, нам необходима ссылка на Службу Указания Имен. Метод resolve_initial_references( ) принимает значимую ссылку на объект Службы Указания Имен, в случае JavaIDL “NameService”, и возвращает ссылку на объект. Он приводит сылку к специфичному типу NamingContext, используя метод narrow( ). Теперь мы можем использовать службу указания имен.
Для связывания обслуживающих объектов со ссылками на строковые объекты, мы сначала создаем объект NameComponent, инициализирует его значением “ExactTime” - строка имени, которую мы хотим связать с обслуживающим объектом. Затем, используя метод rebind( ), строковая ссылка связывается со ссылкой на объект. Мы испоьзуем rebind( ) для присвоения ссылки, даже если она уже существует, тогда как bind( ) выбрасывает исключение, если ссылка уже существует. Имя состоит в CORBA из последовательности NameContexts — поэтому мы используем массив для связывания имени со ссылкой на объект.
Наконец, обслуживающий объект готов к использованию клиентами. После этого серверный процесс входит в состояние ожидания. Опять таки, из-за того, что это временное обслуживание, поэтому время жизни ограничено серверным процессом. JavaIDL в настоящее время не поддерживает постояные объекты — объекты, которые продолжают существовать вне породившего его процесса.
Теперь, когда мы имеем представление о том, что делает серверный код, давайте взглянем на код клиента:
//: c15:corba:RemoteTimeClient.java
import remotetime.*; import org.omg.CosNaming.*; import org.omg.CORBA.*;
public class RemoteTimeClient { // Выбрасываем исключение на консоль:
public static void main(String[] args) throws Exception { // Создание и инициализация ORB:
ORB orb = ORB.init(args, null); // Получение контекста наименования:
org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Получение (разрешение) ссылки на строковый
// объект для сервера времени:
NameComponent nc = new NameComponent("ExactTime", ""); NameComponent[] path = { nc }; ExactTime timeObjRef = ExactTimeHelper.narrow( ncRef.resolve(path)); // Выполнение запроса к серверу:
String exactTime = timeObjRef.getTime(); System.out.println(exactTime); } } ///:~
Первые несколько строк делают то же, что они делали в серверном процессе: инициализируют ORB и разрешают указатель на сервис указания имен. Далее, нам нужна ссылка на объект для обслуживающего объекта, поэтому мы передаем ссылку на строковый объект в метод resolve( ), и приводим результат к ссылке на интерфейс ExactTime, используя метод narrow( ). В конце мы вызываем getTime( ).
Необходимость RTTI
Рассмотрите пример иерархии классов, которые используют полиморфизм. Общий тип - это базовый класс Shape, и классы - наследники - Circle, Square и Triangle:
Это типичная диаграмма иерархии классов, с базовым классом на вершине и базовыми классами, растущими вниз. Стандартная задача в объектно-ориентированном программировании это манипуляции ссылкой на базовый тип (в нашем случае Shape) в больших объемах кода, так, что если Вы решите расширить программу за счет добавления нового класса (например, Rhomboid, наследуемый от Shape), больших изменений в коде не потребуется. В этом примере, методом динамического связывания в интерфейсе Shape является draw(), так что цель клиентского программиста - вызывать метод draw( ) по ссылке на базовый класс Shape. Метод draw( ) перекрывается во всех наследуемых классах, и т.к. это динамически связанный метод, в результате все будет работать правильно, даже несмотря на то, что метод вызывается через ссылку на базовый класс Shape. И это - полиморфизм.
Итак Вы, в большинстве случаев, создаете объект (Circle, Square или Triangle), приводите его к базовому типу Shape (забывая об особенностях этого объекта), и используете эту анонимную ссылку на Shape в остальной части программы.
Вот краткий пример полиморфизма и приведения к базовому типу, показывающий то, что было описано выше:
//: c12:Shapes.java import java.util.*;
class Shape { void draw() { System.out.println(this + ".draw()"); } }
class Circle extends Shape { public String toString() { return "Circle"; } }
class Square extends Shape { public String toString() { return "Square"; } }
class Triangle extends Shape { public String toString() { return "Triangle"; } }
public class Shapes { public static void main(String[] args) { ArrayList s = new ArrayList(); s.add(new Circle()); s.add(new Square()); s.add(new Triangle()); Iterator e = s.iterator(); while(e.hasNext()) ((Shape)e.next()).draw(); } } ///:~
Базовый класс содержит метод draw( ), который неявно использует toString( ) для печати идентификатора класса подстановкой параметра this в функцию System.out.println( ). Если эта функция встречает объект, она автоматически вызывает метод toString( ), чтобы создать строковое представление объекта.
Каждый из наследуемых классов перекрывает метод toString( ) (из объекта Object) так, что draw( ) в любом случае печатает разные данные. В методе main( ), различные типы Shape создаются и добавляются в ArrayList. Именно в этом месте происходит приведение к базовому типу потому, что ArrayList хранит только объекты типа Object. Так как все в Java (за исключением примитивов) является типом Object, ArrayList может хранить также объекты типа Shape. Однако, при привведении к базовому типу Object, теряется специальная информация и то, что они имеют тип Shape. В ArrayList, они имеют тип Object.
В том месте, где Вы достаете элемент из ArrayList с помощью next( ), появляется небольшое оживление. Так как ArrayList хранит только тип Object, next( ) возвращает ссылку на тип Object. Но мы знаем, что, на самом деле, это ссылка на объект типа Shape, и хотим вызвать метод объекта Shape. Итак, нам необходимо приведение к типу Shape. Мы делаем это, используя стандартный метод приведения к типу: “(Shape)”. Это - основная, базовая форма RTTI. Кроме того, в Java все приведения проверяются во время выполнения на корректность. Это в действительности и есть RTTI: идентификация типа объекта во время выполнения.
В этом случае, приведение RTTI является только частичным: тип Object приводится к типу Shape, но не приводится к Circle, Square или Triangle. Это происходит потому, что единственная вещь, которую мы хотим знать, это то, что ArrayList заполнен объектами типа Shape. Во время компиляции это реализуется по Вашему усмотрению, во время выполнения это обеспечивает механизм приведения типа.
Итак полиморфизм работает и нужный метод, вызываемый из Shape определяется в зависимости от того, является ли он ссылкой на Circle, Square или Triangle. И вообще это так и должно быть; Вы хотите, чтобы основной Ваш код знал как можно меньше об особенностях объекта, и просто общался с основными представлениями группы объектов (в нашем случае Shape). В результате Ваш код будет проще читаться, писаться, исправляться, а Ваши намерения и планы будут проще в реализации, понимании и изменении. Итак, полиморфизм - основная задача объектно-ориентированного программирования.
Но если у Вас есть специальная задача, которая существенно упрощается, если Вы знаете точный тип базовой ссылки на объект? Например, представьте, что Вы хотите дать пользователям возможность подсвечивать все формы (Shape), определенного типа перекрашивая их в пурпурный цвет. В этом случае, они смогут найти все треугольники на экране, подсвечивая их. Это выполняет RTTI: Вы можете спросить у ссылки на Shape точный тип объекта, на который она ссылается.
Неподдерживаемые операции
Есть возможность включить массив в List с помощью метода Arrays.asList( ):
//: c09:Unsupported.java
// Иногда метод, определенный в
// интерфейсе Collection не работает!
import java.util.*;
public class Unsupported { private static String[] s = { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", }; static List a = Arrays.asList(s); static List a2 = a.subList(3, 6); public static void main(String[] args) { System.out.println(a); System.out.println(a2); System.out.println( "a.contains(" + s[0] + ") = " + a.contains(s[0])); System.out.println( "a.containsAll(a2) = " + a.containsAll(a2)); System.out.println("a.isEmpty() = " + a.isEmpty()); System.out.println( "a.indexOf(" + s[5] + ") = " + a.indexOf(s[5])); // Проход в обратном порядке:
ListIterator lit = a.listIterator(a.size()); while(lit.hasPrevious()) System.out.print(lit.previous() + " "); System.out.println(); // Установка другого значения элемента:
for(int i = 0; i < a.size(); i++) a.set(i, "47"); System.out.println(a); // Компилируется, но не работает:
lit.add("X"); // Неподдерживаемая операция
a.clear(); // Не поддерживается
a.add("eleven"); // Не поддерживается
a.addAll(a2); // Не поддерживается
a.retainAll(a2); // Не поддерживается
a.remove(s[0]); // Не поддерживается
a.removeAll(a2); // Не поддерживается
} } ///:~
Вы обнаружите, что реализована только часть интерфейсов Collection и List. Оставшиеся методы являются причиной нежелательного появления того, что называется UnsupportedOperationException. Вы выучите все об исключениях в следующей главе, но если сказать коротко, то Collection interface — как и многие интерфейсы в библиотеки контейнеров Java — содержит “необязательные” методы, которые могут поддерживаться, но могут и не поддерживаться классом контейнера, который реализует такой интерфейс. Вызов неподдерживаемого метода является причиной UnsupportedOperationException, указывающий ошибку программы.
“Что?!?” - скажете вы, удивленно. - “Все обещанные методы интерфейсов и базовых классов, делают что- либо полезное! Это нарушает обещание — это говорит о том, что вызов некоторых методов не только не обеспечит значимое поведение, но и остановит программу! Безопасный текст просто выбросит из окна!”
Это не совсем плохо. При использовании Collection, List, Set или Map, компилятор все еще ограничивает вас в вызове методов только этого интерфейса, так как это не как в Smalltalk (в котором вы можете вызвать метод для любого объекта и выйти за пределы только если вы запускаете программу, в которой ваш вызов ничего не значит). Кроме того, большинство методов, принимающий Collection в качестве аргумента только читают из Collection — все методы “чтения” для Collection не являются не обязательными.
Этот подход предотвращает крушение интерфейсов при разработке. Другие дизайны для библиотеки контейнеров всегда заканчивают запутывающим числом интерфейсов, описывающих каждый вариант главной темы, и становятся сложными в изучении. Даже невозможно собрать все возможные особые случаи для интерфейсов, потому что кто-то может всегда инвертировать новый интерфейс. Подход “неподдерживаемого действия” позволяет достигнуть важной цели библиотеки контейнеров Java: контейнеры просты в изучении; не поддерживаемые операции - особый случай, который может быть выучен позднее. Однако, для этого подхода работает:
UnsupportedOperationException должно быть редким событием. То есть, для большинства классов все операции будут работать, только в редких случаях операции будут не поддерживаемыми. Это так для библиотеки контейнеров Java, так как 99% используемых вами классов — это ArrayList, LinkedList, HashSet и HashMap и их конкретные реализации — поддерживают все операции. Дизайн не поддерживает “черный ход”, если вы захотите создать новый Collection, без обеспечения значимых определений для всех методов Collection interface, и после этого добавите его к существующей библиотеке.
Когда операция является не поддерживаемой, должна быть определенная вероятность появления UnsupportedOperationException во время реализации, даже после того, как вы продадите продукт покупателю. Кроме всего прочего, оно указывает на ошибку программы: вы используете реализацию неправильно. Такое случается достаточно редко в тех местах где экстремальная природа вступает в силу. Только через какое-то время мы выясним, как хорошо это работает.
В приведенном выше примере Arrays.asList( ) производит List, который основывается на массиве фиксированного размера. Поэтому, тут имеется в виду, что поддерживаются операции, которые не изменяют размер массива. С другой стороны, если новый интерфейс будет требовать выражения поведения другого рода (возможно, называемого “FixedSizeList”), это откроет дорогу сложности и вскоре вы не будете знать откуда начинать использовать библиотеку.
Документация для метода, получающего Collection, List, Set или Map в качестве аргумента, должна указывать какие дополнительные методы должны быть реализованы. Например, сортировка требует методов set( ) и Iterator.set( ), но не требует add( ) и remove( ). .
Неправильный доступ к ресурсам
Рассмотрим изменение значения счетчиков, использованных в данной главе. В следующем примере каждый процесс имеет два счетчика, которые увеличивают свои значения и отображаются внутри вызова run(). Дополнительно существует другой процесс класса Watcher, который отслеживает равенство значений показаний счетчиков. Это выглядит как необязательное дополнение, поскольку посмотрев на исходный код можно предположить, что значения счетчиков всегда будут одинаковые. Однако нас ждут сюрпризы. Ниже приведена первая версия программы:
//: c14:Sharing1.java
// Problems with resource sharing while threading.
// <applet code=Sharing1 width=350 height=500>
// <param name=size value="12">
// <param name=watchers value="15">
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
public class Sharing1 extends JApplet { private static int accessCount = 0; private static JTextField aCount = new JTextField("0", 7); public static void incrementAccess() { accessCount++; aCount.setText(Integer.toString(accessCount)); } private JButton start = new JButton("Start"), watcher = new JButton("Watch"); private boolean isApplet = true; private int numCounters = 12; private int numWatchers = 15; private TwoCounter[] s; class TwoCounter extends Thread { private boolean started = false; private JTextField t1 = new JTextField(5), t2 = new JTextField(5); private JLabel l = new JLabel("count1 == count2"); private int count1 = 0, count2 = 0; // Add the display components as a panel:
public TwoCounter() { JPanel p = new JPanel(); p.add(t1); p.add(t2); p.add(l); getContentPane().add(p); } public void start() { if(!started) { started = true; super.start(); } } public void run() { while (true) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public void synchTest() { Sharing1.incrementAccess(); if(count1 != count2) l.setText("Unsynched"); } } class Watcher extends Thread { public Watcher() { start(); } public void run() { while(true) { for(int i = 0; i < s.length; i++) s[i].synchTest(); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < s.length; i++) s[i].start(); } } class WatcherL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < numWatchers; i++) new Watcher(); } } public void init() { if(isApplet) { String counters = getParameter("size"); if(counters != null) numCounters = Integer.parseInt(counters); String watchers = getParameter("watchers"); if(watchers != null) numWatchers = Integer.parseInt(watchers); } s = new TwoCounter[numCounters]; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new TwoCounter(); JPanel p = new JPanel(); start.addActionListener(new StartL()); p.add(start); watcher.addActionListener(new WatcherL()); p.add(watcher); p.add(new JLabel("Access Count")); p.add(aCount); cp.add(p); } public static void main(String[] args) { Sharing1 applet = new Sharing1(); // This isn't an applet, so set the flag and
// produce the parameter values from args:
applet.isApplet = false; applet.numCounters = (args.length == 0 ? 12 : Integer.parseInt(args[0])); applet.numWatchers = (args.length < 2 ? 15 : Integer.parseInt(args[1])); Console.run(applet, 350, applet.numCounters * 50); } } ///:~
Как и прежде, каждый счетчик содержит свой собственный компонент для отображения значения: два текстовых поля и надпись, первоначально показывающую что счетчики равны. Эти компоненты добавляются на панель родительского объекта в конструкторе TwoCounter. Так как два процесса начинают выполнение после нажатия пользователем кнопки, можно сделать так, чтобы start() мог быть вызван более одного раза. Так как Thread.start( ) не может быть вызван более одного раза для процесса (иначе генерируется исключение), то в приведенном алгоритме переопределен метод start() и используется флаг started.
В вызове run(), функции count1 и count2 увеличивают и отображают значение, так, что все кажется идентично. Затем вызываетсяsleep( ); без этого вызова программа "повиснет" поскольку CPU будет трудно переключаться между процессами.
Метод synchTest( ) выполняет очевидные функции по сравнению на равенство значения счетчиков count1 и count2; если они не равны то он установит значение надписи на панели в "Unsynched". Но в начале, он вызывает статический член класса Sharing1, который увеличит и отобразит значение счетчика доступа, чтобы показать сколько раз проверка закончилась успешно. (Причина использования данного счетчика будет понятна из следующих примеров.)
Класс Watcher является процессом, работа которого заключается в вызове synchTest() для всех активных объектов TwoCounter. Он выполняет это используя массив, хранящий объекты Sharing1. Можете считать, что Watcher постоянно читает объекты из TwoCounter.
Sharing1 содержит массив объектов TwoCounter инициализируемый при init() и запускаемый как процесс когда нажимается кнопка "start". Позже, когда будет нажата кнопка "Watch", создаются два или более наблюдателя и уничтожают ничего неподозревающие процессы TwoCounter.
Запомните, чтобы запустить данный пример как апплет в броузере, в вызове апплета должны быть следующий строки:
<param name=size value="20">
<param name=watchers value="1">
Можете экспериментировать изменяя значение высоты и ширины и прочие параметры. Изменяя size и watchers вы изменяете поведение программы. Данная программа настроена на выполнение как одиночное приложение с передачей всех параметров через командную строку (или с использованием значений по умолчанию).
А вот и наиболее интересная часть. В вызове TwoCounter.run(), бесконечный цикле просто повторяет следующие строки:
t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++));
(так же как и sleep, но здесь это не важно). Однако, когда программа будет запущена, вы увидите, что значения count1 и count2 будут временами различны (что покажет Watcher)! Это связано с особенностями процесса, он может быть временно приостановлен в любое время. Таким образом в то время, когда приостановка произошла при выполнение двух приведенных выше строк, а процесс Watcher произвел сравнение как раз в это время, то как раз два счетчика и будут различны.
Данный пример показывает фундаментальную проблему использования процессов. Никогда неизвестно когда процесс должен быть запущен. Представьте, что вы сидите за столом, держите вилку и уже готовы взять последний кусочек колбасы со своей тарелке, но как только вилка касается колбасы, она (колбаса конечно) внезапно исчезает (поскольку ваш процесс был приостановлен, то пришел другой процесс и украл всю колбасу). Вот эту проблему вам и надо решить.
Иногда можно не беспокоиться о том, что какие-либо ресурс могут быть использованы в тот момент, когда вы пытаетесь получить к нему доступ. Но в случае множества нитей процессов необходим способ для исключения возможности использования ресурса двумя процессами хотя бы в критические периоды.
Предотвращение подобных коллизий решается просто установкой блокировки на ресурс в момент использования. Первый процесс, который получил доступ к ресурсу блокирует его, после чего другие процессы не могут получить доступ к тому же ресурсу до тех пор, пока он не будет разблокирован. В это момент другой процесс может его заблокировать и использовать. Например, если переднее сиденье в машине представить как ограниченный ресурс, то ребенок с криком "Dibs" может занять это место.
Неудобство контейнеров: неизвестный тип
“Неудобство” при использовании контейнеров Java в том, что вы теряете информацию о типе, когда помещаете объект в контейнер. Это случается потому, что программист такого контейнерного класса не имел идей о том, как указать тип того, что вы хотите поместить в контейнер, а создание контейнеров для хранения только вашего типа не допустит создания инструментов общего назначения. Так что вместо этого контейнер содержит ссылки на Object, который является корнем всех классов, так что он содержит любой тип. (Конечно, сюда не включаются примитивные типы, так как они не наследуются ни от чего.) Это лучшее решение, за исключением:
Так как информация о типе отбрасывается, когда вы помещаете ссылку на объект в контейнер, нет ограничений на тип объекта, который может быть помещен в контейнер, даже если вы предназначаете его только для хранения, скажем, котов. Кто-либо может так же легко поместить в контейнер собаку.
Так как информация о типе теряется, есть только одна вещь, которую контейнер знает, он хранит ссылки на объекты. Вы должны выполнить приведение к правильному типу перед использованием объекта.
С другой стороны, Java не позволит вам неправильно использовать объект, который вы поместили в контейнер. Если вы поместили собаку в контейнер кошек, а затем пробуете трактовать все что есть в контейнере как кошек, вы получите исключение во время выполнения, когда вытянете ссылку на собаку из контейнера кошек и попробуете ее привести к кошке.
Вот пример использование основной рабочей лошадки контейнеров -ArrayList. Для начала вы можете думать об ArrayList, как о “массиве, который автоматически растягивает себя”. Использование ArrayList достаточно простое: создание, помещение в него объектов с помощью add( ), и, позже, получение их с помощью get( ) и индекса, так же как будто вы имеете дело с массивом, но без квадратных скобок [49]. ArrayList также имеет метод size( ), который позволяет вам узнать сколько элементов было добавлено, так что вы по невнимательности не выйдете за пределы и не получите исключение.
Сначала создаются классы Cat и Dog:
//: c09:Cat.java
public class Cat { private int catNumber; Cat(int i) { catNumber = i; } void print() { System.out.println("Cat #" + catNumber); } } ///:~
//: c09:Dog.java
public class Dog { private int dogNumber; Dog(int i) { dogNumber = i; } void print() { System.out.println("Dog #" + dogNumber); } } ///:~
Кошки (Cat) и собаки (Dog) помещаются в контейнер, а затем вытягиваются оттуда:
//: c09:CatsAndDogs.java
// Пример простого контейнера.
import java.util.*;
public class CatsAndDogs { public static void main(String[] args) { ArrayList cats = new ArrayList(); for(int i = 0; i < 7; i++) cats.add(new Cat(i)); // Не проблема добавить к кошкам собаку:
cats.add(new Dog(7)); for(int i = 0; i < cats.size(); i++) ((Cat)cats.get(i)).print(); // Собака обнаружится только во время выполнения
} } ///:~
Классы Cat и Dog отличаются — они не имеют ничего общего, за исключением того, что они оба - Object. (Если вы не можете точно сказать от чего унаследован класс, вы автоматически наследуете от Object.) Так как ArrayList содержит Object, вы можете поместить в контейнер не только объекты Cat с помощью метода add( ) контейнера ArrayList, но вы также можете добавить объекты Dog и при этом не получите ошибок времени компиляции, либо времени выполнения. Когда вы достаете то, о чем выдумаете как об объекте Cat с помощью метода get( ) контейнера ArrayList, вы получаете назад ссылку на объект, который вы должны привести к Cat. Затем вам необходимо взять в круглые скобки все выражение, чтобы навязать вычисление приведения до вызова метода print( ) для Cat, в противном случае вы получите синтаксическую ошибку. Затем, во время выполнения, когда вы попробуете привести объект Dog к типу Cat, вы получите исключение.
Это больше, чем просто неприятность. Это то, что может создать трудные в обнаружении ошибки. Если одна часть (или несколько частей) программы вставляют объекты в контейнер, и вы обнаруживаете только в отдельной части программы исключение, говорящее о том, что в контейнер был помещен неправильный объект, то вы должны найти, где производится неправильное вставление. С другой стороны, программирование удобно начать с каких-то стандартных контейнерных классов, несмотря на недостатки и неуклюжесть.
Неумышленная рекурсия
Так как (как и со всеми классами) стандартные контейнеры Java наследованы от Object, они содержат метод toString( ). Он был перегружен, так как он может производить String представление самого себя, включая хранимые им объекты. Внутри ArrayList, например, toString( ) проходит по элементам ArrayList и вызывает toString( ) для каждого. Предположим, вы хотите напечатать адреса ваших классов. Кажется, что имеет смысл просто обратится к this (обычно С++ программисты склонны к этому подходу):
//: c09:InfiniteRecursion.java
// Неумышленная рекурсия.
import java.util.*;
public class InfiniteRecursion { public String toString() { return " InfiniteRecursion address: " + this + "\n"; } public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new InfiniteRecursion()); System.out.println(v); } } ///:~
Если вы просто создадите объект InfiniteRecursion, а затем напечатаете его, вы получите бесконечную последовательность исключений. Это также верно, если вы поместите объект InfiniteRecursion в ArrayList и напечатаете этот ArrayList, как показано здесь. Что случилось - это автоматическое преобразование к String. Когда вы говорите:
"InfiniteRecursion address: " + this
Компилятор смотрит на String, следующий за ‘+’, а тут что-то не типа String, так что он пробует перевести this в String. Он выполняет это преобразование с помощью вызова toString( ), которое производит рекурсивный вызов.
Если вы действительно хотите напечатать адрес объекта в этом случае, решением может стать вызов метода Object toString( ), который делает это. Так что вместо того, чтобы говорить this, вы должны сказать super.toString( ). (Это работает только если вы прямо наследуете от Object, или если ни один из родительских классов не перегрузил метод toString( ).)
Нисхождение против шаблонов/настроек
Чтобы сделать эти контейнеры пригодными для повторного использования, они хранят один универсальный тип для Java, который упоминался ранее: Object. Иерархия с единым корнем означает, что все является Object, так что контейнер, который хранит Object, может хранить все. Это легко создает для контейнеров возможность повторного использования.
Для использования таких контейнеров вы просто добавляете в него ссылку на объект, а позже просите ее назад. Но так как контейнер хранит только Object, когда вы добавляете в контейнер новую ссылку объекта, выполняется обратное преобразование к Object, здесь происходит потеря уникальности. Когда вы запрашиваете объект назад, вы получаете ссылку на Object, а не ссылку на тип, который вы положили в него. Как вы можете вернуть его назад к какому-то полезному интерфейсу объекта, который вы положили в контейнер?
Здесь снова используется преобразование, но здесь вы выполняете не обратное иерархическое преобразование к более общему классу, вы движетесь вниз по иерархии к более специфичному типу. Такое преобразование называется прямое преобразование. С обратным преобразованием, вы знаете, например, что Окружность
- это тип Форма, так что такое преобразование безопасно, но вы не знаете, что Object - это обязательно Окружность или Форма, так что едва ли безопасно прямое преобразование, если вы не знаете с чем имеете дело.
Однако, это еще не все опасности, поскольку если вы выполняете прямое преобразование к неправильному типу, вы получаете ошибку времени выполнения, называемую исключение, которая скоро будет описана. Когда вы получаете ссылку на объект из контейнера, вы должны иметь способ для запоминания, что точно вы должны выполнить при прямом преобразовании.
Прямое преобразование и проверка во время выполнения требует дополнительного времени для выполнения программы и дополнительных усилий от программиста. Можно ли создать какой-либо способ создания контейнера так, чтобы знать тип того, что хранится, снижая необходимость прямого преобразования и возможность ошибки? Решение - это параметризованные типы, которые для классов компилятор может автоматически настраивать для работы со специфическими типами. Например, при программировании контейнера компилятор может настроить этот контейнер так, чтобы он принимал только Формы и возвращал только Формы.
Параметризированные типы - это важная часть C++, отчасти потому, что C++ не имеет иерархии с единым корнем. В C++ ключевое слово, реализующее параметризированные типы - это “шаблон”. Java в настоящее время не имеет параметризированных типов, так что есть возможность получить тип — однако неудобная — используя иерархию с единым корнем. Однако текущее предложение о параметризированных типах использует синтаксис, который сильно схож с шаблонами C++.
О дизайне обложки книги
Обложка книги Думай на Java вдохновлена American Arts & Crafts Movement, что началось ближе к концу прошлого века и достигла своего зенита между 1900 и 1920 годами. Все началось в Англии, как реакция на Индустриальную Революции, связанную как с производством машин, так и с высокохудожественным стилем орнамента в Викторианскую эру. Arts & Crafts подчеркивает скупость дизайна, природную форму как вид нового художественного течения, ручную работу, важность индивидуального мастерства, и в то же время ратует за использование современного оборудования. Это сильно перекликается с той ситуацией что мы имеет в настоящий момент: конец века, эволюция от начала компьютерной революции до чего-то более совершенного и значительного для каждого человека, и подчеркнутое отношение к созданию программного обеспечения, нежели простому штампованию кода. Я понимаю роль Java на этом пути как попытку отделить программистов от механизма операционных систем в сторону "программных мастеров" (software craftsman). Как автор(книги), так и дизайнер книги/обложки (которые кстати друзья с самого детства) нашли вдохновение в этом движения и оба приобрели мебель, лампы и прочие мелочи относящиеся к тому периоду. Другая тема в обложке подразумевает коробку для сбор и демонстрации насекомых, которую используют натуралисты. Насекомые - это объекты, которые помещаются в коробку - объекты. Коробка - объект в свою очередь помещается в другую коробку, что демонстрирует основные концепции накопления в объектно-ориентированном программировании. Конечно, программист не может помочь (не говориться кому/чему - Прим. перев.), но сделает ассоциацию с "жучками" и тут "жучки" будут пойманы и, надо полагать, убиты в экземпляре коробки, и в конечно счете посажены внутрь небольшой коробочки, как если подразумевать способность Java находить, показывать и устранять ошибки (что по правде говоря один из наиболее важных атрибутов).
Объединение композиции и наследования
Совместное использование композиции и наследования часто и широко используется при программировании. Следующий пример показывает создание более комплексного класса использующего оба метода и композицию, и наследование с необходимыми инициализациями конструкторов:
//: c06:PlaceSetting.java
// Объединение композиции и наследования.
class Plate { Plate(int i) { System.out.println("Plate constructor"); } }
class DinnerPlate extends Plate { DinnerPlate(int i) { super(i); System.out.println( "DinnerPlate constructor"); } }
class Utensil { Utensil(int i) { System.out.println("Utensil constructor"); } }
class Spoon extends Utensil { Spoon(int i) { super(i); System.out.println("Spoon constructor"); } }
class Fork extends Utensil { Fork(int i) { super(i); System.out.println("Fork constructor"); } }
class Knife extends Utensil { Knife(int i) { super(i); System.out.println("Knife constructor"); } }
// Нормальный путь, сделать что-то:
class Custom { Custom(int i) { System.out.println("Custom constructor"); } }
public class PlaceSetting extends Custom { Spoon sp; Fork frk; Knife kn; DinnerPlate pl; PlaceSetting(int i) { super(i + 1); sp = new Spoon(i + 2); frk = new Fork(i + 3); kn = new Knife(i + 4); pl = new DinnerPlate(i + 5); System.out.println( "PlaceSetting constructor"); } public static void main(String[] args) { PlaceSetting x = new PlaceSetting(9); } } ///:~
В то время, как компилятор требует от Вас инициализировать базовые классы и требует, что бы Вы делали это в начале конструктора, он не убеждается, в том, что Вы инициализировали остальные объекты, так что Вам придется быть осторожным.
Объединение процесса с основным классом
В вышеприведенном примере показан класс процесса отделенной от основного класса программы. Это делает пример более характерным и сравнительно легким для понимания. Существует, однако, альтернативная форма использования, которую вы будете часто видеть и которая не столь проста, но в большинстве случаев более кратка (что вероятно и увеличивает ее популярность). Эта форма объединяет класс основной программы и класс процесса, делая класс основной программы процессом. Поскольку для GUI (графический интерфейс пользователя) программы класс основной программы должен быть наследован как от Frame так и от Applete, наследование может быть использовано для добавления функциональности. Данный интерфейс называется Runnable и содержит те же основные методы что и Thread. Фактически Thread также реализует Runnable, что выражается только в наличии метода run().
Использование совмещенной программы-процесса не столь очевидно. Когда запускается программа, создается объект, который Runnable, но процесс не запускается, что должно быть сделано явно. Это можно пронаблюдать в следующем примере, функционально идентичному Counter2:
//: c14:Counter3.java
// Using the Runnable interface to turn the
// main class into a thread.
// <applet code=Counter3 width=300 height=100>
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
public class Counter3 extends JApplet implements Runnable { private int count = 0; private boolean runFlag = true; private Thread selfThread = null; private JButton start = new JButton("Start"), onOff = new JButton("Toggle"); private JTextField t = new JTextField(10); public void run() { while (true) { try { selfThread.sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } if(runFlag) t.setText(Integer.toString(count++)); } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(selfThread == null) { selfThread = new Thread(Counter3.this); selfThread.start(); } } } class OnOffL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); start.addActionListener(new StartL()); cp.add(start); onOff.addActionListener(new OnOffL()); cp.add(onOff); } public static void main(String[] args) { Console.run(new Counter3(), 300, 100); } } ///:~
Теперь run() внутри класса, но и после завершения inti() процесс все еще не запущен. Когда вы нажимаете кнопку start, процесс создается (если он еще не существует) следующим непонятным выражением:
new Thread(Counter3.this);
Когда что-либо имеет интерфейсRunnable, это просто означает, что оно имеет метод run( ), однако ничего особеного в этом нет - не производится ни каких задуманных для процесса действий, кроме как наследование класса от Thread. Таким образом, чтобы сделать процесс из Runnable объекта необходимо создать отдельный объект Thread, как показано выше, передав объект Runnable в специальный конструктор Thread. Затем можно вызвать start() для данного процесса:
selfThread.start();
Выполняется обычная инициализация и затем вызов run().
Удобство использования интерфейса Runnable в том, что все принадлежит тому же классу. Если необходимо обращение к чему-либо еще вы просто выполняете это без использования отдельного класса. Однако, как можно было видеть в предыдущем примере, доступ также прост как и использование внутреннего класса [70].
Объект Class
Чтобы понять, как RTTI работает в Java, Вы должны вначале узнать, как информация о типе представляется во время выполнения. Это реализуется с помощью специального типа объекта называемого Class, который содержит информацию о классе. (Иногда он называется meta-class.) На самом деле, Class используется для создания всех “регулярных” объектов Вашего класса.
Объект Class существует для каждого класса, который является частью Вашей программы. Т.е., каждый раз, когда Вы пишите и компилируете новый класс, также создается единичный объект Class (и записывается в файл, имеющий идентичное имя и расширение .class). Во время выполнения, когда Вы хотите создать объект какого-то класса, виртуальная машина Java (Java Virtual Machine - JVM), которая выполняет Вашу программу сначала проверяет загружен ли объект Class этого класса. Если нет, JVM загружает его, находя файл .class с именем этого класса. Таким образом, программа на Java не загружается полностью перед запуском, и это отличает Java от других языков.
Как только объект Class для этого типа объекта находится в памяти, он используется для создания всех объектов этого типа.
Если Вам это кажется неясным или Вы в это не верите - вот демонстрационная программа, подтверждающая это:
//: c12:SweetShop.java // Исследование механизма загрузки класса.
class Candy { static { System.out.println("Loading Candy"); } }
class Gum { static { System.out.println("Loading Gum"); } }
class Cookie { static { System.out.println("Loading Cookie"); } }
public class SweetShop { public static void main(String[] args) { System.out.println("inside main"); new Candy(); System.out.println("After creating Candy"); try { Class.forName("Gum"); } catch(ClassNotFoundException e) { e.printStackTrace(System.err); } System.out.println( "After Class.forName(\"Gum\")"); new Cookie(); System.out.println("After creating Cookie"); } } ///:~
Каждый из классов Candy, Gum и Cookie содержит предложение static, которое выполняется, когда класс загружается впервый раз. Информация распечатается, когда произойдет загрузка класса. В методе main( ), создание объектов разделяется функциями печати, чтобы помочь определить момент их загрузки.
Особенно интересна строка:
Class.forName("Gum");
Этот метод является статическим членом объекта Class (которому принадлежат все объекты Class). Объект Class является таким же объектом, как и все остальные, так что Вы можете манипулировать ссылкой на него. (Это именно то, что делает загрузчик.) Один из спопобов получить ссылку на объект Class это метод forName( ), которое берет строку String, содержащую текстовое имя (следите за правильным написанием имени класса и не забывайте, что регистр имеет значение!) класса, на который Вам нужна ссылка.. Этот медтод возвращает ссылку на объект Class.
Результаты работы этой программы на консоли JVM:
inside main Loading Candy After creating Candy Loading Gum After Class.forName("Gum") Loading Cookie After creating Cookie
Вы видите, что каждый объект Class загружается только, когда он нужен, и статические and the static инициализации выполняются сразу после загрузки класса.
Объект имеет интерфейс
Аристотель, вероятно, был первым, кто начал старательно изучать концепцию типа; он говорил: “класс рыбы и класс птицы”. Идея, что все объекты, хотя являются уникальными, также являются частью класса объектов, которые имеют общие характеристики и характер поведения, что было использовано в первом объектно-ориентированном языке Симула-67 с этим основополагающим словом класс, которое ввело новый тип в программу.
Симула, как показывает его название, был создан для разработки симуляторов, таких как классическая “проблема банковского кассира”. В ней вы имеете группу кассиров, клиентов, счетов, переводов и денег — множество “объектов”. Объекты, которые идентичны, за исключением своих состояний во время исполнения программы, группируются вместе в “классы объектов”. Так и пришло ключевое слово класс. Создание абстрактных типов данных (классов) - это основополагающая концепция в объектно-ориентированном программировании. Абстрактные типы данных работают почти так же, как и встроенные типы: вы можете создавать переменные этого типа (называемые объектами или экземплярами, если говорить объектно-ориентированным языком) и манипулировать этими переменными (это называется посылка сообщений или запрос; вы посылаете сообщение и объект смотрит что нужно с ним делать). Члены (элементы) каждого класса распределяются с некоторой унифицированностью: каждый счет имеет баланс, каждый кассир может принимать депозит и т.п. В то же время, каждый член имеет свое собственное состояние, каждый счет имеет различный баланс, каждый кассир имеет имя. Поэтому, кассиры, клиенты, счета, переводы и т.п. могут быть представлены как уникальная сущность в компьютерной программе. Эта сущность и есть объект, а каждый объект принадлежит определенному классу, который определяет характеристики и черты поведения.
Так, несмотря на то, что мы реально делаем в объектно-ориентированном программировании - это создание новых типов, фактически все объектно-ориентированные языки используют ключевое слово “класс”. Когда вы видите слово “тип”, то думайте “класс” и наоборот.
[3]
Так как класс описывает набор объектов, которые имеют идентичные характеристики (элементы данных) и черты поведения (функциональность), класс реально является типом данных, потому что, например, число с плавающей точкой также имеет набор характеристик и черт поведения. Отличия в том, что программист определяет класс исходя из проблемы, чтобы представить блок для хранения в машине. Вы расширяете язык программирования, добавляя спецификации новых типов данных, которые вам необходимы. Эта система программирования приветствует новые классы и заботится за ними всеми, выполняя проверку типа, как и для встроенных типов.
Объектно-ориентированный подход не ограничивается построением симуляторов. Независимо от того, согласны вы или нет, что любая разрабатываемая вами программа - это эмуляция системы, использование ООП техники может легко снизить большую часть проблем для упрощения решения.
Как только класс создан, вы можете создать столько объектов этого класса, сколько захотите, а затем манипулировать этими объектами так, как если бы они являлись элементами, которые существуют в проблеме, которую вы пробуете решить. Несомненно, одно из предназначений объектно-ориентированного программирования - это создание связей один-к-одному между элементами в пространстве проблемы и объектами в пространстве решения.
Но как заставить объект стать полезным для вас? Должен существовать способ сделать запрос к объекту, чтобы он что-то сделал, например, законченную транзакцию, что-то нарисовал на экране или включил переключатель. Каждый объект может удовлетворять только определенные запросы. Запросы, которые вы можете сделать к объекту, определяются его интерфейсом и типом, который определяет интерфейс. Простым примером может стать представление электрической лампочки:
Light lt = new Light(); lt.on();
Интерфейс определяет какой запрос вы можете выполнить для определенного объекта. Однако должен существовать определенный код, для удовлетворения этого запроса. Здесь, наряду со спрятанными данными, содержится реализация. С точки зрения процедурного программирования это не сложно. Тип имеет функциональные ассоциации для каждого возможного запроса и, когда вы делаете определенный запрос к объекту, вызывается такая функция. Этот процесс обычно суммируется и можно сказать, что вы “посылаете сообщение” (делаете запрос) объекту, а объект определяет, что он должен сделать с этим сообщением (он исполняет код).
В этом промере имя типа/класса - Light, имя этого обычного объекта Light - lt, а запросы, которые вы можете сделать для объекта Light - это включить его, выключить, сделать ярче или темнее. Вы создаете объект Light, определяя “ссылку” (lt) для объекта и вызываете new для запроса нового объекта этого типа. Для отправки сообщения объекту вы объявляете имя объекта и присоединяете его к сообщению запроса, разделив их (точкой). С точки зрения пользователя, предварительное определение класса - более красивый способ программирования с объектами.
Диаграмма, показанная выше, следует формату Унифицированного Языка Моделирования (Unified Modeling Language (UML). Каждый класс представляется ящиком, с именем типа в верхней части ящика и членами - данными, которые вы описываете в средней части ящика, а члены - функции (принадлежащие объекту функции, которые принимают сообщения, которые вы посылаете этому объекту) в нижней части ящика. Чаще всего только имя класса и публичные члены - функции показаны в диаграмме разработки UML, так что средняя часть не показывается. Если вы интересуетесь только именем класса, нижние части нет необходимости показывать.
Обеспечение клонируемости объектов-наследников
Когда создается новый класс, ему по умолчанию передаются свойства базового класса Object, который по умолчанию является не клонируемым (об этом пойдет речь в следующем разделе), и остается таковым до тех пор, пока вы не захотите этого. Однако, после того как вы добавите возможность клонирования в какой-либо класс, она будет передана всем нижестоящим по иерархии классам:
//: Приложение А:HorrorFlick.java
// Вы можете добавить клонируемость в
// любой уровень иерархии наследования объектов.
import java.util.*;
class Person {} class Hero extends Person {} class Scientist extends Person implements Cloneable { public Object clone() { try { return super.clone(); } catch(CloneNotSupportedException e) { // этого не должно произойти:
// он уже клонируемый!
throw new InternalError(); } } } class MadScientist extends Scientist {}
public class HorrorFlick { public static void main(String[] args) { Person p = new Person(); Hero h = new Hero(); Scientist s = new Scientist(); MadScientist m = new MadScientist();
// p = (Person)p.clone(); // Ошибка компиляции
// h = (Hero)h.clone(); // Ошибка компиляции
s = (Scientist)s.clone(); m = (MadScientist)m.clone(); } } ///:~
Перед тем как добавить клонируемость, компилятор остановит вас при попытке клонировать предметы (things). Когда клонируемость будет добавлена в Scientist, Scientist и все его наследники станут клонируемыми.
Обработчики исключений
Конечно, выбрасывание исключения должно где-то заканчиваться. Это “место” - обработчик исключения, и есть один обработчик для каждого типа исключения, которые вы хотите поймать. Обработчики исключений следуют сразу за блоком проверки и объявляются ключевым словом catch:
try { // Код, который может сгенерировать исключение
} catch(Type1 id1) { // Обработка исключения Type1
} catch(Type2 id2) { // Обработка исключения Type2
} catch(Type3 id3) { // Обработка исключения Type3
}
// и так далее...
Каждое catch предложение (обработчик исключения) как меленький метод, который принимает один и только один аргумент определенного типа. Идентификаторы (id1, id2 и так далее) могут быть использованы внутри обработчика, как аргумент метода. Иногда вы нигде не используете идентификатор, потому что тип исключения дает вам достаточно информации, чтобы разобраться с исключением, но идентификатор все равно должен быть.
Обработчики должны располагаться прямо после блока проверки. Если выброшено исключение, механизм обработки исключений идет охотится за первым обработчиком с таким аргументом, тип которого совпадает с типом исключения. Затем происходит вход в предложение catch, и рассматривается обработка исключения. Поиск обработчика, после остановки на предложении catch, заканчивается. Выполняется только совпавшее предложение catch; это не как инструкция switch, в которой вам необходим break после каждого case, чтобы предотвратить выполнение оставшейся части.
Обратите внимание, что внутри блока проверки несколько вызовов различных методов может генерировать одно и тоже исключение, но вам необходим только один обработчик.
Обработка исключений конструктора
Как только что было замечено, компилятор предлагает Вам поместить конструктор базового класса в конструктор класса наследника. Это означает, что ничего другого не может произойти до его вызова. Как Вы увидите в главе 10, при этом нужно так же позаботится об обработке исключения пришедшего из конструктора базового класса.
Обработка исключений: работа с ошибками
Всегда, начиная язык программирования, обработка ошибок бывает одним из наиболее сложных мест. Потому что так сложно создать при разработке хорошую схему обработки ошибок, многие языки просто игнорируют эту проблему, перекладывая ее решение на разработчика библиотеки, который придумывает на полпути меры, работающие во многих ситуациях, но могут быть легко обмануты, если просто проигнорировать их. Главная проблема с большинством схем обработки ошибок в том, что они опираются на внимание и согласие программиста, которое не навязывается языком. Если программист не внимателен — часто если он торопится — эти схемы могут легко быть забыты.
Обработка исключений связана с обработкой ошибок напрямую в языке программирования и иногда даже в операционной системе. Исключение - это объект, который “бросается” со стороны ошибки и может быть “пойман” подходящим обработчиком исключения, предназначенном для обработки определенного типа ошибки. Это как если исключение обрабатывается, выбирается другой, параллельный путь исполнения, которое может быть выбрано, когда что-то неправильно. И так как используется отличный путь исполнения, нет необходимости пересекаться с кодом нормального выполнения. Это делает такой код проще для написания, так как вам не нужно постоянно уделять внимания на проверку ошибок. В дополнение, выбрасывание исключений не похоже на код ошибки, который возвращается из функции, или на флаг, который устанавливается функцией, чтобы указать на состояние ошибки — он может быть проигнорирован. Исключение не может быть проигнорировано, так что это гарантированно будет замечено в некоторой точке. Наконец, исключение обеспечивает способ надежной защиты от плохой ситуации. Вместо простого выхода вы часто способны установить вещи правильно и восстановить исполнение программы, что делает большинство устойчивых программ.
Обработка ошибок в Java основывается на большинстве языков программирования, поскольку в Java обработка исключений была встроено с самого начала и вы вынуждены использовать это. Если вы не пишете свой код с правильной обработкой исключений, вы получите ошибку времени компиляции. Это гарантирует последовательность, делая обработку ошибок более легкой.
Стоит отметить, что обработка исключения - это не особенность объектно-ориентированного языка, хотя в объектно-ориентированных языках исключение обычно представляется объектом. Обработка исключений появилась раньше объектно-ориентированных языков.
Обработка ошибок
Обработка ошибок в C - печально известная проблема, и она часто игнорируется — при этом обычно скрещивают пальцы. Если вы строите большую, сложную программу, нет ничего хуже, чем наличие ошибки захороненной где-нибудь без намека на то, откуда она появляется. Обработка исключений в Java - это способ гарантировать то, что ошибка замечена и что кое-что произойдет в результате.
Общие ловушки при использовании операторов
Одна из ошибок при использовании операторов - это попытка обходится без круглых скобок, когда вы даже немного не представляете того, как будет вычисляться выражение. Это все еще верно в Java.
Чрезвычайно общая ошибка в C и C++ выглядит так:
while(x = y) { // ....
}
Программист пробовал проверить на равенство (==), а выполнил присвоение. В C и C++ результат присвоения всегда будет true, если y не ноль, и вы, вероятно, получите бесконечный цикл. В Java, результат этого выражения не boolean, а компилятор ожидает boolean и не может преобразовать int, так что он выдаст вам ошибку времени компиляции и выявит проблему до того, как ы запустите программу. Так что ловушка никогда не случится в Java. (Вы не получите сообщение об ошибке времени компиляции, когда x и y - boolean, в таком случае x = y i- допустимое выражение, но в приведенном примере, вероятно, ошибочное.)
Аналогично C и C++ есть проблема использование битовыз И и ИЛИ вместо логической версии. Битовые И и ИЛИ используют один символ (& или |), а логические И и ИЛИ используют два (&& и ||). Как и с = и ==, легко напечатать только один символ вместо двух. В Java компилятор опять предотвратит это, потому что он не позволит вым бесцеремонно использовать один тип, где это не применимо.
Общие ошибки дизайна
Когда начинаете с вашей командой работать в ООП и Java, программисты обычно проходят через ряд общих ошибок дизайна. Это часто случается из-за недостаточной обратной связи с экспертом во время дизайна и реализации ранних проектов, потому что в компании нет экспертов разработки и, поэтому, может быть сопротивление в получении консультации. Легко слишком рано почувствовать, что вы понимаете ООП и уйти по касательной. Что-то являющееся очевидным для кого-то опытного в языке, может быть предметом больших внутренних обсуждений для новичка. Многие из этих травм можно пропустить, используя опят внешнего эксперта для тренировки и руководства.
Обслуживание нескольких клиентов
JabberServer работает, но он может обслуживать только одного клиента одновременно. В типичном сервере, Вы захотите иметь возможность общаться со несколькими клиентами одновременно. Решение - это много поточность, а в языках, которые напрямую не поддерживают многопоточность это означает все виды сложностей. В Главе 14 Вы увидели, что многопоточность в Java настолько просто, насколько это возможно, в то время, как многопоточность вообще является сложной темой. Т.к. поддерка нитей в Java является прямой и открытой, то создание сервера, поддерживающего множество клиентов оказывается относительно простой задачей.
Основная схема это создание единичного объекта ServerSocket в серверной части и вызвать метод accept( ) для ожидания нового соединения. Когда accept( ) возвращает управления, Вы берете возвращенный Socket и используете его для создания новой нити(потока), чьей работой является обслуживание этого клиента. Затем Вы вызываете метод accept( ) снова, для ожидания нового клиента.
В следующем коде серверной части Вы увидите, что это выглядит также как и в примере JabberServer.java за исключением того, что все операции по обслуживанию конкретного клиента перемещены внутрь класса нити (потока):
//: c15:MultiJabberServer.java
// Сервер, использующий многопоточность
// для обслуживания любого числа клиентов.
import java.io.*; import java.net.*;
class ServeOneJabber extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; public ServeOneJabber(Socket s) throws IOException { socket = s; in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Включение автосброса буферов:
out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); // Если какой либо, указанный выше класс выбросит исключение
// вызывающая процедура ответственна за закрытие сокета
// В противном случае нить(поток) закроет его.
start(); // Вызывает run()
} public void run() { try { while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } System.out.println("closing..."); } catch(IOException e) { System.err.println("IO Exception"); } finally { try { socket.close(); } catch(IOException e) { System.err.println("Socket not closed"); } } } }
public class MultiJabberServer { static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Server Started"); try { while(true) { // Останавливает выполнение, до нового соединения:
Socket socket = s.accept(); try { new ServeOneJabber(socket); } catch(IOException e) { // Если неудача - закрываем сокет,
// в противном случае нить закроет его:
socket.close(); } } } finally { s.close(); } } } ///:~
Нить ServeOneJabber берет объект Socket, который создается методом accept( ) в main( ) каждый раз, когда новый клиент создает соединение. Затем, как раньше, он создает объект BufferedReader и объект PrintWriter с авто-сбросом используя Socket. Наконец, он вызывает специальный метод объекта Thread - start( ), который выполняет инициализации в нити и, затем вызывает метод run( ). Здесь выполняются те же действия, что и в предыдущем примере: чтение данных из сокета, а затем возврат этих данных обратно, пока не придет специальный сигнал - строка “END”.
Ответственность за очистку сокета должна быть снова тщательно обработана. В этом случае, сокет создается за пределами ServeOneJabber так что ответственность может быть разделена. Если конструктор ServeOneJabber завершится неудачно, он просто вызовет исключение вызывающему методу, который затем очистит нить. Но если конструктор завершится успешно, то объект ServeOneJabber возьмет ответственность за очистку нить на себя, в его методе run( ).
Посмотрите, насколько протая реализация у MultiJabberServer. Как раньше, ServerSocket создается и вызывается метод accept( ) для ожидания нового соединения. Но в этот момент, возвращаемое значение accept( ) (объекты Socket) передается в конструктор ServeOneJabber, который создает новую нить для обраболтки этого соединения. Когда соединение закрывается, нить завершает свою работу.
Если создание ServerSocket прерывается, снова выбрасывается в main( ). Но если создание успешное, внешний блок try-finally гарантирует его очистку. Внутренний блок try-catch защищает только от ошибок в конструкторе ServeOneJabber; если конструктор выполняется без ошибок, то нить ServeOneJabber закроет связанный с ней сокет.
Для проверки того, что сервер поддерживает несколько клиентов, следующая программа создает множество клиентов (используя нити) которые подключаются к одному и тому же серверу. Максимальное число нитей определяется переменной final int MAX_THREADS.
//: c15:MultiJabberClient.java
// Клиент для проверки MultiJabberServer
// посредством запуска множества клиентов.
import java.net.*; import java.io.*;
class JabberClientThread extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; private static int counter = 0; private int id = counter++; private static int threadcount = 0; public static int threadCount() { return threadcount; } public JabberClientThread(InetAddress addr) { System.out.println("Making client " + id); threadcount++; try { socket = new Socket(addr, MultiJabberServer.PORT); } catch(IOException e) { System.err.println("Socket failed"); // Если сокет не создался,
// ничего не надо чистить.
} try { in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Включение авто-очистки буфера:
out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); start(); } catch(IOException e) { // Сокет должен быть закрыт при появлении любой ошибки
try { socket.close(); } catch(IOException e2) { System.err.println("Socket not closed"); } } // Иначе сокет будет закрыт
// методом run() у нити.
} public void run() { try { for(int i = 0; i < 25; i++) { out.println("Client " + id + ": " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } catch(IOException e) { System.err.println("IO Exception"); } finally { // Всегда закрывает его:
try { socket.close(); } catch(IOException e) { System.err.println("Socket not closed"); } threadcount--; // Завершение нити
} } }
public class MultiJabberClient { static final int MAX_THREADS = 40; public static void main(String[] args) throws IOException, InterruptedException { InetAddress addr = InetAddress.getByName(null); while(true) { if(JabberClientThread.threadCount() < MAX_THREADS) new JabberClientThread(addr); Thread.currentThread().sleep(100); } } } ///:~
Конструктор JabberClientThread берет InetAddress и использует его для открытия Socket. Вы возпожно уже начинаете видешь шаблон: Socket используется всегда для создания некоторых типов Reader и/или Writer (или InputStream и/или OutputStream) объект, что является единственным способом, в котором Socket может быть использован. (Вы можете, конечно, написать один-два класса для автоматизации этого процесса, вместо того , чтобы заново все это набирать, если для Вас это сложно.) Итак, start( ) выполняет инициализации нити и вызывает метод run( ). Здесь, сообщения отсылаются серверу и информация с сервера печатается на экране. Однако, нить имеет ограниченное время жизни и в один прекрасный момент завершается. Обратите внимание, что сокет очищается если конструктор завершается неуспешно, после того как сокет создается, но перед тем, как конструктор завершится. В противном случае ответственность за вызов метода close( ) для сокета ложится на метод run( ).
Переменная threadcount хранит число - сколько объектов JabberClientThread в данный момент существует. Она увеличивается в конструкторе и уменьшается при выходе из метода run( ) (и это значит, что нить завершается). В методе MultiJabberClient.main( ), Вы видите, что число нитей проверяется, и если нитей слишком много - новые не создаются. Затем метод засыпает. При этом, некоторые нити будут завершаться и новые смогут быть созданы. Вы можете поэкспериментировать с MAX_THREADS, чтобы посмотреть когда у Вашей системы появятся проблемы с обслуживанием большого количества соединений.
Очистка: финализация и сборщик мусора
Программисты знают о важности инициализации, но часто забывают о важности очистки. Помимо всего, кому понадобится очищать значения, типа int? Но с библиотеками, просто “позволить идти своей дорогой” тем объектам, с которыми вы закончили работать не всегда безопасно. Конечно, Java имеет сборщик мусора для освобождения памяти объектов, которые более не используются. Теперь об очень редком случае. Предположим, что ваш объект зарезервировал “специальную” память, не используя new. Сборщик мусора знает только, как освобождать память, выделенную с помощью new, так что он не может освободить “специальную” память объектов. Для обработки этого случая Java обеспечивает метод, называемый finalize( ), который вы можете определить в своем классе. Вот как это должно работать. Когда сборщик мусора готов освободить хранилище, используемое вашим объектом, он сначала вызовет finalize( ), а только на следующем этапе сборки мусора он освободит память объекта. Так что, если вы выбрали использование finalize( ), это даст вам возможность выполнить некоторые важные для очистки операции во время сборки мусора.
Это потенциальная ловушка программистам, так как некоторые из них, особенно программисты на C++, могут ошибочно полагать, что finalize( ) аналогично деструкторам в C++, которые являются функциями, которые всегда вызываются при разрушении объекта. Но в этом заключается важное различие между C++ и Java, потому что в C++ объекты всегда разрушаются (в программах без ошибок), в то время как в Java объекты не всегда попадают под сборку мусора. Или, говоря другими словами:
Сборка мусора - это не разрушение.
Если вы запомните это, вы не встретите трудностей. Это означает, что если есть какие-то действия, которые необходимо выполнить прежде, чем вы закончите работать с объектом, вы должны выполнить эти действия самостоятельно. Java не имеет деструкторов или аналогичной концепции, так что вы должны создать обычный метод для выполнения этой очистки. Например, предположим, что в процессе создания вашего объекта он рисует себя на экране. Если вы явно не вызовите стирание этого изображения с экрана, оно может никогда не очистится. Если вы помещаете некоторое стирание внутри функции finalize( ), то если объект подвергнется сборке мусора, изображение сначала будет стерто с экрана, но если этого не произойдет, то изображение останется. Так что второе, что вы должны запомнить:
Ваши объекты могут не подвергнуться сборке мусора.
Вы можете обнаружить, что хранилище никогда не освобождается, потому что ваша программа никогда не приближается к точке переполнения хранилища. Если ваша программа завершена, и сборщик мусора ни разу не приступил к освобождению хранилища для любого из ваших объектов, то хранилище будет возвращено операционной системе целиком после завершения программы. Это хорошо, потому что сбор мусора приводит к дополнительным накладным расходам, и если вы никогда не достигаете этого, ваши затраты никогда не увеличиваются.