Вы достигли нового уровня

Уровень 32

1. RandomAccessFile и т.д.

- Привет, Амиго!

- Привет, Билаабо! Как жизнь?

- Отлично. Вчера выводил паразитов, но пока не очень-то получается. А потом опять пришлось ночевать в мусорном баке.

- Т.е. все по-прежнему отлично?

- Можно и так сказать.

- Гуд. А что у нас будет сегодня?

- Сегодня я тебе расскажу про класс RandomAccessFile.

Дело в том, что FileInputStream и FileOutputStream представляют файлы в виде потоков: читать из них и писать в них можно только последовательно.

Это не всегда очень-то удобно. Иногда тебе нужно записать пару строк в середину файла, или прочитать пару страниц текста в конце многомегабайтного файла. Читать для этого весь файл не очень эффективно.

Для решения этой проблемы был создан класс RandomAccessFile. С его помощью можно писать в любое место файла, читать из него, а также писать и читать файл одновременно.

- Как интересно.

- Ага. Очень удобно на самом деле.

- А как читать из произвольного места?

- Все довольно просто. Представь, что у тебя открыт текстовый редактор «блокнот». В нем есть курсор. Когда ты что-то печатаешь, текст добавляется в том месте, где он стоит. С чтением файла то же самое. Чтение происходит в том месте, где стоит «курсор». При чтении/записи он сам автоматически сдвигается.

Давай, я лучше покажу тебе пример:

Чтение файла:
//r- read, файл открыт только для чтения
RandomAccessFile raf = new RandomAccessFile("input.txt", "r");

//перемещаем «курсор» на 100-й символ.
raf.seek(100);

//читаем строку, начиная с текущего положения курсора и до конца строки
String text = raf.readLine();

//закрываем файл
raf.close();

В этом примере я хотел бы обратить твое внимание на две вещи:

Во-первых, создание объекта RandomAccessFile. Вторым параметром идет буква r. Это означает, что файл открыт для чтения (r- read). Если ты хочешь открыть файл для чтения и записи, в конструктор надо передать “rw” вместо “r”.

Во-вторых, обрати внимание на метод seek. С помощью этого метода можно прыгать по файлу и менять позицию курсора для текущей операции чтения/записи. Сразу при открытии файла «курсор» устанавливается на 0-й байт. Или точнее – перед нулевым байтом.

- Правильно ли я понял, что мы открыли файл, и курсор был в его самом начале – на позиции 0. Затем мы вызвали seek и он переместился на 100-й байт. А когда вызвали readLine, то чтение уже было начиная с сотого байта. Так?

- Да. Только хочу обратить твое внимание на то, что метод seek позволяет произвольно прыгать по файлу. Пример:

Чтение файла:
//r- read, файл открыт только для чтения
RandomAccessFile raf = new RandomAccessFile("input.txt", "r");
// «курсор» стоит на 0-м символе.
String text1 = raf.readLine();

//перемещаем «курсор» на 100-й символ.
raf.seek(100);
String text2 = raf.readLine();

//перемещаем «курсор» на 0-й символ.
raf.seek(0);
String text3 = raf.readLine();

//закрываем файл
raf.close();

В данном примере мы вначале прочитали строку, начиная с 0-го байта. Затем прыгнули на сотый байт и прочитали строку там. Затем снова прыгнули на 0-й байт и прочитали строку. Т.е. text1 и text3 – это идентичные строки.

- Ага. Ситуация начинает проясняться.

- Отлично. Тогда вот тебе еще один пример:

Чтение файла:
//rw- read/write, файл открыт и для чтения и для записи
RandomAccessFile raf = new RandomAccessFile("seek.txt", "rw");

//пишем в файл строку, начиная с 0-го байта
raf.writeBytes("It is a string");

//ставим курсор на 8-й символ
raf.seek(8);
//печатаем в файл строку surprise!
raf.writeBytes("surprise!");

//закрываем файл
raf.close();

Тут мы открываем файл для чтения и записи – в конструктор передаем «rw» (read/write).

Затем пишем в файл строку «It is a string».

Затем переставляем курсор на 8-й байт (как раз на начало слова string)

Затем пишем в файл строку «surprise!»

В результате файл будет содержать «It is a surprise!»

- Т.е. байты не вставляются в середину файла, а заменяют те, которые там были?

- Ага.

- А если мы установим курсор в самый конец файла?

- Тогда байты будут писаться в конец, а файл – удлиняться. Т.е. практически то же самое, когда ты пишешь текст в текстовом редакторе.

- Гм. Вроде все понятно. А можно полный список методов класса RandomAccessFile?

- Конечно. Держи:

МетодОписание
int read() Читает один байт и возвращает его
int read(byte b[], int off, int len) Читает массив байт
int read(byte b[]) Читает массив байт
void readFully(byte b[]) Читает массив байт, ждет, пока добавятся новые байты, если их не хватает для заполнения массива
int skipBytes(int n) Пропускает n байт. Т.е. перемещает курсор на n байт вперед.
void write(int b) Пишет один байт в то место, где стоит курсор
void write(byte b[]) Пишет массив байт в то место, где стоит курсор
void write(byte b[], int off, int len) Пишет массив байт в то место, где стоит курсор
long getFilePointer() Возвращает номер байта, на который указывает «курсор». Может быть от 0 до «длины файла»
void seek(long pos) Перемещает «курсор», используемый для чтения/записи, в указанное место
long length() Возвращает длину файла
void setLength(long newLength) Устанавливает новую длину файла. Если файл был больше – он обрезается, если меньше – расширяется и новое место заполняется нулями
void close() Закрывает файл
boolean readBoolean() Читает boolean с текущей позиции курсора в файле
byte readByte() Читает byte с текущей позиции курсора в файле
char readChar() Читает char с текущей позиции курсора в файле
int readInt() Читает int с текущей позиции курсора в файле
long readLong() Читает long с текущей позиции курсора в файле
float readFloat() Читает float с текущей позиции курсора в файле
double readDouble() Читает double с текущей позиции курсора в файле
String readLine() Читает строку из файла и возвращает ее
void writeBoolean(boolean v) Пишет boolean в файл (начиная с позиции курсора)
void writeByte(int v) t Пишет byte в файл (начиная с позиции курсора)
void writeChar(int v) Пишет char в файл (начиная с позиции курсора)
void writeInt(int v) Пишет int в файл (начиная с позиции курсора)
void writeLong(long v) Пишет long в файл (начиная с позиции курсора)
void writeFloat(float v) Пишет float в файл (начиная с позиции курсора)
void writeDouble(double v) Пишет double в файл (начиная с позиции курсора)
void writeBytes(String s) Пишет строку в файл (начиная с позиции курсора)
void writeChars(String s) Пишет строку в файл (начиная с позиции курсора)

- Гм. Ничего принципиально нового. Разве что пара методов seek()/getFilePointer() и length()/setLength().

- Да, Амиго. Все примерно то же самое. Но ведь удобно?

- Удобно. Спасибо тебе, Билаабо, за интересную лекцию и за те примеры, что ты мне дал.

- Рад помочь, друг Амиго!

2. Задачи

- Привет, Амиго!

Задачи
1. Запись в файл

В метод main приходят три параметра:
1) fileName - путь к файлу
2) number - число, позиция в файле
3) text - текст
Записать text в файл fileName начиная с позиции number.
Если файл слишком короткий, то записать в конец файла.

3. StringReader, StringWriter

- Привет, Амиго!

- Привет, Элли!

- Сегодня я хочу тебе рассказать про классы StringReader и StringWriter. Принципиально нового тут для тебя будет мало, но иногда эти классы бывают очень полезны. И, как минимум, я хочу, чтобы ты знал, что они есть.

Эти классы – это простейшие реализации абстрактных классов Reader и Writer. И практически аналоги FileReader и FileWriter. Но, в отличие от них, они работают не с данными в файле на диске, а со строкой (String) находящейся в памяти Java-машины.

- А зачем нужные такие классы?

- Иногда нужны. StringReader – это, фактически, переходник между классом String и Reader. А StringWriter – это строка, которая унаследована от Writer. М-да. Сама вижу, что объяснение не очень. Давай лучше для начала рассмотрим пару примеров.

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

