java.util.AbstractCollection – данный класс реализует все методы, определенные в интерфейсе Collection, за исключением iterator и size, так что для того, чтобы создать немодифицируемую коллекцию, нужно переопределить эти методы. Для реализации модифицируемой коллекции необходимо еще переопределить метод public void add(Object o) (в противном случае при его вызове будет возбуждено исключение UnsupportedOperationException).
Необходимо также определить два конструктора: без аргументов и с аргументом Collection. Первый должен создавать пустую коллекцию, второй – коллекцию на основе существующей. Данный класс расширяется классами AbstractList и AbstractSet.
java.util.AbstractList – этот класс расширяет AbstractCollection и реализует интерфейс List. Для создания немодифицируемого списка необходимо имплементировать методы public Object get(int index) и public int size(). Для реализации модифицируемого списка необходимо также реализовать метод public void set(int index,Object element) (в противном случае при его вызове будет возбуждено исключение UnsupportedOperationException).
В отличие от AbstractCollection, в этом случае нет необходимости реализовывать метод iterator, так как он уже реализован поверх методов доступа к элементам списка get, set, add, remove.
java.util.AbstractSet – данный класс расширяет AbstractCollection и реализует основную функциональность, определенную в интерфейсе Set. Следует отметить, что этот класс не переопределяет функциональность, реализованную в классе AbstractCollection.
java.util.AbstractMap – этот класс расширяет основную функциональность, определенную в интерфейсе Map. Для реализации немодифицируемого класса, унаследованного от AbstractMap, достаточно определить метод entrySet, который должен возвращать объект, приводимый к типу AbstractSet. Этот набор (Set) не должен обеспечивать методов для добавления и удаления элементов из набора. Для реализации модифицируемого класса Map необходимо также переопределить метод put и добавить в итератор, возвращаемый entrySet().iterator(), поддержку метода remove.
java.util.AbstractSequentialList – этот класс расширяет AbstractList и является основой для класса LinkedList. Основное отличие от AbstractList заключается в том, что этот класс обеспечивает не только последовательный, но и произвольный доступ к элементам списка, с помощью методов get(int index), set(int index, Object element), add(int index, Object element) и remove(int index). Для того, чтобы реализовать данный класс, необходимо переопределить методы listIterator и size. Причем, если реализуется немодифицируемый список, для итератора достаточно реализовать методы hasNext, next, hasPrevious, previous и index. Для модифицируемого списка необходимо дополнительно реализовать метод set, а для списков переменной длины еще и add, и remove.
Назад
Аплет PlayClip демонстрирует использование интерфейса AudioClip. В его окне (рис. 1) имеются три кнопки с названиями Play, Loop и Stop.
Рис. 1. Окно аплета PlayClip
Сразу после запуска аплета кнопка Stop находится в заблокированном состоянии. Если нажать кнопку Play или Loop, начнется, соответственно, однократное проигрывание или проигрывание в цикле файла с именем kaas.au, распложенного в том же каталоге, что и двоичный файл аплета PlayClip.
Когда начинается проигрывание звукового файла, кнопка Stop разблокируется, что позволяет остановить проигрывание.
public class Test { public Test() { } public static void main(String[] args) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy MMMM dd HH:mm:ss"); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR,2002); cal.set(Calendar.MONTH,Calendar.AUGUST); cal.set(Calendar.DAY_OF_MONTH,31); System.out.println(" Initialy set date: " + sdf.format(cal.getTime())); cal.set(Calendar.MONTH,Calendar.SEPTEMBER); System.out.println(" Date with month changed : " + sdf.format(cal.getTime())); cal.set(Calendar.DAY_OF_MONTH,30); System.out.println(" Date with day changed : " + sdf.format(cal.getTime())); } } |
Пример 14.1. |
Закрыть окно |
Initialy set date: 2002 August 31 22:57:47 Date with month changed : 2002 October 01 22:57:47 Date with day changed : 2002 October 30 22:57:47 |
Пример 14.2. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy MMMM dd HH:mm:ss"); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR,2002); cal.set(Calendar.MONTH,Calendar.AUGUST); cal.set(Calendar.DAY_OF_MONTH,31); System.out.println(" Initialy set date: " + sdf.format(cal.getTime())); cal.set(Calendar.MONTH,Calendar.SEPTEMBER); cal.set(Calendar.DAY_OF_MONTH,30); System.out.println(" Date with day and month changed : " + sdf.format(cal.getTime())); } } |
Пример 14.3. |
Закрыть окно |
Initialy set date: 2002 August 31 23:03:51 Date with day and month changed: 2002 September 30 23:03:51 |
Пример 14.4. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy MMMM dd HH:mm:ss"); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR,2002); cal.set(Calendar.MONTH,Calendar.AUGUST); cal.set(Calendar.DAY_OF_MONTH,31); cal.set(Calendar.HOUR_OF_DAY,19); cal.set(Calendar.MINUTE,30); cal.set(Calendar.SECOND,00); System.out.println("Current date: " + sdf.format(cal.getTime())); cal.add(Calendar.SECOND,75); System.out.println("Current date: " + sdf.format(cal.getTime())); cal.add(Calendar.MONTH,1); System.out.println("Current date: " + sdf.format(cal.getTime())); } } |
Пример 14.5. |
Закрыть окно |
Current date: 2002 August 31 19:30:00 Rule 1: 2002 August 31 19:31:15 Rule 2: 2002 September 30 19:31:15 |
Пример 14.6. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy MMMM dd HH:mm:ss"); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR,2002); cal.set(Calendar.MONTH,Calendar.AUGUST); cal.set(Calendar.DAY_OF_MONTH,31); cal.set(Calendar.HOUR_OF_DAY,19); cal.set(Calendar.MINUTE,30); cal.set(Calendar.SECOND,00); System.out.println("Current date: " + sdf.format(cal.getTime())); cal.roll(Calendar.SECOND,75); System.out.println("Rule 1: " + sdf.format(cal.getTime())); cal.roll(Calendar.MONTH,1); System.out.println("Rule 2: " + sdf.format(cal.getTime())); } } |
Пример 14.7. |
Закрыть окно |
Current date: 2002 August 31 19:30:00 Rule 1: 2002 August 31 19:30:15 Rule 2: 2002 September 30 19:30:15 |
Пример 14.8. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); TimeZone tz = TimeZone.getDefault(); int rawOffset = tz.getRawOffset(); System.out.println("Current TimeZone" + tz.getDisplayName() + tz.getID() + "\n\n"); // Dispaly all available TimeZones System.out.println("All Available TimeZones \n"); String[] idArr = tz.getAvailableIDs(); for(int cnt=0;cnt < idArr.length;cnt++){ tz = TimeZone.getTimeZone(idArr[cnt]); System.out.println(test.padr(tz.getDisplayName() + tz.getID(),64) + " raw offset=" + tz.getRawOffset() + ";hour offset=(" + tz.getRawOffset()/ (1000 * 60 * 60 ) + ")"); } // Dispaly all available TimeZones same as for Moscow System.out.println("\n\n TimeZones same as for Moscow \n"); idArr = tz.getAvailableIDs(rawOffset); for(int cnt=0;cnt < idArr.length;cnt++){ tz = TimeZone.getTimeZone(idArr[cnt]); System.out.println(test.padr(tz.getDisplayName()+ tz.getID(),64) + " raw offset=" + tz.getRawOffset() + ";hour offset=(" + tz.getRawOffset()/ (1000 * 60 * 60 ) + ")"); } } String padr(String str,int len){ if(len - str.length() > 0){ char[] buf = new char[len - str.length()]; Arrays.fill(buf,' '); return str + new String(buf); }else{ return str.substring(0,len); } } } |
Пример 14.9. |
Закрыть окно |
Current TimeZone Moscow Standard TimeEurope/ Moscow TimeZones same as for Moscow Eastern African TimeAfrica/Addis_Aba raw offset=10800000;hour offset=(3) Eastern African TimeAfrica/Asmera raw offset=10800000;hour offset=(3) Eastern African TimeAfrica/Dar_es_Sa raw offset=10800000;hour offset=(3) Eastern African TimeAfrica/Djibouti raw offset=10800000;hour offset=(3) Eastern African TimeAfrica/Kampala raw offset=10800000;hour offset=(3) Eastern African TimeAfrica/Khartoum raw offset=10800000;hour offset=(3) Eastern African TimeAfrica/Mogadishu raw offset=10800000;hour offset=(3) Eastern African TimeAfrica/Nairobi raw offset=10800000;hour offset=(3) Arabia Standard TimeAsia/Aden raw offset=10800000;hour offset=(3) Arabia Standard TimeAsia/Baghdad raw offset=10800000;hour offset=(3) Arabia Standard TimeAsia/Bahrain raw offset=10800000;hour offset=(3) Arabia Standard TimeAsia/Kuwait raw offset=10800000;hour offset=(3) Arabia Standard TimeAsia/Qatar raw offset=10800000;hour offset=(3) Arabia Standard TimeAsia/Riyadh raw offset=10800000;hour offset=(3) Eastern African TimeEAT raw offset=10800000;hour offset=(3) Moscow Standard TimeEurope/Moscow raw offset=10800000;hour offset=(3) Eastern African TimeIndian/Antananar raw offset=10800000;hour offset=(3) Eastern African TimeIndian/Comoro raw offset=10800000;hour offset=(3) Eastern African TimeIndian/Mayotte raw offset=10800000;hour offset=(3) |
Пример 14.10. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); SimpleTimeZone stz = new SimpleTimeZone( TimeZone.getDefault().getRawOffset() ,TimeZone.getDefault().getID() ,Calendar.MARCH ,-1 ,Calendar.SUNDAY ,test.getTime(2,0,0,0) ,Calendar.OCTOBER ,-1 ,Calendar.SUNDAY ,test.getTime(3,0,0,0) ); System.out.println(stz.toString()); } int getTime(int hour,int min,int sec,int ms){ return hour * 3600000 + min * 60000 + sec * 1000 + ms; } } |
Пример 14.11. |
Закрыть окно |
java.util.SimpleTimeZone[id=Europe/Moscow,offset=10800000,dstSavings=3600000,useDaylight=true, startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=7200000,startTimeMode=0, endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=10800000,endTimeMode=0] |
Пример 14.12. |
Закрыть окно |
public class TestObservable extends java.util.Observable { private String name = ""; public TestObservable(String name) { this.name = name; } public void modify() { setChanged(); } public String getName() { return name; } } public class TestObserver implements java.util.Observer { private String name = ""; public TestObserver(String name) { this.name = name; } public void update(java.util.Observable o,Object arg) { String str = "Called update of " + name; str += " from " + ((TestObservable)o).getName(); str += " with argument " + (String)arg; System.out.println(str); } } public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); TestObservable to = new TestObservable("Observable"); TestObserver o1 = new TestObserver("Observer 1"); TestObserver o2 = new TestObserver("Observer 2"); to.addObserver(o1); to.addObserver(o2); to.modify(); to.notifyObservers("Notify argument"); } } |
Пример 14.13. |
Закрыть окно |
Called update of Observer 2 from Observable with argument Notify argument Called update of Observer 1 from Observable with argument Notify argument |
Пример 14.14. |
Закрыть окно |
List l = Collections.synchronizedList(new ArrayList(...)); public class Test { public Test() { } public static void main(String[] args) { Test t = new Test(); ArrayList al = new ArrayList(); al.add("First element"); al.add("Second element"); al.add("Third element"); Iterator it = al.iterator(); while(it.hasNext()) { System.out.println((String)it.next()); } System.out.println("\n"); al.add(2,"Insertion"); it = al.iterator(); while(it.hasNext()){ System.out.println((String)it.next()); } } } |
Пример 14.15. |
Закрыть окно |
First element Second element Third element Firts element Second element Insertion Third element |
Пример 14.16. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); LinkedList ll = new LinkedList(); ll.add("Element1"); ll.addFirst("Element2"); ll.addFirst("Element3"); ll.addLast("Element4"); test.dumpList(ll); ll.remove(2); test.dumpList(ll); String element = (String)ll.getLast(); ll.remove(element); test.dumpList(ll); } private void dumpList(List list){ Iterator it = list.iterator(); System.out.println(); while(it.hasNext()){ System.out.println((String)it.next()); } } } |
Пример 14.17. |
Закрыть окно |
Element3 Element2 Element1 Element4 Element3 Element2 Element4 Element3 Element2 |
Пример 14.18. |
Закрыть окно |
public class Test { private class TestObject{ String text = ""; public TestObject(String text){ this.text = text; }; public String getText(){ return this.text; } public void setText(String text){ this.text = text; } } public Test() { } public static void main(String[] args) { Test t = new Test(); TestObject to = null; HashMap hm = new HashMap(); hm.put("Key1",t.new TestObject("Value 1")); hm.put("Key2",t.new TestObject("Value 2")); hm.put("Key3",t.new TestObject("Value 3")); to = (TestObject)hm.get("Key1"); System.out.println("Object value for Key1 = " + to.getText() + "\n"); System.out.println("Iteration over entrySet"); Map.Entry entry = null; Iterator it = hm.entrySet().iterator(); // Итератор для перебора всех точек входа в Map while(it.hasNext()){ entry = (Map.Entry)it.next(); System.out.println("For key = " + entry.getKey() + " value = " + ((TestObject)entry.getValue()).getText()); } System.out.println(); System.out.println("Iteration over keySet"); String key = ""; // Итератор для перебора всех ключей в Map it = hm.keySet().iterator(); while(it.hasNext()){ key = (String)it.next(); System.out.println( "For key = " + key + " value = " + ((TestObject)hm.get(key)).getText()); } } } |
Пример 14.19. |
Закрыть окно |
Object value for Key1 = Value 1 Iteration over entrySet For key = Key3 value = Value 3 For key = Key2 value = Value 2 For key = Key1 value = Value 1 Iteration over keySet For key = Key3 value = Value 3 For key = Key2 value = Value 2 For key = Key1 value = Value 1 |
Пример 14.20. |
Закрыть окно |
public class Test { private class TestObject { private String name = ""; public TestObject(String name) { this.name = name; } } private class MyComparator implements Comparator { public int compare(Object l,Object r) { String left = (String)l; String right = (String)r; return -1 * left.compareTo(right); } } public Test() { } public static void main(String[] args) { Test test = new Test(); Vector v = new Vector(); v.add("bbbbb"); v.add("aaaaa"); v.add("ccccc"); System.out.println("Default elements order"); test.dumpList(v); Collections.sort(v); System.out.println("Default sorting order"); test.dumpList(v); System.out.println("Reverse sorting order with providing imlicit comparator"); Collections.sort(v,test.new MyComparator()); test.dumpList(v); } private void dumpList(List l) { Iterator it = l.iterator(); while(it.hasNext()) { System.out.println(it.next()); } } } |
Пример 14.21. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); Properties props = new Properties(); StringWriter sw = new StringWriter(); sw.write("Key1 = Vlaue1 \n"); sw.write(" Key2 : Vlaue2 \r\n"); sw.write(" Key3 Vlaue3 \n "); InputStream is = new ByteArrayInputStream(sw.toString().getBytes()); try { props.load(is); } catch (IOException ex) { ex.printStackTrace(); } props.list(System.out); props.setProperty("Key1","Modified Value1"); props.setProperty("Key4","Added Value4"); props.list(System.out); } } |
Пример 14.22. |
Закрыть окно |
-- listing properties -- Key3=Vlaue3 Key2=Vlaue2 Key1=Vlaue1 -- listing properties -- Key4=Added Value4 Key3=Vlaue3 Key2=Vlaue2 Key1=Modified Value1 |
Пример 14.23. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); Random r = new Random(100); // Generating the same sequence numbers for(int cnt=0;cnt<9;cnt++){ System.out.print(r.nextInt() + " "); } System.out.println(); r = new Random(100); for(int cnt=0;cnt<9;cnt++) { System.out.print(r.nextInt() + " "); } System.out.println(); // Generating sequence of bytes byte[] randArray = new byte[8]; r.nextBytes(randArray); test.dumpArray(randArray); } void dumpArray(byte[] arr) { for(int cnt=0;cnt< arr.length;cnt++) { System.out.print(arr[cnt]); } System.out.println(); } } |
Пример 14.24. |
Закрыть окно |
-1193959466 - 1139614796 837415749 -1220615319 -1429538713 118249332 -951589224 -1193959466 -1139614796 837415749 -1220615319 -1429538713 118249332 -951589224 81;-6;-107;77;118;17;93; -98; |
Пример 14.25. |
Закрыть окно |
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); Locale l = Locale.getDefault(); System.out.println(l.getCountry() + " " + l.getDisplayCountry() + " " + l.getISO3Country()); System.out.println(l.getLanguage() + " " + l.getDisplayLanguage() + " " + l.getISO3Language()); System.out.println(l.getVariant() + " " + l.getDisplayVariant()); l = new Locale("ru","RU","WINDOWS"); System.out.println(l.getCountry() + " " + l.getDisplayCountry() + " " + l.getISO3Country()); System.out.println(l.getLanguage() + " " + l.getDisplayLanguage() + " " + l.getISO3Language()); System.out.println(l.getVariant() + " " + l.getDisplayVariant()); } } |
Пример 14.26. |
Закрыть окно |
US United States USA en English eng RU Russia RUS ru Russian rus WINDOWS WINDOWS |
Пример 14.27. |
Закрыть окно |
baseclass + "_" + language1 + "_" + country1 + "_" + variant1 baseclass + "_" + language1 + "_" + country1 + "_" + variant1 + ".properties" baseclass + "_" + language1 + "_" + country1 baseclass + "_" + language1 + "_" + country1 + ".properties" baseclass + "_" + language1 baseclass + "_" + language1 + ".properties" baseclass + "_" + language2 + "_" + country2 + "_" + variant2 baseclass + "_" + language2 + "_" + country2 + "_" + variant2 + ".properties" baseclass + "_" + language2 + "_" + country2 baseclass + "_" + language2 + "_" + country2 + ".properties" baseclass + "_" + language2 baseclass + "_" + language2 + ".properties" baseclass baseclass + ".properties" |
Пример 14.28. |
Закрыть окно |
public class MyResource extends ResourceBundle { private Hashtable res = null; public MyResource() { res = new Hashtable(); res.put("TestKey","English Variant"); } public Enumeration getKeys() { return res.keys(); } protected Object handleGetObject(String key) throws java.util.MissingResourceException { return res.get(key); } } public class MyResource_ru_RU extends ResourceBundle { private Hashtable res = null; public MyResource_ru_RU() { res = new Hashtable(); res.put("TestKey","Русский варинат"); } public Enumeration getKeys() { return res.keys(); } protected Object handleGetObject(String key) throws java.util.MissingResourceException { return res.get(key); } } public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); ResourceBundle rb = ResourceBundle.getBundle("experiment.MyResource",Locale.getDefault()); System.out.println(rb.getString("TestKey")); rb = ResourceBundle.getBundle("experiment.MyResource", new Locale("ru","RU")); System.out.println(rb.getString("TestKey")); } } |
Пример 14.29. |
Закрыть окно |
public interface Behavior { public String getBehavior(); public String getCapital(); } public class EnglishBehavior implements Behavior{ public EnglishBehavior() { } public String getBehavior(){ return "English behavior"; } public String getCapital(){ return "London"; } } public class RussianBehavior implements Behavior { public RussianBehavior() { } public String getBehavior(){ return "Русский вариант поведения"; } public String getCapital(){ return "Москва"; } } public class MyResourceBundle_ru_RU extends ResourceBundle { Hashtable bundle = null; public MyResourceBundle_ru_RU() { bundle = new Hashtable(); bundle.put("Bundle description","Набор ресурсов для русской локали"); bundle.put("Behavior",new RussianBehavior()); } public Enumeration getKeys() { return bundle.keys(); } protected Object handleGetObject(String key) throws java.util.MissingResourceException { return bundle.get("key"); } } public class MyResourceBundle_en_EN { Hashtable bundle = null; public MyResourceBundle_en_EN() { bundle = new Hashtable(); bundle.put("Bundle description","English resource set"); bundle.put("Behavior",new EnglishBehavior()); } public Enumeration getKeys() { return bundle.keys(); } protected Object handleGetObject(String key) throws java.util.MissingResourceException { return bundle.get("key"); } } public class MyResourceBundle extends ResourceBundle { Hashtable bundle = null; public MyResourceBundle() { bundle = new Hashtable(); bundle.put("Bundle description","Default resource bundle"); bundle.put("Behavior",new EnglishBehavior()); } public Enumeration getKeys() { return bundle.keys(); } protected Object handleGetObject(String key) throws java.util.MissingResourceException { return bundle.get(key); } } public class Using { public Using() { } public static void main(String[] args) { Using u = new Using(); ResourceBundle rb = ResourceBundle.getBundle("lecture.MyResourceBundle", Locale.getDefault()); System.out.println((String)rb.getObject("Bundle description")); Behavior be = (Behavior)rb.getObject("Behavior"); System.out.println(be.getBehavior()); System.out.println(be.getCapital()); rb = ResourceBundle.getBundle("lecture.MyResourceBundle", new Locale("en","EN")); System.out.println((String)rb.getObject("Bundle description")); Behavior be = (Behavior)rb.getObject("Behavior"); System.out.println(be.getBehavior()); System.out.println(be.getCapital()); } |
Пример 14.30. |
Закрыть окно |
Русский набор ресурсов Русский вариант поведения Москва English resource bundle English behavior London |
Пример 14.31. |
Закрыть окно |
public class MyResource extends ListResourceBundle { Vector v = new Vector(); Object[][] resources = { {"StringKey","String"}, {"DoubleKey",new Double(0.0)}, {"VectorKey",v}, }; public MyResource() { super(); v.add("Element 1"); v.add("Element 2"); v.add("Element 3"); } protected Object[][] getContents() { return resources; } } public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); ResourceBundle rb = ResourceBundle.getBundle("experiment.MyResource",Locale.getDefault()); Vector v = (Vector)rb.getObject("VectorKey"); Iterator it = v.iterator(); while(it.hasNext()) { System.out.println(it.next()); } } } |
Пример 14.32. |
Закрыть окно |
Данный интерфейс является корнем всей иерархии классов-коллекций. Он определяет базовую функциональность любой коллекции – набор методов, которые позволяют добавлять, удалять, выбирать элементы коллекции. Классы, которые реализуют интерфейс Collection, могут содержать дубликаты и пустые (null) значения.
AbstractCollection, как абстрактный класс, служит основой для создания конкретных классов коллекций и содержит реализацию некоторых методов, определенных в интерфейсе Collection.
В коллекциях многие методы сортировки или сравнения требуют передачи в качестве одного из параметров объекта, который реализует интерфейс Comparator. Этот интерфейс определяет единственный метод compare(Object obj1,Object obj2), который на основании определенного пользователем алгоритма сравнивает объекты, переданные в качестве параметров. Метод compare должен вернуть:
-1 если obj1 < obj2 0 если obj1 = obj2 1 если obj1 > obj2
В Java 1 для перебора элементов коллекции использовался интерфейс Enumeration. В Java 2 для этих целей должны применяться объекты, которые реализуют интерфейс Iterator. Все классы, которые реализуют интерфейс Collection, должны реализовать метод iterator, который возвращает объект, реализующий интерфейс Iterator. Iterator весьма похож на Enumeration, с тем лишь отличием, что в нем определен метод remove, который позволяет удалить объект из коллекции, для которой Iterator был создан.
Таким образом, подводя итог, перечислим интерфейсы, используемые при работе с коллекциями:
java.util.Collection java.util.Set java.util.List java.util.Map java.util.SortedSet java.util.SortedMap java.util.Iterator
Классы, реализующие этот интерфейс, содержат упорядоченную последовательность объектов (объекты хранятся в том порядке, в котором они были добавлены). В JDK 1.2 был переделан класс Vector, так, что он теперь реализует интерфейс List. Интерфейс List расширяет интерфейс Collection, и любой класс, имплементирующий List, реализует все методы, определенные в Collection, и в то же время вводятся новые методы, которые позволяют добавлять и удалять элементы из списка. List также обеспечивает ListIterator, который позволяет перемещаться как вперед, так и назад по элементам списка.
AbstractList, как абстрактный класс, представляет собой основу для реализации различных вариантов интерфейса List.
Классы, которые реализуют этот интерфейс, хранят неупорядоченный набор объектов парами ключ/значение. Каждый ключ должен быть уникальным. Hashtable после модификации в JDK 1.2 реализует интерфейс Map. Порядок следования пар ключ/значение не определен.
Интерфейс Map не расширяет интерфейс Collection. AbstractMap, будучи абстрактным классом, представляет собой основу для реализации различных вариантов интерфейса Map.
Интерфейс Observer определяет всего один метод, update (Observable o, Object arg), который вызывается, когда обозреваемый объект изменяется.
Класс Observable предназначен для поддержки обозреваемого объекта в парадигме MVC (model-view-controller), которая, как и другие проектные решения и шаблоны, описана в специальной литературе. Этот класс должен быть унаследован, если возникает необходимость в том, чтобы отслеживать состояние какого-либо объекта. Обозреваемый объект может иметь несколько обозревателей. Соответственно, они должны реализовать интерфейс Observer.
После того, как в состоянии обозреваемого объекта что-то меняется, необходимо вызвать метод notifyObservers, который, в свою очередь, вызывает методы update у каждого обозревателя.
Порядок, в котором вызываются методы update обозревателей, заранее не определен. Реализация по умолчанию подразумевает их вызов в порядке регистрации. Регистрация осуществляется с помощью метода addObserver(Observer o). Удаление обозревателя из списка выполняется с помощью deleteObserver(Observer o). Перед вызовом notifyObservers необходимо вызвать метод setChanged, который устанавливает признак того, что обозреваемый объект был изменен.
Рассмотрим пример организации взаимодействия классов:
Пример 14.13.
(html, txt)
В результате работы на консоль будет выведено:
Пример 14.14.
(html, txt)
На практике использовать Observer не всегда удобно, так как в Java отсутствует множественное наследование и Observer необходимо наследовать в самом начале построения иерархии классов. Как вариант, можно предложить определить интерфейс, задающий функциональность, сходную с Observer, и реализовать его в подходящем классе.
Классы, которые реализуют этот интерфейс, не допускают наличия дубликатов. В коллекции этого типа разрешено наличие только одной ссылки типа null. Интерфейс Set расширяет интерфейс Collection, таким образом, любой класс, имплементирующий Set, реализует все методы, определенные в Collection. Любой объект, добавляемый в Set, должен реализовать метод equals, чтобы его можно было сравнить с другими.
AbstractSet, являясь абстрактным классом, представляет собой основу для реализации различных вариантов интерфейса Set.
Этот интерфейс расширяет Map, требуя, чтобы содержимое коллекции было упорядочено по значениям ключей.
Этот интерфейс расширяет Set, требуя, чтобы содержимое набора было упорядочено. Такие коллекции могут содержать объекты, которые реализуют интерфейс Comparable, либо могут сравниваться с использованием внешнего Comparator.
Основной файл исходных текстов приложения приведен в листинге 1.
Листинг 1. Файл PlayClip.java
import java.applet.*; import java.awt.*;
public class PlayClip extends Applet { private String m_ClipName = "kaas.au"; private final String PARAM_ClipName = "ClipName"; AudioClip auClip; Button btPlay; Button btLoop; Button btStop; boolean fLoopPlay = false;
public String getAppletInfo() { return "Name: PlayClip"; }
public String[][] getParameterInfo() { String[][] info = { { PARAM_ClipName, "String", "Audioclip filename" }, }; return info; }
public void init() { String param; param = getParameter(PARAM_ClipName); if (param != null) m_ClipName = param;
btPlay = new Button("Play"); btLoop = new Button("Loop"); btStop = new Button("Stop");
btStop.disable();
add(btPlay); add(btLoop); add(btStop);
auClip = this.getAudioClip(getCodeBase(), m_ClipName); }
public boolean action(Event evt, Object obj) { Button btn;
if(evt.target instanceof Button) { btn = (Button)evt.target;
if(evt.target.equals(btPlay)) { auClip.play(); btStop.enable(); }
else if(evt.target.equals(btLoop)) { auClip.loop(); fLoopPlay = true; btStop.enable(); }
else if(evt.target.equals(btStop)) { auClip.stop(); fLoopPlay = false; btStop.disable(); }
else { return false; }
return true; }
return false; }
public void paint(Graphics g) { Dimension dimAppWndDimension = size();
g.setColor(Color.yellow); g.fillRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1);
g.setColor(Color.black); g.drawRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); }
public void start() { if(fLoopPlay) auClip.loop(); }
public void stop() { if(fLoopPlay) auClip.stop(); } }
В листинге 2 вы найдете исходный текст документа HTML, созданного автоматически для нашего приложения системой Java WorkShop.
Листинг 2. Файл PlayClip.tmp.html
<applet name="PlayClip" code="PlayClip" codebase= "file:/e:/sun/articles/vol14/src/PlayClip" width="200" height="100" align="Top" alt="If you had a java-enabled browser, you would see an applet here."> <param name="ClipName" value="kaas.au"> <hr> If your browser recognized the applet tag, you would see an applet here. <hr> </applet>
Статический класс Arrays обеспечивает набор методов для выполнения операций над массивами, таких, как поиск, сортировка, сравнение. В Arrays также определен статический метод public List aList(a[] arr), который возвращает список фиксированного размера, основанный на массиве. Изменения в List можно внести, изменив данные в массиве.
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); String[] arr = {"String 1","String 4", "String 2","String 3"}; test.dumpArray(arr); Arrays.sort(arr); test.dumpArray(arr); int ind = Arrays.binarySearch(arr, "String 4"); System.out.println( "\nIndex of \"String 4\" = " + ind); } void dumpArray(String arr[]){ System.out.println(); for(int cnt=0;cnt < arr.length;cnt++) { System.out.println(arr[cnt]); } } }
Класс BitSet предназначен для работы с последовательностями битов. Каждый компонент этой коллекции может принимать булево значение, которое обозначает, установлен бит или нет. Содержимое BitSet может быть модифицировано содержимым другого BitSet с использованием операций AND, OR или XOR (исключающее или).
BitSet имеет текущий размер (количество установленных битов), может динамически изменяться. По умолчанию все биты в наборе устанавливаются в 0 (false). Установка и очистка битов в BitSet осуществляется методами set(int index) и clear(int index).
Метод int length() возвращает "логический" размер набора битов, int size() возвращает количество памяти, занимаемой битовой последовательностью BitSet.
public class Test {
public Test() { } public static void main(String[] args) { Test test = new Test(); BitSet bs1 = new BitSet(); BitSet bs2 = new BitSet(); bs1.set(0); bs1.set(2); bs1.set(4); System.out.println("Length = " + bs1.length()+" size = "+bs1.size()); System.out.println(bs1); bs2.set(1); bs2.set(2); bs1.and(bs2); System.out.println(bs1); } }
Результатом будет:
Length = 5 size = 64 {0, 2, 4} {2}
Проанализировав первую строку вывода на консоль, можно сделать вывод, что для внутреннего представления BitSet использует значения типа long.
Класс Collections является классом-утилитой и содержит несколько вспомогательных методов для работы с классами, обеспечивающими различные интерфейсы коллекций. Например, для сортировки элементов списков, для поиска элементов в упорядоченных коллекциях и т.д. Но, пожалуй, наиболее важным свойством этого класса является возможность получения синхронизированных вариантов классов-коллекций. Например, для получения синхронизированного варианта Map можно использовать следующий подход:
HashMap hm = new HashMap(); … Map syncMap = Collections.synchronizedMap(hm); …
Как уже отмечалось ранее, начиная с JDK 1.2, класс Vector реализует интерфейс List. Рассмотрим пример сортировки элементов, содержащихся в классе Vector.
Пример 14.21.
(html, txt)
Класс Date изначально предоставлял набор функций для работы с датой – для получения текущего года, месяца и т.д. Однако сейчас все перечисленные методы не рекомендованы к использованию и практически всю функциональность для этого предоставляет класс Calendar.
Существует несколько конструкторов класса Date, однако рекомендовано к использованию два:
Date() и Date(long date)
Второй конструктор принимает в качестве параметра значение типа long, указывающее на количество миллисекунд, прошедших с 1 января 1970 г., 00:00:00 по Гринвичу. Первый конструктор создает экземпляр, соответствующий текущему моменту. Фактически это эквивалентно второму варианту new Date(System.currentTimeMillis()). Можно уже после создания экземпляра класса Date использовать метод setTime(long time) для того, чтобы задать нужное время.
Для сравнения дат служат методы after(Date date) и before(Date date), которые возвращают булевское значение, в зависимости от того, выполнено условие или нет. Метод compareTo(Date anotherDate) возвращает значение типа int, которое равно -1, если дата меньше сравниваемой, 1 – если больше и 0 – если даты равны. Метод toString() возвращает строковое описание даты. Однако для более понятного и удобного преобразования даты в текст рекомендуется пользоваться классом SimpleDateFormat, определенным в пакете java.text.
Класс Locale предназначен для отображения определенного региона. Под регионом принято понимать не только географическое положение, но также языковую и культурную среду. Например, помимо того, что указывается страна Швейцария, можно указать также и язык – французский или немецкий.
Определено два варианта конструкторов в классе Locale:
Locale(String language, String country) Locale(String language, String country, String variant)
Первые два параметра в обоих конструкторах определяют язык и страну, для которой определяется локаль, согласно кодировке ISO. Список поддерживаемых стран и языков можно получить и с помощью вызова статических методов Locale.getISOLanguages() Locale.getISOCountries(), соответственно. Во втором варианте конструктора указан также строковый параметр variant, в котором кодируется информация о платформе. Если здесь необходимо указать дополнительные параметры, то их требуется разделить символом подчеркивания, причем, более важный параметр должен следовать первым.
Пример использования:
Locale l = new Locale("ru","RU"); Locale l = new Locale("en","US","WINDOWS");
Статический метод getDefault() возвращает текущую локаль, сконструированную на основе настроек операционной системы, под управлением которой функционирует JVM.
Для наиболее часто использующихся локалей заданы константы. Например, Locale.US или Locale.GERMAN.
После того как экземпляр класса Locale создан, с помощью различных методов можно получить дополнительную информацию о локали.
Пример 14.26.
(html, txt)
Результатом будет:
Пример 14.27.
(html, txt)
Класс Properties предназначен для хранения набора свойств (параметров). Методы
String getProperty(String key) String getProperty(String key, String defaultValue)
позволяют получить свойство из набора.
С помощью метода setProperty(String key, String value) это свойство можно установить.
Метод load(InputStream inStream) позволяет загрузить набор свойств из входного потока (потоки данных подробно рассматриваются в лекции 15). Как правило, это текстовый файл, в котором хранятся параметры. Параметры – это строки, которые представляют собой пары ключ/значение. Предполагается, что по умолчанию используется кодировка ISO 8859-1. Каждая строка должна оканчиваться символами \r,\n или \r\n. Строки из файла будут считываться до тех пор, пока не будет достигнут его конец. Строки, состоящие из одних пробелов, или начинающиеся со знаков ! или #, игнорируются, т.е. их можно трактовать как комментарии. Если строка оканчивается символом /, то следующая строка считается ее продолжением. Первый символ с начала строки, отличный от пробела, считается началом ключа. Первый встретившийся пробел, двоеточие или знак равенства считается окончанием ключа. Все символы окончания ключа при необходимости могут быть включены в название ключа, но при этом перед ними должен стоять символ \. После того, как встретился символ окончания ключа, все аналогичные символы будут проигнорированы до начала значения. Оставшаяся часть строки интерпретируется как значение. В строке, состоящей только из символов \t, \n, \r, \\, \", \', \ и \uxxxx, они все распознаются и интерпретируются как одиночные символы. Если встретится сочетание \ и символа конца строки, то следующая строка будет считаться продолжением текущей, также будут проигнорированы все пробелы до начала строки-продолжения.
Метод save(OutputStream inStream,String header) сохраняет набор свойств в выходной поток в виде, пригодном для вторичной загрузки с помощью метода load. Символы, считающиеся служебными, кодируются так, чтобы их можно было считать при вторичной загрузке. Символы в национальной кодировке будут приведены к виду \uxxxx. При сохранении используется кодировка ISO 8859-1. Если указан header, то он будет помещен в начало потока в виде комментария (т.е. с символом # в начале), далее будет следовать комментарий, в котором будет указано время и дата сохранения свойств в потоке.
В классе Properties определен еще метод list(PrintWriter out), который практически идентичен save. Отличается лишь заголовок, который изменить нельзя. Кроме того, строки усекаются по ширине. Поэтому данный метод для сохранения Properties не годится.
Пример 14.22.
(html, txt)
Результатом будет:
Пример 14.23.
(html, txt)
Класс Random используется для получения последовательности псевдослучайных чисел. В качестве "зерна" применяется 48-битовое число. Если для инициализации Random задействовать одно и то же число, будет получена та же самая последовательность псевдослучайных чисел.
В классе Random определено также несколько методов, которые возвращают псевдослучайные величины для примитивных типов Java.
Дополнительно следует отметить наличие двух методов: double nextGaussian() – возвращает случайное число в диапазоне от 0.0 до 1.0 распределенное по нормальному закону, и void nextBytes(byte[] arr) – заполняет массив arr случайными величинами типа byte.
Пример использования Random:
Пример 14.24.
(html, txt)
Результатом будет:
Пример 14.25.
(html, txt)
Абстрактный класс ResourceBundle предназначен для хранения объектов, специфичных для локали. Например, когда необходимо получить набор строк, зависящих от локали, используют ResourceBundle.
Применение ResourceBundle настоятельно рекомендуется, если предполагается использовать программу в многоязыковой среде. С помощью этого класса легко манипулировать наборами ресурсов, зависящих от локалей, их можно менять, добавлять новые и т.д.
Набор ресурсов – это фактически набор классов, имеющих одно базовое имя. Далее наименование класса дополняется наименованием локали, с которой связывается этот класс. Например, если имя базового класса будет MyResources, то для английской локали имя класса будет MyResources_en, для русской – MyResources_ru. Помимо этого, может добавляться идентификатор языка, если для данного региона определено несколько языков. Например, MyResources_de_CH – так будет выглядеть швейцарский вариант немецкого языка. Кроме того, можно указать дополнительный признак variant (см. описание Locale). Так, описанный раннее пример для платформы UNIX будет выглядеть следующим образом: MyResources_de_CH_UNIX .
Загрузка объекта для нужной локали производится с помощью статического метода getBundle.:
ResourceBundle myResources = ResourceBundle.getBundle("MyResources", someLocale);
На основе указанного базового имени (первый параметр), указанной локали (второй параметр) и локали по умолчанию (задается настройками ОС или JVM) генерируется список возможных имен ресурса. Причем, указанная локаль имеет более высокий приоритет, чем локаль по умолчанию. Если обозначить составляющие указанной локали (язык, страна, вариант) как 1, а локали по умолчанию – 2, то список примет следующий вид:
Пример 14.28.
(html, txt)
Например, если необходимо найти ResourceBundle для локали fr_CH (Швейцарский французский), а локаль по умолчанию en_US, при этом название базового класса ResourceBundle MyResources, то порядок поиска подходящего ResourceBundle будет таков.
MyResources_fr_CH MyResources_fr MyResources_en_US MyResources_en MyResources
Класс SimpleTimeZone, как потомок TimeZone, реализует его абстрактные методы и предназначен для применения в настройках, использующих Григорианский календарь. В большинстве случаев нет необходимости создавать экземпляр данного класса с помощью конструктора. Вместо этого лучше использовать статические методы, которые возвращают тип TimeZone, рассмотренные в предыдущем параграфе. Единственная, пожалуй, причина для использования конструктора – необходимость задания нестандартных правил перехода на зимнее и летнее время.
В классе SimpleTimeZone определено три конструктора. Рассмотрим наиболее полный с точки зрения функциональности вариант, который, помимо временной зоны, задает летнее и зимнее время.
public SimpleTimeZone(int rawOffset, String ID, int startMonth, int startDay, int startDayOfWeek, int startTime, int endMonth, int endDay, int endDayOfWeek, int endTime)
rawOffset – временное смещение относительно гринвича;
ID – идентификатор временной зоны (см. пред.параграф);
startMonth – месяц перехода на летнее время;
startDay – день месяца перехода на летнее время*;
startDayOfWeek – день недели перехода на летнее время*;
startTime – время перехода на летнее время (указывается в миллисекундах);
endMonth – месяц окончания действия летнего времени;
endDay – день окончания действия летнего времени*;
endDayOfWeek– день недели окончания действия летнего времени*;
endTime – время окончания действия летнего времени (указывается в миллисекундах).
Перевод часов на зимний и летний вариант исчисления времени определяется специальным правительственным указом. Обычно переход на летнее время происходит в 2 часа в последнее воскресенье марта, а переход на зимнее время – в 3 часа в последнее воскресенье октября.
Алгоритм расчета таков:
если startDay=1 и установлен день недели, то будет вычисляться первый день недели startDayOfWeek месяца startMonth (например, первое воскресенье); если startDay=-1 и установлен день недели, то будет вычисляться последний день недели startDayOfWeek месяца startMonth (например, последнее воскресенье); если день недели startDayOfWeek установлен в 0, то будет вычисляться число startDay конкретного месяца startMonth; для того, чтобы установить день недели после конкретного числа, специфицируется отрицательное значение дня недели. Например, чтобы указать первый понедельник после 23 февраля, используется вот такой набор: startDayOfWeek=-MONDAY, startMonth=FEBRUARY, startDay=23 для того, чтобы указать последний день недели перед каким-либо числом, указывается отрицательное значение этого числа и отрицательное значение дня недели. Например, для того, чтобы указать последнюю субботу перед 23 февраля, необходимо задать такой набор параметров: startDayOfWeek=-SATURDAY, startMonth=FEBRUARY, startDay=-23; все вышеперечисленное относится также и к окончанию действия летнего времени.
Рассмотрим пример получения текущей временной зоны с заданием перехода на зимнее и летнее время для России по умолчанию.
Пример 14.11.
(html, txt)
Результатом будет:
Пример 14.12.
(html, txt)
Этот класс предназначен для разбора строки по лексемам (tokens). Строка, которую необходимо разобрать, передается в качестве параметра конструктору StringTokenizer(String str). Определено еще два перегруженных конструктора, которым дополнительно можно передать строку-разделитель лексем StringTokenizer(String str, String delim) и признак возврата разделителя лексем StringTokenizer(String str, String delim, Boolean returnDelims).
Разделителем лексем по умолчанию служит пробел.
public class Test {
public Test() { } public static void main(String[] args) { Test test = new Test(); String toParse = "word1;word2;word3;word4"; StringTokenizer st = new StringTokenizer(toParse,";"); while(st.hasMoreTokens()){ System.out.println(st.nextToken()); } } }
Результатом будет:
word1 word2 word3 word4
Класс TimeZone предназначен для совместного использования с классами Calendar и DateFormat. Класс абстрактный, поэтому от него порождать объекты нельзя. Вместо этого определен статический метод getDefault(), который возвращает экземпляр наследника TimeZone с настройками, взятыми из операционной системы, под управлением которой работает JVM. Для того, чтобы указать произвольные параметры, можно воспользоваться статическим методом getTimeZone(String ID), в качестве параметра которому передается наименование конкретного временного пояса, для которого необходимо получить объект TimeZone. Набор полей, определяющих возможный набор параметров для getTimeZone, нигде явно не описывается. Вместо этого определен статический метод String[] getAvailableIds(), который возвращает возможные значения для параметра getTimeZone. Так можно определить набор возможных параметров для конкретного временного пояса (рассчитывается относительно Гринвича) String[] getAvailableIds(int offset).
Рассмотрим пример, в котором на консоль последовательно выводятся:
временная зона по умолчанию; список всех возможных временных зон; список временных зон, которые совпадают с текущей временной зоной.
Пример 14.9.
(html, txt)
Результатом будет:
Пример 14.10.
(html, txt)
Более развитые средства для работы с датами представляет класс Calendar. Calendar является абстрактным классом. Для различных платформ реализуются конкретные подклассы календаря. На данный момент существует реализация Григорианского календаря – GregorianCalendar. Экземпляр этого класса получается путем вызова статического метода getInstance(), который возвращает экземпляр класса GregorianCalendar. Подклассы класса Calendar должны интерпретировать объект Date по-разному. В будущем предполагается реализовать также лунный календарь, используемый в некоторых странах.
Calendar обеспечивает набор методов, позволяющих манипулировать различными "частями" даты, т.е. получать и устанавливать дни, месяцы, недели и т.д.
Если при задании параметров календаря некоторые параметры упущены, то для них будут использованы значения по умолчанию для начала отсчета, т.е.
YEAR = 1970, MONTH = JANUARY, DATE = 1 и т.д.
Для считывания и установки различных "частей" даты используются методы get(int field), set(int field, int value), add(int field, int amount), roll(int field, int amount), переменная типа int с именем field указывает на номер поля, с которым нужно произвести операцию. Для удобства все эти поля определены в Calendar как статические константы типа int.
Рассмотрим подробнее порядок выполнения перечисленных методов.
У класса ResourceBundle определено два прямых потомка ListResourceBundle и PropertiesResourceBundle. PropertiesResourceBundle хранит набор ресурсов в файле, который представляет собой набор строк.
Алгоритм конструирования объекта, содержащего набор ресурсов, был описан в предыдущем параграфе. Во всех случаях, когда в качестве последнего элемента используется .properties, например, baseclass + "_" + language1 + "_" + country1 + ".properties", речь идет о создании ResourceBundle из файла с наименованием baseclass + "_" + language1 + "_" + country1 и расширением properties. Обычно класс ResourceBundle помещают в пакет resources, а файл свойств – в каталог resources. Тогда для того, чтобы инстанциировать нужный класс, необходимо указать полный путь к этому классу (файлу):
getBundle("resources.MyResource", Locale.getDefault());
ListResourceBundle хранит набор ресурсов в виде коллекции и является абстрактным классом. Классы, которые наследуют ListResourceBundle, должны обеспечить:
переопределение метода Object[][] getContents(), который возвращает массив ресурсов; собственно двумерный массив, содержащий ресурсы.
Рассмотрим пример:
Пример 14.32.
(html, txt)
Результатом будет:
Element 1 Element 2 Element 3
Создание ресурсов для локалей, отличных от локали по умолчанию, осуществляется так же, как было показано для ResourceBundle.
Зачастую в программе работа идет не с одним объектом, а с целой группой более или менее однотипных экземпляров (например, автопарк организации). Проще всего сделать это с помощью массивов. Однако, несмотря на то, что это достаточно эффективное решение для многих случаев, оно имеет некоторые ограничения. Так, обращаться к элементу массива можно только по его номеру (индексу). Также необходимо заранее задать длину массива и больше ее не менять.
Массивы существовали в Java изначально. Кроме того, было определено два класса для организации более эффективной работы с наборами объектов: Hashtable и Vector. В JDK 1.2 набор классов, поддерживающих работу с коллекциями, был существенно расширен.
Существует несколько различных типов классов-коллекций. Все они разрабатывались, по возможности, в соответствии с единой логикой и определенными интерфейсами и там, где это возможно, работа с ними унифицирована. Однако все коллекции отличаются внутренними механизмами хранения, скоростью доступа к элементам, потребляемой памятью и другими деталями. Например, в некоторых коллекциях объекты (также называемые элементами коллекций), могут быть упорядочены, в некоторых – нет. В некоторых типах коллекций допускается дублирование ссылок на объект, в некоторых – нет. Далее мы рассмотрим каждый из классов-коллекций.
Классы, обеспечивающие манипулирование коллекциями объектов, объявлены в пакете java.util.
java.util.ArrayList – этот класс расширяет AbstractList и весьма похож на класс Vector. Он также динамически расширяется, как Vector, однако его методы не являются синхронизированными, вследствие чего операции с ним выполняются быстрее. Для того, чтобы воспользоваться синхронизированной версией ArrayList, можно применить вот такую конструкцию:
Пример 14.15.
(html, txt)
Результатом будет:
Пример 14.16.
(html, txt)
java.util.LinkedList – представляет собой реализацию интерфейса List. Он реализует все методы интерфейса List, помимо этого добавляются еще новые методы, которые позволяют добавлять, удалять и получать элементы в конце и начале списка. LinkedList является двухсвязным списком и позволяет перемещаться как от начала в конец списка, так и наоборот. LinkedList удобно использовать для организации стека.
Пример 14.17.
(html, txt)
Результатом будет:
Пример 14.18.
(html, txt)
Классы LinkedList и ArrayList имеют схожую функциональность. Однако с точки зрения производительности они отличаются. Так, в ArrayList заметно быстрей (примерно на порядок) осуществляются операции прохода по всему списку (итерации) и получения данных. LinkedList почти на порядок быстрее выполняет операции удаления и добавления новых элементов.
java.util.Hashtable – расширяет абстрактный класс Dictionary. В JDK 1.2 класс Hashtable также реализует интерфейс Map. Hashtable предназначен для хранения объектов в виде пар ключ/значение. Из самого названия следует, что Hаshtable использует алгоритм хэширования для увеличения скорости доступа к данным. Для того, чтобы выяснить принципы работы данного алгоритма, рассмотрим несколько примеров.
Предположим, имеется массив строк, содержащий названия городов. Для того, чтобы найти элемент массива, содержащий название города, в общем случае требуется просмотреть весь массив, а если необходимо найти все элементы массива, то для поиска каждого, в среднем, потребуется просматривать половину массива. Такой подход может оказаться приемлемым только для небольших массивов.
Как уже отмечалось ранее, для того, чтобы увеличить скорость поиска, используется алгоритм хэширования. Каждый объект в Java унаследован от Object. Как уже отмечалось ранее, hash определено как целое число, которое уникально идентифицирует экземпляр класса Object и, соответственно, все экземпляры классов, унаследованных от Object. Это число возвращает метод hashCode(). Именно оно используется при сохранении ключа в Hashtable следующим образом: разделив длину массива, предназначенного для хранения ключей, на код, получаем некое целое число, которое служит индексом для хранения ключа в массиве array.length % hashCode().
Далее, если необходимо добавить новую пару ключ/значение, вычисляется новый индекс, и если этот индекс совпадает с уже имеющимся, то создается список ключей, на который указывает элемент массива ключей. Таким образом, при обратном извлечении ключа необходимо вычислить индекс массива по тому же алгоритму и получить его. Если ключ в массиве единственный, то используется значение элемента массива, если хранится несколько ключей, то необходимо обойти список и выбрать нужный.
Есть несколько соображений, относящихся к производительности классов, использующих для хранения данных алгоритм хэширования. В частности, размер массива. Если массив окажется слишком мал, то связанные списки будут слишком длинными и скорость поиска станет существенно снижаться, так как просмотр элементов списка будет такой же, как в обычном массиве. Чтобы этого избежать, задается некий коэффициент заполнения. При заполнении элементов массива, в котором хранятся ключи (или списки ключей) на эту величину, происходит увеличение массива и производится повторное реиндексирование. Таким образом, если массив окажется слишком мал, то он будет быстро заполняться и будет производиться операция повторного индексирования, которая отнимает достаточно много ресурсов. С другой стороны, если массив сделать большим, то при необходимости просмотреть последовательно все элементы коллекции, использующей алгоритм хэширования, придется обрабатывать большое количество пустых элементов массива ключей.
Начальный размер массива и коэффициент загрузки коллекции задаются при конструировании. Например:
Метод action получает управление, когда пользователь нажимает на одну из кнопок, расположенных в окне аплета. В зависимости от того, какая именно кнопка была нажата, выполняются различные действия.
Если пользователь нажал кнопку Play, вызывается метод play для запуска однократного проигрывания звукового файла:
auClip.play(); btStop.enable();
Сразу после того как проигрывание будет запущено, приложение разблокирует кнопку Stop, предоставляя пользователю возможность прервать звучание.
В том случае, когда пользователь нажал кнопку Loop, вызывается метод loop, запусчкающий проигрывание звукового файла в цикле:
auClip.loop(); fLoopPlay = true; btStop.enable();
После запуска устанавливается флаг fLoopPlay и разблокируется кнопка Stop.
И, наконец, если пользователь нажимает кнопку Stop, выполняется остановка проигрывания методом stop:
auClip.stop(); fLoopPlay = false; btStop.disable();
Флаг fLoopPlay сбрасывается, после чего кнопка Stop блокируется.
Добавляет некоторое смещение к существующей величине поля. В принципе, то же самое можно сделать с помощью set(f, get(f) + delta).
В случае использования метода add следует помнить о двух правилах:
Если величина поля изменения выходит за диапазон возможных значений данного поля, то производится деление по модулю данной величины, частное суммируется со следующим по старшинству полем. Если изменяется одно из полей, причем, после изменения младшее по отношению к изменяемому полю принимает некорректное значение, то оно изменяется на то, которое максимально близко к "старому".
Пример 14.5.
(html, txt)
Результатом будет:
Пример 14.6.
(html, txt)
Метод getParameterInfo возвращает описание единственного параметра нашего аплета, через который передается имя звукового файла.
Сразу после запуска аплета метод init получает значение параметра - имя звукового файла, и если этот параметр задан в документе HTML, записывает полученное имя в поле m_ClipName:
param = getParameter(PARAM_ClipName); if(param != null) m_ClipName = param;
Далее создаются три кнопки, управляющие звучанием аплета:
btPlay = new Button("Play"); btLoop = new Button("Loop"); btStop = new Button("Stop");
Кнопка Stop блокируется, так как на данный момент проигрывание еще не запущено:
btStop.disable();
Для блокирования вызывается метод disable, определенный в классе Button.
Подготовленные таким образом кнопки добавляются в окно аплета:
add(btPlay); add(btLoop); add(btStop);
Последнее, что делает метод init перед тем как возвратить управление, это получение ссылки на интерфейс AudioClip:
auClip = this.getAudioClip( getCodeBase(),m_ClipName);
Адрес URL каталога, в котором расположен аплет, определяется с помощью метода getCodeBase, о котором мы говорили в предыдущей главе.
Добавляет некоторое смещение к существующей величине поля и не производит изменения старших полей. Рассмотрим приведенный ранее пример, но с использованием метода roll.
Пример 14.7.
(html, txt)
Результатом будет:
Пример 14.8.
(html, txt)
Как видно из результатов работы приведенного выше кода, действие правила 1 изменилось по сравнению с методом add, а правило 2 действует так же.
Как уже говорилось, данный метод производит установку какого-либо поля даты. На самом деле после вызова этого метода немедленного пересчета даты не производится. Пересчет даты будет осуществлен только после вызова методов get(), getTime() или getTimeInMillis(). Таким образом, последовательная установка нескольких полей не вызовет ненужных вычислений. Помимо этого, появляется еще один интересный эффект. Рассмотрим следующий пример. Предположим, что дата установлена на последний день августа. Необходимо перевести ее на последний день сентября. Если бы внутреннее представление даты изменялось после вызова метода set, то при последовательной установке полей мы получили бы вот такой эффект:
Пример 14.1.
(html, txt)
Результатом будет:
Пример 14.2.
(html, txt)
Как мы видим, в данном примере при изменении месяца день месяца остался неизменным и было унаследовано его предыдущее значение. Но поскольку в сентябре 30 дней, дата автоматически была переведена на 1 октября, и когда было бы установлено 30 число, оно относилось бы уже к октябрю. В следующем примере считывание даты не производится, соответственно, ее вычисление не выполняется до тех пор, пока все поля не установлены:
Пример 14.3.
(html, txt)
Результатом будет:
Пример 14.4.
(html, txt)
Метод start получает управление при первом запуска аплета, а также когда страница документа появляется вновь после того как пользователь временно переходил к просмотру другой страницы.
Наша реализация метода start возобновляет циклическое проигрывание, если оно выполнялось, когда пользователь покинул страницу с аплетом:
if(fLoopPlay) auClip.loop();
В главном классе аплета определено несколько полей и методов. Рассмотрим эти поля и наиболее важные методы.
В поле m_ClipName хранится имя звукового файла, которое передается через параметр ClipName из документа HTML. По умолчанию для этого параметра используется значение kaas.au.
Строка PARAM_ClipName хранит имя указанного выше параметра.
Ссылка на интерфейс AudioClip хранится в поле auClip:
AudioClip auClip;
Следующие три поля хранят ссылки на кнопки, предназначенные для управления проигрыванием звукового файла:
Button btPlay; Button btLoop; Button btStop;
Поле fLoopPlay типа boolean используется для флага, которым отмечается режим проигрывания звукового файла в цикле.
Назад Вперед
Работа со звуковыми файлами во многом напоминает работу с растровыми графическими файлами. Вначале вы должны получить ссылку на интерфейс AudioClip, а затем, пользуясь его методами, вы сможете выполнять проигрывание содержимого этого файла.
Для получения интерфейса AudioClip вы должны воспользоваться одним из двух вариантов метода getAudioClip, определенных в классе Applet:
public AudioClip getAudioClip(URL url): public AudioClip getAudioClip(URL url, String name);
Первый вариант метода предполагает указание адреса URL звукового файла через единственный параметр, второй допускает раздельное указание адреса URL каталога, содержащего файл, и имени файла.
В документации на метод getAudioClip сказано, что этот метод фактически не выполняет загрузку звуковых данных, а только возвращает ссылку на интерфейс AudioClip и немедленно возвращает управление. Загрузка звуковых данных выполняется методами, предназначенными для проигрывания файла.
Однако в книге "The Java Tutorial. Object-Oriented Programming for the Internet", подготовленной специалистами группы JavaSoft, утверждается, что текущие реализации Java работают по другому: метод getAudioClip возвращает управление только после завершения загрузки звукового файла. Очевидно, вам не стоит полагаться на то, что так будет всегда. В тех случаях, когда нежелательно блокирование работы аплета на время загрузки звукового файла, загрузку и проигрывание следует выполнять в отдельном потоке.
Интерфейс AudioClip определен следующим образом:
public interface java.applet.AudioClip { public abstract void play(); public abstract void loop(); public abstract void stop(); }
Метод play запускает однократное проигрывание звукового файла, которое выполняется от начала файла и до его конца.
Метод loop запускает проигрывание звукового файла в цикле, которое будет продолжаться до тех пор, пока вы не остановите его, вызвав метод stop.
Метод stop, как нетрудно догадаться из его названия, останавливает проигрывание звукового файла, как однократное, так и выполняемое в цикле.
© 2003-2007 INTUIT.ru. Все права защищены. |
Назад Вперед
Нельзя сказать, что звуковые возможности аплетов Java чрезмерно велики. Скорее наоборот, они минимальны. Тем не менее, аплеты могут проигрывать звуковые клипы, записанные в файлах формата AU, который пришел из мира компьютеров фирмы Sun.
Сказанное, однако, не означает, что если у вас нет рабочей станции Sun, то вы не сможете озвучить свои аплеты. Во-первых, в сети Internet можно найти много готовых звуковых файлов AU, а во-вторых, там же есть программы для преобразования форматов звуковых файлов. Одну из таких условно-бесплатных программ, которая называется GoldWave, вы можете загрузить с сервера ftp.winsite.com.
Назад Вперед
Контакты
О компании
Новости
Вакансии
Правовые аспекты
Условия использования
Торговые марки
Copyright 1994-2005 Sun Microsystems, Inc.
printmenus();
Программные продуктыРабочие станции и тонкие клиенты
Серверы
Системы хранения данных
Посмотреть все
»Java 2 Standard Edition
Developer Tools
Top Downloads
New Downloads
Патчи и обновления
Посмотреть все
»Истории успеха
The Sun Grid
Партнерские программы
Посмотреть все
»Программы SunSpectrum
Консалтинг
Услуги инсталляции
Поддержка ПО
Посмотреть все
»Сертификация
Авторизованные учебные центры
Посмотреть все
»События
Lab Downloads
Посмотреть все
»На практике при считывании с внешних устройств ввод данных почти всегда необходимо буферизировать. Для буферизации данных служат классы BufferedInputStream и BufferedOutputStream.
BufferedInputStream содержит массив байт, который служит буфером для считываемых данных. То есть когда байты из потока считываются либо пропускаются (метод skip()), сначала заполняется буферный массив, причем, из надстраиваемого потока загружается сразу много байт, чтобы не требовалось обращаться к нему при каждой операции read или skip. Также класс BufferedInputStream добавляет поддержку методов mark() и reset(). Эти методы определены еще в классе InputStream, но там их реализация по умолчанию бросает исключение IOException. Метод mark() запоминает точку во входном потоке, а вызов метода reset() приводит к тому, что все байты, полученные после последнего вызова mark(), будут считываться повторно, прежде, чем новые байты начнут поступать из надстроенного входного потока.
BufferedOutputStream предоставляет возможность производить многократную запись небольших блоков данных без обращения к устройству вывода при записи каждого из них. Сначала данные записываются во внутренний буфер. Непосредственное обращение к устройству вывода и, соответственно, запись в него, произойдет, когда буфер заполнится. Инициировать передачу содержимого буфера на устройство вывода можно и явным образом, вызвав метод flush(). Так же буфер освобождается перед закрытием потока. При этом будет закрыт и надстраиваемый поток (так же поступает BufferedInputStream).
Следующий пример наглядно демонстрирует повышение скорости считывания данных из файла с использованием буфера:
Пример 15.7.
(html, txt)
Результатом могут быть, например, такие значения:
Пример 15.8.
(html, txt)
В данном случае не производилось никаких дополнительных вычислений, занимающих процессорное время, только запись и считывание из файла. При этом считывание с использованием буфера заняло в 10 (!) раз меньше времени, чем аналогичное без буферизации. Для более быстрого выполнения программы запись в файл производилась с буферизацией, однако ее влияние на скорость записи нетрудно проверить, убрав из программы строку, создающую BufferedOutputStream.
Классы BufferedI/OStream добавляют только внутреннюю логику обработки запросов, но не добавляют никаких новых методов. Следующие два фильтра предоставляют некоторые дополнительные возможности для работы с потоками.
До сих пор речь шла только о считывании и записи в поток данных в виде byte. Для работы с другими примитивными типами данных Java определены интерфейсы DataInput и DataOutput и их реализации – классы-фильтры DataInputStream и DataOutputStream. Их место в иерархии классов ввода/вывода можно увидеть на рис.15.1.
Интерфейсы DataInput и DataOutput определяют, а классы DataInputStream и DataOutputStream, соответственно, реализуют методы считывания и записи значений всех примитивных типов. При этом происходит конвертация этих данных в набор byte и обратно. Чтение необходимо организовать так, чтобы данные запрашивались в виде тех же типов, в той же последовательности, как и производилась запись. Если записать, например, int и long, а потом считывать их как short, чтение будет выполнено корректно, без исключительных ситуаций, но числа будут получены совсем другие.
Это наглядно показано в следующем примере:
Пример 15.9.
(html, txt)
Результат выполнения программы:
Чтение в правильной последовательности:
readByte: -128 readInt: 128 readLong: 128 readDouble: 128.0
Чтение в измененной последовательности:
readInt: -2147483648 readDouble: -0.0 readLong: -9205252085229027328
Итак, значение любого примитивного типа может быть передано и считано из потока данных.
byte[] bytesToWrite = {1, 2, 3}; byte[] bytesReaded = new byte[10]; String fileName = "d:\\test.txt"; try { // Создать выходной поток FileOutputStream outFile = new FileOutputStream(fileName); System.out.println("Файл открыт для записи"); // Записать массив outFile.write(bytesToWrite); System.out.println("Записано: " + bytesToWrite.length + " байт"); // По окончании использования должен быть закрыт outFile.close(); System.out.println("Выходной поток закрыт"); // Создать входной поток FileInputStream inFile = new FileInputStream(fileName); System.out.println("Файл открыт для чтения"); // Узнать, сколько байт готово к считыванию int bytesAvailable = inFile.available(); System.out.println("Готово к считыванию: " + bytesAvailable + " байт"); // Считать в массив int count = inFile.read(bytesReaded,0,bytesAvailable); System.out.println("Считано: " + count + " байт"); for (i=0;i<count;i++) System.out.print(bytesReaded[i]+","); System.out.println(); inFile.close(); System.out.println("Входной поток закрыт"); } catch (FileNotFoundException e) { System.out.println("Невозможно произвести запись в файл: " + fileName); } catch (IOException e) { System.out.println("Ошибка ввода/вывода: " + e.toString()); } |
Пример 15.1. |
Закрыть окно |
Файл открыт для записи Записано: 3 байт Выходной поток закрыт Файл открыт для чтения Готово к считыванию: 3 байт Считано: 3 байт 1,2,3, Входной поток закрыт |
Пример 15.2. |
Закрыть окно |
try { int countRead = 0; byte[] toRead = new byte[100]; PipedInputStream pipeIn = new PipedInputStream(); PipedOutputStream pipeOut = new PipedOutputStream(pipeIn); // Считывать в массив, пока он полностью не будет заполнен while(countRead<toRead.length) { // Записать в поток некоторое количество байт for(int i=0; i<(Math.random()*10); i++) { pipeOut.write((byte)(Math.random()*127)); } // Считать из потока доступные данные, // добавить их к уже считанным. int willRead = pipeIn.available(); if(willRead+countRead>toRead.length) //Нужно считать только до предела массива willRead = toRead.length-countRead; countRead += pipeIn.read(toRead, countRead, willRead); } } catch (IOException e) { System.out.println ("Impossible IOException occur: "); e.printStackTrace(); } |
Пример 15.3. |
Закрыть окно |
// inStream - объект класса PipedInputStream try { while(true) { byte[] readedBytes = null; synchronized(inStream) { int bytesAvailable = inStream.available(); readedBytes = new byte[bytesAvailable]; inStream.read(readedBytes); } // обработка полученных данных из readedBytes // … } catch(IOException e) { /* IOException будет брошено, когда поток inStream, либо связанный с ним PipedOutputStream, уже закрыт, и при этом производится попытка считывания из inStream */ System.out.println("работа с потоком inStream завершена"); } |
Пример 15.4. |
Закрыть окно |
FileInputStream inFile1 = null; FileInputStream inFile2 = null; SequenceInputStream sequenceStream = null; FileOutputStream outFile = null; try { inFile1 = new FileInputStream("file1.txt"); inFile2 = new FileInputStream("file2.txt"); sequenceStream = new SequenceInputStream(inFile1, inFile2); outFile = new FileOutputStream("file3.txt"); int readedByte = sequenceStream.read(); while(readedByte!=-1){ outFile.write(readedByte); readedByte = sequenceStream.read(); } } catch (IOException e) { System.out.println("IOException: " + e.toString()); } finally { try{sequenceStream.close();}catch(IOException e){}; try{outFile.close();}catch(IOException e){}; } |
Пример 15.5. |
Закрыть окно |
Vector vector = new Vector(); vector.add(new StringBufferInputStream("Begin file1\n")); vector.add(new FileInputStream("file1.txt")); vector.add(new StringBufferInputStream("\ nEnd of file1, begin file2\n")); vector.add(new FileInputStream("file2.txt")); vector.add(new StringBufferInputStream("\nEnd of file2")); Enumeration enum = vector.elements(); sequenceStream = new SequenceInputStream(enum); |
Пример 15.6. |
Закрыть окно |
try { String fileName = "d:\\file1"; InputStream inStream = null; OutputStream outStream = null; //Записать в файл некоторое количество байт long timeStart = System.currentTimeMillis(); outStream = new FileOutputStream(fileName); outStream = new BufferedOutputStream(outStream); for(int i=1000000; --i>=0;) { outStream.write(i); } long time = System.currentTimeMillis() - timeStart; System.out.println("Writing time: " + time + " millisec"); outStream.close(); // Определить время считывания без буферизации timeStart = System.currentTimeMillis(); inStream = new FileInputStream(fileName); while(inStream.read()!=-1){ } time = System.currentTimeMillis() - timeStart; inStream.close(); System.out.println("Direct read time: " + (time) + " millisec"); // Теперь применим буферизацию timeStart = System.currentTimeMillis(); inStream = new FileInputStream(fileName); inStream = new BufferedInputStream(inStream); while(inStream.read()!=-1){ } time = System.currentTimeMillis() - timeStart; inStream.close(); System.out.println("Buffered read time: " + (time) + " millisec"); } catch (IOException e) { System.out.println("IOException: " + e.toString()); e.printStackTrace(); } |
Пример 15.7. |
Закрыть окно |
Writing time: 359 millisec Direct read time: 6546 millisec Buffered read time: 250 millisec |
Пример 15.8. |
Закрыть окно |
try { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream outData = new DataOutputStream(out); outData.writeByte(128); // этот метод принимает аргумент int, но записывает // лишь младший байт outData.writeInt(128); outData.writeLong(128); outData.writeDouble(128); outData.close(); byte[] bytes = out.toByteArray(); InputStream in = new ByteArrayInputStream(bytes); DataInputStream inData = new DataInputStream(in); System.out.println("Чтение в правильной последовательности: "); System.out.println("readByte: " + inData.readByte()); System.out.println("readInt: " + inData.readInt()); System.out.println("readLong: " + inData.readLong()); System.out.println("readDouble: " + inData.readDouble()); inData.close(); System.out.println("Чтение в измененной последовательности:"); in = new ByteArrayInputStream(bytes); inData = new DataInputStream(in); System.out.println("readInt: " + inData.readInt()); System.out.println("readDouble: " + inData.readDouble()); System.out.println("readLong: " + inData.readLong()); inData.close(); } catch (Exception e) { System.out.println("Impossible IOException occurs: " + e.toString()); e.printStackTrace(); } |
Пример 15.9. |
Закрыть окно |
// Родительский класс, не реализующий Serializable public class Parent { public String firstName; private String lastName; public Parent(){ System.out.println("Create Parent"); firstName="old_first"; lastName="old_last"; } public void changeNames() { firstName="new_first"; lastName="new_last"; } public String toString() { return super.toString()+",first="+firstName+",last="+lastName; } } // Класс Child, впервые реализовавший Serializable public class Child extends Parent implements Serializable { private int age; public Child(int age) { System.out.println("Create Child"); this.age=age; } public String toString() { return super.toString()+",age="+age; } } // Наследник Serializable-класса public class Child2 extends Child { private int size; public Child2(int age, int size) { super(age); System.out.println("Create Child2"); this.size=size; } public String toString() { return super.toString()+",size="+size; } } // Запускаемый класс для теста public class Test { public static void main(String[] arg) { try { FileOutputStream fos=new FileOutputStream("output.bin"); ObjectOutputStream oos=new ObjectOutputStream(fos); Child c=new Child(2); c.changeNames(); System.out.println(c); oos.writeObject(c); oos.writeObject(new Child2(3, 4)); oos.close(); System.out.println("Read objects:"); FileInputStream fis=new FileInputStream("output.bin"); ObjectInputStream ois=new ObjectInputStream(fis); System.out.println(ois.readObject()); System.out.println(ois.readObject()); ois.close(); } catch (Exception e) { // упрощенная обработка для краткости e.printStackTrace(); } } } |
Пример 15.10. |
Закрыть окно |
Create Parent Create Child Child@ad3ba4,first=new_first,last=new_last,age= 2 Create Parent Create Child Create Child2 Read objects: Create Parent Child@723d7c,first=old_first,last=old_last,age=2 Create Parent Child2@22c95b,first=old_first,last=old_last,age=3,size=4 |
Пример 15.11. |
Закрыть окно |
import java.io.*; class Point implements Serializable { double x; double y; public Point(double x, double y) { this.x = x; this.y = y; } public String toString() { return "("+x+","+y+") reference="+super.toString(); } } class Line implements Serializable { Point point1; Point point2; int index; public Line() { System.out.println("Constructing empty line"); } Line(Point p1, Point p2, int index) { System.out.println("Constructing line: " + index); this.point1 = p1; this.point2 = p2; this.index = index; } public int getIndex() { return index; } public void setIndex(int newIndex) { index = newIndex; } public void printInfo() { System.out.println("Line: " + index); System.out.println(" Object reference: " + super.toString()); System.out.println(" from point "+point1); System.out.println(" to point "+point2); } } public class Main { public static void main(java.lang.String[] args) { Point p1 = new Point(1.0,1.0); Point p2 = new Point(2.0,2.0); Point p3 = new Point(3.0,3.0); Line line1 = new Line(p1,p2,1); Line line2 = new Line(p2,p3,2); System.out.println("line 1 = " + line1); System.out.println("line 2 = " + line2); String fileName = "d:\\file"; try{ // записываем объекты в файл FileOutputStream os = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(line1); oos.writeObject(line2); // меняем состояние line1 и записываем его еще раз line1.setIndex(3); //oos.reset(); oos.writeObject(line1); // закрываем потоки // достаточно закрыть только поток-надстройку oos.close(); // считываем объекты System.out.println("Read objects:"); FileInputStream is = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(is); for (int i=0; i<3; i++) { // Считываем 3 объекта Line line = (Line)ois.readObject(); line.printInfo(); } ois.close(); } catch(ClassNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } |
Пример 15.12. |
Закрыть окно |
Constructing line: 1 Constructing line: 2 line 1 = Line@7d39 line 2 = Line@ 4ec Read objects: Line: 1 Object reference: Line@331e from point (1.0,1.0) reference=Point@36bb to point (2.0,2.0) reference=Point@386e Line: 2 Object reference: Line@6706 from point (2.0,2.0) reference=Point@386e to point (3.0,3.0) reference=Point@68ae Line: 1 Object reference: Line@331e from point (1.0,1.0) reference=Point@36bb to point (2.0,2.0) reference=Point@386e |
Пример 15.13. |
Закрыть окно |
Constructing line: 1 Constructing line: 2 line 1 = Line@ea2dfe line 2 = Line@ 7182c1 Read objects: Line: 1 Object reference: Line@a981ca from point (1.0,1.0) reference=Point@1503a3 to point (2.0,2.0) reference=Point@a1c887 Line: 2 Object reference: Line@743399 from point (2.0,2.0) reference=Point@a1c887 to point (3.0,3.0) reference=Point@e7b241 Line: 3 Object reference: Line@67d940 from point (1.0,1.0) reference=Point@e83912 to point (2.0,2.0) reference=Point@fae3c6 |
Пример 15.14. |
Закрыть окно |
String fileName = "d:\\file.txt"; //Строка, которая будет записана в файл String data = " Some data to be written and read.\n"; try{ FileWriter fw = new FileWriter(fileName); BufferedWriter bw = new BufferedWriter(fw); System.out.println("Write some data to file: " + fileName); // Несколько раз записать строку for(int i=(int)(Math.random()*10);--i>=0;) bw.write(data); bw.close(); // Считываем результат FileReader fr = new FileReader(fileName); BufferedReader br = new BufferedReader(fr); String s = null; int count = 0; System.out.println("Read data from file: " + fileName); // Считывать данные, отображая на экран while((s=br.readLine())!=null) System.out.println("row " + ++count + " read:" + s); br.close(); } catch(Exception e) { e.printStackTrace(); } |
Пример 15.15. |
Закрыть окно |
import java.io.*; public class FileDemo { public static void findFiles( File file, FileFilter filter, PrintStream output) throws IOException{ if (file.isDirectory()) { File[] list = file.listFiles(); for (int i=list.length; --i>=0;) { findFiles(list[i], filter, output); } } else { if (filter.accept(file)) output.println("\t" + file.getCanonicalPath()); } } public static void main(String[] args) { class NameFilter implements FileFilter { private String mask; NameFilter(String mask) { this.mask = mask; } public boolean accept(File file){ return (file.getName().indexOf(mask)!=-1)?true:false; } } File pathFile = new File("."); String filterString = ".java"; try { FileFilter filter = new NameFilter(filterString); findFiles(pathFile, filter, System.out); } catch(Exception e) { e.printStackTrace(); } System.out.println("work finished"); } } |
Пример 15.16. |
Закрыть окно |
До этого мы рассматривали объекты, которые имеют поля лишь примитивных типов. Если же сериализуемый объект ссылается на другие объекты, их также необходимо сохранить (записать в поток байт), а при десериализации – восстановить. Эти объекты, в свою очередь, также могут ссылаться на следующие объекты. При этом важно, что если несколько ссылок указывают на один и тот же объект, то этот объект должен быть сериализован лишь однажды, а при восстановлении все ссылки должны вновь указывать на него одного. Например, сериализуемый объект A ссылается на объекты B и C, каждый из которых, в свою очередь, ссылается на один и тот же объект D. После деeсериализации не должно возникать ситуации, когда B ссылается на D1, а C – на D2, где D1 и D2 – равные, но все же различные объекты.
Для организации такого процесса стандартный механизм сериализации строит граф, включающий в себя все участвующие объекты и ссылки между ними. Если очередная ссылка указывает на некоторый объект, сначала проверяется – нет ли такого объекта в графе. Если есть – объект второй раз не сериализуется. Если нет – новый объект добавляется в граф.
При построении графа может встретиться объект, порожденный от класса, не реализующего интерфейс Serializable. В этом случае сериализация прерывается, генерируется исключение java.io.NotSerializableException.
Рассмотрим пример:
Пример 15.12.
(html, txt)
В этой программе работа идет с классом Line (линия), который имеет 2 поля типа Point (линия описывается двумя точками). Запускаемый класс Main создает два объекта класса Line, причем, одна из точек у них общая. Кроме этого, линия имеет номер (поле index). Созданные линии (номера 1 и 2) записываются в поток, после чего одна из них получает новый номер (3) и вновь сериализуется.
Выполнение этой программы приведет к выводу на экран примерно следующего:
Пример 15.13.
(html, txt)
Из примера видно, что после восстановления у линий сохраняется общая точка, описываемая одним и тем же объектом (хеш-код 386e).
Третий записанный объект идентичен первому, причем, совпадают даже объектные ссылки. Несмотря на то, что при записи третьего объекта значение index было изменено на 3, в десериализованном объекте оно осталось равным 1. Так произошло потому, что объект, описывающий первую линию, уже был задействован в сериализации и, встретившись во второй раз, повторно записан не был.
Чтобы указать, что сеанс сериализации завершен, и получить возможность передавать измененные объекты, у ObjectOutputStream нужно вызвать метод reset(). В рассматриваемом примере для этого достаточно убрать комментарий в строке
//oos.reset();
Если теперь запустить программу, то можно увидеть, что третий объект получит номер 3.
Пример 15.14.
(html, txt)
Однако это будет уже новый объект, ссылка на который отличается от первой считанной линии. Более того, обе точки будут также описываться новыми объектами. То есть в новом сеансе все объекты были записаны, а затем восстановлены заново.
Если классы потоков осуществляют реальную запись и чтение данных, то класс File – это вспомогательный инструмент, призванный обеспечить работу с файлами и каталогами.
Объект класса File является абстрактным представлением файла и пути к нему. Он устанавливает только соответствие с ним, при этом для создания объекта неважно, существует ли такой файл на диске. После создания можно выполнить проверку, вызвав метод exists, который возвращает значение true, если файл существует. Создание или удаление объекта класса File никоим образом не отображается на реальных файлах. Для работы с содержимым файла можно получить экземпляры FileI/OStream.
Объект File может указывать на каталог (узнать это можно путем вызова метода isDirectory). Метод list возвращает список имен (массив String) содержащихся в нем файлов (если объект File не указывает на каталог – будет возвращен null).
Следующий пример демонстрирует использование объектов класса File:
Пример 15.16.
(html, txt)
При выполнении этой программы на экран будут выведены названия (в каноническом виде) всех файлов, с расширением .java, содержащихся в текущем каталоге и всех его подкаталогах.
Для определения того, что файл имеет расширение .java, использовался интерфейс FileFilter с реализацией в виде внутреннего класса NameFilter. Интерфейс FileFilter определяет только один метод accept, возвращающий значение, определяющее, попадает ли переданный файл в условия фильтрации. Помимо этого интерфейса, существует еще одна разновидность интерфейса фильтра – FilenameFilter, где метод accept определен несколько иначе: он принимает не объект файла к проверке, а объект File, указывающий на каталог, где находится файл для проверки, и строку его названия. Для проверки совпадения, с учетом регулярных выражений, нужно соответствующим образом реализовать метод accept. В конкретном приведенном примере можно было обойтись и без использования интерфейсов FileFilter или FilenameFilter. На практике их можно использовать для вызова методов list объектов File – в этих случаях будут возвращены файлы с учетом фильтра.
Также класс File предоставляет возможность получения некоторой информации о файле.
Методы canRead и canWrite – возвращается boolean значение, можно ли будет приложению производить чтение и изменение содержимого из файла, соответственно.
Этот класс реализует сразу два интерфейса – DataInput и DataOutput – следовательно, может производить запись и чтение всех примитивных типов Java. Эти операции, как следует из названия, производятся с файлом. При этом их можно производить поочередно, произвольным образом перемещаясь по файлу с помощью вызова метода seek(long) (переводит на указанную позицию в файле). Узнать текущее положение указателя в файле можно вызовом метода getFilePointer.
При создании объекта этого класса конструктору в качестве параметров нужно передать два параметра: файл и режим работы. Файл, с которым будет проводиться работа, указывается либо с помощью String – название файла, либо объектом File, ему соответствующим. Режим работы (mode) – представляет собой строку либо "r"(только чтение), либо "rw"(чтение и запись). Попытка открыть несуществующий файл только на чтение приведет к исключению FileNotFoundException. При открытии на чтение и запись он будет незамедлительно создан (или же будет брошено исключение FileNotFoundException, если это невозможно осуществить).
После создания объекта RandomAccessFile можно воспользоваться методами интерфейсов DataInput и DataOutput для проведения с файлом операций считывания и записи. По окончании работы с файлом его следует закрыть, вызвав метод close.
Экземпляр StreamTokenizer создается поверх существующего объекта, либо InputStream, либо Reader. Как и java.util.StringTokenizer, этот класс позволяет разбивать данные на лексемы (token), выделяемые из потока по определенным свойствам. Поскольку работа ведется со словами, конструктор, принимающий InputStream, объявлен как deprecated (предлагается оборачивать байтовый поток классом InputStreamReader и вызывать второй конструктор). Общий принцип работы такой же, как и у StringTokenizer, – задаются параметры разбиения, после чего вызывается метод nextToken(), пока не будет достигнут конец потока. Способы задания разбиения у StreamTokenizer довольно разнообразны, но просты, и поэтому здесь не рассматриваются.
Самый естественный и простой источник, откуда можно считывать байты, – это, конечно, массив байт. Класс ByteArrayInputStream представляет поток, считывающий данные из массива байт. Этот класс имеет конструктор, которому в качестве параметра передается массив byte[]. Соответственно, при вызове методов read() возвращаемые данные будут браться именно из этого массива. Например:
byte[] bytes = {1,-1,0}; ByteArrayInputStream in = new ByteArrayInputStream(bytes); int readedInt = in.read(); // readedInt=1 System.out.println("first element read is: " + readedInt); readedInt = in.read(); // readedInt=255. Однако // (byte)readedInt даст значение -1
System.out.println("second element read is: " + readedInt); readedInt = in.read(); // readedInt=0 System.out.println("third element read is: " + readedInt);
Если запустить такую программу, на экране отобразится следующее:
first element read is: 1 second element read is: 255 third element read is: 0
При вызове метода read() данные считывались из массива bytes, переданного в конструктор ByteArrayInputStream. Обратите внимание, в данном примере второе считанное значение равно 255, а не -1, как можно было бы ожидать. Чтобы понять, почему это произошло, нужно вспомнить, что метод read считывает byte, но возвращает значение int, полученное добавлением необходимого числа нулей (в двоичном представлении). Байт, равный -1, в двоичном представлении имеет вид 11111111 и, соответственно, число типа int, получаемое приставкой 24-х нулей, равно 255 (в десятичной системе). Однако если явно привести его к byte, получим исходное значение.
Аналогично, для записи байт в массив применяется класс ByteArrayOutputStream. Этот класс использует внутри себя объект byte[], куда записывает данные, передаваемые при вызове методов write(). Чтобы получить записанные в массив данные, вызывается метод toByteArray(). Пример:
ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write(10); out.write(11); byte[] bytes = out.toByteArray();
В этом примере в результате массив bytes будет состоять из двух элементов: 10 и 11.
Использовать классы ByteArrayInputStream и ByteArrayOutputStream может быть очень удобно, когда нужно проверить, что именно записывается в выходной поток. Например, при отладке и тестировании сложных процессов записи и чтения из потоков. Эти классы хороши тем, что позволяют сразу просмотреть результат и не нужно создавать ни файл, ни сетевое соединение, ни что-либо еще.
Класс FileInputStream используется для чтения данных из файла. Конструктор такого класса в качестве параметра принимает название файла, из которого будет производиться считывание. При указании строки имени файла нужно учитывать, что она будет напрямую передана операционной системе, поэтому формат имени файла и пути к нему может различаться на разных платформах. Если при вызове этого конструктора передать строку, указывающую на несуществующий файл или каталог, то будет брошено java.io.FileNotFoundException. Если же объект успешно создан, то при вызове его методов read() возвращаемые значения будут считываться из указанного файла.
Для записи байт в файл используется класс FileOutputStream. При создании объектов этого класса, то есть при вызовах его конструкторов, кроме имени файла, также можно указать, будут ли данные дописываться в конец файла, либо файл будет перезаписан. Если указанный файл не существует, то сразу после создания FileOutputStream он будет создан. При вызовах методов write() передаваемые значения будут записываться в этот файл. По окончании работы необходимо вызвать метод close(), чтобы сообщить системе, что работа по записи файла закончена. Пример:
Пример 15.1.
(html, txt)
Результатом работы программы будет:
Пример 15.2.
(html, txt)
При работе с FileInputStream метод available() практически наверняка вернет длину файла, то есть число байт, сколько вообще из него можно считать. Но не стоит закладываться на это при написании программ, которые должны устойчиво работать на различных платформах,– метод available() возвращает число байт, которое может быть на данный момент считано без блокирования. Тот факт, что, скорее всего, это число и будет длиной файла, является всего лишь частным случаем работы на некоторых платформах.
В приведенном примере для наглядности закрытие потоков производилось сразу же после окончания их использования в основном блоке. Однако лучше закрывать потоки в finally блоке.
... } finally { try{inFile.close();}catch(IOException e){}; }
Такой подход гарантирует, что поток будет закрыт и будут освобождены все связанные с ним системные ресурсы.
Задачи, возникающие при вводе/выводе весьма разнообразны - это может быть считывание байт из файлов, объектов из файлов, объектов из массивов, буферизованное считывание строк из массивов и т.д. В такой ситуации решение с использованием простого наследования приводит к возникновению слишком большого числа подклассов. Более эффективно применение надстроек (в ООП этот шаблон называется адаптер) Надстройки – наложение дополнительных объектов для получения новых свойств и функций. Таким образом, необходимо создать несколько дополнительных объектов – адаптеров к классам ввода/вывода. В java.io их еще называют фильтрами. При этом надстройка-фильтр включает в себя интерфейс объекта, на который надстраивается, поэтому может быть, в свою очередь, дополнительно надстроена.
В java.io интерфейс для таких надстроек ввода/вывода предоставляют классы FilterInputStream (для входных потоков) и FilterOutputStream (для выходных потоков). Эти классы унаследованы от основных базовых классов ввода/вывода – InputStream и OutputStream, соответственно. Конструктор FilterInputStream принимает в качестве параметра объект InputStream и имеет модификатор доступа protected.
Классы FilterI/OStream являются базовыми для надстроек и определяют общий интерфейс для надстраиваемых объектов. Потоки-надстройки не являются источниками данных. Они лишь модифицируют (расширяют) работу надстраиваемого потока.
InputStream – это базовый класс для потоков ввода, т.е. чтения. Соответственно, он описывает базовые методы для работы с байтовыми потоками данных. Эти методы необходимы всем классам, которые наследуются от InputStream.
Простейшая операция представлена методом read() (без аргументов). Он является абстрактным и, соответственно, должен быть определен в классах-наследниках. Этот метод предназначен для считывания ровно одного байта из потока, однако возвращает при этом значение типа int. В том случае, если считывание произошло успешно, возвращаемое значение лежит в диапазоне от 0 до 255 и представляет собой полученный байт (значение int содержит 4 байта и получается простым дополнением нулями в двоичном представлении). Обратите внимание, что полученный таким образом байт не обладает знаком и не находится в диапазоне от -128 до +127, как примитивный тип byte в Java.
Если достигнут конец потока, то есть в нем больше нет информации для чтения, то возвращаемое значение равно -1.
Если же считать из потока данные не удается из-за каких-то ошибок, или сбоев, будет брошено исключение java.io.IOException. Этот класс наследуется от Exception, т.е. его всегда необходимо обрабатывать явно. Дело в том, что каналы передачи информации, будь то Internet или, например, жесткий диск, могут давать сбои независимо от того, насколько хорошо написана программа. А это означает, что нужно быть готовым к ним, чтобы пользователь не потерял нужные данные.
Метод read() – это абстрактный метод, но именно с соблюдением всех указанных условий он должен быть реализован в классах-наследниках.
На практике обычно приходится считывать не один, а сразу несколько байт – то есть массив байт. Для этого используется метод read(), где в качестве параметров передается массив byte[]. При выполнении этого метода в цикле производится вызов абстрактного метода read() (определенного без параметров) и результатами заполняется переданный массив. Количество байт, считываемое таким образом, равно длине переданного массива. Но при этом может так получиться, что данные в потоке закончатся еще до того, как будет заполнен весь массив. То есть возможна ситуация, когда в потоке данных (байт) содержится меньше, чем длина массива. Поэтому метод возвращает значение int, указывающее, сколько байт было реально считано. Понятно, что это значение может быть от 0 до величины длины переданного массива.
Если же мы изначально хотим заполнить не весь массив, а только его часть, то для этих целей используется метод read(), которому, кроме массива byte[], передаются еще два int значения. Первое – это позиция в массиве, с которой следует начать заполнение, второе – количество байт, которое нужно считать. Такой подход, когда для получения данных передается массив и два int числа – offset (смещение) и length (длина), является довольно распространенным и часто встречается не только в пакете java.io.
При вызове методов read() возможно возникновение такой ситуации, когда запрашиваемые данные еще не готовы к считыванию. Например, если мы считываем данные, поступающие из сети, и они еще просто не пришли. В таком случае нельзя сказать, что данных больше нет, но и считать тоже нечего - выполнение останавливается на вызове метода read() и получается "зависание".
Чтобы узнать, сколько байт в потоке готово к считыванию, применяется метод available(). Этот метод возвращает значение типа int, которое показывает, сколько байт в потоке готово к считыванию. При этом не стоит путать количество байт, готовых к считыванию, с тем количеством байт, которые вообще можно будет считать из этого потока. Метод available() возвращает число – количество байт, именно на данный момент готовых к считыванию.
Когда работа с входным потоком данных окончена, его следует закрыть. Для этого вызывается метод close(). Этим вызовом будут освобождены все системные ресурсы, связанные с потоком.
Точно так же, как InputStream – это базовый класс для потоков ввода, класс OutputStream – это базовый класс для потоков вывода.
В классе OutputStream аналогичным образом определяются три метода write() – один принимающий в качестве параметра int, второй – byte[] и третий – byte[], плюс два int-числа. Все эти методы ничего не возвращают (void).
Метод write(int) является абстрактным и должен быть реализован в классах-наследниках. Этот метод принимает в качестве параметра int, но реально записывает в поток только byte – младшие 8 бит в двоичном представлении. Остальные 24 бита будут проигнорированы. В случае возникновения ошибки этот метод бросает java.io.IOException, как, впрочем, и большинство методов, связанных с вводом-выводом.
Для записи в поток сразу некоторого количества байт методу write() передается массив байт. Или, если мы хотим записать только часть массива, то передаем массив byte[] и два int-числа – отступ и количество байт для записи. Понятно, что если указать неверные параметры – например, отрицательный отступ, отрицательное количество байт для записи, либо если сумма отступ плюс длина будет больше длины массива, – во всех этих случаях кидается исключение IndexOutOfBoundsException.
Реализация потока может быть такой, что данные записываются не сразу, а хранятся некоторое время в памяти. Например, мы хотим записать в файл какие-то данные, которые получаем порциями по 10 байт, и так 200 раз подряд. В таком случае вместо 200 обращений к файлу удобней будет скопить все эти данные в памяти, а потом одним заходом записать все 2000 байт. То есть класс выходного потока может использовать некоторый внутренний механизм для буферизации (временного хранения перед отправкой) данных. Чтобы убедиться, что данные записаны в поток, а не хранятся в буфере, вызывается метод flush(), определенный в OutputStream. В этом классе его реализация пустая, но если какой-либо из наследников использует буферизацию данных, то этот метод должен быть в нем переопределен.
Когда работа с потоком закончена, его следует закрыть. Для этого вызывается метод close(). Этот метод сначала освобождает буфер (вызовом метода flush), после чего поток закрывается и освобождаются все связанные с ним системные ресурсы. Закрытый поток не может выполнять операции вывода и не может быть открыт заново. В классе OutputStream реализация метода close() не производит никаких действий.
Итак, классы InputStream и OutputStream определяют необходимые методы для работы с байтовыми потоками данных. Эти классы являются абстрактными. Их задача – определить общий интерфейс для классов, которые получают данные из различных источников. Такими источниками могут быть, например, массив байт, файл, строка и т.д. Все они, или, по крайней мере, наиболее распространенные, будут рассмотрены далее.
Рассмотренные классы – наследники InputStream и OutputStream – работают с байтовыми данными. Если с их помощью записывать или считывать текст, то сначала необходимо сопоставить каждому символу его числовой код. Такое соответствие называется кодировкой.
Известно, что Java использует кодировку Unicode, в которой символы представляются двухбайтовым кодом. Байтовые потоки зачастую работают с текстом упрощенно – они просто отбрасывают старший байт каждого символа. В реальных же приложениях могут использовать различные кодировки (даже для русского языка их существует несколько). Поэтому в версии Java 1.1 появился дополнительный набор классов, основывающийся на типах Reader и Writer. Их иерархия представлена на рис. 15.2.
Эта иерархия очень схожа с аналогичной для байтовых потоков InputStream и OutputStream. Главное отличие между ними – Reader и Writer работают с потоком символов (char). Только чтение массива символов в Reader описывается методом read(char[]), а запись в Writer – write(char[]).
В таблице 15.1 приведены соответствия классов для байтовых и символьных потоков.
InputStream |
Reader |
OutputStream |
Writer |
ByteArrayInputStream |
CharArrayReader |
ByteArrayOutputStream |
CharArrayWriter |
Нет аналога |
InputStreamReader |
Нет аналога |
OutputStreamWriter |
FileInputStream |
FileReader |
FileOutputStream |
FileWriter |
FilterInputStream |
FilterReader |
FilterOutputStream |
FilterWriter |
BufferedInputStream |
BufferedReader |
BufferedOutputStream |
BufferedWriter |
PrintStream |
PrintWriter |
DataInputStream |
Нет аналога |
DataOutputStream |
Нет аналога |
ObjectInputStream |
Нет аналога |
ObjectOutputStream |
Нет аналога |
PipedInputStream |
PipedReader |
PipedOutputStream |
PipedWriter |
StringBufferInputStream |
StringReader |
Нет аналога |
StringWriter |
LineNumberInputStream |
LineNumberReader |
PushBackInputStream |
PushBackReader |
SequenceInputStream |
Нет аналога |
Класс LineNumberInputStream во время чтения данных производит подсчет, сколько строк было считано из потока. Номер строки, на которой в данный момент происходит чтение, можно узнать путем вызова метода getLineNumber(). Также можно и перейти к определенной строке вызовом метода setLineNumber(int lineNumber).
Под строкой при этом понимается набор байт, оканчивающийся либо '\n', либо '\r', либо их комбинацией '\r\n', именно в этой последовательности.
Аналогичный класс для исходящего потока отсутствует. LineNumberInputStream, начиная с версии 1.1, объявлен deprecated, то есть использовать его не рекомендуется. Его заменил класс LineNumberReader (рассматривается ниже), принцип работы которого точно такой же.
Классы PipedInputStream и PipedOutputStream характеризуются тем, что их объекты всегда используются в паре – к одному объекту PipedInputStream привязывается (подключается) один объект PipedOutputStream. Они могут быть полезны, если в программе необходимо организовать обмен данными между модулями (например, между потоками выполнения).
Эти классы применяются следующим образом: создается по объекту PipedInputStream и PipedOutputStream, после чего они могут быть соединены между собой. Один объект PipedOutputStream может быть соединен с ровно одним объектом PipedInputStream, и наоборот. Затем в объект PipedOutputStream записываются данные, после чего они могут быть считаны именно в подключенном объекте PipedInputStream. Такое соединение можно обеспечить либо вызовом метода connect() с передачей соответствующего объекта PipedI/OStream (будем так кратно обозначать пару классов, в данном случае PipedInputStream и PipedOutputStream), либо передать этот объект еще при вызове конструктора.
Использование связки PipedInputStream и PipedOutputStream показано в следующем примере:
Пример 15.3.
(html, txt)
Данный пример носит чисто демонстративный характер (в результате его работы массив toRead будет заполнен случайными числами). Более явно выгода от использования PipedI/OStream в основном проявляется при разработке многопоточного приложения. Если в программе запускается несколько потоков исполнения, организовать передачу данных между ними удобно с помощью этих классов. Для этого нужно создать связанные объекты PipedI/OStream, после чего передать ссылки на них в соответствующие потоки. Поток выполнения, в котором производится чтение данных, может содержать подобный код:
Пример 15.4.
(html, txt)
Если с объектом inStream одновременно могут работать несколько потоков выполнения, то необходимо использовать блок synchronized (как и сделано в примере), который гарантирует, что в период между вызовами inStream.available() и inStream.read(…) ни в каком другом потоке выполнения не будет производиться считывание из inStream. Поэтому вызов inStream.read(readedBytes) не приведет к блокировке и все данные, готовые к считыванию, будут считаны.
Этот класс используется для конвертации и записи строк в байтовый поток. В нем определен метод print(…), принимающий в качестве аргумента различные примитивные типы Java, а также тип Object. При вызове передаваемые данные будут сначала преобразованы в строку вызовом метода String.valueOf(), после чего записаны в поток. Если возникает исключение, оно обрабатывается внутри метода print и дальше не бросается (узнать, произошла ли ошибка, можно с помощью метода checkError()). При записи символов в виде байт используется кодировка, принятая по умолчанию в операционной системе (есть возможность задать ее явно при запуске JVM).
Этот класс также является deprecated, поскольку работа с кодировками требует особого подхода (зачастую у двухбайтовых символов Java старший байт просто отбрасывается). Поэтому в версии Java 1.1 появился дополнительный набор классов, основывающийся на типах Reader и Writer. Они будут рассмотрены позже. В частности, вместо PrintStream теперь рекомендуется применять PrintWriter. Однако старый класс продолжает активно использоваться, поскольку статические поля out и err класса System имеют именно это тип.
Этот фильтр позволяет вернуть во входной поток считанные из него данные. Такое действие производится вызовом метода unread(). Понятно, что обеспечивается подобная функциональность за счет наличия в классе специального буфера – массива байт, который хранит считанную информацию. Если будет произведен откат (вызван метод unread), то во время следующего считывания эти данные будут выдаваться еще раз как только полученные. При создании объекта можно указать размер буфера.
Некоторым сложно организованным классам требуется особый подход для сериализации. Для расширения стандартного механизма можно объявить в классе два метода с точно такой сигнатурой:
private void writeObject( java.io.ObjectOutputStream out) throws IOException; private void readObject( java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
Если в классе объявлены такие методы, то при сериализации объекта для записи его состояния будет вызван writeObject, который должен сгенерировать последовательность байт и записать ее в поток out, полученный в качестве аргумента. При этом можно вызвать стандартный механизм записи объекта путем вызова метода
out.defaultWriteObject();
Этот метод запишет все не-transient и не-static поля в поток данных.
В свою очередь, при десериализации метод readObject должен считать данные из потока in (также полученного в качестве аргумента) и восстановить значения полей класса. При реализации этого метода можно обратиться к стандартному механизму с помощью метода:
in.defaultReadObject();
Этот метод считывает описание объекта из потока и присваивает значения соответствующих полей в текущем объекте.
Если же процедура сериализации в корне отличается от стандартной, то для таких классов предназначен альтернативный интерфейс java.io.Externalizable.
При использовании этого интерфейса в поток автоматически записывается только идентификация класса. Сохранить и восстановить всю информацию о состоянии экземпляра должен сам класс. Для этого в нем должны быть объявлены методы writeExternal() и readExternal() интерфейса Externalizable. Эти методы должны обеспечить сохранение состояния, описываемого полями самого класса и его суперкласса.
При восстановлении Externalizable-объекта экземпляр создается путем вызова конструктора без аргументов, после чего вызывается метод readExternal.
Метод writeExternal имеет сигнатуру:
void writeExternal(ObjectOutput out) throws IOException;
Для сохранения состояния вызываются методы ObjectOutput, с помощью которых можно записать как примитивные, так и объектные значения. Для корректной работы в соответствующем методе
void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;
эти значения должны быть считаны в том же самом порядке.
Класс SequenceInputStream объединяет поток данных из других двух и более входных потоков. Данные будут вычитываться последовательно – сначала все данные из первого потока в списке, затем из второго, и так далее. Конец потока SequenceInputStream будет достигнут только тогда, когда будет достигнут конец потока, последнего в списке.
В этом классе имеется два конструктора – принимающий два потока и принимающий Enumeration (в котором, конечно, должны быть только экземпляры InputStream и его наследников). Когда вызывается метод read(), SequenceInputStream пытается считать байт из текущего входного потока. Если в нем больше данных нет (считанное из него значение равно -1), у него вызывается метод close() и следующий входной поток становится текущим. Так продолжается до тех пор, пока не будут получены все данные из последнего потока. Если при считывании обнаруживается, что больше входных потоков нет, SequenceInputStream возвращает -1. Вызов метода close() у SequenceInputStream закрывает все содержащиеся в нем входные потоки.
Пример:
Пример 15.5.
(html, txt)
В результате выполнения этого примера в файл file3.txt будет записано содержимое файлов file1.txt и file2.txt – сначала полностью file1.txt, потом file2.txt. Закрытие потоков производится в блоке finally. Поскольку при вызове метода close() может возникнуть IOException, необходим try-catch блок. Причем, каждый вызов метода close() взят в отдельный try-catch блок - для того, чтобы возникшее исключение при закрытии одного потока не помешало закрытию другого. При этом нет необходимости закрывать потоки inFile1 и inFile2 – они будут автоматически закрыты при использовании в sequnceStream - либо когда в них закончатся данные, либо при вызове у sequenceStream метода close().
Объект SequenceInputStream можно было создать и другим способом: сначала получить объект Enumeration, содержащий все потоки, и передать его в конструктор SequenceInputStream:
Пример 15.6.
(html, txt)
Если заменить в предыдущем примере инициализацию sequenceStream на приведенную здесь, то в файл file3.txt, кроме содержимого файлов file1.txt и file2.txt, будут записаны еще три строки – одна в начале файла, одна между содержимым файлов file1.txt и file2.txt и еще одна в конце file3.txt.
Для объектов процесс преобразования в последовательность байт и обратно организован несколько сложнее – объекты имеют различную структуру, хранят ссылки на другие объекты и т.д. Поэтому такая процедура получила специальное название - сериализация (serialization), обратное действие, – то есть воссоздание объекта из последовательности байт – десериализация.
Поскольку сериализованный объект – это последовательность байт, которую можно легко сохранить в файл, передать по сети и т.д., то и объект затем можно восстановить на любой машине, вне зависимости от того, где проводилась сериализация. Разумеется, Java позволяет не задумываться при этом о таких факторах, как, например, используемая операционная система на машине-отправителе и получателе. Такая гибкость обусловила широкое применение сериализации при создании распределенных приложений, в том числе и корпоративных (enterprise) систем.
Подавляющее большинство программ обменивается данными с внешним миром. Это, безусловно, делают любые сетевые приложения – они передают и получают информацию от других компьютеров и специальных устройств, подключенных к сети. Оказывается, можно точно таким же образом представлять обмен данными между устройствами внутри одной машины. Так, например, программа может считывать данные с клавиатуры и записывать их в файл, или же наоборот - считывать данные из файла и выводить их на экран. Таким образом, устройства, откуда может производиться считывание информации, могут быть самыми разнообразными – файл, клавиатура, входящее сетевое соединение и т.д. То же касается и устройств вывода – это может быть файл, экран монитора, принтер, исходящее сетевое соединение и т.п. В конечном счете, все данные в компьютерной системе в процессе обработки передаются от устройств ввода к устройствам вывода.
Обычно часть вычислительной платформы, которая отвечает за обмен данными, так и называется – система ввода/вывода. В Java она представлена пакетом java.io (input/output). Реализация системы ввода/вывода осложняется не только широким спектром источников и получателей данных, но еще и различными форматами передачи информации. Ею можно обмениваться в двоичном представлении, символьном или текстовом, с применением некоторой кодировки (только для русского языка их насчитывается более 4 штук), или передавать числа в различных представлениях. Доступ к данным может потребоваться как последовательный (например, считывание HTML-страницы), так и произвольный (сложная работа с несколькими частями одного файла). Зачастую для повышения производительности применяется буферизация.
В Java для описания работы по вводу/выводу используется специальное понятие поток данных (stream). Поток данных связан с некоторым источником, или приемником, данных, способным получать или предоставлять информацию. Соответственно, потоки делятся на входящие – читающие данные и выходящие – передающие (записывающие) данные. Введение концепции stream позволяет отделить основную логику программы, обменивающейся информацией с любыми устройствами одинаковым образом, от низкоуровневых операций с такими устройствами ввода/вывода.
В Java потоки естественным образом представляются объектами. Описывающие их классы как раз и составляют основную часть пакета java.io. Они довольно разнообразны и отвечают за различную функциональность. Все классы разделены на две части – одни осуществляют ввод данных, другие – вывод.
Существующие стандартные классы помогают решить большинство типичных задач. Минимальной "порцией" информации является, как известно, бит, принимающий значение 0 или 1 (это понятие также удобно применять на самом низком уровне, где данные передаются электрическим сигналом; условно говоря, 1 представляется прохождением импульса, 0 – его отсутствием). Традиционно используется более крупная единица измерения – байт, объединяющая 8 бит. Таким образом, значение, представленное одним байтом, находится в диапазоне от 0 до 28-1=255, или, если использовать знак, – от -128 до +127. Примитивный тип byte в Java в точности соответствует последнему – знаковому диапазону.
Базовые, наиболее универсальные, классы позволяют считывать и записывать информацию именно в виде набора байт. Чтобы их было удобно применять в различных задачах, java.io содержит также классы, преобразующие любые данные в набор байт.
Например, если нужно сохранить результаты вычислений – набор значений типа double – в файл, то их можно сначала превратить в набор байт, а затем эти байты записать в файл. Аналогичные действия совершаются и в ситуации, когда требуется сохранить объект (т.е. его состояние) – преобразование в набор байт и последующая их запись в файл. Понятно, что при восстановлении данных в обоих рассмотренных случаях проделываются обратные действия – сначала считывается последовательность байт, а затем она преобразуется в нужный формат.
На рисунке 15.1 представлены иерархии классов ввода/вывода. Как и говорилось, все типы поделены на две группы. Представляющие входные потоки классы наследуются от InputStream, а выходные – от OutputStream.
Для представления объектов в виде последовательности байт определены унаследованные от DataInput и DataOutput интерфейсы ObjectInput и ObjectOutput, соответственно. В java.io имеются реализации этих интерфейсов – классы ObjectInputStream и ObjectOutputStream.
Эти классы используют стандартный механизм сериализации, который предлагает JVM. Для того, чтобы объект мог быть сериализован, класс, от которого он порожден, должен реализовывать интерфейс java.io.Serializable. В этом интерфейсе не определен ни один метод. Он нужен лишь для указания, что объекты класса могут участвовать в сериализации. При попытке сериализовать объект, не имеющий такого интерфейса, будет брошен java.io.NotSerializableException.
Чтобы начать сериализацию объекта, нужен выходной поток OutputStream, в который и будет записываться сгенерированная последовательность байт. Этот поток передается в конструктор ObjectOutputStream. Затем вызовом метода writeObject() объект сериализуется и записывается в выходной поток. Например:
ByteArrayOutputStream os = new ByteArrayOutputStream(); Object objSave = new Integer(1); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(objSave);
Чтобы увидеть, во что превратился объект objSave, можно просмотреть содержимое массива:
byte[] bArray = os.toByteArray();
А чтобы восстановить объект, его нужно десериализовать из этого массива:
ByteArrayInputStream is = new ByteArrayInputStream(bArray); ObjectInputStream ois = new ObjectInputStream(is); Object objRead = ois.readObject();
Теперь можно убедиться, что восстановленный объект идентичен исходному:
System.out.println("readed object is: " + objRead.toString()); System.out.println("Object equality is: " + (objSave.equals(objRead))); System.out.println("Reference equality is: " + (objSave==objRead));
Результатом выполнения приведенного выше кода будет:
readed object is: 1 Object equality is: true Reference equality is: false
Как мы видим, восстановленный объект не совпадает с исходным (что очевидно – ведь восстановление могло происходить и на другой машине), но равен сериализованному по значению.
Как обычно, для упрощения в примере была опущена обработка ошибок. Однако, сериализация (десериализация) объектов довольно сложная процедура, поэтому возникающие сложности не всегда очевидны. Рассмотрим основные исключения, которые может генерировать метод readObject() класса ObjectInputStream.
Предположим, объект некоторого класса TestClass был сериализован и передан по сети на другую машину для восстановления. Может случиться так, что у считывающей JVM на локальном диске не окажется описания этого класса (файл TestClass.class). Поскольку стандартный механизм сериализации записывает в поток байт лишь состояние объекта, для успешной десериализации необходимо наличие описание класса. В результате будет брошено исключение ClassNotFoundException.
Причина появления java.io.StreamCorruptedException вполне очевидна из названия – неправильный формат входного потока. Предположим, происходит попытка считать сериализованный объект из файла. Если этот файл испорчен (для эксперимента можно открыть его в текстовом редакторе и исправить несколько символов), то стандартная процедура десериализации даст сбой. Эта же ошибка возникнет, если считать некоторое количество байт (с помощью метода read) непосредственно из надстраиваемого потока InputStream. В таком случае ObjectInputStream снова обнаружит сбой в формате данных и будет брошено исключение java.io.StreamCorruptedException.
Поскольку ObjectOutput наследуется от DataOutput, ObjectOutputStream может быть использован для последовательной записи нескольких значений как объектных, так и примитивных типов в произвольной последовательности. Если при считывании будет вызван метод readObject, а в исходном потоке следующим на очереди записано значение примитивного типа, будет брошено исключение java.io.OptionalDataException. Очевидно, что для корректного восстановления данных из потока их нужно считывать именно в том порядке, в каком были записаны.
Иногда бывает удобно работать с текстовой строкой String как с потоком байт. Для этого можно воспользоваться классом StringBufferInputStream. При создании объекта этого класса необходимо передать конструктору объект String. Данные, возвращаемые методом read(), будут считываться именно из этой строки. При этом символы будут преобразовываться в байты с потерей точности – старший байт отбрасывается (напомним, что символ char состоит из двух байт).
Итак, сериализация объекта заключается в сохранении и восстановлении состояния объекта. В Java в большинстве случаев состояние описывается значениями полей объекта. Причем, что важно, не только тех полей, которые были явно объявлены в классе, от которого порожден объект, но и унаследованных полей.
Предположим, мы бы попытались своими силами реализовать стандартный механизм сериализации. Нам передается выходной поток, в который нужно записать состояние нашего объекта. С помощью DataOutput интерфейса можно легко сохранить значения всех доступных полей (будем для простоты считать, что они все примитивного типа). Однако в большинстве случаев в родительских классах могут быть объявлены недоступные нам поля (например, private). Тем не менее, такие поля, как правило, играют важную роль в определении состояния объекта, так как они могут влиять на результат работы унаследованных методов. Как же сохранить их значения?
С другой стороны, не меньшей проблемой является восстановление объекта. Как говорилось раньше, объект может быть создан только вызовом его конструктора. У класса, от которого порожден десериализуемый объект, может быть несколько конструкторов, причем, некоторые из них, или все, могут иметь аргументы. Какой из них вызвать? Какие значения передать в качестве аргументов?
После создания объекта необходимо установить считанные значения его полей. Однако многие классы имеют специальные set-методы для этой цели. В таких методах могут происходить проверки, могут меняться значения вспомогательных полей. Пользоваться ли этими методами? Если их несколько, то как выбрать правильный и какие параметры ему передать? Снова возникает проблема работы с недоступными полями, полученными по наследству. Как же в стандартном механизме сериализации решены все эти вопросы?
Во-первых, рассмотрим подробнее работу с интерфейсом Serializable. Заметим, что класс Object не реализует этот интерфейс. Таким образом, существует два варианта – либо сериализуемый класс наследуется от Serializable-класса, либо нет. Первый вариант довольно прост. Если родительский класс уже реализовал интерфейс Serializable, то наследникам это свойство передается автоматически, то есть все объекты, порожденные от такого класса, или любого его наследника, могут быть сериализованы.
Если же наш класс впервые реализует Serializable в своей ветке наследования, то его суперкласс должен отвечать специальному требованию – у него должен быть доступный конструктор без параметров. Именно с помощью этого конструктора будет создан десериализуемый объект и будут проинициализированы все поля, унаследованные от классов, не наследующих Serializable.
Рассмотрим пример: