13. Лекция: Пакет java.lang
13. Лекция: Пакет java.lang
В этой лекции рассматривается основная библиотека Java – java.lang. В ней содержатся классы Object и Class, классы-обертки для примитивных типов, класс Math, классы для работы со строками String и StringBuffer, системные классы System, Runtime и другие. В этом же пакете находятся типы, уже рассматривавшиеся ранее,– для работы с исключительными ситуациями и потоками исполнения.
Введение
В состав пакета java.lang входят классы, составляющие основу для всех других, и поэтому он является наиболее важным из всех, входящих в Java API. Поскольку без него не может обойтись ни один класс, каждый модуль компиляции содержит неявное импортирование этого пакета ( import java.lang.*; ).
Перечислим классы, составляющие основу пакета.
Object – является корневым в иерархии классов.
Class – экземпляры этого класса являются описаниями объектных типов в памяти JVM.
String – представляет собой символьную строку, содержит средства работы с нею.
StringBuffer – используется для работы (создания) строк.
Number – абстрактный класс, являющийся суперклассом для классов-объектных оберток числовых примитивных типов Java.
Character – объектная обертка для типа char.
Boolean – объектная обертка для типа boolean.
Math – реализует набор базовых математических функций.
Throwable – базовый класс для объектов, представляющих исключения. Любое исключение, которое может быть брошено и, соответственно, перехвачено блоком catch, должно быть унаследовано от Throwable.
Thread – позволяет запускать и работать с потоками выполнения в Java. Runnable – может использоваться в сочетании с классом Thread для описания потоков выполнения.
ThreadGroup – позволяет объединять потоки в группу и производить действия сразу над всеми потоками в ней. Существуют ограничения по безопасности на манипуляции с потоками из других групп.
System – содержит полезные поля и методы для работы системного уровня.
Runtime – позволяет приложению взаимодействовать с окружением, в котором оно запущено.
Process – представляет интерфейс к внешней программе, запущенной при помощи Runtime.
ClassLoader – отвечает за загрузку описания классов в память JVM.
SecurityManager – для обеспечения безопасности накладывает ограничения на данную среду выполнения программ.
Compiler – используется для поддержки Just-in-Time компиляторов.
Интерфейсы:
Cloneable – должен быть реализован объектами, которые планируется клонировать с помощью средств JVM;
Comparable – позволяет упорядочивать (сортировать, сравнивать) объекты каждого класса, реализующего этот интерфейс.
Object
Класс Object является базовым для всех остальных классов. Он определяет методы, которые поддерживаются любым классом в Java.
Метод public final native Class getClass() возвращает объект типа Class, соответствующий классу объекта. Этот метод уже рассматривался в лекции 4.
Метод public boolean equals(Object obj) определяет, являются ли объекты одинаковыми. Если оператор == проверяет равенство по ссылке (указывают на один и тот же объект), то метод equals() – равенство по значению (состояния объектов одинаковы). Поскольку класс Object не содержит полей, реализация в нем этого метода такова, что значение true будет возвращено только в случае равенства по ссылке, то есть:
public boolean equals(Object obj) {
return (this == obj);
}
В классах-наследниках этот метод при необходимости может быть переопределен, чтобы поддержать расширенное состояние объекта (например, если добавилось поле, характеризующее состояние). Рассмотрим сравнение объектов-оберток целых чисел (класс Integer ). Оно должно по всей логике возвращать значение true, если равны значения int чисел, которые обернуты, даже если это два различных объекта.
Метод equals() может быть переопределен любым способом (например, всегда возвращать false, или, наоборот, true ) – компилятор, конечно же, не будет проводить анализ реализации и давать рекомендации. Однако существуют соглашения, которые необходимо соблюдать, чтобы программа имела предсказуемое поведение, в том числе и с точки зрения других программистов:
* рефлексивность: для любой объектной ссылки x, отличной от null, вызов x.equals(x) возвращает true ;
* симметричность: для любых объектных ссылок x и y, вызов x.equals(y) возвращает true только в том случае, если вызов y.equals(x) возвращает true ;
* транзитивность: для любых объектных ссылок x, y и z, если x.equals(y) возвращает true и y.equals(z) возвращает true, то вызов x.equals(z) должен вернуть true ;
* непротиворечивость: для любых объектных ссылок x и y многократные последовательные вызовы x.equals(y) возвращают одно и то же значение (либо всегда true, либо всегда false );
* для любой не равной null объектной ссылки x вызов x.equals(null) должен вернуть значение false.
Пример:
package demo.lang;
public class Rectangle {
public int sideA;
public int sideB;
public Rectangle(int x, int y) {
super();
sideA = x; sideB = y;
}
public boolean equals(Object obj) {
if(!(obj instanceof Rectangle))
return false;
Rectangle ref = (Rectangle)obj;
return (((this.sideA==ref.sideA)&&(this.sideB==ref.sideB))||
(this.sideA==ref.sideB)&&(this.sideB==ref.sideA));
}
public static void main(String[] args) {
Rectangle r1 = new Rectangle(10,20);
Rectangle r2 = new Rectangle(10,10);
Rectangle r3 = new Rectangle(20,10);
System.out.println("r1.equals(r1) == " + r1.equals(r1));
System.out.println("r1.equals(r2) == " + r1.equals(r2));
System.out.println("r1.equals(r3) == " + r1.equals(r3));
System.out.println("r2.equals(r3) == " + r2.equals(r3));
System.out.println("r1.equals(null) == " + r1.equals(null));
}
}
Пример 13.1.
Запуск этой программы, очевидно, приведет к выводу на экран следующего:
r1.equals(r1) == true
r1.equals(r2) == false
r1.equals(r3) == true
r2.equals(r3) == false
r1.equals(null) == false
Пример 13.2.
В этом примере метод equals() у класса Rectangle был переопределен таким образом, чтобы прямоугольники были равны, если их можно наложить друг на друга (геометрическое равенство).
Большинство стандартных классов переопределяет этот метод, строго следуя всем соглашениям.
Метод public int hashCode() возвращает хеш-код ( hash code ) для объекта. Хеш-код – это целое число, которое сопоставляется с данным объектом. Оно позволяет организовать хранение набора объектов с возможностью быстрой выборки (стандартная реализация такого механизма присутствует в Java и будет описана в следующей лекции).
Для этого метода также принят ряд соглашений, которым стоит следовать при переопределении:
* если два объекта идентичны, то есть вызов метода equals(Object) возвращает true, то вызов метода hashCode() у каждого из этих двух объектов должен возвращать одно и то же значение;
* во время одного запуска программы для одного объекта при вызове метода hashCode() должно возвращаться одно и то же значение, если между этими вызовами не были затронуты данные, используемые для проверки объектов на идентичность в методе equals(). Это число не обязательно должно быть одним и тем же при повторном запуске той же программы, даже если все данные будут идентичны.
В классе Object этот метод реализован на уровне JVM. Сама виртуальная машина генерирует хеш-код, основываясь на расположении объекта в памяти. Это позволяет для различных объектов (неравенство по ссылке) получать различные хеш-коды.
В силу первого соглашения при переопределении метода equals() необходимо переопределить также метод hashCode(). При этом нужно стремиться, во-первых, к тому, чтобы метод возвращал значение как можно быстрее, иначе основная цель – быстрая выборка – не будет достигнута. Во-вторых, желательно для различных объектов, то есть когда метод equals(Object) возвращает false, генерировать различные хеш-коды. В этом случае хеш-таблицы будут работать особенно эффективно. Однако, понятно, что это не всегда возможно. Диапазон значений int – 232, а количество различных строк, или двумерных точек, с координатами типа int – заведомо больше.
Большинство стандартных классов переопределяет этот метод, строго следуя всем соглашениям.
Метод public String toString() возвращает строковое представление объекта. В классе Object этот метод реализован следующим образом:
public String toString() {
return getClass().getName() + "@" +
Integer.toHexString(hashCode());
}
То есть возвращает строку, содержащую название класса объекта и его хеш-код в шестнадцатеричном формате.
В классах-наследниках этот метод может быть переопределен для получения более наглядного описания объекта. Обычно это значения некоторых полей, характеризующих экземпляр. Например, для книги это может быть название, автор и количество страниц:
package demo.lang;
public class Book {
private String title;
private String author;
private int pagesNumber;
public Book(String title, String author,
int pagesNumber) {
super();
this.title = title;
this.author = author;
this.pagesNumber = pagesNumber;
}
public static void main(String[] args) {
Book book = new Book("Java2","Sun",1000);
System.out.println("object is: " + book);
}
public String toString() {
return "Book: " + title + " ( " + author +
", " + pagesNumber + " pages )";
}
}
При запуске этой программы на экран будет выведено следующее:
object is: Book: Java2 ( Sun, 1000 pages )
Большинство стандартных классов переопределяет этот метод. Экземпляры класса String возвращают ссылку на самих себя ( this ).
Метод wait(), notify(), notifyAll() используются для поддержки многопоточности и были подробно рассмотрены в лекции 12. Они определены с атрибутом final и не могут быть переопределены в классах-наследниках.
Метод protected void finalize() throws Throwable вызывается Java-машиной перед тем, как garbage collector (сборщик мусора) освободит память, занимаемую объектом. Этот метод уже подробно рассматривался в лекции 4.
Метод protected native Object clone() throws CloneNotSupportedException создает копию объекта. Механизм клонирования подробно рассматривался в лекции 9.
Class
В запущенной программе Java каждому классу соответствует объект типа Class. Этот объект содержит информацию, необходимую для описания класса – поля, методы и т.д.
Класс Class не имеет открытого конструктора – объекты этого класса создаются автоматически Java-машиной по мере загрузки описания классов из class -файлов. Получить экземпляр Class для конкретного класса можно с помощью метода forName():
public static Class forName(String name, boolean initialize, ClassLoader loader) – возвращает объект Class, соответствующий классу, или интерфейсу, с названием, указанным в name (необходимо указывать полное название класса или интерфейса), используя переданный загрузчик классов. Если в качестве загрузчика классов loader передано значение null, будет взят ClassLoader, который применялся для загрузки вызывающего класса. При этом класс будет инициализирован, только если значение initialize равно true и класс не был инициализирован ранее.
Зачастую проще и удобнее воспользоваться методом forName(), передав только название класса: public static Class forName(String className),– при этом будет использоваться загрузчик вызывающего класса и класс будет инициализирован (если до этого не был).
public Object newInstance() – создает и возвращает объект класса, который представляется данным экземпляром Class. Создание будет происходить с использованием конструктора без параметров. Если такового в классе нет, будет брошено исключение InstantiationException. Это же исключение будет брошено, если объект Class соответствует абстрактному классу, интерфейсу, или какая-то другая причина помешала созданию нового объекта.
Каждому методу, полю, конструктору класса также соответствуют объекты, список которых можно получить вызовом соответствующих методов объекта Class: getMethods(), getFields(), getConstructors(), getDeclaredMethods() и т.д. В результате будут получены объекты, которые отвечают за поля, методы, конструкторы объекта. Их можно использовать для формирования динамических вызовов Java – этот механизм называется reflection . Необходимые классы содержатся в пакете java.lang.reflection.
Рассмотрим пример использования этой технологии:
package demo.lang;
interface Vehicle {
void go();
}
class Automobile implements Vehicle {
public void go() {
System.out.println("Automobile go!");
}
}
class Truck implements Vehicle {
public Truck(int i) {
super();
}
public void go() {
System.out.println("Truck go!");
}
}
public class VehicleStarter {
public static void main(String[] args) {
Vehicle vehicle;
String[] vehicleNames = {"demo.lang.Automobile",
"demo.lang.Truck", "demo.lang.Tank"};
for(int i=0; i<vehicleNames.length; i++) {
try {
String name = vehicleNames[i];
System.out.println("look for class for: " + name);
Class aClass = Class.forName(name);
System.out.println("creating vehicle...");
vehicle = (Vehicle)aClass.newInstance();
System.out.println("create vehicle: " + vehicle.getClass());
vehicle.go();
} catch(ClassNotFoundException e) {
System.out.println("Exception: " + e);
} catch(InstantiationException e) {
System.out.println("Exception: " + e);
}
}
}
}
Пример 13.3.
Если запустить эту программу, на экран будет выведено следующее:
look for class for: demo.lang.Automobile
creating vehicle...
create vehicle: class demo.lang.Automobile
Automobile go!
look for class for: demo.lang.Truck
creating vehicle...
Exception: java.lang.InstantiationException
look for class for: demo.lang.Tank
Class not found: java.lang.ClassNotFoundException: demo.lang.Tank
Пример 13.4.
В этом примере делается попытка создать с помощью reflection три объекта. Имена классов, от которых они должны быть порождены, записаны в массив vehicleNames. Объект класса Automobile был успешно создан, причем, дальнейшая работа с ним велась через интерфейс Vehicle. Класс Truck был найден, но при попытке создания объекта было брошено, а затем обработано исключение java.lang.InstantiationException, поскольку конструктор без параметров отсутствует. Класс java.lang.Tank определен не был и поэтому при попытке получить соответствующий ему объект Class было выброшено исключение java.lang.ClassNotFoundException.
Классы-обертки
Во многих случаях предпочтительней работать именно с объектами, а не с примитивными типами. Так, например, при использовании коллекций просто необходимо значения примитивных типов представлять в виде объектов.
Для этих целей и предназначены так называемые классы-обертки. Для каждого примитивного типа Java существует свой класс-обертка . Такой класс является неизменяемым (если необходим объект, хранящий другое значение, его нужно создать заново), к тому же имеет атрибут final – от него нельзя наследовать класс. Все классы-обертки (кроме Void ) реализуют интерфейс Serializable, поэтому объекты любого (кроме Void ) класса-обертки могут быть сериализованы. Все классы-обертки содержат статическое поле TYPE, ссылающееся на объект Class, соответствующий примитивному оборачиваемому типу.
Также классы-обертки содержат статические методы для обеспечения удобного манипулирования соответствующими примитивными типами, например, преобразование к строковому виду.
В таблице 13.1 приведены примитивные типы и соответствующие им классы-обертки.
Таблица 13.1. Примитивные типы и соответствующие им классы-обертки.
Класс-обертка
Примитивный тип
Byte
byte
Short
short
Character
char
Integer
int
Long
long
Float
float
Double
double
Boolean
boolean
При этом классы-обертки числовых типов Byte, Short, Integer, Long, Float, Double наследуются от одного класса – Number. В нем объявлены методы, возвращающие числовое значение во всех числовых форматах Java ( byte, short, int, long, float и double ).
Все классы-обертки реализуют интерфейс Comparable. Все классы-обертки числовых типов имеют метод equals(Object), сравнивающий примитивные значения объектов.
Рассмотрим более подробно некоторые из классов-оберток.
Integer
Наиболее часто используемые статические методы:
* public static int parseInt(String s) – преобразует строку, представляющую десятичную запись целого числа, в int ;
* public static int parseInt(String s, int radix) – преобразует строку, представляющую запись целого числа в системе счисления radix, в int.
Оба метода могут возбуждать исключение NumberFormatException, если строка, переданная на вход, содержит нецифровые символы.
Не следует путать эти методы с другой парой похожих методов:
public static Integer valueOf(String s)
public static Integer valueOf(String s, int radix)
Данные методы выполняют аналогичную работу, только результат представляют в виде объекта-обертки.
Существует также два конструктора для создания экземпляров класса Integer:
* Integer(String s) – конструктор, принимающий в качестве параметра строку, представляющую числовое значение.
* Integer(int i) – конструктор, принимающий числовое значение.
public static String toString(int i) – используется для преобразования значения типа int в строку.
Далее перечислены методы, преобразующие int в строковое восьмеричное, двоичное и шестнадцатеричное представление:
* public static String toOctalString(int i) – восьмеричное;
* public static String toBinaryString(int i) – двоичное;
* public static String toHexString(int i) – шестнадцатеричное.
Имеется также две статические константы:
* Integer.MIN_VALUE – минимальное int значение;
* Integer.MAX_VALUE – максимальное int значение.
Аналогичные константы, описывающие границы соответствующих типов, определены и для всех остальных классов-оберток числовых примитивных типов.
public int intValue() возвращает значение примитивного типа для данного объекта Integer. Классы-обертки остальных примитивных целочисленных типов – Byte, Short, Long – содержат аналогичные методы и константы (определенные для соответствующих типов: byte, short, long ).
Рассмотрим пример:
public static void main(String[] args) {
int i = 1;
byte b = 1;
String value = "1000";
Integer iObj = new Integer(i);
Byte bObj = new Byte(b);
System.out.println("while i==b is " +
(i==b));
System.out.println("iObj.equals(bObj) is "
+ iObj.equals(bObj));
Long lObj = new Long(value);
System.out.println("lObj = " +
lObj.toString());
Long sum = new Long(lObj.longValue() +
iObj.byteValue() +
bObj.shortValue());
System.out.println("The sum = " +
sum.doubleValue());
}
В данном примере произвольным образом используются различные варианты классов-оберток и их методов. В результате выполнения на экран будет выведено следующее:
while i==b is true
iObj.equals(bObj) is false
lObj = 1000
The sum = 1002.0
Оставшиеся классы-обертки числовых типов Float и Double, помимо описанного для целочисленных примитивных типов, дополнительно содержат определения следующих констант (они подробно разбирались в лекции 4):
* NEGATIVE_INFINITY – отрицательная бесконечность;
* POSITIVE_INFINITY – положительная бесконечность;
* NaN – нечисловое значение.
Кроме того, другой смысл имеет значение MIN_VALUE – вместо наименьшего значения оно представляет минимальное положительное (строго > 0) значение, которое может быть представлено этим примитивным типом.
Кроме классов-оберток для примитивных числовых типов, таковые определены и для остальных примитивных типов Java.
Character
Реализует интерфейсы Comparable и Serializable.
Из конструкторов имеет только один, принимающий char в качестве параметра.
Кроме стандартных методов equals(), hashCode(), toString(), содержит только два нестатических метода:
* public char charValue() – возвращает обернутое значение char;
* public int compareTo(Character anotherCharacter) – сравнивает обернутые значения char как числа, то есть возвращает значение return this.value – anotherCharacter.value.
Также для совместимости с интерфейсом Comparable метод compareTo() определен с параметром Object:
* public int compareTo(Object o) – если переданный объект имеет тип Character, результат будет аналогичен вызову compareTo((Character)o), иначе будет брошено исключение ClassCastException, так как Character можно сравнивать только с Character.
Статических методов в классе Character довольно много, но все они просты и логика их работы понятна из названия. Большинство из них - это методы, принимающие char и проверяющие всевозможные свойства. Например:
public static boolean isDigit(char c)
// проверяет, является ли char цифрой.
Эти методы возвращают значение истина или ложь, в соответствии с тем, выполнен ли критерий проверки.
Boolean
Представляет класс-обертку для примитивного типа boolean.
Реализует интерфейс java.io.Serializable и во всем напоминает аналогичные классы-обертки.
Для получения примитивного значения используется метод booleanValue().
Void
Этот класс-обертка, в отличие от остальных, не реализует интерфейс java.io.Serializable. Он не имеет открытого конструктора. Более того, экземпляр этого класса вообще не может быть получен. Он нужен только для получения ссылки на объект Class, соответствующий void. Эта ссылка представлена статической константой TYPE.
Делая краткое заключение по классам-оберткам, можно сказать, что:
* каждый примитивный тип имеет соответствующий класс-обертку ;
* все классы-обертки могут быть сконструированы как с использованием примитивных типов, так и с использованием String, за исключением Character, который может быть сконструирован только по char ;
* классы-обертки могут сравниваться с использованием метода equals() ;
* примитивные типы могут быть извлечены из классов-оберток с помощью соответствующего метода xxxxValue() (например intValue() );
* классы-обертки также являются классами-утилитами, т.е. предоставляют набор статических методов для работы с примитивными типами;
* классы-обертки являются неизменяемыми.
Math
Класс Math состоит из набора статических методов, производящих наиболее популярные математические вычисления, и двух констант, имеющих особое значение в математике, – это число Пи и основание натурального логарифма. Часто этот класс еще называют классом-утилитой (Utility class). Так как все методы класса статические, нет необходимости создавать экземпляр данного класса, потому он и не имеет открытого конструктора. Нельзя также и наследоваться от этого класса, так как он объявлен с модификатором final.
Итак, константы определены следующим образом:
public static final double Math.PI – задает число ? ("пи");
public static final double Math.E – основание натурального логарифма.
В таблице 13.2 приведены все методы класса и дано их краткое описание.
Таблица 13.2. Методы класса Math и их краткое описание.
Возвращаемое значение
Имя метода и параметры
Описание
…
abs(… a)
абсолютное значение (модуль) для типов double, float, int, long
double
acos(double a)
арккосинус
double
asin(double a)
арксинус
double
atan(double a)
арктангенс
double
ceil(double a)
наименьшее целое число, большее a
double
floor(double a)
целое число, меньшее a
double
IEEEremainder (double a, double b)
остаток по стандарту IEEE 754 (подробно рассматривался в лекции 3)
double
sin(double a)
синус (здесь и далее: аргумент должен быть в радианах)
double
cos(double a)
косинус
double
tan(double a)
тангенс
double
exp(double a)
e в степени a
double
log(double a)
натуральный логарифм a
…
max(… a, … b)
большее из двух чисел (для типов double, float, long, int )
…
min(… a, … b)
меньшее из двух чисел (для типов double, float, long, int )
double
pow(double a, double b)
a в степени b
double
random()
случайное число от 0.0 до 1.0
double
rint(double a)
значение int, ближайшее к a
…
round(… a)
значение long для double ( int для float ), ближайшее к a
double
sqrt(double a)
квадратный корень числа a
double
toDegrees(double a)
преобразование из радианов в градусы
double
toRadians(double a)
преобразование из градусов в радианы
Строки
String
Этот класс используется в Java для представления строк. Он обладает свойством неизменяемости. После того как создан экземпляр этого класса, его содержимое уже не может быть модифицировано.
Существует много способов создать объект String. Наиболее простой, если содержимое строки известно на этапе компиляции, – написать текст в кавычках:
String abc = "abc";
Можно использовать и различные варианты конструктора. Наиболее простой из них – конструктор, получающий на входе строковый литерал.
String s = new String("immutable");
На первый взгляд, эти варианты создания строк отличаются только синтаксисом. На самом же деле различие есть, хотя в большинстве случаев оно несущественно. Рассмотрим пример:
public class Test {
public Test() {
}
public static void main(String[] args) {
Test t = new Test();
String s1 = "Hello world !!!";
String s2 = "Hello world !!!";
System.out.println("String`s equally = " +
(s1.equals(s2)));
System.out.println(
"Strings are the same = " + (s1==s2));
}
}
В результате на консоль будет выведено:
String`s equally = true
Strings are the same = true
Теперь несколько модифицируем код:
public class Test {
public Test() {
}
public static void main(String[] args) {
Test t = new Test();
String s1 = "Hello world !!!";
String s2 = new String("Hello world !!!");
System.out.println("String`s equally = " +
(s1.equals(s2)));
System.out.println(
"Strings are the same = " + (s1==s2));
}
}
В результате на консоль будет выведено:
String`s equally = true Strings are the same = false
Почему результат изменился? Дело в том, что создание нового объекта – это одна из самых трудоемких процедур в Java. Поэтому компилятор стремится уменьшить их количество, если это не приводит к непредсказуемому поведению программы.
В примере объявляются две переменные, которые инициализируются одинаковым значением. Поскольку класс String неизменяемый, их значения всегда будут одинаковыми. Это позволяет компилятору завести скрытую вспомогательную текстовую переменную, которая будет хранить такое значение, а все остальные переменные будут ссылаться на него же, а не порождать новые объекты. В результате в первом варианте программы создается лишь один объект String. Для большинства операций это несущественная разница. Исключение составляют действия, которые привязаны к конкретному объекту, а не к его значению. Это метод equals, методы wait/notify.
Во втором варианте указано динамическое обращение к конструктору. В этом случае компилятор уже не имеет возможности заниматься оптимизацией и JVM во время исполнения программы действительно создаст второй объект с точно таким же значением. Что мы и видим по результату выполнения примера.
В Java для строк определен оператор +. При использовании этого оператора производится конкатенация строк. В классе String также определен метод:
public String concat(String s);
Он возвращает новый объект-строку, дополненный справа строкой s.
Рассмотрим другой пример.
public class Test {
public static void main(String[] args) {
Test t = new Test();
String s = " prefix !";
System.out.println(s);
s = s.trim();
System.out.println(s);
s = s.concat(" suffix");
System.out.println(s);
}
}
prefix !
prefix !
prefix ! suffix
В данном случае может сложиться впечатление, что строку (объект String, на который ссылается переменная s ), можно изменять. В действительности это не так. В результате выполнения методов trim (отсечение пробелов в начале и конце строки) и concat создаются новые объекты-строки и ссылка s начинает указывать на новый объект-строку. Таким образом, меняется значение ссылки, объекты же неизменяемы.
Как уже отмечалось, строка состоит из двухбайтных Unicode-символов. Однако во многих случаях требуется работать со строкой как с набором байт (ввод/вывод, работа с базой данных и т.д.). Преобразование строки в последовательность байтов производится следующими методами:
* byte[] getBytes() – возвращает последовательность байтов в кодировке, принятой по умолчанию (как правило, зависит от настроек операционной системы);
* byte[] getBytes(String encoding) – возвращает последовательность байтов в указанной кодировке encoding.
Для выполнения обратной операции (преобразования байтов в строку) необходимо сконструировать новый объект-строку с помощью следующих методов:
* String(byte[] bytes) – создает строку из последовательности байтов в кодировке, принятой по умолчанию;
* String(byte[] bytes, String enc) – создает строку из последовательности байтов в указанной кодировке.
StringBuffer
Этот класс используется для создания и модификации строковых выражений, которые после можно превратить в String. Он реализован на основе массива char[], что позволяет, в отличие от String, модифицировать его значение после создания объекта.
Рассмотрим наиболее часто используемые конструкторы класса StringBuffer:
* StringBuffer() – создает пустой StringBuffer ;
* StringBuffer(String s) – буфер заполняется указанным значением s ;
* StringBuffer(int capacity) – создает экземпляр класса StringBuffer с указанным размером (длина char[] ). Задание размера не означает, что нельзя будет оперировать строками с большей длиной, чем указано в конструкторе. На самом деле этим гарантируется, что при работе со строками меньшей длины дополнительное выделение памяти не потребуется.
Разница между String и StringBuffer может быть продемонстрирована на следующем примере:
public class Test {
public static void main(String[] args) {
Test t = new Test();
String s = new String("ssssss");
StringBuffer sb =
new StringBuffer("bbbbbb");
s.concat("-aaa");
sb.append("-aaa");
System.out.println(s);
System.out.println(sb);
}
}
В результате на экран будет выведено следующее:
ssssss
bbbbbb-aaa
В данном примере можно заметить, что объект String остался неизменным, а объект StringBuffer изменился.
Основные методы, используемые для модификации StringBuffer, это:
* public StringBuffer append(String str) – добавляет переданную строку str в буфер;
* public StringBuffer insert(int offset, String str) – вставка строки, начиная с позиции offset (пропустив offset символов).
Стоит обратить внимание, что оба метода имеют варианты, принимающие в качестве параметров различные примитивные типы Java вместо String. При использовании этих методов аргумент предварительно приводится к строке (с помощью String.valueOf() ).
Еще один важный момент, связанный с этими методами, – они возвращают сам объект, у которого вызываются. Благодаря этому, возможно их использование в цепочке. Например:
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("abc");
String str = sb.append("e").insert(4,
"f").insert(3,"d").toString();
System.out.println(str);
}
В результате на экран будет выведено:
abcdef
При передаче экземпляра класса StringBuffer в качестве параметра метода следует помнить, что этот класс изменяемый:
public class Test {
public static void main(String[] args) {
Test t = new Test();
StringBuffer sb = new StringBuffer("aaa");
System.out.println("Before = " + sb);
t.doTest(sb);
System.out.println("After = " + sb);
}
void doTest(StringBuffer theSb) {
theSb.append("-bbb");
}
}
В результате на экран будет выведено следующее:
Before = aaa
After = aaa-bbb
Поскольку все объекты передаются по ссылке, в методе doTest, при выполнении операций с theSB, будет модифицирован объект, на который ссылается sb.
Системные классы
Следующие классы, которые будут рассмотрены, обеспечивают взаимодействие с внутренними механизмами JVM и средой исполнения приложения:
* ClassLoader – загрузчик классов; отвечает за загрузку описания классов в память JVM;
* SecurityManager – менеджер безопасности; содержит различные методы проверки допустимости запрашиваемой операции;
* System – содержит набор полезных статических полей и методов;
* Runtime – позволяет приложению взаимодействовать со средой исполнения;
* Process – представляет интерфейс для взаимодействия с внешней программой, запущенной при помощи Runtime.
ClassLoader
Это абстрактный класс, ответственный за загрузку типов. По имени класса или интерфейса он находит и загружает в память данные, которые составляют определение типа. Обычно для этого используется простое правило: название типа преобразуется в название class -файла, из которого и считывается вся необходимая информация.
Каждый объект Class содержит ссылку на объект ClassLoader, с помощью которого он был загружен.
Для добавления альтернативного способа загрузки классов можно реализовать свой загрузчик, унаследовав его от ClassLoader. Например, описание класса может загружаться через сетевое соединение. Метод defineClass() преобразует массив байт в экземпляр класса Class. С помощью метода newInstance() могут быть получены экземпляры такого класса. В результате загруженный класс становится полноценной частью исполняемого Java-приложения.
Для иллюстрации приведем пример, как может выглядеть простая реализация загрузчика классов, использующего сетевое соединение:
class NetworkClassLoader extends ClassLoader {
String host; int port;
public NetworkClassLoader(String host, int port) {
this.host = host; this.port = port;
}
public Class findClass(String className) {
byte[] bytes = loadClassData(className);
return defineClass(className, bytes, 0,
bytes.length);
}
private byte[] loadClassData(
String className) {
byte[] result = null;
// open connection, load the class data
return result;
}
}
В этом примере только показано, что наследник загрузчика классов должен определить и реализовать методы findClass() и loadClassData() для загрузки описания класса. Когда описание получено, массив байт передается в метод defineClass() для создания экземпляра Class. Для простоты в примере приведен только шаблонный код, без реализации получения байт из сетевого соединения.
Для получения экземпляров классов, загруженных с помощью этого загрузчика, можно воспользоваться методом loadClass():
try {
ClassLoader loader =
new NetworkClassLoader(host, port);
Object main = loader.loadClass(
"Main").newInstance();
}
catch(ClassNotFoundException e) {
e.printStackTrace();
}
catch(InstantiationException e) {
e.printStackTrace();
}
catch(IllegalAccessException e) {
e.printStackTrace();
}
Если такой класс не будет найден, будет брошено исключение ClassNotFoundException, если класс будет найден, но произойдет какая-либо ошибка при создании объекта этого класса – будет брошено исключение InstantiationException, и, наконец, если у вызывающего потока не имеется соответствующих прав для создания экземпляров этого класса (что проверяется менеджером безопасности), будет брошено исключение IllegalAccessException.
SecurityManager – менеджер безопасности
С помощью методов этого класса приложения перед выполнением потенциально опасных операций проверяют, является ли операция допустимой в данном контексте.
Класс SecurityManager содержит много методов с именами, начинающимися с приставки check ("проверить"). Эти методы вызываются из стандартных классов библиотек Java перед тем, как в них будут выполнены потенциально опасные операции. Типичный вызов выглядит примерно следующим образом:
SecurityManager security =
System.getSecurityManager();
if(security != null) {
security.checkX(…);
}
где X – название потенциально опасной операции: Access, Read, Write, Connect, Delete, Exec, Listen и т.д.
Предотвращение вызова производится путем бросания исключения – SecurityException, если вызов операции не разрешен (кроме метода checkTopLevelWindow, который возвращает boolean значение).
Для установки менеджера безопасности в качестве текущего вызывается метод setSecurityManager() в классе System. Соответственно, для его получения нужно вызвать метод getSecurityManager().
В большинстве случаев, если приложение запускается локально, будут разрешены все действия, поскольку в системе SecurityManager отсутствует. Предполагается, что запускаемому локально приложению можно полностью доверять. Если же приложение может быть опасно (например, его код был загружен из сети, как это происходит в случае апплетов), то менеджер безопасности выставляется и его уже нельзя убрать или заменить (попытки вызовут SecurityException ). Он контролирует работу с локальной файловой системой, сетевыми соединениями, потоками исполнения и т.д.
System
Класс System содержит набор полезных статических методов и полей. Экземпляр этого класса не может быть создан или получен.
Пожалуй, наиболее широко используемой возможностью, предоставляемой System, является стандартный вывод, доступный через переменную System.out. Ее тип – PrintStream (потоки данных будут подробно рассматриваться в лекции 15). Стандартный вывод можно перенаправить в другой поток (файл, массив байт и т.д., главное, чтобы это был объект PrintStream ):
public static void main(String[] args) {
System.out.println("Study Java");
try {
PrintStream print = new PrintStream(new
FileOutputStream("d:\file2.txt"));
System.setOut(print);
System.out.println("Study well");
}
catch(FileNotFoundException e) {
e.printStackTrace();
}
}
При запуске этого кода на экран будет выведено только
Study Java
И в файл "d:file2.txt" будет записано
Study well
Аналогично могут быть перенаправлены стандартный ввод System.in – вызовом System.setIn(InputStream) и поток вывода сообщений об ошибках System.err – вызовом System.setErr(PrintStream) (по умолчанию все потоки – in, out, err – работают с консолью приложения).
Следующие методы класса System позволяют работать с некоторыми параметрами системы:
* public static void runFinalizersOnExit(boolean value) – определяет, будет ли производиться вызов метода finalize() у всех объектов (у кого еще не вызывался), когда выполнение программы будет окончено (по умолчанию выставлено значение false );
* public static native long currentTimeMillis() – возвращает текущее время; это время представляется как количество миллисекунд, прошедших с 1 января 1970 года;
* public static String getProperty(String key) – возвращает значение свойства с именем key.
Чтобы получить все свойства, определенные в системе, можно воспользоваться следующим методом:
public static java.util.Properties getProperties() – возвращает объект java.util.Properties, в котором содержатся значения всех определенных системных свойств.
Метод arrayCopy(Object source, int srcPos, Object target, int trgPos, int length) предоставляет возможность быстрого копирования содержимого одного массива в другой. Первый параметр задает исходный массив, второй – номер позиции, начиная с которой брать элементы для копирования. Третий параметр – массив-"получатель", четвертый – номер позиции в нем, начиная с которого будут записываться скопированные элементы. Наконец, последний параметр задает количество элементов, которые надо скопировать. Оба массива должны быть созданы, иметь совместимые типы и достаточную длину, иначе будут сгенерированы соответствующие исключения.
Runtime
Во время исполнения приложению Java сопоставляется экземпляр класса Runtime. Этот объект позволяет взаимодействовать с окружением, в котором запущена Java-программа. Получить его можно с помощью статического метода Runtime.getRuntime().
Объект этого класса:
* public void exit(int status) – осуществляет завершение программы с кодом завершения status (при использовании этого метода особое внимание нужно уделить обработке исключений – выход будет осуществлен моментально и в конструкциях try-catch-finally управление в finally передано не будет);
* public native void gc() – сигнализирует сборщику мусора о необходимости запуска;
* public void runFinalization() – производит запуск выполнения методов finalize() у всех объектов, этого ожидающих;
* public native long freeMemory() – возвращает количество свободной памяти, доступной приложению JVM. В некоторых случаях это количество может быть увеличено, если вызвать у объекта Runtime метод gc() ;
* public native long totalMemory() – возвращает суммарное количество памяти, выделенное Java-машине. Это количество может изменяться даже в течение одного запуска, что зависит от реализации платформы, на которой запущена Java-машина. Также не стоит закладываться на объем памяти, занимаемой одним определенным объектом, – эта величина тоже зависит от реализации Java-машины;
* public void loadLibrary(String libname) – загружает библиотеку с указанным именем.
Обычно загрузка библиотек производится следующим образом: в классе, использующем native реализации методов, добавляется статический инициализатор, например:
static { System.loadLibrary("LibFile");}
Таким образом, когда класс будет загружен и инициализирован, необходимый код для реализации native методов также будет загружен. Если будет произведено несколько вызовов загрузки библиотеки с одним и тем же именем, произведен будет только первый, а все остальные будут проигнорированы.
public void load(String filename) – подгружает файл с указанным названием в качестве библиотеки. В принципе, этот метод работает так же, как и метод loadLibrary(), только принимает в качестве параметра именно название файла, а не библиотеки, тем самым позволяя загрузить любой файл с native кодом;
public Process exec(String command) – в отдельном процессе запускает команду, представленную переданной строкой. Возвращаемый объект Process может быть использован для взаимодействия с этим процессом.
Process
Объекты этого класса получаются вызовом метода exec() у объекта Runtime, запускающего отдельный процесс. Объект класса Process может использоваться для управления процессом и получения информации о нем.
Process – абстрактный класс, определяющий, какие методы должны присутствовать в реализациях для конкретных платформ. Методы класса Process:
* public InputStream getInputStream() – дает возможность получать поток ввода процесса;
* getErrorStream(), getOutputStream() – методы, аналогичные getInputStream(), но получающие, соответственно, стандартные потоки сообщений об ошибках и вывода;
* public void destroy() – уничтожает процесс; все подпроцессы, запущенные из него, также будут уничтожены;
* public int exitValue() – возвращает код завершения процесса; по соглашению, код завершения, равный 0, означает нормальное завершение;
* public int waitFor() – вынуждает текущий поток выполнения приостановиться до тех пор, пока не будет завершен процесс, представленный этим экземпляром Process; возвращает значение кода завершения процесса.
Даже если в приложении Java не будет ни одной ссылки на объект Process, процесс не будет уничтожен и будет продолжать асинхронно выполняться до своего завершения. Спецификацией не оговаривается механизм, с помощью которого будет выделяться процессорное время на выполнение процессов Process и потоков Java. Поэтому при проектировании программ не стоит полагаться ни на какой из них, так как различные Java-машины могут демонстрировать различное поведение.
Потоки исполнения
Многопоточная архитектура в Java была подробно рассмотрена в лекции 12. Остановимся более подробно на методах применяемых классов.
Runnable
Runnable – это интерфейс, содержащий один-единственный метод без параметров: run().
Thread
Объекты этого класса представляют возможность запускать и управлять потоками исполнения.
Итак, для управления потоками в классе Thread предусмотрены следующие методы:
* public void start() – производит запуск нового потока;
* public final void join() – если поток A вызывает этот метод у объекта Thread, представляющего поток B ( threadB.join() ), то выполнение потока A приостанавливается до тех пор, пока не закончит выполнение поток B ;
* public static void yield() – поток, из которого вызван этот метод, временно приостанавливается, чтобы дать возможность выполняться другим потокам;
public static void sleep(long millis) – поток, из которого вызван этот метод, перейдет в состояние "сна" на указанное количество миллисекунд, после чего сможет продолжить выполнение. При этом нужно учесть, что через время millis миллисекунд этому потоку может быть выделено процессорное время, а может, ему придется и подождать немного дольше. Можно сказать, что поток продолжит выполнение не раньше, чем через время millis миллисекунд.
Существует еще несколько методов, которые объявлены deprecated и рекомендуется их избегать. Это: suspend() – временно прекратить выполнение, resume() – продолжить выполнение (приостановленное вызовом suspend()), stop() – остановить выполнение потока.
При вызове метода stop() в потоке, который представляет этот объект Thread, будет брошена ошибка ThreadDeath. Этот класс унаследован от Error. Если ошибка не будет обработана в программе и, соответственно, произойдет прекращение работы потока, сообщение о ненормальном завершении выведено не будет, так как такое завершение рассматривается как нормальное. Если же в программе эта ошибка обрабатывается (например, для проведения каких-то дополнительных действий перед закрытием потока), то очень важно позаботиться о том, чтобы эта же ошибка была брошена дальше, чтобы поток действительно закончил свое выполнение. Класс ThreadDeath специально унаследован от Error, а не от Exception, так как очень часто используется перехват всех исключений класса Exception, что не позволит корректно остановить поток.
Также Thread позволяет выставлять такие свойства потока, как:
* Name – значение типа String, которое можно использовать для более наглядного обращения с потоками в группе;
* Daemon – выполнение программы не будет прекращено до тех пор, пока выполняется хотя бы один не daemon поток;
* Priority – определяет приоритет потока. В классе Thread определены константы, задающие минимальное и максимальное значения для приоритетов потока,– MIN_PRIORITY и MAX_PRIORITY, а также значение приоритета по умолчанию – NORM_PRIORITY.
Эти свойства могут быть изменены только до того момента, когда поток будет запущен, то есть вызван метод start() объекта Thread.
Получить эти значения можно, конечно же, в любой момент жизни потока – и после его запуска, и после прекращения выполнения. Также можно узнать, в каком состоянии сейчас находится поток: вызовом методов isAlive() – выполняется ли еще, isInterrupted() – прерван ли.
ThreadGroup