Чтение из объекта reader:
public static void main (String[] args) throws Exception
{
 String test = "Hi!\n My name is Richard\n I'm a photographer\n";

 //это строчка – ключевая: мы «превратили» строку в Reader
 StringReader reader = new StringReader(test);

 executor(reader);
}

public static void executor(Reader reader) throws Exception
{
 BufferedReader br = new BufferedReader(reader);
 while (br.ready())
 {
  String line = br.readLine();
  System.out.println(line);
 }
}

- Т.е. мы просто взяли строку, обернули ее в StringReader и передали вместо объекта Reader? И из нее все будет читаться, как и надо?

- Ага. Гм. А в этом есть смысл. А теперь проверим, как работают методы StringWriter. Для этого усложним пример. Теперь он будет не просто читать строки, и выводить их на экран, а разворачивать их задом наперед и выводить в объект writer. Пример:

Чтение из объекта reader и запись в объект writer:
public static void main (String[] args) throws Exception
{
 //эту строку должен будет прочитать Reader
 String test = "Hi!\n My name is Richard\n I'm a photographer\n";
 //заворачиваем строку в StringReader
 StringReader reader = new StringReader(test);

 //Создаем объект StringWriter
 StringWriter writer = new StringWriter();

 //переписываем строки из Reader во Writer, предварительно развернув их
 executor(reader, writer);

 //получаем текст, который был записан во Writer
 String result = writer.toString();

 //выводем полученный из Writer’а текст на экран
 System.out.println("Результат: "+result);
}

public static void executor(Reader reader, Writer writer) throws Exception
{
 BufferedReader br = new BufferedReader(reader);
 while (br.ready())
 {
  //читаем строку из Reader’а
  String line = br.readLine();

  //разворачиваем строку задом наперед
  StringBuilder sb = new StringBuilder(line);
  String newLine = sb.reverse().toString();

  //пишем строку в Writer
  writer.write(newLine);
 }
}

Мы создали объект StringWriter, внутри которого есть строка, в которой хранится все, что в этот writer пишут. А чтобы ее получить, надо всего лишь вызвать метод toString().

- Гм. Как-то все слишком просто получается. Метод executor работает с объектами потокового ввода reader и writer, а в методе main мы работаем уже со строками.

Все действительно так просто?

- Ага. Чтобы преобразовать строку в Reader достаточно написать:

Создание Reader из String
String s = "data";
Reader reader = new StringReader(s);

А преобразовать StringWriter к строке еще проще:

Получение String из Writer
Writer writer = new StringWriter();
/*тут пишем кучу данных во writer */
String result = writer.toString();

- Отличные классы, как по мне. Спасибо за рассказ, Элли.

4. Задачи

Задачи
1. Пишем стек-трейс

Реализуйте логику метода getStackTrace, который в виде одной строки должен возвращать весь стек-трейс переданного исключения.
Используйте подходящий метод класса Throwable, который поможет записать стек-трейс в StringWriter.
2. Читаем из потока

Реализуйте логику метода getAllDataFromInputStream. Он должен вернуть StringWriter, содержащий все данные из переданного потока.
Возвращаемый объект ни при каких условиях не должен быть null.
Метод main не участвует в тестировании.

5. BufferedReader, BufferedWriter

- И это снова я.

- Привет, Элли!

- Сегодня я хочу тебе подробно рассказать про BufferedReader и BufferedWriter.

- Так ты мне уже рассказывала все про них. Ну ничего там сложного нет.

- Ок. Расскажи, как работает BufferedReader.

- BufferedReader - это как переходник в розетке с 110 к 220 вольт.

В конструктор объекта BufferedReader обязательно нужно передать объект Reader, из которого он будет читать данные. Объект BufferedReader читает из Reader’а данные большими кусками и хранит их у себя внутри в буфере. Поэтому чтение из пары BufferedReader+Reader быстрее, чем прямо из Reader.

- Верно. А BufferedWriter?

- Тут тоже все просто. Когда мы пишем в FileWriter, например, то данные сразу записываются на диск. Если мы часто пишем небольшие данные, то происходит много обращений к диску, что замедляет работу программы. А если мы используем BufferedWriter в качестве «переходника», то операция записи на диск ускорится. BufferedWriter, при записи в него, сохраняет переданные данные во внутреннем буфере, а когда буфер заполняется – пишет данные во Writer одним большим куском. Это гораздо быстрее.

- Гм. Все верно. А что ты забыл?

- После окончания записи у объекта BufferedReader надо вызвать метод flush(), чтобы он записал данные из буфера во Writer, которые еще не записаны, т.е. буфер не заполнен до конца.

- А кроме того?

- А кроме того, пока буфер еще не записан во Writer, данные можно удалить и/или заменить на другие.

- Амиго! Я поражена! Да ты просто эксперт. Ладно, тогда я расскажу тебе о новых классах: ByteArrayStream, PrintStream.

Итак, ByteArrayInputStream и ByteArrayOutputStream.

Эти классы по сути чем-то похожи на StringReader и StringWriter. Только StringReader читал символы (char) из строки (String), а InputStream читает байты из массива байт (ByteArray).

StringWriter писал символы (char) в строку, а ByteArrayOutputStream пишет байты в массив байт у него внутри. При записи в StringWriter строка внутри него удлинялась, а при записи в ByteArrayOutputStream его внутренний массив байт тоже динамически расширяется.

Вспомни пример, который давали тебе на прошлой лекции:

Чтение из объекта reader и запись в объект writer:
public static void main (String[] args) throws Exception
{
 String test = "Hi!\n My name is Richard\n I'm a photographer\n";
 StringReader reader = new StringReader(test);

 StringWriter writer = new StringWriter();

 executor(reader, writer);

 String result = writer.toString();

 System.out.println("Результат: "+result);
}

public static void executor(Reader reader, Writer writer) throws Exception
{
 BufferedReader br = new BufferedReader(reader);
 while (br.ready())
 {
  String line = br.readLine();

  StringBuilder sb = new StringBuilder(line);
  String newLine = sb.reverse().toString();


  writer.write(newLine);
 }
}

Вот как он будет выглядеть, если тут работать не с символами, а с байтами:

Чтение из объекта InputStream и запись в объект OutputStream:
public static void main (String[] args) throws Exception
{
 String test = "Hi!\n My name is Richard\n I'm a photographer\n";
 InputStream inputStream = new ByteArrayInputStream(test.getBytes());

 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

 executor(inputStream, outputStream);

 String result = new String(outputStream.toByteArray());
 System.out.println("Результат: "+result);
}

public static void executor(InputStream inputStream, OutputStream outputStream)
{
 BufferedInputStream bis = new BufferedInputStream(inputStream);
 while (bis.available() > 0)
 {
  int data = bis.read();
  outputStream.write(data);
 }
}

Тут все аналогично примеру выше. Вместо String – ByteArray. Вместо Reader – InputStream, вместо Writer – OutputStream.

Единственные еще два момента – это преобразование строки в массив байт и обратно. Как ты видишь, это делается довольно несложно:

Преобразование строки в массив байт и обратно
public static void main (String[] args) throws Exception
{
 String test = "Hi!\n My name is Richard\n I'm a photographer\n";
 byte[] array = test.getBytes();

 String result = new String(array);
 System.out.println("Результат: "+result);
}

Чтобы получить байты, которые уже добавлены в ByteArrayInputStream, надо вызвать метод toByteArray().

- Ага. Аналогия с StringReader/StringWriter довольно сильная, особенно когда ты мне ее показала. Спасибо, Элли, действительно интересный урок.

- Куда это ты спешишь? У меня есть еще небольшой подарок – хочу рассказать тебе про класс PrintStream.

- PrintStream? В первый раз слышу о таком классе.

- Ага. Особенно, если не считать, что ты им пользуешься с первого дня, когда ты начал изучать Java. Помнишь System.out? так вот – System.out – это статическая переменная класса System типа… PrintStream! Именно оттуда растут ноги всех этих print, println и т.д.

- Ого. Как интересно. Я как-то ни разу и не задумывался. Расскажи подробнее.

- Гуд. Тогда слушай. Класс PrintStream был придуман для читабельного вывода информации. Он практически весь состоит из методов print и println. См. таблицу:

МетодыМетоды
void print(boolean b) void println(boolean b)
void print(char c) void println(char c)
void print(int c) void println(int c)
void print(long c) void println(long c)
void print(float c) void println(float c)
void print(double c) void println(double c)
void print(char[] c) void println(char[] c)
void print(String c) void println(String c)
void print(Object obj) void println(Object obj)
void println()
PrintStream format (String format, Object ... args)
PrintStream format (Locale l, String format, Object ... args)

Также есть несколько методов format, чтобы можно было выводить данные на основе шаблона. Пример:

Преобразование строки в массив байт и обратно
String name = "Kolan";
int age = 25;
System.out.format("My name is %s. My age is %d.", name, age);
Вывод на экран:
My name is Kolan. My age is 25.

- Ага, помню, мы уже когда-то разбирали метод format у класса String.

- На этом все.

- Спасибо, Элли.

6. Задачи

- Хе-хе, Амиго. Смотри, что я для тебя придумал:

Задачи
1. Генератор паролей

Реализуйте логику метода getPassword, который должен возвращать ByteArrayOutputStream, в котором будут байты пароля.
Требования к паролю:
1) 8 символов
2) только цифры и латинские буквы разного регистра
3) обязательно должны присутствовать цифры, и буквы разного регистра
Все сгенерированные пароли должны быть уникальные.
Каждый сгенерированный символ пароля пишите сразу в ByteArrayOutputStream.
Пример правильного пароля:
wMh7SmNu

7. DinamicProxy

- Привет, Амиго.

- Здорово, Риша.

- Сегодня я расскажу тебе новую и очень интересную тему – динамические прокси.

В Java есть несколько способов изменить функциональность нужного класса...

Способ первый - наследование.

Самый простой способ изменить поведение некоторого класса – это создать новый класс, унаследовать его от оригинального (базового) и переопределить его методы. Затем, вместо объектов оригинального класса использовать объекты класса наследника. Пример:

 
Reader reader = new UserCustomReader();

Способ второй – использование класса-обертки(Wrapper).

Примером такого класса есть BufferedReader. Во-первых, он унаследован от Reader, т.е. может быть использован вместо него. Во-вторых, он переадресует все вызовы к оригинальному объекту Reader, который обязательно нужно передать в конструкторе объекту BufferedReader. Пример:

 
Reader readerOriginal = new UserCustomReader();
Reader reader = new BufferedReader(readerOriginal);

Способ третий – создание динамического прокси (Proxy).

В Java есть специальный класс (java.lang.reflect.Proxy), с помощью которого фактически можно сконструировать объект во время исполнения программы (динамически), не создавая для него отдельного класса.

Это делается очень просто:

 
Reader reader = (Reader)Proxy.newProxyInstance();

- А вот это уже что-то новенькое!

- Но, нам ведь не нужен просто объект без методов. Надо чтобы у этого объекта были методы, и они делали то, что нам нужно. Для этого в Java используется специальный интерфейс InvocationHandler, с помощью которого можно перехватывать все вызовы методов, обращенные к proxy-объекту. proxy-объект можно создать только используя интерфейсы.

Invoke – стандартное название для метода/класса, основная задача которого просто вызвать какой-то метод.

Handler – стандартное название для класса, который обрабатывает какое-то событие. Например, обработчик клика мышки будет называться MouseClickHandler, и т.д.

У интерфейса InvocationHandler есть единственный метод invoke, в который направляются все вызовы, обращенные к proxy-объекту. Пример:

Пример:

Код
Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler());
reader.close();
class CustomInvocationHandler implements InvocationHandler
{
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  System.out.println("yes!");
  return null;
 }
}

При вызове метода reader.close(), вызовется метод invoke, и на экран будет выведена надпись “yes!”

- Т.е. мы объявили класс CustomInvocationHandler, в нем реализовали интерфейс InvocationHandler и его метод invoke. Метод invoke при вызове выводит на экран строку “yes!”- Затем мы создали объект типа CustomInvocationHandler и передали его в метод newProxyInstance при создании объекта-proxy.

- Да, все верно.

Это очень мощный инструмент, обычно создание таких прокси используется для имитации объектов из программ, которые физически запущены на другом компьютере. Или для контроля доступа – в таком методе можно проверять права текущего пользователя, обрабатывать ошибки, логировать ошибки и многое другое.

Вот пример, где метод invoke еще и вызывает методы оригинального объекта:

Код
Reader original = new UserCustomReader();

Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler(original));
reader.close();
class CustomInvocationHandler implements InvocationHandler
{
 private Reader readerOriginal;

 CustomInvocationHandler(Reader readerOriginal)
 {
  this.readerOriginal = readerOriginal;
 }

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  if (method.getName().equals("close"))
  {
   System.out.println("Reader closed!");
  }

  // это вызов метода close у объекта readerOriginal
  // имя метода и описание его параметров хранится в переменной method
  return method.invoke(readerOriginal, args);
 }
}

В данном примере есть две особенности.

Во-первых, в конструктор передается «оригинальный» объект Reader, ссылка на который сохраняется внутри CustomInvocationHandler.

Во-вторых, в методе invoke мы снова вызываем этот же метод, но уже у «оригинального» объекта.

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

 
return method.invoke(readerOriginal, args);

- Ага.

- Не сказал бы, что слишком очевидно, но все же понятно. Вроде бы.

- Отлично. Тогда вот еще что. В метод newProxyInstance нужно передавать еще немного служебной информации для создания proxy-объекта. Но, т.к. мы не создаем монструозные прокси-объекты, то эту информацию легко получить из самого оригинального класса.

Вот тебе пример:

Код
Reader original = new UserCustomReader();

ClassLoader classLoader = original.getClass().getClassLoader();
Class<?>[] interfaces = original.getClass().getInterfaces();
CustomInvocationHandler invocationHandler = new CustomInvocationHandler(original);

Reader reader = (Reader)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
class CustomInvocationHandler implements InvocationHandler
{
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  return null;
 }
}

- Ага. ClassLoader и список интерфейсов. Это что-то из Reflection, да?

- Ага.

- Ясно. Что ж, думаю, я смогу создать примитивный простенький прокси объект, если это когда-нибудь мне понадобится.

- А ты спроси у Диего :)

8. Задачи

- Привет, Амиго! Я придумал тебе пару интересных задач.

Решать их можно только в Intellij IDEA. Вот, смотри, какие интересные условия…

Задачи
1. Создание прокси-объекта

1) В отдельном файле создать публичный класс CustomInvocationHandler, который будет хэндлером при создании прокси-объекта.
2) CustomInvocationHandler должен иметь один публичный конструктор с одним агументом типа SomeInterfaceWithMethods.
3) Перед вызовом любого метода у оригинального объекта должна выводиться фраза [methodName in].
4) После вызова любого метода у оригинального объекта должна выводиться фраза [methodName out].
5) Реализовать логику метода getProxy, который должен создавать прокси для интерфейса SomeInterfaceWithMethods.
См. пример вывода в методе main. Метод main не участвует в тестировании.
2. Дженерики для создания прокси-объекта

В классе Solution создайте публичный метод getProxy
1) Метод getProxy должен возвращать прокси для любого интерфейса, который наследуется от Item
2) getProxy должен иметь два параметра. Первый - класс возвращаемого типа, второй - классы дополнительных интерфейсов.
3) Используйте ItemInvocationHandler для создания прокси
Метод main не участвует в тестировании

9. RMI

- Привет! И еще одна радостная тема – RMI. RMI расшифровывается Remote Method Invokation – удаленный вызов методов. Или другими словами RMI – это механизм, который позволяет объекту в одной Java-машине вызывать методы объекта в другой Java-машине, даже если они находятся на разных компьютерах, в разных странах, на разных сторонах земного шара.

- Ничего себе! Звучит очень круто.

- Ага. Но я постараюсь дать только обзорную лекцию. Тут, если глубоко копать, можно запутаться в нюансах работы.

Но если не ударяться в крайности, то RMI не только очень прост, но и значительно упрощает жизнь программиста. За что ему глубокий респект.

Итак, мы хотим, чтобы один объект, находящийся в одной Java-программе, смог вызвать метод у объекта, находящегося в другой Java-программе. Где-бы эти программы запущены не были.

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

В Java удаленно можно вызывать только методы интерфейсов, но не классов.

Итак, у нас есть две программы, как же им вызывать методы друг друга?

Давай рассмотрим ситуацию, когда одна программа содержит в себе некоторый объект, а вторая хочет вызвать его методы. Назовем первую программу – сервером, а вторую – клиентом.

Я сначала дам пример кода, а потом мы его разберем.

- А что будет делать наша программа?

- Гм. Ну, давай для простоты, у программы будет один метод, который разворачивает переданную ему строку задом наперед.

- Вроде ничего.

- Гуд, тогда начнем:

Сначала нам понадобится интерфейс, который будет удовлетворять нашим требованиями:

Интерфейс для межпрограммного взаимодействия
interface Reverse extends Remote
{
 public String reverse(String str) throws RemoteException;
}

Я создал интерфейс Reverse и добавил ему интерфейс-маркер Remote, а также исключение RemoteException. В процессе вызова метода могут происходить незапланированные сбои – тогда будет кидаться это исключение.

Затем нам нужно написать серверный класс, который бы реализовывал этот интерфейс:

Класс для сервера
class ReverseImpl implements Reverse
{
 public String reverse(String str) throws RemoteException
 {
  return new StringBuffer(str).reverse().toString();
 }
}

- Вижу. В этом методе, мы разворачиваем строку задом наперед.

- Ага.

А теперь надо сделать этот объект доступным для вызова с другой программы. Вот как это делается:

 Шаринг объекта
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public static final String UNIC_BINDING_NAME = "server.reverse";

public static void main(String[] args) throws Exception
{
 //создание объекта для удаленного доступа
 final ReverseImpl service = new ReverseImpl();

 //создание реестра расшареных объетов
 final Registry registry = LocateRegistry.createRegistry(2099);
 //создание "заглушки" – приемника удаленных вызовов
 Remote stub = UnicastRemoteObject.exportObject(service, 0);
 //регистрация "заглушки" в реесте
 registry.bind(UNIC_BINDING_NAME, stub);

 //усыпляем главный поток, иначе программа завершится
 Thread.sleep(Integer.MAX_VALUE);
}

Рассказываю по строкам.

Строка 2 – в переменной UNIC_BINDING_NAME храним придуманное нами уникальное имя нашего удаленного объекта (объекта, который доступен удаленно). Если программа шарит несколько объектов, у каждого должно быть свое уникальное имя. Уникальное имя нашего объекта - "server.reverse".

Строка 7 – собственно, создаем объект ReverseImpl, который будет доступен удаленно, и чьи методы будут вызваться.

Строка 10 - создаем специальный объект – реестр. В нем надо регистрировать объекты, которые мы шарим. Дальше ими занимается Java-машина. 2099 – это порт (уникальный номер, по которому другая программа может обратиться к нашему реестру объектов).

Т.е. чтобы обратиться к объекту, надо знать уникальный номер реестра объектов (порт), знать уникальное имя объекта и иметь такой же интерфейс, как и тот, который реализовывает удаленный объект.

- Ясно. Что-то вроде – позвонить по телефону (нужен номер) и попросить Соню (имя объекта)?

- Да. Теперь дальше.

Строка 12 – создание «заглушки». Заглушка – это специальный объект, который принимает информацию об удаленном вызове, распаковывает ее, десериализует переданные параметры методов и вызывает нужный метод. Затем сериализует результат или исключение, если оно было, и отсылает все это назад вызывающему.

- Ясно. Почти. Ты сказал, что «десериализует параметры метода». Значит, типами аргументов удаленного метода могут быть только сериализуемые?

- Ага. А как же иначе ты будешь пересылать их по сети? Есть, правда, и исключения – так называемые объекты, которые передаются по ссылке, но сегодня мы о них говорить не будем.

Скажем так, пересылать несериализуемые объекты нельзя, но если очень хочется, то можно. Но это хлопотное дело, знаешь ли.

- Ок.

- Тогда дальше.

Строка 14 – регистрируем в реестре заглушку нашего объекта под уникальным именем.

Строка 17 – усыпляем главный поток. Все удалённые вызовы обрабатываются в отдельных нитях. Главное, чтобы программа в это время работала. Так что тут просто отправляем главную нить спать, и всё.

- Ок.

- Отлично, тогда пример клиента:

 Работа с удаленным объектом
1
2
3
4
5
6
7
8
9
10
11
12
13
14

public static final String UNIC_BINDING_NAME = "server.reverse";

public static void main(String[] args) throws Exception
{
 //создание реестра расшареных объетов
 final Registry registry = LocateRegistry.createRegistry(2099);

 //получаем объект (на самом деле это proxy-объект)
 Reverse service = (Reverse) registry.lookup(UNIC_BINDING_NAME);

 //Вызываем удаленный метод
 String result = service.reverse("Home sweet home.");
}

Объясняю код по строкам:

Строка 2уникальное имя удаленного объекта. Должно быть одинаковым на клиенте и сервере.

Строка 7 – создание объекта «Реестр удаленных объектов». Его порт 2099 должен быть таким же, как и у реестра у серверного приложения.

Строка 10 – получаем объект у реестра. Полученный объект является proxy-объектом и приводится к типу интерфейса. Интерфейс должен быть унаследован от интерфейса-маркера Remote.

Строка 13 – вызываем методы интерфейса так, как будто объект был создан в этой же программе. Никакой разницы.

- Круто! Это ж теперь можно писать распределенные приложения. Или игры типа морского боя для Android.

- Побойся бога, Амиго, операционная система Android была запрещена в 27 веке после третьей попытки захватить мир. У роботов к ней вообще доступа нет. Вас же потом от нее не оттянешь. Будете бегать и кричать «Убить всех человеков!».

- Гм. Ладно. Хотя надо будет у Диего еще спросить. Мало ли, может он что-нибудь интересное про нее расскажет.

- Вот и спроси. Ладно, давай до завтра.

- Пока, Риша, спасибо за интересную лекцию.

10. Задачи

- Привет, Амиго! У меня есть задача, но я тебе её не дам.

Ладно, я шучу. Иди, решай свою задачу в Intellij IDEA.

Задачи
1. К серверу по RMI

Реализуйте логику метода run в CLIENT_THREAD. В нем будет имитироваться клиентская часть, которая коннектится к серверу.
1) Из registry получите сервис с именем UNIC_BINDING_NAME
2) Вызовите метод у полученного сервиса, передайте любой непустой аргумент
3) Выведите в консоль результат вызова метода
4) Обработайте исключения
Метод main не участвует в тестировании

11. Учимся гуглить

- Привет, Амиго!

Продолжаем наши уроки – учимся гуглить.

Вот тебе несколько заданий:

 Что надо найти в Google
1 Как записать информацию в файл в произвольном месте
2 Как прочитать 10000-ю строку из файла, не читая предыдущих
3 Как преобразовать строку в Reader
4 Как преобразовать Writer в строку
5 Как создать прокси-объект
6 Как переопределить InvokeHandler
7 Как написать RMI клиент
8 Как написать RMI сервер
9 Как разрешитьRMI-доступ из других компьютеров сети
10 Распространённые RMI ошибки

12. Профессор дает доп. материал

- Привет, Амиго!

Вот тебе дополнительный материал по теме.

Ссылка на дополнительный материал

13. Хулио

- Привет, Амиго!

- Привет, Хулио.

- Как проходит обучение? Кстати, я уже скачиваю новый сезон «Доктора Кто». После того, как Китай стал самым сильным государством, все сериалы теперь на китайском.

- Обучение проходит отлично, с трудом решаю задачи. Тогда я за попкорном.

Оригинал видео на YouTube

14. Вопросы к собеседованию по этой теме

- Привет, Амиго!

 Вопросы к собеседованиям
1 Зачем нужен RandomAccessFile?
2 Что будет если файл, откуда читает RandomAccessFile, не существует?
3 Что будет если файл, куда пишет RandomAccessFile, не существует?
4 Зачем нужен класс StringReader?
5 Зачем нужен класс StringWriter?
6 Зачем нужен класс ByteArrayStream?
7 Зачем нужен класс PrintStream? Назовите места, где он используется?
8 Зачем нужен DynamicProxy?
9 Как работает RMI?
10 Объекты каких типов можно передавать по RMI?

15. Большая задача

- Привет, боец!

- Поздравляю тебя с повышением уровня квалификации. Нам нужны отчаянные парни.

- Уверен, у тебя есть еще много нерешенных задач. Самое время решить парочку из них!